[TOC]
**前文:以下是我ctfshow的PWN做题记录,欢迎各位前来观看,小弟领教!**还有就是ctfshow-pwn是我入门PWN的第一套题,比较基础,看官方writeup可以知道比较简洁,但有些我看不懂,这是我自己做的过程中写的,都是发自内心理解它们的话,有些可能比较啰嗦,但也必须这样,因为站在新手角度来看必须要这种啰嗦方式去理解才能更深刻,有耐心的朋友可以一直看下去,当然也欢迎PWN大佬前来挑战我!!!😄
Test_your_nc
PWN_000
专用虚拟机镜像,全套在这里,提取码show;虚拟机镜像,用户名为ctfshow,密码是ctfshow。
看了课也啥都不会或者工具不会下看这–>CTF Wiki-PWN
解题过程
ssh连接(记得给虚拟机kali\Ubuntu或者主机设置打开SSH):
ssh ctfshow@pwn.challenge.ctf.show -pxxxx
一开始会让你输入yes/no的,直接yes,然后输入密码,等进程结束会有个交互shell
用命令ls打开看一下有啥文件,看到有个ctfshow_flag的文件,猜是个装flag的文件夹,直接用cat命令打开就好。注意斜杠/。
PWN_001
题目 PWN_001 :提供后门函数,连上即可得到flag
这里有用到“nc
”连接,可以了解一下,方便后续做题。当然Kali自带nc连接的,你用Kali干PWN也行。
这题送分的,nc连接一下就爆flag了。
不过说实话我是真不知道PWN_001文件给来干嘛的,直接nc交互一下就出flag了,这题没搞懂ctfshow意图在哪。
不过我用了下刚学的指令checksec看了看ELF文件信息,发现是没开canary保护的,可以利用栈溢出漏洞…
可以看到是64位仅关闭Canary保护。
接着用64位IDA打开查看main函数(按F5进入反汇编):
怪不得直接爆flag出来呢。后门system函数直接运行了。
PWN_002
题目: PWN_002 给你一个shell,这次需要你自己去获得flag
到这里度过新手期了。
这里需要我们输入指令了,一般来说做题习惯看到这样直接ls秒的,不过像我这样入门级别的还得用IDA琢磨琢磨:
可以看到是没canary保护的
看到这里有个疑问,为什么system有这么大威力?,这次system里边是一个/bin/sh的软链接,很类似我们的文件地址
system("/bin/sh");
是漏洞利用中获取系统 shell 的核心操作,其工作原理涉及函数调用、系统调用和进程创建三个层次,本质是通过标准库函数system
启动一个/bin/sh
进程(即命令行解释器),让攻击者获得交互权限。
system
是 C 标准库(libc)中的函数,原型为:
|
|
它的功能是执行参数
command
指向的字符串作为系统命令,相当于在终端中输入该命令并执行。
system()函数先fork一个子进程,在这个子进程中调用/bin/sh -c来执行command指定的命令。/bin/sh在系统中一般是个软链接,指向dash或者bash等常用的shell,-c选项是告诉shell从字符串command中读取要执行的命令(shell将扩展command中的任何特殊字符)。父进程则调用waitpid()函数来为变成僵尸的子进程收尸,获得其结束状态,然后将这个结束状态返回给system()函数的调用者。
system("/bin/sh")
的工作原理可简化为: 通过system
函数创建子进程 → 子进程执行/bin/sh
程序 → 启动交互式 shell 环境。这也是漏洞利用中最常用的获取权限的方式 —— 无论通过栈溢出、ROP 还是 ret2libc,最终目标都是让程序执行这条语句(或等效的系统调用)。
PWN_003
题目: PWN_003 哪一个函数才能读取flag?
通过前面的分析,我们可以很明显的看出来选项”6”为我们所需要的后门函数,其他的均不会得到我们所想要的flag,在system函数里面的命令都是在终端执行的,赋予system权限较大,在我所写的文章里,包括PWN入门知识讲解和操作系统都明确指出SYSTEM是与操作系统内核接触的,比Administrators的权限还要稍大。
直接选6秒了,但除这种送分的,其实还需要搞清运行的逻辑:
跟进一下menu函数里边:
回到main跟进一下这个“6”的函数,看看里头是不是真在执行system后门函数:
可以看到,确实是执行了。那么我们也就能得到我们需要的flag。
PWN_004
题目: PWN_004 或许需要先得到某个神秘字符
真够阴间的,啥也不给了,才第几道啊就上强度了。
checksec看看,然后IDA反编译看看:
64位二进制保护全开…
可以看到有几个似曾相识的函数:strcpy(复制函数把xx复制给s1,也可起到赋值的作用)、strcmp(比较函数,对比,判断作用)、execve();我曾说过汇编语言的cmp就有比较作用,这也算是类比吧,回想起来的,不过用AI搜一下也行。
来说下execve吧,上题讲过system函数,这个也应该知道的:
execve
是 Unix/Linux 系统中最核心的系统调用(system call) 之一,用于在当前进程中加载并执行一个新的程序,替换当前进程的代码、数据和堆栈,实现进程 “变身”。它是所有程序启动和替换的底层基础,包括 system
函数、shell 执行命令等最终都会依赖 execve
完成。
看到这段代码,大致意思是,让s1等于“CTFshowPWN”这段字符串,然后到if判断语句这进行比较:如果s1和s2相等(这里的!不是我们C语言中的“不”的意思啊),就执行execve函数,启动交互权限,从而得到flag。
至此:下一节就开始前置基础了,开始应用PWN-1、PWN-2、PWN-3所学的知识了。
前置基础
PWN_005
题目:运行此文件,将得到的字符串以ctfshow{xxxxx}提交。
如:运行文件后 输出的内容为 Hello_World
提交的flag值为:ctfshow{Hello_World}
注:计组原理题型后续的flag中地址字母大写
Welcome_to_CTFshow.asm文本打开后发现是汇编语言文件:
|
|
checksec一下这个PWN文件:
ohoh这个保护全关的!IDA的x32位
这个没main函数,那我们回看那段汇编语言asm,这段汇编语言更像是在帮助我们了解汇编语言的逻辑语法规则的,在PWN-1文章中就有讲过汇编语言,这里就直接跳过详解
这题开始就有点变样了,“Welcome_to_CTFshow.asm”这文件是一个存放汇编语言的文本文件,需要运行一下转换成像exe这样的可执行文件,在PWN-1讲过elf文件:
将汇编语言(.asm)文件转换为可执行文件,需要经过汇编(Assemble) 和链接(Link) 两个核心步骤.
在Kali/Linux上下载nasm编辑器:
|
|
然后下载ld链接器(通常自带):
|
|
汇编转换:将 .asm 转换为目标文件(.obj 或 .o)
|
|
汇编生成可执行文件:
|
|
(Welcome_to_CTFshow1是可执行文件)
我的建议汇编转换和生成可执行文件的过程都在Kali/Linux/Ubuntu上,比较方便,windows下载比较麻烦。
生成这个可执行文件之后,直接运行就能得到输出结果了,按照题目要求,加个{}包皮去提交就好了。
PWN_006
题目:立即寻址方式结束后eax寄存器的值为?
按照PWN-005那样去转换生成这个可执行文件
汇编转换:将 .asm 转换为目标文件(.obj 或 .o)
1
nasm -f elf Welcome_to_CTFshow.asm
汇编生成可执行文件:
1
ld -m elf_i386 -s -o Welcome_to_CTFshow1 Welcome_to_CTFshow.o
(Welcome_to_CTFshow1是可执行文件)
Welcome_to_CTFshow_PWN???是flag,错了,题目是立即寻址方式结束后eax寄存器的值为?
那就是得把这个转换出的可执行文件放到IDA上,不过之前记得checksec一下看看是x32还是x64位哦…
|
|
那就加减法咯:11+114504-1=114514
ctfshow{114514}
PWN_007
题目:寄存器寻址方式结束后edx寄存器
的值为?
按照上述方法去得出可执行文件,放入IDA:
|
|
所以ctfshow{0x36D}
PWN_008
题目:直接寻址方式结束后ecx寄存器的值为?
|
|
当你编译后发现找不到地址[msg],然后我们倒回来找题目的Welcome_to_CTFshow文件,用IDA:
故flag:ctfshow{0x80490E8}
其实不用去进行编译的,在题目给的文件就能做,编译出来的其实就是题目给的文件。
PWN_009
题目:寄存器间接寻址方式结束后eax寄存器的值为?
|
|
不是80490E8哈别被误导了,这是虚拟地址哦
点击跟进:
dd 636C6557h
:dd
是汇编指令中的伪操作符,意为 “定义双字(define double word)”,表示在当前地址处存放一个 4 字节的数值。这里存放的数值是 636C6557h
(十六进制)。
flag:ctfshow{0x636C6557}
PWN_010
题目:寄存器相对寻址方式结束后eax寄存器的值为?
|
|
然后我们跟进看一下地址:
也就是这里将msg的地址(0x80490E8)+ 4 处所执向的地址的值赋给eax
4是十进制的,地址是16进制的,记得转换一下:
hex(0x80490E8+4)–> 080490EC
对应地址得值是ome_to_CTFshow_PWN
故flag:ctfshow{ome_to_CTFshow_PWN}
PWN_011
题目:基址变址寻址方式结束后的eax寄存器的值为?
|
|
同样的,放到IDA上找这个基址变址寻址方式:
双击点dword去跟进到地址:
计算最终也是 [0x80490E8 + 2*2 ] = [0X80490EC]
其实也是和PWN_010一样:hex(0x80490E8+4)
就是说hex(0x80490E8 + 2*2 )=0X80490EC,对应的是“ome_to_CTFshow_PWN”。
flag:ctfshow{ome_to_CTFshow_PWN}
PWN_012
题目:相对基址变址寻址方式结束后eax寄存器的值为?
|
|
跟进offset dword_80490E8:
那就是:[8 + 0x80490E8 + 1*2 - 6] = [0x80490EC]
故flag:ctfshow{ome_to_CTFshow_PWN}
PWN_013
题目:如何使用GCC?编译运行后即可获得flag PWN_013.c
学过C语言都会安装配置编译器环境的,我这里使用的是visual-studio-code。
这段代码是一个简单的 C 程序,它使用字符数组 flag 存储了一个加密的字符串,并通过 printf函数将其打印出来。
在这段代码中, flag 数组存储了一串整数值,这些整数值代表了字符的 ASCII 码。通过将这些整数值转换为相应的字符,就可以还原出原始的字符串。
运行该程序, printf 函数使用 %s 格式字符串将 flag 数组作为参数进行打印。由于 flag 数组的最后一个元素为零(NULL 字符), printf 函数会将其之前的字符依次打印,直到遇到 NULL 字符为止。根据给定的整数值数组,还原出的字符串为: ctfshow{hOw_t0_us3_GCC?} 。
PWN_014
题目:请你阅读以下源码,给定key为”CTFshow”,编译运行即可获得flag。 PWN_014.c
这个文件有强度了,直接编译运行会说nothing is here,不给flag,先读一下这个C语言文件在干什么的:
|
|
也就是说我们需要一个名为key的文件,让这个程序知道,然后他就能把flag字符串吐出来了。
|
|
答案是ctfshow{01000011_01010100_01000110_01110011_01101000_01101111_01110111_00001010}
题目要求在key里加CTFshow字符串,加其它是错的,输出结果不同。
PWN_015
题目:编译汇编代码到可执行文件,即可拿到flag。 flag.asm
汇编转换:将 .asm 转换为目标文件(.obj 或 .o)
|
|
汇编生成可执行文件:
|
|
(flag1是可执行文件)
比较简单,和PWN_005很类似,都是汇编语言编译运行之类的。
ctfshow{@ss3mb1y_1s_3@sy}
PWN_016
题目:使用gcc将其编译为可执行文件。 flag.s
|
|
可以使用 gcc 命令直接编译汇编语言源文件( .s 文件)并将其链接为可执行文件。 gcc 命令具有适用于多种语言的编译器驱动程序功能,它可以根据输入文件的扩展名自动选择适当的编译器和链接器。
故flag:ctfshow{daniuniuda}
PWN_017
题目:有些命令好像有点不一样?不要一直等,可能那样永远也等不到flag。
比较奇葩的是,这里选择3的话,一直在loading的,可能是不通的,我换到主机的nc也不行,那就是选2进程的
选了2后,程序开始发癫了….,一直循环搞得我Kali虚拟机卡死了,进到IDAx64去看:
这个case2的read函数只读取10字节以内的,也就是说ctfshow_flag(12byte)这串是超字节限制了。不过之前学了那个指令有通配符的cat ctf*(因为当你选择”ls ./“,打开全部文件后,发现只有一个ctfshow_flag时由ctf字眼的,那我们正好可以绕过超字节的限制了)
搜索打开由ctf这串字符的文件内容:
PWN_018
题目:仔细看看源码,或许有惊喜;假作真时真亦假,真作假时假亦真。 PWN_018
连接nc后一上来就问:
这我哪知道啊?去看IDAx64:
分别跟进一下real和fake:
解读一下发现一个机制:当v4不等于9时就会执行real函数,这时real会执行echo 'flag is here'>/ctfshow_flag
,这个命令将字符串 ‘flag is here’ 覆盖写入 /ctfshow_flag 文件中。 > 符号表示以覆盖的方式写入文件,如果文件不存在则创建新文件。如果 /ctfshow_flag 文件已经存在,那么该命令会将文件中原有的内容替换为 ‘flag is here’ 。也就是说你第一次和靶机交互时没输入9,你以后都不得不到flag了,只能重开靶机…..比较CS吧…
echo 'flag is here'>>/ctfshow_flag
这个命令将字符串 ‘flag is here’ 追加写入 /ctfshow_flag 文件中。 » 符号表示以追加的方式写入文件,如果文件不存在则创建新文件。如果 /ctfshow_flag 文件已经存在,那么该命令会在文件的末尾添加 ‘flag is here’ 。
这两个命令都用于将 ‘flag is here’ 写入 /ctfshow_flag 文件中,不同之处在于写入方式的不同。第一个命令使用追加方式,在文件末尾添加内容;第二个命令使用覆盖方式,将文件内容替换为新内容。具体使用哪个命令取决于需求和文件操作的预期结果。也就是所假的其实是我们需要的真的,真的反而是假的在远程环境中,我们需要在第一次读到flag,否则后续得到的flag都已经被覆写再追加,真实的flag内容已经没了。
PWN_019
题目:关闭了输出流,一定是最安全的吗? PWN_019
在IDA上看到,是由read和system函数的,按道理我直接cat /ctf*应该就出flag了啊…
问题就出在这个fclose函数(确实没辙了,只能用AI解释一下了)作用是关闭文件输出流,标准输出被关闭了(无法显示)。
那还有输入流啊,可以用重定向:1>&0
这是 Linux 的 I/O 重定向语法:
1
代表标准输出(stdout)(正常情况下,命令的输出会送到这里,显示在终端);0
代表标准输入(stdin)(正常情况下,用户输入从这里读取,通常指向终端);1>&0
表示:将标准输出重定向到标准输入所指向的位置。
因为代码中标准输出被关闭了(无法显示),但标准输入(通常是终端)仍然可用,所以通过这个重定向,cat
命令的输出会被 “转移” 到标准输入对应的终端,从而让内容能显示出来。
简单说就是:正常显示内容的 “输出通道” 被关了,但接收输入的 “输入通道” 还能用。通过 1>&0 把原本该从 “输出通道” 显示的内容,转到 “输入通道” 对应的终端上,这样就能看到 flag 了。ctfshow{a390a91b-5203-46ae-812d-0396de95184f}
本质上是利用了 “输入通道和终端还连着” 这个漏洞,让内容 “借道” 显示出来~。
PWN_020
题目:提交ctfshow{【.got表与.got.plt是否可写(可写为1,不可写为0)】,【.got的地址】,【.got.plt的地址】}
例如 .got可写.got.plt表可写其地址为0x400820 0x8208820
最终flag为ctfshow{1_1_0x400820_0x8208820}
若某个表不存在,则无需写其对应地址
如不存在.got.plt表,则最终flag值为ctfshow{1_0_0x400820}
checksec一下:
在这里只开了NX保护。
在PWN-1的Linux安全防护机制这一章的学习中,我们有认识到RELRO这个保护。很显然no RELRO意味着.got和.got.plt表都可写。
用指令查表地址:
|
|
耐心找一下就找到了,故flag:ctfshow{1_1_0x600f18_0x600f28}
PWN_021
题目:提交ctfshow{【.got表与.got.plt是否可写(可写为1,不可写为0)】,【.got的地址】,【.got.plt的地址】}
例如 .got可写.got.plt表可写其地址为0x400820 0x8208820
最终flag为ctfshow{1_1_0x400820_0x8208820}
若某个表不存在,则无需写其对应地址
如不存在.got.plt表,则最终flag值为ctfshow{1_0_0x400820}
checksec一下:
要还不记得Relro保护机制原理的就回PWN-1去看吧…
这里是 GOT 表的前半部分(.got.plt)设置为只读,后半部分(.got)仍可写。
用readelf命令去爱看表地址就好了:
flag:ctfshow{0_1_0x600ff0_0x601000}
PWN_021
题目:提交ctfshow{【.got表与.got.plt是否可写(可写为1,不可写为0)】,【.got的地址】,【.got.plt的地址】}
例如 .got可写.got.plt表可写其地址为0x400820 0x8208820
最终flag为ctfshow{1_1_0x400820_0x8208820}
若某个表不存在,则无需写其对应地址
如不存在.got.plt表,则最终flag值为ctfshow{1_0_0x400820}
道理一样checksec看relro的保护、readelf看表…..
这次就只有一个.got表了,那就是flag:ctfshow{0_0_0x600fc0}
PWN_023
题目:用户名为 ctfshow 密码 为 123456 请使用 ssh软件连接
|
|
不是nc连接 PWN_023
没有canary保护,这可以是个突破点:栈溢出漏洞。
看回IDA:
跟进ctfshow函数
可以知道,一开始dgets函数读取flag字符串到内存中
跟进signal函数:
这行代码会将内存中存储的 flag
字符串,输出到标准错误流(stderr
)。因为程序启动时已经通过 fgets
从 /ctfshow_flag
文件读取了 flag 内容到 flag
变量中,所以这里能直接打印出正确的 flag。
所以说我们让程序报错也就是让它栈溢出,覆盖后的地址还不能是有效地址,让它全部报错,也就是我们可以输入好多个a就行,几十个a的16进制地址是无效地址。
关于栈溢出这部分,请看PWN-2详细学习,PWN板块占比也是重要的。
逐渐开始上强度了,这时需要仔细看IDA的代码分析了,看看是什么工作原理。
flag:ctfshow{0f207f1f-71bb-4cdd-bdab-7bfb9acde27a}
PWN_024
题目:
你可以使用pwntools
的shellcraft
模块来进行攻击
我来介绍一下shellcraft模块:它是 pwntools 库中的一个子模块,用于生成各种不同体系结构的 Shellcode(这里的不同体系是我们之前学过的操作系统基础的有关shell的那一章节)有zsh、bash等。Shellcode 是一段以二进制形式编写的代码,用于利用软件漏洞、执行特定操作或获取系统权限。shellcraft 模块提供了一系列函数和方法,用于生成特定体系结构下的Shellcode。
生成的汇编代码可直接通过 asm() 函数转换为机器码(二进制),无需手动处理格式,例如:
|
|
IDA:可以看到这题似乎和ret2shellcode有关
看题目提示,进入新大陆了…用pwntools了。
这里有提示,可以看一下PWN-1-NX保护,有啥启发?
在IDA分析可知,ctfshow函数无法跟进源代码,只能看它的伪代码去分析了(比较长):
|
|
如果之前是打逆向或者强行入门PWN的话,最坐牢的我相信应该是用IDA吧,除了找明文字符串flag外,你学了汇编语言你也看不懂这是啥意思,感觉一小串源代码转换成汇编语言非常长、枯燥,其实UP主也深有体会,在系统性入门后,也初步知道IDA是怎么用的,这里我把下划线“===== S U B R O U T I N E =====”也复制进来了,按我理解这是函数分界,分隔线上方是前一个函数的结尾(通常以
retn
指令结束,代表函数返回)过了这条线,说明汇编语言在描述的就是另外一个函数了。也就是说这上下分界线是代表囊括着一个函数,例如这ctfshow函数。另外我想说的是汇编语言这么长是有道理的,它更像是一种“啰嗦”,它能在每一段代码运行后不断向你汇报地址变化,这种 “啰嗦” 本质上是对计算机底层操作的直接映射,这种特性确实让它在跟踪程序运行细节、分析逻辑和排查问题时具有独特优势。
从这里开始就要用pwntools了,这里介绍一下:
from pwn import *
是在 Python 中使用pwntools
库的常见导入方式 。pwntools
是一个功能强大的用于二进制漏洞利用(binary exploitation)的 Python 库,在 CTF(Capture The Flag,夺旗赛 )竞赛、安全研究以及漏洞分析等领域广泛应用,以下是其具体作用:与目标程序交互
- 本地进程交互:
pwntools
能轻松创建本地进程,模拟用户输入、获取程序输出。例如,p = process('./vulnerable_program')
可以启动本地的可执行文件vulnerable_program
,然后通过p.sendline('input data')
向程序发送数据,用p.recvuntil('prompt')
接收程序输出直到遇到特定字符串。- 远程服务器交互:在面对远程存在漏洞的服务时,
pwntools
提供了便捷的方法。比如r = remote('target.com', 8080)
可以连接到target.com
服务器的 8080 端口,后续同样能发送和接收数据,方便对远程服务进行测试和漏洞利用。二进制数据处理
- 数据打包和解包:在二进制程序中,经常需要处理不同字节序(大端序、小端序)的数据。
pwntools
提供了pack
和unpack
函数,比如p32(0x12345678)
可以将整数0x12345678
按照小端序打包成 4 字节的二进制数据;u32(data)
则可以将 4 字节的二进制数据按照小端序解包为整数。- 数据转换:它还支持各种数据格式之间的转换,如将字符串转换为字节串,或者进行十六进制与二进制数据之间的转换等。
辅助漏洞利用
- 生成 Payload:对于缓冲区溢出等漏洞,需要精心构造 Payload(攻击载荷)。
pwntools
提供了生成填充数据、构造 ROP(Return-Oriented Programming,返回导向编程 )链等功能。比如cyclic(100)
可以生成一个 100 字节的循环模式字符串,用于确定缓冲区溢出的偏移量;通过ROP
模块可以方便地构建 ROP 链来绕过一些安全机制,实现代码执行。- 符号解析:在分析二进制程序时,有时需要解析程序中的函数地址、变量地址等。
pwntools
可以与ELF
(Executable and Linkable Format,可执行与可链接格式,常用于 Linux 系统中的可执行文件、共享库等 )文件交互,获取这些符号信息,例如elf = ELF('./target_binary')
可以加载目标二进制文件,然后通过elf.symbols['main']
获取main
函数的地址。密码学相关辅助
在 CTF 中,有时会涉及密码学题目,
pwntools
也提供了一些基础的密码学辅助功能,比如常见加密算法的简单实现、编码解码等。多线程和异步支持
pwntools
一定程度上支持多线程和异步操作,这在同时与多个目标程序交互,或者需要在等待程序输出的同时执行其他任务时非常有用。
所以为啥要学Python,当然能用豆包、DeepSeek之类的AI帮你写代码exp,但你得先了解Python再让它写才对的,有时会出错,你得有判断力。
这里推荐使用Pycharm、Visual Studio Code;或者你可以在Kali上用终端来运行,这样你在Kali用nc连接靶机时在同一系统运行交互比较方便,当然Pycharm、Visual Studio Code也有终端功能,只不过要配PWN环境稍微麻烦而已,看你习不习惯用Linux系统吧。
题目后续:
讲得比较啰嗦,这里开始分析这段汇编代码(重新粘贴了一遍,和上面一样):
|
|
这代码是在看不懂可以上AI嗦一下,让它帮你解析一下,毕竟刚入门……(这段代码是 IDA 反编译出的 32 位 x86 架构程序中ctfshow
函数)
checksec一下:记住上面的参数,后面写exp要用…
可知,对这些保护有点遗忘的可以回去看PWN-1的Linux安全保护机制
保护项 | 状态 | 对利用的影响 |
---|---|---|
Arch | i386-32-Little | 32 位 x86 架构,shellcode 需用 32 位版本(对应shellcraft.i386.sh() ) |
Canary(栈溢出保护) | No canary found | 无栈溢出保护,缓冲区溢出后可直接覆盖栈上数据(无需绕过 canary) |
NX(栈不可执行) | NX disabled | 栈段可执行,shellcode 直接放在栈上就能运行(无需 ROP 绕过 NX) |
PIE(地址随机化) | No PIE (0x8048000) | 程序地址固定,ctfshow 函数、buf 缓冲区地址不会变(无需泄露地址) |
RWX segments | Has RWX segments | 存在 “可读可写可执行” 的内存段,进一步确保 shellcode 能正常执行 |
总结:程序保护机制极弱,直接用 “栈溢出注入 shellcode” 即可,无需复杂绕过。
回到本题目,在nc连接之后交互一会发现并无实际效果,PWN大致思路是找到后门函数或者系统调用函数进行提权
,提权之后就进行交互去执行一些靶机的终端命令例如:cat ctfshow_flag、ls之类的。那在此之前都得先找到这“后门”,也就是/bin/sh、zsh、system函数等等,但我发现这个程序并无这些地址存在,所以说这个程序从被制造出来都没想过要执行这些有关提权的代码…..
因为NX、canary没开、又有RWX,注入一段我们设计的有关执行/bin/sh的exp还是可以的,大致思路有了,接下来开始执行吧。
围绕 “直接执行 shellcode” 设计方案
可以用pwntools的脚本啊,单纯直接调用函数的,而且pwntools有局限啊,就那用这个pwntools的shellcraft模块生成的shellcode比较晦涩难懂,而且注释放到Kali上比较杂乱,而且没有涉及到汇编语言调用,比较简单我就不用了,我这里手敲shellcode方便大家了解一下底层逻辑:
|
|
创建一个python文件,输入这些代码放到终端上运行,记得提前打开另外一个窗口进行nc连接远程靶机。
上面其它的python代码没啥可讲的,这是远程交互的模板代码,直接套就好,我现在重在讲解这个手搓的shellcode,它和shellcraft模块产生的shellcode效果一样的,而且还好看懂:
|
|
这 7 行代码通过精准设置寄存器和栈数据,完成了
execve
系统调用的参数准备,最终实现 “用/bin/sh
替换当前进程” 的效果,是 CTF 中获取 shell 的基础手段,逻辑清晰且精简(仅 23 字节)。
逐行解析手敲shellcode
思路:注入一段能执行/bin/sh的汇编语言代码
① mov eax, 0xb
eax
寄存器在 32 位 Linux 系统调用中专门用于传递 “系统调用号”(告诉内核要执行哪个系统调用)。0xb
是十六进制,转换为十进制是11
,而11
正是execve
系统调用对应的编号(内核通过这个值识别要执行execve
)。
② mov ecx, 0
ecx
寄存器用于传递execve
的第二个参数argv
(参数列表)。- 传
0
(即NULL
)表示 “没有参数”,等价于在命令行直接输入/bin/sh
(不带任何参数)。
③ mov edx, 0
edx
寄存器用于传递execve
的第三个参数envp
(环境变量列表)。- 传
0
(即NULL
)表示 “没有环境变量”,内核会使用默认环境变量。
④ push 0x0068732f
-
push
指令将数据压入栈中(栈是向下增长的内存区域)。 -
1
0x0068732f
是十六进制的 “小端序” 存储(x86 架构默认小端序,即低地址存低位字节),转换为 ASCII 码:
0x2f
→/
0x73
→s
0x68
→h
0x00
→ 字符串结束符\0
所以这行实际是往栈上压入字符串"/sh\0"
。
⑤ push 0x6e69622f
-
同样是压栈操作,
0x6e69622f
转换为 ASCII 码:0x2f
→/
0x62
→b
0x69
→i
0x6e
→n
所以这行往栈上压入字符串"/bin"
。
结合上一行,栈上现在的内容是
"/bin/sh\0"
(因为栈先压入"/bin"
,再压入"/sh\0"
,栈顶到栈底的顺序就是/b/i/n//s/h/\0
)。
⑥ mov ebx, esp
esp
是栈指针寄存器,永远指向当前栈顶的地址。- 经过两次
push
后,栈顶正好是"/bin/sh\0"
字符串的起始地址(第一个字符'/'
的位置)。 - 这行指令将栈顶地址存入
ebx
,而ebx
寄存器在execve
系统调用中用于传递第一个参数filename
(程序路径),所以ebx
现在指向我们要执行的"/bin/sh"
。
⑦ int 0x80
int
是中断指令,0x80
是 Linux 系统调用的中断号。- 执行这条指令会触发一个软中断,让 CPU 从用户态切换到内核态,内核会根据
eax
中的系统调用号(11
)执行execve
,并使用ebx
、ecx
、edx
中的参数。
总之:必须要定义好这几个寄存器,在最底层操作系统运行时都会用到的,这里不详细讲述,反正你知道这几个寄存器一开始就得这么定义就好啦。(不耐烦了)
|
|
push压栈时要记得字节限制,因为架构是x32,所以push最多4个字节,也就是push两个:/bin、/sh的地址(不满四个字节如/sh要记得用00填满哦不然会报错)
然后就是记得esp寄存器是动态的…
这是调用pwntools的shellcraft模块的exp:
|
|
这是它的shellcode(原版未修改过,直接复制粘贴上来的):
|
|
看着确实比较乱…..😅
最后的最后我想对这题做些评价,其实题不难主要是我初次做,讲得比较啰嗦,如果你会手敲shellcode或者调用pwntools的shellcraft模块的话还是很简单秒掉的。只能怪小生太cai了😄
大致思路知道就好,靶机的flag是动态变化的。
PWN_025
题目:开启NX保护,或许可以试试ret2libc
根据题目,也就是说具体攻击手法为:ret2libc
ret2libc攻击
ret2libc
是一种在二进制漏洞利用中的经典技术,全称为 “return - to - libc”,中文可理解为 “返回至 libc 库”,常用于绕过栈不可执行(NX
,No - eXecute)保护机制。
ret2libc实现步骤
- 信息泄漏:通过栈溢出漏洞,结合一些函数(如
write
函数),泄漏出程序中libc
库函数的真实地址。因为在不同系统中,libc
库加载的基地址是随机的(ASLR
保护机制),但库内函数之间的相对偏移是固定的,所以需要先获取一个已知函数(比如write
)的真实地址,进而计算出其他函数(如system
)的地址。- 计算关键地址:根据泄漏出的
libc
函数地址,计算出system
函数地址和/bin/sh
字符串在内存中的地址。由于libc
中函数和字符串的相对位置固定,所以可以通过已知函数地址与偏移量来计算其他关键地址。- 构造 payload:再次利用栈溢出漏洞,构造合适的 payload。payload 一般包含填充数据(用于填满缓冲区直到覆盖返回地址)、
system
函数地址(覆盖返回地址,使程序返回时跳转到system
函数)、一个无效的返回地址(system
函数执行完后要返回的地址,随便填充一个地址,因为获取到 shell 后通常不会再关注这个返回操作 )、/bin/sh
字符串的地址(作为system
函数的参数,这样system
函数执行时就相当于执行了system("/bin/sh")
,从而打开一个 shell )。- 发送 payload:将构造好的 payload 发送给存在漏洞的程序,使程序按照攻击者期望的流程执行,最终获取到一个交互式 shell,达到漏洞利用的目的。
checksec一下,发现开了NX保护,也就是说不能用shellcode了。
跟进ctfshow函数:
可以发现buf大小为 132 字节,但read允许读取 0x100(256)字节存在栈溢出漏洞。
跟进write函数:
IDAx32分析,也没有/bin/sh、后门system:
这时候就需要使用利用libc(动态链接库)的system函数和/bin/sh字符串(这题思路很类似PWN_024的编写shellcode一样,程序本身没有后门可入只能从外部链接进这些函数,从而达到进入交互…),这些东西在程序运行时的真实地址计算方法为:@got真实地址 = libc基址 + 偏移
,所以要先计算出偏移。
以write函数为例 ,偏移 = 真实地址 - libc基址,真实地址存在write@got、write@got可以通过write@plt来运行这个函数从而泄露出来,libc 版本和基址可以导入 libcSearcher 库来查找计算这里需要去系统学习一下动态链接的过程(有关.got、.plt表的知识),简单画个图说明一下黄色部分是静态的,绿色部分是动态变化的:
elf.got[‘write’]的作用是获取 write函数真实地址的存储位置,即write@got的地址;
elf.plt[‘write’]的作用是获取 write函数跳板地址的存储位置,即write@plt的地址;
具体思路:
- 解析目标程序的 ELF 文件,读取write@plt的地址 、write@got的地址和ctfshow的函数地址。
- 以栈溢出漏洞作为入口,跳转到write@plt执行函数泄露write@got的地址,再跳转回ctfshow函数。
- 读取泄露出的write@got的地址,用 libcSearcher 库查找正确的 libc 版本,再计算偏移。
- 计算system函数和/bin/sh字符串的真实地址。
- 继续从ctfshow函数里的栈溢出作为入口,跳转到system函数真实地址,同时将/bin/sh作为参数传入,从而拿到权限。
这里之前我是跳过的,比较难,我做到PWN_044之后才倒回来做的。
PWN044 的利用逻辑是 “先调用
gets
向bss
段写入/bin/sh
,再调用system
执行”(类似文档中 PWN044 的gets
调用场景)。此时调用gets
仅需其 “函数入口” 即可完成数据写入,无需知道gets
在libc
中的真实地址 —— 直接使用gets
的 PLT 表地址(p32(gets)
本质是p32(gets_plt)
),就能触发gets
执行,实现写入/bin/sh
的目标。这就是为什么在PWN_044是直接是p32(gets),而在这调用write函数是要分.got和.plt,因为它俩功能都不同。
write_plt
负责 “触发write
执行”,write_got
负责 “提供待泄露的真实地址”,二者缺一不可,因此必须明确区分。
总之:ret2libc 核心逻辑:“泄露地址→计算地址→构造调用”
exp备注版:
|
|
原版exp:
|
|
这部分主要涉及到栈溢出的章节知识点,我已经讲解的很详细很啰嗦了,如果看不懂就跳过吧,把栈溢出搞懂后再跳回来做。我当时也是跳过的,之后再回来写wp的。
感觉强度上来了,做得有点吃力。
PWN_026
题目:设置好 ASLR 保护参数值即可获得flag。为确保flag正确,本题建议用提供虚拟机运行。 PWN libc-2.27.so (提取码show)
这题坑到我了,运行程序发现直接爆flag,交上去发现是假的/(ㄒoㄒ)/~~
IDAx64分析,有system后门函数:
而且可以知道这个flag是有几个printf、puts共同打印出的、字符串由地址拼接(%p打印指针或者地址,以16进制形式)而成。
运行一下:
每次flag都不一样,说明地址一直在变化,也就是地址随机化。
在Linux保护机制有讲过ASLR,关于ASLR保护可以看回PWN-1:ASLR(Address Space Layout Randomization)是一种操作系统级别的安全保护机制,旨在增加软件系统的安全性。它通过随机化程序在内存中的布局,使得攻击者难以准确地确定关键代码和数据的位置。
gdb调试一下,题目早给出了2,在这里单纯演示一下;
|
|
|
|
虽然 checksec
输出中没有直接的 ASLR 开启状态,但可以结合系统层面的设置(通过 cat/proc/sys/kernel/randomize_va_space
查看,输出 0
表示关闭 ASLR,1
表示部分开启,2
表示完全开启 )来判断程序运行时的 ASLR 情况。
既然地址随机化保护,我们就把它关掉(记得提权root):
|
|
这里有个点就是我的flag是ctfshow{0x400687_0x400560_0x6032a0_0x7ffff7fbf6b0}
但不知为啥和官方不一样flag:ctfshow{0x400687_0x400560_0x603260_0x7ffff7fd64f0}
但我的程序运行了三次,ASRL也关了,flag出来的是一样的,代表地址是真实的,flag没错的。反正大体思路是正确的。
总之记住:/proc/sys/kernel/randomize_va_space
PWN_027
题目:设置好 ASLR 保护参数值即可获得flag。libc-2.27.so PWN
这题也是一样的,”If the result is 0 or 1, then you get the correct flag!“将ASLR保护的数值改成1或0就好。(套上题的公式)
怪怪的:
官方flag是ctfshow{0x400687_0x400560_0x603260}
欸我发现….0x603260的后面的6变成a了,有点招笑…应该是Kali环境和Ubuntu不同,我去网上查了下,发现大部分PWN的WP都是用Ubuntu解的:
这里的flag就是正常的了。从现在开始我将彻底使用Ubuntu了。
PWN_028
题目:设置好 ASLR 保护参数值即可获得flag;libc-2.27.so、 PWN (show)
这题送分的,ASLR直接关的,flag还是地址拼出来的,那flag就不变得就是真的了。不信可以多运行几次,你看flag会变吗?
flag is :ctfshow{0x400687_0x400560}
PWN_029
题目:ASLR和PIE开启后;libc-2.27.so、 PWN
试着ASLR和PIE保护开启后,地址都会将随机化,这里值得注意的是,由于粒度问题,虽然地址都被随机化了,但是被随机化的都仅仅是某个对象的起始地址,而在其内部还是原来的结构,也就是相对偏移是不会变化的。
|
|
直接运行……
flag(有点长但是对的…):ctfshow{Address_Space_Layout_Randomization&&Position-Independent_Executable_1s_C0000000000l!}
这题只是让你了解下ASLR和PIE保护的特点,flag不是目的:
PWN_030
题目:关闭PIE后;程序的基地址固定,攻击者可以更容易地确定内存中函数和变量的位置。 PWN_030
checksec一下:没开canary和PIE,开了NX不好用Shellcode。
IDAx32看一下:
buf ,用于存储从标准输入读取的数据。该变量在栈上分配,相对于函数栈帧指针 ebp 的偏移为-0x88 。调用 read 函数从标准输入读取数据。 read 函数的第一个参数是文件描述符,这里使用 0 表示标准输入。第二个参数是指向存储数据的缓冲区的指针,这里是 &buf 。第三个参数是要读取的最大字节数,这里是 0x100u ,即 256 字节,但这char定义buf是132字节,所以存在栈溢出漏洞。
并无找到后门/bin/sh,通过ret2libc,那从栈溢出下手:
|
|
无备注代码:
|
|
需要注意的点:
|
|
这里的地址是可以手写的,但我不建议,AI给的是确切地址,这里我给改了,下次AI写脚本,地址一定不要手动硬写出来,帮AI改一下改成自动获取的,比如上面这样,系统会自动获取地址的,这样比较准确不会出错崩溃。
说实话,给我肯定写不到的,不熟悉编写python远程交互脚本,但我可以读懂,我可以用AI帮我写代码,我就把意愿告诉它,把IDA汇编代码以及跟进的函数复制发给它,附上我的思路,不如已经知道是利用栈溢出漏洞编写ret2libc、知道开启了哪些保护、一步步修改得到的。
PWN_031(待定)
题目:开启 ASLR 和 PIE 的情况下,仍可能被利用; PWN_031
checksec一下,无栈保护:
跟进ctfshow函数:
发现同样的代码,有溢出,似乎可用上题的exp,不过checksec看到的保护是不一样的,exp肯定不一样了….
运行之后莫名其妙,会有报随机化的地址:
…..深入分析CTFshow-PWN入门-pwn31的解法与原理 - Claire_cat - 博客园
这题和PWN_025差不多,有关栈溢出的知识点,这里先跳…
FORTIFY缓冲区边界检查
FORTIFY_SOURCE
是 GCC 提供的一种轻量级缓冲区溢出保护机制,通过对危险函数(如 strcpy
、memcpy
等)进行增强检查,降低缓冲区溢出漏洞的利用风险。它有 3 个级别:_FORTIFY_SOURCE=0
(关闭)、_FORTIFY_SOURCE=1
(基础保护)、_FORTIFY_SOURCE=2
(强化保护),各级别区别如下:
级别 | 启用条件 | 核心检查逻辑 | 防护范围 | 性能影响 |
---|---|---|---|---|
0 | 默认(无需定义宏) | 无任何检查,使用原始函数 | 无 | 无 |
1 | -D_FORTIFY_SOURCE=1 -O1 | 检查 “源长度 < 目标缓冲区长度”(仅限编译期可知) | 部分静态场景 | 极小 |
2 | -D_FORTIFY_SOURCE=2 -O2 | 检查 “操作长度 ≤ 目标缓冲区实际大小”(含运行时) | 静态 + 动态场景,覆盖更多函数 | 轻微 |
FORTIFY_SOURCE
仅能防护部分已知的危险函数,无法替代NX
、Stack Canary
等更全面的保护机制。- 必须配合优化选项(
-O1
/-O2
)才能生效,否则编译器会忽略该宏定义。 - 对于动态分配的内存(如
malloc
分配的缓冲区),FORTIFY_SOURCE
无法获取其大小,因此无法防护相关操作(如memcpy(malloc_buf, src, n)
)。
PWN_032
题目:FORTIFY_SOURCE=0:
禁用 Fortify 功能。 不会进行任何额外的安全检查。 可能导致潜在的安全漏洞。
这题主要是在函数逻辑运行,知道运行的原理就好。
IDA分析main:
跟进undefined函数:
可以看到,关键在于成功进入到undefined函数才有机会下一步获得flag:
从main函数出发:
|
|
大致是:起初先打印ctfshow的logo信息(其实一开始那个v3把我搞懵了,其实对解题过程没啥帮助的),在最顶端的__fastcall main已经把一些参数给定义了比如argv数组,第一个参数argv[1]被赋给v4,v4作中介把数值传递给了buf1,之后程序将v4[10]数组内容赋值给buf1[10],接着通过strcpy函数,把字符串“CTFshowPWN”复制/赋值给buf2,printf打印buf1、buf2的当前的字符串内容,最后fget读取_bss_start的11个字符给buf1,打印buf1和num的信息,最后通过if判断argc是否大于4(默认情况下argc是等于1的,因为argv[0]必定存在,我们输入一个参数,argc就会等于2,依次递推),大于4就进入undefined函数内
因为本题目FORTIFY_SOURCE没有开启,代表我们启动函数直接输入4个参数(这时argc=5 > 4,为什么五个?因为你输入的4个+argc[0] = 5)就行了,而且这4个参数没有长度限制,如果开启FORTIFY_SOURCE就不好说了,因为开启了之后,由于程序存在strcpy和memcpy函数会检测长度,如果长度超过了限制,可能会使程序抛出异常而退出执行。
PWN_033
题目:FORTIFY_SOURCE=1:
启用 Fortify 功能的基本级别。 在编译时进行一些安全检查,如缓冲区边界检查、格式化字符串检查等。 在运行时进行某些检查,如检测函数返回值和大小的一致性。 如果检测到潜在的安全问题,会触发运行时错误,并终止程序执行。 PWN_033
IDAx64分析:
FORTIFY_SOURCE被打开了,代码和PWN_032几乎一样的,memcpy和strcpy这两个函数被替换成了__mencpy_chk和__strcpy__chk安全函数,可以看到这两个函数相比前两个函数只是加上了11LL这个参数加以限制,因为buf1和buf2在声明的时候的长度就是11,所以程序为了防止溢出,使用后两个函数加上这两个数组的长度加以限制以防溢出。
但这不妨碍我们拿flag啊,只要运行./pwnme时输入的字符总长度不超过11就好:
|
|
PWN_034
题目:FORTIFY_SOURCE=2:
启用 Fortify 功能的高级级别。 包括基本级别的安全检查,并添加了更多的检查。 在编译时进行更严格的检查,如更精确的缓冲区边界检查。 提供更丰富的编译器警告和错误信息。
IDA分析,题目描述也说了该程序包括基本级别的安全检查,并添加了更多的检查。 在编译时进行更严格的检查,如更精确的缓冲区边界检查。 提供更丰富的编译器警告和错误信息。 使用ida64反编译的加过略微与前两道题目有所不同,大部分还是一样的,还是把危险函数替换成了安全函数。如下:
大致能看出原函数是什么吧,这里就不多解释了…
# 格式化函数与解题操作说明 __printf__chk
函数与 printf
的区别在于:不能使用 %x$n
不连续地打印,也就是说如果要使用 %3$n
,则必须同时使用 %1$n
和 %2$n
。在使用 %n
的时候会做一些检查。 这涉及到格式化字符串漏洞,但本题不涉及此漏洞,所以对本道题几乎没有阻碍。后续我们还是通过 SSH 连接后运行文件,输入 4 个长度为 1 的参数,不出意外就能拿到 flag!
栈溢出
PWN_035
题目:正式开始栈溢出了,先来一个最最最最简单的吧
用户名为 ctfshow 密码 为 123456 请使用 ssh软件连接
|
|
**不是nc连接 **
checksec一下:
没开canary保护,对应了本章主题——栈溢出。
IDA分析:
跟进函数:
由此可知:ctfshow函数存在栈溢出漏洞,超过104字节就会发生栈溢出;
对于SIGSEGV
函数,当栈溢出触发 SIGSEGV 时,sigsegv_handler 函数会被调用,然后触发函数内部——fprintf打印出flag字符串,也正好对应了fget函数读取的flag值。
栈溢出会破坏栈上的 “返回地址”“栈帧信息” 或其他关键内存结构,最终引发非法内存访问(比如函数返回时跳转到无效地址),触发
SIGSEGV
信号。
所以说我们只要在运行的时候给程序打入超过104个的字符串就好,可以是105个“a”。
/bin/sh地址的跳转利用
相较于32位程序,64 位程序调用函数需满足 栈对齐,因此构造 payload 时需注意:
在实际解题中,ret
地址需要针对当前题目动态获取,用 ROPgadget
工具直接搜索程序中的 ret
指令(最常用):
|
|
PWN_036
题目:存在后门函数,如何利用? PWN_036
非常不安全!!!!没有canary保护。可以用ret2shellcode、ret2libc。
这题开始怪怪的,找不到/bin/sh的后门,sigsegv_handler 函数没有我们想要的flag,不过在左边函数目录看到熟悉的故人——get_flag函数,直接跟进看看:
可以看到下面的fgets读取和返回的printf(s)打印函数,输出flag字符串,主函数没有引用它啊,怎么办?我们只需要利用栈溢出漏洞覆盖返回地址,将程序的执行流程转向 get_flag 函数,从而获取flag。(可以用ret2shellcode、ret2libc)
main函数的突破点在这:
没开canary保护,s数组可存储大小范围是36,超过了就覆盖返回地址了。
gets 读取函数是非常不安全的,容易导致缓冲区溢出漏洞。因为它无法限制输入的长度,可能会超出s 数组的容量,导致覆盖栈上的其他数据或执行任意代码。这也是明显的栈溢出漏洞,s距ebp仅有0x28,而gets不限制输入长度。
安全性来看一般都会用fgets函数,搭配FORTIFY 缓冲区边界检查来提高程序自身安全性。
exp:
|
|
这里的44个‘a’是0x28 + 4 ,0x28(十进制 40)是缓冲区到 ebp(栈基址寄存器)的距离,+4 是覆盖 ebp 本身(32 位程序中寄存器占 4 字节),总共 44 字节后,后续数据才能覆盖函数的 “返回地址”,从而跳转到 get_flag 函数。当然你可以用cyclic(0x28 + 4),产生44字节的垃圾值。
PWN_037
题目:32位的 system(“/bin/sh”) 后门函数给你。 PWN_037
checksec发现是有无栈溢出保护的,关了canary。
IDA分析:
可以发现main函数没有什么可以利用的,但是在函数目录可以看到有个backdoor的函数有猫腻….
这个函数居然有/bin/sh!!!!!
跟进ctfshow函数:
有read读取函数,那我们可以以此为注入点,用垃圾值0x12+4先覆盖函数变量和寄存器ebp本身,然后再上传一个后门函数的地址。
exp:
|
|
脚本exp万岁。
PWN_038
题目:64位的 system(“/bin/sh”) 后门函数给你。
checksec:
没开canary保护,开NX保护(不能ret2shellcode)
buf数组距离ebp寄存器为0xAh,加上本身寄存器ebp的8字节。
跟进backdoor函数看到/bin/sh,本题思路和上题PWN_037一样,只不过架构变成64位而已。
64 位程序调用
system
需考虑堆栈平衡,本质是 64 位 ABI 对 “栈对齐” 的硬性要求—— 若不满足,函数执行时访问栈内存会崩溃;而 32 位 ABI 无此要求,仅需调用后清理参数栈即可,因此无需额外关注平衡。
所以说exp需要考虑到堆栈平衡加上ret返回地址:
在实际解题中,ret
地址需要针对当前题目动态获取,常用方法:
工具查找:用 ROPgadget
工具直接搜索程序中的 ret
指令(最常用):
|
|
exp:
|
|
PWN_039
题目:32位的 system(); “/bin/sh”。 PWN_039
checksec一下:
可以看到没开启canary、PIE保护的,存在可利用栈溢出漏洞的可能。
IDA:
跟进ctfshow函数发现栈溢出:
可以读取50个字符给buf,但buf容量只有14,所以超过14就会造成栈溢出。
接着在函数目录看到hint函数有猫腻:
那么大致的exp思路就来了:也就是先cyclic函数生成垃圾值覆盖局部变量和ebp寄存器本身;但是只有system函数和/bin/sh,却没有system(“/bin/sh”);的后门,所以我们只能构造出来。
在编写exp时,因为程序是32位,所以说不用去考虑它的堆栈平衡:
|
|
这里的p32(0)等于4字节,可以用四个a或者cyclic(4)代替也行,这时32位下system函数的返回地址占位符,然后就到执行/bin/sh,那就是跳转到它的地址:
这样子我们就构造出system("/bin/sh");了,然后进入交互模式获得flag。
这题主要考察我们对于函数的构造以及函数地址的拼接、栈的结构组成。需要多看看我的PWN-2啊。。。。
PWN_040
题目:64位的 system(); “/bin/sh”。
IDA64位分析,跟进ctfshow函数:
checksec看到未打开canary保护,而且在ctfshow函数这发现有栈溢出。
跟进hint函数,发现后门。
用ROPgadget命令查询ret地址:
|
|
查询/bin/sh的地址:
exp:
|
|
欸为什么exp的payload中的参数不太一样?
32位和64位可执行程序的参数传递顺序
即使思路一样但架构不一样,在写payload的时候注入地址的顺序也是不一样的:
- 32 位程序(PWN039):32 位程序中,函数参数通过栈传递。在 PWN039 里,
system
函数地址紧跟在填充数据之后,这是因为溢出后直接覆盖返回地址为system
函数地址,让程序执行system
函数。在payload = 'a'*(0x12 + 4) + p32(system) + p32(0) + p32(bin_sh)
中,'a'*(0x12 + 4)
用于填充缓冲区并覆盖原返回地址,p32(system)
将返回地址修改为system
函数地址 ,p32(0)
是为了满足栈结构,作为system
函数执行后的返回地址(实际执行中并不重要,可随意填充 4 字节),p32(bin_sh)
则是system
函数的参数,即/bin/sh
字符串的地址。这样的顺序符合 32 位程序通过栈传参和控制程序执行流的特点。- 64 位程序(PWN40):64 位程序的函数调用规则有所不同,前 6 个参数优先通过寄存器传递,其中第一个参数存放在
rdi
寄存器。在 PWN40 中,payload = 'a'*(0xA + 8) + p64(pop_rdi) + p64(bin_sh) + p64(ret) + p64(system)
,'a'*(0xA + 8)
用于填充缓冲区和覆盖原返回地址;p64(pop_rdi)
将pop_rdi
指令地址放入栈中,执行pop_rdi
指令时,会把紧跟其后的p64(bin_sh)
中的/bin/sh
地址弹出到rdi
寄存器,作为system
函数的第一个参数;p64(ret)
用于栈对齐(64 位程序调用函数前需要保证栈 16 字节对齐,ret
指令可调整栈指针);最后p64(system)
将system
函数地址放入栈中,当程序执行到这里时,会跳转到system
函数执行,此时rdi
寄存器中已正确设置了参数/bin/sh
的地址。
PWN_041
题目:
32位的 system(); 但是没"/bin/sh" ,好像有其他的可以替代。 PWN_041
checksec一下,然后IDA分析
不能用ret2shellcode了。
这里和上题差不多,找一下/bin/sh:
找不到,但找到sh:
/bin/sh和sh的核心区别
关键场景:能否替代取决于「调用函数的要求」
PWN 中最常用的是通过
execve、system
函数执行 Shell(如构造execve("/bin/sh", NULL, NULL)
获得交互权限),此外也可能涉及system
函数,两者对参数的要求不同:场景 1:
execve
函数(必须用绝对路径,sh
不可替代/bin/sh
)
execve
是 Linux 系统调用,作用是加载并执行新程序,其第一个参数必须是程序的绝对路径(或相对路径,但绝对路径是 PWN 中的常规选择),否则会报错「No such file or directory」。
- 正确用法:
execve("/bin/sh", ...)
(系统直接定位到/bin/sh
,执行成功)。- 错误用法:
execve("sh", ...)
(系统不知道sh
在哪里,会遍历$PATH
查找,但execve
不支持通过$PATH
解析命令名,直接失败)。因此,在构造
execve
调用时,只能用/bin/sh
,不能用sh
。场景 2:
system
函数(sh
和/bin/sh
效果一致,可替代)
system
函数的底层逻辑是「调用/bin/sh -c
来执行参数中的命令」,其参数支持「命令名」或「绝对路径」:
- 若传
system("sh")
:system
会自动通过$PATH
找到sh
(即/bin/sh
),等价于执行/bin/sh -c sh
,最终启动 Shell。- 若传
system("/bin/sh")
:等价于执行/bin/sh -c /bin/sh
,同样启动 Shell。因此,在使用
system
函数时,sh
和/bin/sh
效果完全一致,可以互相替代。特殊情况:环境变量
$PATH
被篡改(sh
可能失效,/bin/sh
更可靠)虽然多数情况下
$PATH
包含/bin
,但如果目标程序运行时篡改了$PATH
(如删除/bin
目录),此时:
- 用
sh
会因$PATH
中找不到sh
而失败;- 用
/bin/sh
因是绝对路径,不受$PATH
影响,依然能成功执行。PWN 利用中为了「兼容性和稳定性」,即使是
system
函数,也更推荐写/bin/sh
而非sh
(避免因环境变量异常导致利用失败)。
这里跟进hint函数:
发现这有现成的system函数,也就是说可以调用sh去进行提权。
知道了sh地址是0x80487BA,就可以写exp了:
|
|
PWN_042
题目:64位的 system(); 但是没"/bin/sh" ,好像有其他的可以替代。 PWN_042
来看IDA64位分析:
checksec:
在跟进hint函数也是如此,和PWN_041一样的,这次程序是64位,在exp编写上有所不同,需要pop_rdi:ret、ret的地址:
ret:0x40053e
pop_rdi:0x400843
再找sh的地址吧:
exp:
|
|
flag就不给了,因为靶机给的flag是动态的。
PWN_043
题目:32位的 system(); 但是好像没"/bin/sh" 上面的办法不行了,想想办法。 PWN_043
IDA32位跟进ctfshow函数:
gets函数很危险的,一般都用fgets,用这个必溢出的好吧……
跟进hint函数:
我们发现了system函数,但却不提供sh、/bin/sh给我们,只能利用gets函数。(gets函数进行读入数据,它可以无限读取到内存,不会判断上限,可以包含空格,以回车结束读取。所以这里就存在了明显的溢出)
那大致的exp思路来了;先用cyclic产生垃圾值填充缓冲区和ebp本身,出现栈溢出后,将返回值覆盖为gets函数地址,加上ebx寄存器弹栈,gets函数读取数据,把我们所给的/bin/sh字符串写入这个内存里面,再次利用栈溢出,将返回地址覆盖为 system 函数的地址,使得 gets 执行完后,程序自动跳转到 system 执行,执行 system("/bin/sh")
获取 shell。
这个内存去哪找?
在bss字段(未初始化字段)找到了buf2变量:
查询pop_ebx的地址
exp:
|
|
这里有个疑问点就是为什么要用buf2内存变量呢?我直接调用system函数后再注入/bin/sh不就好了吗?这显然不行,因为此时的/bin/sh是一个普通的字符串,并没有分配地址,可执行程序的PIE和ASLR保护机制会让布局发生随机化改变,你给的字符串没地址,而system函数需要接受地址才能执行,而非直接注入无地址的纯字符串,buf2是bss字段的全局变量,地址固定,能给/bin/sh一个固定地址,让system精准找到它并执行。
PWN_044
题目:64位的 system(); 但是好像没"/bin/sh" 上面的办法不行了,想想办法。 PWN_044
IDA分析:
和上一题区别是参数传递,64位的前6参数是靠寄存器rdi传递的。查询pop rdi;ret
汇编命令的地址:
pop rdi ; ret:0x4007f3
ret:0x4004fe
那exp大致不变,主要是payload吧:
|
|
可以发现这是错误的,进入不了交互,错就错在ret,这里不需要进行手动调栈堆平衡:
gets
执行完后的总偏移是 44 字节(36+8)。后续执行
pop_rdi
(8 字节)+buf2
地址(8 字节)后,总偏移变为 44+8+8=60 字节。60 字节 ÷ 16 字节 = 3 余 12?不对 —— 此时调用
system
前,rsp
指向system
函数的地址,而60 + 8
(system
地址本身)= 68 字节,68 ÷ 16 = 4 余 4?这显然不对。正确理解:不用纠结静态数字,而是要看到
gets
的执行引入了一次 “额外的栈弹出”(8 字节),恰好抵消了部分偏移的 “非对齐量”,使得最终调用system
时,rsp
刚好落在 16 字节对齐的地址上 —— 这是动态执行中栈自然调整的结果,而非静态计算的总和。
在 64 位 PWN 题中,“动态对齐” 是否需要加
ret
,核心看 “中间是否有函数执行并自动调整栈指针”。
正确的exp:
|
|
PWN_045
题目:32位 无 system 无 “/bin/sh”。 PWN_045
checksec一下:
32位关闭栈保护关闭PIE。
IDA分析main:
跟进ctfshow函数:
还是明显的溢出漏洞了,只是其中的偏移变了,大差不差,现在我们关心的是如何去找到我们所需要的东西呢?现在既没有system,也没有“/bin/sh”
思路:地址随机化+NX保护禁用ret2shellcode,那就是暗示我用ret2libc咯,用LibcSearcher获取当前libc版本,利用write泄露出write本身函数地址,再获取程序中因开启NX保护时地址随机化的write虚拟地址,两个地址相减得到地址偏移量,接着就能反推出system、/bin/sh的真实地址了。在此之前要记得先填充好变量的缓冲区。
上半部分exp:
|
|
这时候已经把write函数地址泄露出来了,不过看到这里比较懵逼哈,payload为什么要上传这么多,直接调用write函数不行了吗?还要got、plt啥的(堆plt、got不熟悉的可以看回PWN-1中plt和got表的讲解)。让我给你细分一下:先调用write_plt,这指write函数的执行入口,告诉write我要使用你了,调用write后,会从栈上读取这个地址作为下一条指令的位置。这里指定 main 函数,是为了让程序在泄露地址后回到主函数,继续接收我们的第二次payload(用于后续调用 system 获取 shell),避免程序直接退出。第一次payload是获得write地址,第二次payload是执行system函数。
接着到p32(1) + p32(write_got) + p32(4):write 函数的三个参数:
32 位程序中参数通过栈从右往左传递,因此栈上顺序为:fd
→ buf
→ n
,也就是 write(1, write_got, 4)
来下半部分exp:
|
|
write = u32(io.recvuntil(’\xf7’)[-4:])这一段比较长,做过之前题目对此有点印象,不应该是write = u32(io.recv(4))?
核心原因是程序输出中存在干扰信息,导致无法直接读取到纯净的 4 字节地址,你执行一下程序就知道了,会有“O.o?”冒出来,这是干扰。
io.recv(4)
是有局限性的,io.recv(4)
的作用是从程序输出中读取固定的 4 字节数据,但它有一个严格前提:程序必须只输出我们需要的 4 字节地址,没有任何多余内容(如提示文本、换行符、其他数据等)。此时recv直接读取4个字节,那就会读到O.o?的字节导致错误。
recvuntil('\xf7')[-4:]
:适用于程序输出存在干扰信息,且地址以 \xf7
开头(32 位 libc 典型特征)的场景,通过特征定位 + 截取来提取正确地址。
|
|
PWN_046(待定)
题目:64位 无 system 无 “/bin/sh”。 PWN_046
IDA64分析:
和上题思路差不多,改下参数就好,还有这个O.o?干扰你……
题目有点怪我先跳一跳,
待定
PWN_047
题目:ez ret2libc PWN_047
就提示用ret2libc
checksec一下:
IDA32位分析:
运行程序:
一上来就给了几个函数的地址,目前来看还没给出system的地址。
跟进ctfshow函数还发现了gets
函数:
那就是通过栈溢出ret2libc来搞flag咯。
在data字段还找到了/bin/sh:
你有无发现:这个/bin/sh的地址就是gift
那思路就是用puts进行泄露地址,得出偏移量再反推出system函数的真实地址
|
|
PWN_048
题目:没有write了,试试用puts吧,更简单了呢。 PWN_048
IDA32分析:
跟进ctfshow函数:
编写exp泄露puts的地址,得出偏移量区反推出system函数、/bin/sh的地址。
exp:
|
|
PWN_049
题目:静态编译?或许你可以找找mprotect函数。 PWN_049
checksec一下,没开canary、PIE、NX保护。
IDA32:
可以看到有非常多的函数,这就是静态编译。
跟进ctfshow函数,看到read有栈溢出漏洞。
找mprotect函数:
Mprotect函数
|
|
代码解析:
- 函数参数:
a1
:要修改权限的内存起始地址(需页对齐)。a2
:内存区域的长度(字节数)。a3
:新的保护权限(如PROT_READ
、PROT_WRITE
、PROT_EXEC
、PROT_NONE
、prot=7
)。prot可以取这几个值,并且可以用“|”将几个属性合起来使用。
- 核心逻辑:
- 调用
sys_mprotect
:这是真正的内核级系统调用(用户态到内核态的桥梁),负责实际执行内存权限修改操作。- 错误处理:
sys_mprotect
的返回值若>= 0xFFFFF001
(这是 Unix/Linux 中系统调用错误码的典型范围,通常表示操作失败),则通过_syscall_error()
处理错误(例如设置errno
并返回错误码)。- 成功返回:若系统调用成功,直接返回
sys_mprotect
的结果(通常为 0)。
这段代码是用户态 mprotect
函数的封装,本质是通过调用内核的 sys_mprotect
系统调用来实现功能,最终目的是:
允许进程动态修改某段内存的读写执行权限(例如将只读内存改为可写,或给数据段添加执行权限),常用于内存管理、动态代码加载、漏洞利用等场景。
简单说,这段代码是用户程序与内核交互的 “中间层”,让开发者可以通过简单的函数调用修改内存权限,而无需直接操作内核接口。
那么我们可以利用它修改内存的权限为可读可写可执 行(RWX),然后我们就可以往栈上写入shellcode、执行,最后获取shell。
在构造 payload 前,需先确定 3 个核心要素:目标内存地址、mprotect
参数、控制寄存器的 Gadget,这些是 payload 的 “积木”。
我们要选择的内存地址是0x80DA000
,等会?这不是bss段吧?
- 选择原因:
mprotect
要求操作的内存地址必须 页对齐(即地址是 4KB/0x1000 的整数倍,32 位系统默认页大小为 4KB)(64位就是8kb咯)。 - 程序 BSS 段起始地址(如
0x80DB320
)不满足页对齐,而0x80DA000
是内存页的起始地址(末尾三位为 000,符合 4KB 对齐),且属于程序可访问的内存区域。
然后就是mprotect函数参数
|
|
a1是start起始地址,也就是0x80DA000;a2是长度范围,这里选择0x1000,足够我们容纳shellcode大小了;a3是prot,也就是修改成的权限范围,这里直接改为RWX,也就是简写0x7
。
最后是控制函数参数的工具——关键gadgets
32位程序用栈传参的,这里的mprotect函数有三个参数,且调用后需继续控制流程,因此需要 pop ebx ; pop esi ; pop ebp ; ret
这样的 Gadget,用ROPgadget指令查询地址:
pop ebx ; pop esi ; pop ebp ; ret的地址是0x080a019b
exp:
|
|
对于payload:
|
|
先填充缓冲区,调用mprotect函数,接着输入mprotect函数的3个参数,在此之前就要用到gadget:pop_ebx_esi_ebp_ret进行传参,按照mprotect函数的三个参数对应意义进行注入start、size、proc,这时已经给bss内存片段开放了可执行权限,然后读取read函数地址进而调用read函数,再用gadget:pop_ebx_esi_ebp_ret进行传read的3个参数,读取 后面我们所上传的shellcode 到目标内存来执行。最后上传shellcode。
其实你可以发现这也是ret2shellcode的其中一种,只不过多加了mprotect函数控制流程罢了。
整个流程的核心逻辑就是通过栈溢出构建 “函数调用链”,逐步完成 “开权限→写代码→执行代码” 的攻击流程。
对于这道题而言对我感触比较深,加深我对寄存器、gadget、整个函数调用流程、参数传递过程的理解!如果你觉得很难而跳过,那你就错过了一次PWN入门者的提升机会,博主当时啃这题花了快两天时间,凌晨才写完这题的writeup呢,花点耐心去学,PWNer!
PWN_050
题目:好像哪里不一样了;远程libc环境 Ubuntu 18。 PWN_050
当然,你可以继续使用ret2libc来完成这题,甚至会更简单,而我的目的是为了让大家进一步学mprotect函数。
这里叫我们启动Ubuntu 18,就ctfshow配给我们的,那我们就听劝用它的虚拟机。虽说比较老……
跟进ctfshow、main函数:
明显看到危险的gets函数
,ctfshow函数这有栈溢出漏洞:
主要流程如下(4步payload):
- 泄漏内存地址,通过计算得到libc地址
- 通过mprotect函数来修改一段区域的权限,位rwx
- 向这段区域写入shellcode
- 跳转到写入shellcode的区域,并执行
我们要利用mprotect函数改变内存执行权限,那我们需要找地址,正好这次和上次一样不是页对齐:
bss_start_address = 0x602000
64位程序靠寄存器传参,用ROPgadget命令查询程序本身的gadget:
pop_rdi_ret = 0x00000000004007e3
用ROPgadget命令查libc库的通用gadget:
|
|
pop_rdx_ret1 = 0x0000000000001b96
pop_rsi_ret1 = 0x0000000000023a6a
前面加个1是因为,后续还有一个类似的gadget变量,好区分一些。
为什么第一个gadget可以从程序本身去查到,而第二三个gadget地址却要在libc库获取?
从这可以看到程序本身是不带第二、三个的gadget的,因为我们要利用漏洞去获取flag,而本身程序运行是不用后面俩的gadget的。
exp;
|
|
让我给你逐步解释:
|
|
这里前面有个1是方便区分,这两个gadget是在libc库上的通用gadget,受ASLR保护,地址是动态变化的,需要用所有函数的真实地址 = libc 基地址 + 函数的本地偏移
得出地址:
|
|
泄露puts在本地程序的地址,再泄露puts再libc库的地址推出偏移量,已知本地程序的每个函数地址偏移量相同,知道偏移量后,泄露libc标准库的gadget(/lib/x86_64-linux-gnu/libc.so.6
)就能反推出本地程序gadget的真实地址:
|
|
这里有个问题:为什么gets = libc.sym[‘gets’]就是真实地址呢,你会认为它就是libc库的通用地址,是不是应该加上本地偏移量呢?
libc.sym['gets']
最终是真实地址的原因是:
- 它原本是 “偏移量”(相对于 libc 基地址);
- 当你设置
libc.address = libc_base
后,pwntools
自动将 “基地址 + 偏移量” 合并,得到真实地址。
libc.address
不是一个函数,而是pwntools
中ELF
类的一个属性(attribute),用于存储和设置 ELF 文件(这里特指 libc 库)在内存中的基地址。当你通过
libc = ELF('libc.so.6')
加载 libc 库后,libc.address
默认值为0
(表示尚未设置基地址)。
总的来说这题质量不错,耗了我两天空闲时间去攻克。
PWN_051
题目:I‘m IronMan; PWN_051
IDA抽象🤦😄…
跟进main函数,这次函数名字比较难记,可以改个名字:
跟进ctfshow函数:
很明显这是用C++写的,这里我看的不太懂,借用AI解读…
在往s中read的时候大小没有问题,但是程序在下面将字符"I"替换成 了"IronMan",最后在strcpy的时候发生了溢出。只需要简单计算一下,那么在输入16个“I” 就会被替换为 16 * 7 = 112个字符,而s距ebp的距离为 0x6c = 108。
正常溢出覆盖到返回地址就是 cyclic(0x6c+4) = cyclic(112) ,输入16个I刚好替换成 16个"IronMan",刚好是112个,也就刚好覆盖到返回地址,但用其它字符比如a不可以,因为会再调用 std::string::operator+=
将输入的 "a"*112
拼接到这个固定前缀之后。此时,字符串总长度已变为 “固定前缀长度 + 112 字节”,必然超过 112 字节。
让我发现了cat /ctfshow_flag,可以利用栈溢出来执行这个catflag的命令:
找它地址:
地址:804902E
exp:
|
|
PWN_052
题目:迎面走来的flag让我如此蠢蠢欲动; PWN_052
IDA32位分析,跟进ctfshow函数:
让我看到了gets函数。
跟进flag函数:
这段 C 语言代码定义了一个名为flag的函数,主要功能是读取并输出特定文件中的内容,但有条件限制。我们来分析一下: 函数首先尝试打开/ctfshow_flag文件用于读取, 如果文件打开失败,会输出错误信息并退出程序; 如果文件打开成功,会读取文件中的内容到s数组。(最多 64 个字符)
函数的返回逻辑: 当参数a1等于 0x36C 且a2等于 0x36D 时,使用printf(s)输出文件内容并返回;否则,直接返回fgets的结果。(即读取到的字符串)
也就是说我们利用栈溢出漏洞覆盖返回地址,返回到这个flag函数并执行就能获得flag。
exp:
|
|
类canary爆破
PWN_053
题目:再多一眼看一眼就会爆炸; PWN_053
checksec看信息….没开canary、PIE,有NX
跟进flag函数:
函数的执行流程如下: 定义一个 64 字节的字符数组s和一个文件指针stream。尝试打开路径为/ctfshow_flag的文件用于读取。
如果文件打开失败(stream为 NULL),则输出错误信息并退出程序;如果文件打开成功,使用fgets从文件中读取最多 64 个字符到数组s中,通过puts(s)输出读取到的内容(会自动在末尾添加换行符),调用fflush(stdout)刷新标准输出缓冲区,并将其返回值作为函数的返回值。
这段代码的逻辑比上一个版本更直接,不需要特定参数即可输出 flag 内容,只要成功打开文件就会直接打印文件中的内容。fflush(stdout)的作用是确保内容立即输出到终端,而不会停留在缓冲区中。也就是说只要执行到flag函数就直接给flag字符串。
在函数目录上看到了canary函数,这还是第一次见到手动放置的canary:
这段代码的功能是从标准输入读取用户输入的字节数,并将相应字节数的数据读取到缓冲区 buf 中。然后,它会检查堆栈的完整性,如果堆栈被破坏,则输出错误信息并终止程序。
跟进ctfshow函数:
解释下这段函数,也就是说开局我给程序输入一个1000,它就在后续给我设置1000容量大小的缓冲区:
|
|
那输入1000,程序在后续就让我能输入1000字节的东西。
看main函数:
可以看到先经过canary的栈溢出检查,如果发现栈溢出立刻终止程序进行,那就得绕过这个canary保护了。
手动放置的canary函数中的canary.txt的值是不变的,况且是4字节大小的值,也就是说我们编一个循环四次,从0~255范围找字节的双重循环就可以爆破出canary值,从而绕过memcmp的检查,实现栈溢出返回flag函数地址去执行flag函数获得flag字符串。
python2 exp:
|
|
这里定义了一个PWN1,起初为空值,只要每爆破出一位正确字节的canary值,就会通过PWN1 += chr(k)
被储存进PWN1内,chr(k) 将这个整数转换为对应的字符(字节),用于拼接测试 payload,爆破 canary 的每一个字节。
发送 payload 的方式(
sendline
vssend
)是关键差异之前的题目好像对发送payload的函数并不严格要求,但在这题就必须要求严格了,要用send函数,如果是sendline的话,会在payload后面多加一个换行符\n,因附加换行符破坏栈布局。也会导致一直循环爆破却最终爆破失败。
PWN_054
题目:再近一点靠近点快被融化; PWN_054
IDA32:
跟进flag函数:
分析main函数:
只要密码正确,输出欢迎信息 “Welcome! Here’s what you want:",然后main就会调用flag函数直接获取flag字符串;但密码不正确就会输出”You has been banned!“的提示。
我们查看重要参数在堆栈中的位置:
从v5和s的位置来看,它俩所处的栈位置距离比较近欸!0x160 - 0x60 = 0x100。
这里的v5数组的容量刚好0x100可以填满,这不是巧合,突破点就在这的。当你用0x100填满v5后,正好把puts的终止符**\x00
**给破坏了,这就会导致puts继续执行,直接把s一起输出了。
怎么判断终止符 \x00 存储在数组内部?
看 “数组容量” 与 “字符串最大长度” 的关系:若数组容量 > 字符串最大长度,终止符必在数组内部:
v5
数组容量为256
字节(0x100
),而读取用户名的fgets(v5, 256, stdin)
最多读取255
字节(留1
字节自动补\x00
)判断
\x00
是否存储在数组内部,可按优先级依次验证:
- 优先看 数组容量与字符串最大长度的关系(最直接,如
数组容量 > 最大读取长度
);- 再看 字符串的生成 / 处理函数(如
fgets
自动补\x00
、strchr
替换生成\x00
);- 最后看 后续字符串操作是否正常(反向验证
\x00
位置合法)。 三者均指向\x00
在数组内部时,即可确定该结论。
exp1,得出密码:
|
|
整串复制,不要只复制h3r3!!
PWN_055
题目:你是我的谁,我的我是你的谁; PWN_055
IDA,跟进main函数:
跟进flag函数:
要想flag字符串被打印出来,就要满足第一个条件:flag1 && flag2 && a1 == 0xBDBDBDBD
flag1、flag2须为真值,而a1要等于0xBDBDBDBD就行。
分别跟进flag_func1、flag_func2函数:
大致思路就是:先调用flag_func1函数,使flag1等于1,再调用flag_func2函数,满足函数条件将flag2赋值为1,最后再调用flag函数,给参数,最终打印flag字符串,exp:
|
|
1
payload = cyclic(0x2c+4) + p32(flag_func1) + p32(flag_func2) + p32(flag) + p32(0xACACACAC) + p32(0xBDBDBDBD)
这里你想必把有点懵,为什么要先flag再到参数,不是应该先执行完函数、传完参再到另外一个函数吗?感觉很乱啊….
32 位程序调用函数时,遵循两个关键规则(这是理解 payload 的前提):
- 参数压栈顺序:函数的参数要按 “从右到左” 的顺序,依次放到栈上;
- 返回地址跟着参数:参数压完后,要压入 “函数执行完后要跳转到的地址”(即下一个函数的地址),最后才是 “当前要调用的函数地址”。
现在这些题目需要我们对代码具有一定阅读理解能力,理解代码的运作原理,而不再像新手入门那样无脑套ret2shellcode、ret2libc、脚本了。
认识32位和64位的shellcode
PWN_056
题目:先了解一下简单的32位shellcode吧。 PWN_056
IDA32分析:
这里就是送分的了,把/bin/sh赋值给v1,然后调用execve函数(类似system函数)去执行,这不就是直接终端交互吗?
exp:
|
|
现在来看汇编语言:
|
|
这段代码是x86汇编语言的代码,用于在Linux系统上执行一个系统调用来执行execve("/bin/sh", NULL, NULL)
。让我们逐行解析代码的功能:
|
|
这行代码将十六进制值 0x68 (104的十进制表示)压入栈中。这是为了将后续的字符串 “/bin/sh"的长度(11个字符)放入栈中,以便后续使用。
|
|
这行代码将十六进制值 0x732f2f2f 压入栈中。这是字符串 “/bin/sh” 的前半部分字符的逆序表示,即 “sh//"。这是因为x86架构是小端字节序的,字符串需要以逆序方式存储在内存中。(但其实2f2f可以换成0000,补空位,那就是0x732f0000:sh,不要//都可以的)
|
|
这行代码将十六进制值 0x6e69622f 压入栈中。这是字符串 “/bin/sh” 的后半部分字符的逆序表示,即 “/bin”。
|
|
这行代码将栈顶的地址(即字符串 “/bin/sh” 的起始地址)复制给寄存器 ebx 。 ebx 寄存器将用作execve
系统调用的第一个参数,即要执行的可执行文件的路径。
|
|
这两行代码使用异或操作将 ecx 和 edx 寄存器的值设置为零。 ecx 和 edx 分别将用作 execve系统调用的第二个和第三个参数,即命令行参数和环境变量。在此情况下,我们将它们设置为 NULL,表示没有命令行参数和环境变量。(这不就是手敲shellcode的那段代码吗?在一开始的wp:PWN_024)过这么久了忘得差不多了,在这里回忆一下啊。
|
|
这两行代码将值 11 ( 0xb )压入栈中,然后从栈中弹出到寄存器 eax 。 eax 寄存器将用作系统调用号, 11 表示 execve 系统调用的系统调用号。
|
|
这行代码触发中断 0x80 ,这是Linux系统中用于执行系统调用的中断指令。通过设置适当的寄存器值( eax 、 ebx 、 ecx 、 edx ), int 0x80 指令将执行 execve("/bin/sh”, NULL, NULL) 系统调用,从而启动一个新的 shell 进程。
总结起来,这段汇编代码的功能是利用系统调用在Linux系统上执行 execve("/bin/sh”, NULL,NULL) ,即打开一个新的shell进程。
PWN_057
题目:先了解一下简单的64位shellcode吧。 PWN_057
IDA64:
内联汇编
syscall; LINUX -
的作用
syscall
:是 x86-64 架构下的汇编指令,用于触发 Linux 系统调用(调用操作系统内核提供的功能,如进程退出、文件操作等)。LINUX -
可能是简化或注释残留,实际有效的指令只有syscall
。系统调用的具体功能由rax
寄存器的值决定(例如rax=60
表示exit
系统调用,用于终止当前进程)。
总的来说这俩题是让我们认识shellcode的样子,以便后面使用…
exp同上。
重点还是看汇编:
|
|
这段代码是x86-64汇编语言的代码,用于在Linux系统上执行 execve("/bin/sh", NULL,NULL)
。让我们逐行解析代码的功能:
|
|
这行代码将 rax 寄存器的值(通常用于存放函数返回值)压入栈中。这里的目的是保留 rax 的值,以便后续使用。
|
|
这两行代码使用异或操作将 rdx 和 rsi 寄存器的值设置为零。 rdx 和 rsi 分别将用作 execve系统调用的第三个和第二个参数,即环境变量和命令行参数。在此情况下,我们将它们设置为 NULL,表示没有环境变量和命令行参数。
|
|
这行代码将字符串 ‘/bin//sh’ 的地址赋值给 rbx 寄存器。字符串 ‘/bin//sh’ 是我们要执行的可执行文件的路径。在x86-64汇编中,字符串被当作地址处理。
|
|
这行代码将 rbx
寄存器的值(字符串 ‘/bin//sh’ 的地址)压入栈中。这是为了将可执行文件路径传递给 execve
系统调用的第一个参数。
|
|
这两行代码将栈顶的地址(即字符串 ‘/bin//sh’ 的地址)弹出到 rdi
寄存器。 rdi 寄存器将用作execve 系统调用的第一个参数,即可执行文件路径。
|
|
这行代码将 al 寄存器设置为值 59 , 59 是 execve 系统调用的系统调用号。
|
|
这行代码触发系统调用。通过设置适当的寄存器值( rax
、 rdi
、 rsi
、rdx
), syscall 指令将执行 execve("/bin/sh", NULL, NULL)
系统调用,从而启动一个新的 shell 进程。
总结起来,这段汇编代码的功能是利用系统调用在Linux系统上执行 execve("/bin/sh”, NULL,NULL) ,即打开一个新的shell进程。与前一个示例相比,这段代码是x86-64架构下的汇编代码,使用通用寄存器进行操作。
pwn_058
题目:32位 无限制。 PWN_058
32位仅部分开启RELRO,其他保护全关,并且有可读,可写,可执行段。可以用shellcode。
看不到源码,那就只能看汇编代码:
大致就是:会调用puts函数
看text段:
这里看到调用gets函数了,参数对应的是 [ebp+s] 的地址,也就是在返回地址上一栈内存单元处,接着往下就是调用puts函数了:
紧接着就是ctfshow函数:
从这可看出,gets函数读入存进[ebp+s]这块的内存,到puts、ctfshow主函数都会读取其中内容调用,那我们往这块注入shellcode不就好了吗?
exp:
|
|
当然shellcode这部分你可以选择手敲汇编代码进去,这里用了shellcraft模块。
PWN_059
题目:64位 无限制。 PWN_059
分析流程同上,需要注意的是在生成shellcode的时候需要注明架构为64位:
exp:
|
|
PWN_060
题目:入门难度shellcode。 PWN_060
IDA32分析:
发现gets函数。
使用strncpy函数将对应的字符串复制到 buf2 处。跟进查看:
可以看到buf2在bss段,地址:0x804A080
用gdb调试查看bss是否有执行权限:
|
|
buf2所在的bss内存段确实有rwx权限,那我们直接注入shellcode到buf2运行就可以。
exp:
|
|
你会看到为什么是112?不应该是104吗?100字节+ebp本身的4字节,还有8字节哪来的?记住以下规则:
“多填充 8 字节” 并非随意设置,而是由程序实际栈帧结构决定的—— 可能是编译器的栈对齐策略,也可能是隐式局部变量占用空间,最终通过调试确认 “只有填充至 112 字节才能覆盖返回地址”。这也符合 PWN 题的通用原则:栈溢出的填充长度需以 “实际调试 / 逆向结果” 为准,而非仅依赖理论计算的 “缓冲区大小 + 旧 EBP”。
不过在新版本的Ubuntu虚拟机中就可能会误导你:
灵活掌控shellcode长度
PWN_061
题目:输出了什么? PWN_061
checksec查看保护信息:没栈溢出保护、没开NX,可以尝试注入shellcode:
IDA64:
欸我们分析这个main函数看到v5地址在每次程序运行都会打印出来,但因为是有PIE保护,所以每次地址都会随机化,所以在构造exp时需要接收这个v5地址。
gets函数会读取v5参数然后存入内存段中。
可以看到会打印出v5地址:
exp大致思路是先覆盖缓冲区,然后跳转到v5地址,注入shellcode,但要注意:
这里执行了leave指令,
等价于两条汇编指令的组合:
mov rsp, rbp
:将栈指针rsp
(栈顶指针)指向栈底指针rbp
,此时rsp
从 “v5
起始地址” 跳转到 “rbp
所在地址”,直接跳过v5
占用的栈空间;pop rbp
:将栈中rbp
地址处存储的 “旧 RBP 值” 弹出到rbp
寄存器,同时rsp
自动增加 8 字节(64 位系统栈操作以 8 字节对齐)。
v5[0]、v5[1]、旧rbp,再加上预留给shellcode的返回地址的8字节长度,总共32字节,这32字节不能使用。等放到32之后shellcode的内容才能正被执行。
exp:
|
|
在这里解释下:
|
|
输入流中首先接收数据直到遇到 ‘[’ 字符为止。接下来再次从输入流中接收数据,直到遇到 ‘]’ 字符为止,将其保存在变量 v5 中。最后,将变量 v5 解析为一个十六进制的整数,并将其存储回变量 v5 中。
p64(v5+32),将 “v5 地址 + 32 字节” 这个目标地址,打包为 64 位小端序二进制数据,用于覆盖程序的返回地址,最终让程序跳转到该目标地址执行 shellcode
若 v5 是字符串(如
'7ffefd78ff20'
),Python 无法对字符串进行加法运算,这就是为什么需要v5 = int(v5, 16)
的原因。
PWN_062
题目:短了一点。 PWN_062
checksec一下:
IDA64分析:
和上题差不多,但这里换成read函数了,read(0, buf, 56uLL)
意思是读取56长度的数据到buf中。
在text字段查看read函数附近的汇编指令:
可以看到这里和上题一样调用完read函数后会执行leave汇编指令。
运行程序依旧一样,有PIE保护情况下,buf地址动态变化。
但这里有个问题是,read只读取56位长度字符,56本身减去shellcode地址长度8字节、buf的16字节长度(buf[0]、buf[1]各占8字节长度)以及ebp本身8字节,共32,即56-32=24,那就是说只留给24位长度给到shellcode的内容长度限制,pwntools的shellcraft模块的shellcode长度(29字节)超过了24。因此需要另寻shellcode,这里我推荐一个shellcode,刚好24字节长度https://www.exploit-db.com/shellcodes/43550。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
global _start section .text _start: push 59 pop rax cdq push rdx mov rbx,0x68732f6e69622f2f push rbx push rsp pop rdi push rdx push rdi push rsp pop rsi syscall
1 2 3 4 5 6 7 8 9 10
#include <stdio.h> #include <string.h> char code[] = "\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05"; // char code[] = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"; int main() { printf("len:%d bytes\n", strlen(code)); (*(void(*)()) code)(); return 0; }
exp:
|
|
PWN_063
题目:又短了一点。 PWN_063
checksec,不多说:
IDA64:
这次更狠了,read只读取55长度了…
还是这个网站,找一个23字节的shellcode😄。
exp:
|
|
建议可以把一些长度较短的shellcode保存到桌面,下次遇到断网的线下赛就可以直接套用了。
PWN_064
题目:有时候开启某种保护并不代表这条路不通。 PWN_064
checksec:
IDA32分析:
看到一个新函数mmap,mmap
(memory map)是 Linux 系统调用,主要功能是将文件或设备映射到进程的虚拟内存空间,但在 PWN 漏洞利用中,更常用其 “匿名映射” 功能,参数通过寄存器传递(rdi, rsi, rdx, r10, r8, r9
)
|
|
address
(rdi 寄存器):期望的内存起始地址,通常设为
0
(让系统自动分配)。
length
(rsi 寄存器):分配的内存大小(字节),需根据 shellcode 长度设置(如文档中设为
0x1000
,即 4KB,足够存放 ORW shellcode)。
prot
(rdx 寄存器):内存保护属性,PWN 中常用
PROT_READ | PROT_WRITE | PROT_EXEC
(对应值0x7
),表示内存可读、可写、可执行。
flags
(r10 寄存器):映射类型标志,漏洞利用中常用
MAP_ANONYMOUS | MAP_PRIVATE
(对应值0x22
):
MAP_ANONYMOUS
:匿名映射,不关联文件;MAP_PRIVATE
:私有映射,修改不影响其他进程。
fd
(r8 寄存器):文件描述符,匿名映射时设为
-1
(或0
,不影响)。
offset
(r9 寄存器):文件偏移量,匿名映射时设为
0
。
这行代码使用 mmap 函数分配一块内存区域,将其起始地址保存在变量 buf 中。 mmap 函数通常用于在内存中分配一块连续的地址空间,并指定相应的权限和属性。
这里buf用mmap映射了地址,可读可写可执行,直接传入shellcode,((void (*)(void))buf)()。调用了buf,运行shellcode 即可获取shell。
那我们完全可以用pwntools的shellcraft模块下的shellcode(29字节长度)
exp:
|
|
PWN_065
题目:你是一个好人。 PWN_065
checksec:
居然开了RELRO完全保护。
运行下看看:
可以发现这里要我们输入一个符合要求的字符串才行。
IDA64看不了源码,只能看汇编代码:
在这里我们看到我们输入的字符经read函数读取然后通过jg比较是var_8否大于零,符合要求就跳转到loc_11AC
cdqe
使用eax的最高位拓展rax高32位的所有位 movzx则是按无符号数传送+扩展(16-32) EAX是32位的寄存器,而AX是EAX的低16位,AH是ax的高8位,而AL是ax的低8位大致就是将我们输入的字符串每一位进行比较,如果不在0x60~0x7A
这个范围就跳转剩下几个就是跳转的范围。意思可能就是从第一个字符开始一直做循环判断,如果每一个字符都在一定区间,结束最后一个字符判断时就能够绕过上面的 loc_11B8 来执行 shellcode 了。
1.第一组范围标识:允许 0x60~0x7A
(对应 ASCII ‘`’~‘z’)
对应代码段(loc_11B8
至loc_1236
):
|
|
2.第二组范围标识:允许 0x40~0x5A
(对应 ASCII ‘@’~‘Z’)
对应代码段(loc_11DA
至loc_1236
):
|
|
3.第三组范围标识:允许 0x2F~0x3F
(对应 ASCII ‘/’~’?’)
对应代码段(loc_11FC
至loc_1236
):
|
|
综上:这段汇编通过 cmp al, 60h
、cmp al, 7Ah
、cmp al, 40h
、cmp al, 5Ah
、cmp al, 2Fh
这 5 处cmp
指令,明确标识了输入字符的允许范围(0x2F~0x5A
或 0x60~0x7A
)。其实就是要我们输入的字符都是可打印字符,shellcode 有些是不可打印字符,这个叫string.printable,就是可见字shellcode。这里使用alpha3
就可以生成了:
|
|
刚好pwntools的shellcode是有不可打印的字符:
|
|
jhh///sh/bin\x89�h\x814$ri1�Qj\x04�Q��1�j\x0b̀
将shellcode重定向到一个文件中 切换到alpha3目录中,使用alpha3生成string.printable:
|
|
把输出的shellcode可见字符串粘贴到exp上的变量。
exp:
|
|
PWN_066
题目:简单的shellcode?不对劲,十分得有十二分的不对劲。 PWN_066
checksec、IDA64分析:
看到了熟悉的mmap函数;buf存在溢出点,往buf里写入shellcode,然后程序会执行shellcod但是有一个check函数,跟进查看:
check
函数的作用是:验证输入字符串a1中的每个字符是否都属于unk_400F20
指向的允许字符集。
继续跟进unk_400F20:
我们输入的shellcode的每一位字符要在unk_400F20中,检查的时候*j==0会退出,可以使用\x00来突破程序队shellcode的字符白名单校验
;而常规的shellcraft.sh()
生成的 shellcode 包含大量白名单外字符(如\x3b
、\x0f
等),无法直接通过校验,因此必须绕过。
如何绕过从check函数下手:
看到这里你会发现while里面是一个a1
参数,如果我让a1 == 0,while的循环条件不成立,直接退出循环。
所以exp:
|
|
\x00\xc0
是 **“截断校验 + 合法执行” 的双重工具 **:\x00
负责突破check
函数的逐字节白名单限制,\xc0
负责确保指令合法以避免执行错误,二者组合是该题目绕过字符合法性校验、成功执行 shellcode 的核心关键。
nop sled空操作雪橇
“nop”(No Operation 的缩写):是一条汇编指令,在 32 位 x86 架构下,其机器码为
0x90
。nop 指令执行时,CPU 不进行实际的运算或数据处理操作,仅将程序计数器(如 32 位架构下的 EIP 寄存器)的值增加 1,指向下一条指令,起到占位和使程序流程顺序执行的作用。“sled”(雪橇):形象地描述了攻击原理。通过在 shellcode 前面填充大量的 nop 指令,形成一个 “指令雪橇”。当程序的执行流被劫持到这个 “雪橇” 区域内的任意位置时,就像雪橇从高处滑下一样,程序会顺着一个个 nop 指令依次执行,最终到达并执行位于 “雪橇” 后方的 shellcode。这一过程可类比为在不确定 shellcode 确切起始地址的情况下,扩大可命中的范围,只要返回地址能落入 nop 指令组成的区域,就能保证 shellcode 被执行。
PWN_067*(待定)
题目:32bit nop sled; PWN_067
checksec:
这次只有栈溢出保护了。
IDA32分析:
这个v5地址被打印输出,但一直是动态变化的。
代码的大概意思是先输出一些信息,然后输出的会有栈中的地址,从query_position函数可以发现函数返回值是v1的地址加上v2的值,而v1是局部变量,那么它的地址就是栈里的地址,加上v2就代表接收函数返回值的变量position=v1的地址+v2的值(即&v1+v2)。然后程序会让我们输入大小为4096的字符串给seed变量,之后再让我们输入一个地址,将其赋给v5,然后使用v5从我们输入的这个地址执行这个地址的代码。
因此我们可以从堆栈中执行。向程序提供 shellcode 很容易,因为它只要求输入。现在我们只需要找到一种方法来跳转到我们的 shellcode。
刚好有个 fgets 可以读取 shellcode 到 seed,但是我们并不知道 seed 的地址。
跟进query_position函数:
char *query_position()
作用是生成一个基于随机偏移的栈地址,得到一个范围在-668到 668之间的随机整数,并将其存储在变量 v2 中。该地址后续用于构造 “nop 雪橇” 攻击以绕过栈地址随机性。
看到这里就有点印象了,这不就是类似ASLR(Address Space Layout Randomization)保护吗?
我们这里需要了解一下 nop sled 空操作雪橇:
攻击者通过输入字符串注入攻击代码。在实际的攻击代码前注入很长的 nop 指令(无操作,仅使程序计数器加一)序列,只要程序的控制流指向该序列任意一处,程序计数器逐步加一,直到到达攻击代码的存在的地址,并执行。由于栈地址在一定范围的随机性,攻击者不能够知道攻击代码注入的地址,而要执行攻击代码需要将函数的返回地址更改为攻击代码的地址(可通过缓冲区溢出的方式改写函数返回地址)。所以,只能在一定范围内(栈随机导致攻击代码地址一定范围内随机)枚举攻击代码位置(有依据的猜) 。
不用 nop sled , 函数返回地址 ——-> 攻击代码。
使用 nop sled , 函数返回地址 ——-> nop 序列(顺序执行) 直到攻击代码地址。
为了安全地“绕过”不知道缓冲区的确切开始位置,我们可以:
1、将 shellcode 填充为以 1336 nop 条指令开头 ( 0x90 )。
2、使用 的返回值 query_position ,添加 0x2d (如前所述),然后添加 668。
python3运行的exp:
|
|
在这里特别解释一下:
|
|
addr
:query_position()
返回的栈地址参考(v1
的地址);
+0x2d
:补偿栈帧偏移( 计算得出:0x15 + 4 + 4 + 0x10 = 0x2d
),将 “v1
的参考地址” 修正为 “实际 payload 缓冲区(seed
数组)的起始地址”;
较低地址 | ⇒ | ⇒ | ⇒ | 更高地址 |
---|---|---|---|---|
stk[ebp-0x15] | ebp⇒<旧ebp>[4] | <返回地址>[4] | padding[0x10] | buffer[0x1000] |
21 (0x15) + 4 + 4 + 16 (0x10) = 45 字节 (0x2d)
+668
:覆盖程序的最大负偏移(-668
),确保跳转地址落入 1336 个 nop 的范围内(无论随机偏移如何波动,地址都能命中 nop 序列)。
看回*query_position函数:
可以看到v1在query_position函数ebp-0x15的位置。
————参考CTFshow-pwn入门-pwn67(nop sled空操作雪橇)-CSDN博客
在这里我想说的是nop sled 并非专门用于突破 ASLR 保护,其核心作用是 “扩大 shellcode 的可命中地址范围”,可用于应对多种导致 “shellcode 地址不确定” 的场景,ASLR 保护只是其中一种。
ASLR(地址空间布局随机化)的核心是让程序的栈、堆、库等内存区域的起始地址每次运行时随机变化( PWN_061、PWN_062 等题目开启 PIE,本质是 ASLR 的延伸),导致攻击者无法提前确定 shellcode 的精确地址。
nop sled 的作用是:通过在 shellcode 前填充大量
\x90
(nop 指令),形成一个 “指令序列区域”—— 只要程序执行流跳转到该区域的任意位置,都会顺着 nop 指令顺序执行,最终命中后续的 shellcode。从这个角度看,nop sled可以用于突破 ASLR 带来的地址随机性,但这并非其唯一用途。
这题难到我了,对于栈的结构了解不够多,nop、0x2d琢磨很久才明白……等到后续再回来完善一下,如果看不懂就算了。
PNW_068(待定)
题目:64bit nop sled。 PWN_068
PWN_069
题目:可以尝试用ORW读flag flag文件位置为/ctfshow_flag。 PWN_069
checksec一下:
64位仅部分开启RELRO,其他保护全关。
IDA查看main函数(依据函数功能修改对应函数名):
复习下mmap函数作用,主要用途有三个:
1、将一个普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,这样用内存读写取代I/O读写,以获得较高的性能;
2、将特殊文件进行匿名内存映射,可以为关联进程提供共享内存空间;
3、为无关联的进程提供共享内存空间,一般也是将一个普通文件映射到内存中。
把从0x123000开始的地址,大小为0x1000的长度,权限改为可写可执行
。
跟进seccomp函数:
seccomp主要用于配置和加载 Seccomp(Secure Computing Mode,安全计算模式) 策略,来限制程序可以执行的系统调用,以增强程序的安全性。
wow这里新大陆….沙盒过滤:
|
|
只有read,write,open,exit可以使用,使用 open–>read–>write 这样的orw的方式。
ORW指的是Open-Read-Write技术,是一种利用系统调用读取文件内容(如flag文件)的攻击方法。
ORW通过以下三个系统调用实现:
open:打开目标文件,获取文件描述符。
read:通过文件描述符读取文件内容到缓冲区。
write:将缓冲区的内容写入标准输出。
跟进ctfshow函数:
这里有明显的栈溢出漏洞,在上面我提及过mmap函数,在main函数已经执行过一次了,在0x123000这里已经有可写可执行权限了。到这里,攻击思路就比较清晰了,我们想办法往mmap给的这个地址段里面写shellcode(ORW),然后跳转到这里执行,就OK了。
那我们的orw_shellcode:
|
|
shellcode已经编好了,现在编payload让程序听我们话,将shellcode注入进内存中去。
shellcraft.read的作用是执行read函数的后续流程——读取0x100长度的字符(也就是后续的orw_shellcode),文件描述符fd=3(0、1、2已被标准文件占用)
shellcraft.write(1, mmap, 0x100)
的 1
是 Linux 标准输出(stdout)的固定文件描述符,其功能是将 mmap
区域中存储的 \ctfshow_flag
内容输出到屏幕,当成printf也行。
payload:
|
|
我在汇编代码分析的过程中找到关键的jmp_rsp后门地址,可以调用这行汇编代码来实现跳转回起始地址:
payload = asm(shellcraft.read(0, mmap, 0x100)) 的意思是启动read函数,开启“读取模式”处于待读取状态,等待后面的orw_shellcode的出现,并存到mmap的可读可写区域(2+4);
最正确理解:执行后因缺少输入数据(orw_shellcode)才进入 “等待状态”。
asm(“mov rax, 0x123000; jmp rax”)的意思是跳到mmap=0x123000区域,也就是指向这个区域,等待后续orw_shellcode注入执行。
payload = asm(shellcraft.read(0, mmap, 0x100)) + asm(“mov rax, 0x123000; jmp rax”)加起来不足0x28个字节(0x20+8),让buf刚好充满,以便后续将 rsp
调整到 buf
缓冲区起始地址(buf复原)。
python3的exp:
|
|
PWN_070
题目:可以开始你的个人秀了 flag文件位置为/flag。 PWN_070
checksec:
64位程序部分开启RELRO,开启栈保护。告诉了我们flag文件位置,hint中还是让我们用ORW读flag。
具体规则逻辑:
- 0000-0001 行:检查程序架构,仅允许
ARCH_X86_64
(64 位 x86 架构),否则终止程序(跳转到 0007 行返回KILL
)。- 0002 行:将当前系统调用号加载到寄存器
A
。- 0003-0004 行:过滤 “非标准系统调用”(编号 >=
0x40000000
的内核私有调用),仅允许编号为0xffffffff
的特殊调用(实际很少用到),其他此类调用会被终止。- 0005 行:明确禁止
execve
系统调用(编号0x3b
,用于执行新程序),触发时会终止程序。- 0006 行:允许所有未被上述规则禁止的系统调用(返回
ALLOW
)。- 0007 行:对被禁止的情况,直接终止程序(返回
KILL
)。
完全支持**
ORW
**操作!唯一被禁止的是execve
(执行新程序),但 ORW 操作不依赖它,因此可以正常执行。
跟进到is_printable函数:
条件是 a1[i] <= 31 || a1[i] == 127。这意味着如果字符的 ASCII 值小于等于 31,或者等于 127,则认为该字符不可打印。
可以在开头加上\x00 \xc0,做过pwn66,有strlen 的话可以找\x00开头的shellcode,
\x00
是 ASCII 码为 0 的字符(也叫 空字符),它在 C 语言中唯一的特殊作用是 标记字符串的结束。这里的\x00
只是 “告诉strlen
停止计算”,没有任何 “赋值” 操作(既不修改寄存器,也不修改内存值)。
这波直接不给看main的C伪代码,只能看汇编语言了。
我的python3的exp(没用到ORW,巧合):
|
|
手写汇编的exp:
|
|
建议大家在以后要多学学汇编语言,有时候手敲是真的实用,好理解。
ret2syscall
ret2syscall的核心思想是通过控制程序的执行流程,使程序执行系统调用(syscall)指令来获取系统资源或执行特定操作。系统调用是操作系统提供给用户程序的接口,用于请求内核服务。例如,在Linux系统中,execve系统调用可以用来启动一个新的程序(如/bin/sh),从而获取shell。
ret2syscall 是通过 拼接程序中的 gadget(指令片段),手动设置系统调用所需的寄存器值,最终触发 int 0x80
(32 位 Linux 系统调用入口)来执行命令的攻击方法。
1.在32位Linux系统中,系统调用通过int 0x80指令触发,需要设置以下寄存器:
eax:系统调用号(例如,execve的调用号为0xb)
ebx:第一个参数(例如,/bin/sh的地址)
ecx:第二个参数(通常为0)
edx:第三个参数(通常为0)
2.在64位系统中,系统调用通过syscall指令触发,寄存器分配有所不同:
rax:系统调用号(例如,execve的调用号为0x3b)
rdi:第一个参数
rsi:第二个参数
rdx:第三个参数
PWN_071
题目:32位的ret2syscall。 PWN_071
checksec:
32位关闭栈保护与PIE。
IDA32位分析:
有明显栈溢出漏洞、gets函数
;题目描述以及各处都提示了让用ret2syscall来进行攻击,我们可以利用程序中的 gadgets 来获得shell,而对应的 shell 获取则是利用系统调用。
大致思路是先栈溢出填充缓冲区,利用gadgets调用syscall,也就是传递它的调用号0xb
,用rax寄存器传递,然后调用execve函数:
|
|
需要用到eax系统调用参数传递的寄存器的gadget:
|
|
execve函数有三个参数,32位架构靠栈传递参数,所以要有参数寄存器:ebx
(第 1 参)、ecx
(第 2 参)、edx
(第 3 参),也就是:
|
|
同理,int 0x80调用号的gadget地址亦是如此:
/bin/sh的地址:
同理int 0x80
我以为的exp……
|
|
试半天爆EOF,也是无语,后来才知道想起这个程序是静态编译的,而运行程序是动态的,在IDA上呈现的数据可能是有偏差,这让我想起了缓冲区大小,寄存器esp是动态寄存器,ebp-64h可能是错误的,于是我开始用动态调试pwngdb
,既然gets是我们唯一能和程序交互的地方,我们可以通过gets函数,输入一些垃圾值,那就aaaa吧,刚好4字节(建议不要太大,万一溢出了程序就崩溃了,当然aaa、ctf也行),让esp把我所输入的值运走,这会就能看到esp的地址了,然后用ebp地址和它相减得到偏移量了。
在右边的运行框上,先给程序进行break main指令给main函数断点进行动态调试准备,然后开始了,用n
指令(也就是next缩写,一步步进行,左边的调试框也会看到变化),接着回车换行一直进行下一步操作,直到看到有熟悉的ctfshow交互界面和让你输入的交互操作时就可以了:
这时输入AAAA,左边跟着变化,esp地址出现了。
查询栈帧底部ebp寄存器地址:
|
|
ebp - esp => 0xffffcf58 - 0xffffceec => 6C = 108
这是偏移量,再覆盖ebp本身,那就是112了
最终exp:
|
|
所以说后续看到静态编译一定要多注意偏移量,后续要熟练运用pwngdb动态调试和pwndbg。
PWN_072
题目:接着练ret2syscall,多系统函数调用。 PWN_072
checksec一下:
32位关闭栈保护与PIE。
IDA分析,依旧静态编译:
看到gets函数,和上题差不多,都是利用栈溢出漏洞。
/bin/sh在IDA上搜索不到,不能返回地址执行getshell了,那就找另外一种方法。确实难找,read衍生函数很多,找到个单纯“read”的函数是真难……
不会就丢给AI吧,这确实有点难去读懂……
有read函数那我们可以用它去读取字符串到内存呐,利用read函数来进行手动写入“/bin/sh”字符串。
在main函数中没看到调用read函数,只能我们写exp时利用syscall去调用read函数,搭配gadgets去让read通过寄存器去读取/bin/sh字符串。可以看到read函数需要三个参数,加上自身系统调用号,我们就需要使用eax、ebx、ecx、edx的gadgets。
gadget查询:
pop_edx_ecx_ebx_ret:0x0806ecb0
pop_eax_ret:0x080bb2c6
int 0x80:0x08049421
调用read函数,read函数系统调用号是0x3,用eax寄存,三个参数用ebx、ecx、edx分别暂时寄存,int 0x80去开启执行read函数,把/bin/sh
写入内存bss段后,接着执行execve
函数,也是用一样的gadgets:
read函数的解释:
a1
:表示文件描述符(file descriptor),是一个非负整数。在 Linux 系统中,文件描述符是一个指向已打开文件的引用,常见的值有:0
代表标准输入(stdin
)。1
代表标准输出(stdout
)。2
代表标准错误输出(stderr
)。- 当打开一个普通文件或其他类型的文件时,系统会分配一个大于 2 的文件描述符。
a2
:指向用于存储读取数据的缓冲区的地址。也就是说,从文件描述符a1
所指向的文件中读取的数据,会被存储到a2
所指向的内存区域中。a3
:表示要读取的字节数,即指定从文件描述符a1
所指向的文件中读取多少个字节的数据到a2
所指向的缓冲区中。总的来说,
read
函数的作用就是从指定的文件描述符所对应的文件中,读取指定字节数的数据到给定的缓冲区中,并且在多线程环境下,对异步取消等情况进行了一定的处理。
想起上一题,我怕有坑,还特定去用pwngdb调了下,发现真有猫腻:
这确实和IDA静态分析的偏移量大小不一样,所以下次看到静态编译的程序我直接留一手心眼用pwngdb看一眼偏移量先😄。
exp:
|
|
执行后,程序会阻塞等待用户输入,此时发送
bin_sh = "/bin/sh\x00"
,read
会将该字符串写入bss
段(0x080eb000
地址处);此时
bss
地址指向的内容就是"/bin/sh\x00"
,满足execve
对 “命令路径参数” 的要求。
PWN_073
题目:愉快的尝试一下一把梭吧! PWN_073
checksec:
看出来是静态编译了:
IDA分析main函数:
还是明显的栈溢出,但是由于是静态编译,我们无法再使用ret2libc来进行getshell,程序中也没有system函数,我们可以尝试直接使用ROPgadget来帮助我们构造一个ROP链:
|
|
|
|
exp:
|
|
可以看到,它已经很智能的帮我们构造好了这些,我们仅仅需要将它构造好的payload提取出来,然后填充上偏移即可进行我们的攻击。
这个比较类似杂项的一把梭的感觉,在这个阶段不要我们学会,当然现在的我也不会,以后就会了。
使用前提:程序为 “静态编译” 且无现成的
system
/execve
函数、关闭 PIE 且开启 NX(栈不可执行)、存在 “可控栈溢出” 且溢出空间足够。使用该指令的核心原因是程序为 静态编译,
file pwn
显示statically linked
,且未直接提供system
、execve
等可直接 getshell 的函数:
- 静态编译的程序会将所有依赖的库函数(如
read
、write
、int 0x80
系统调用入口)打包到自身中,不存在 “动态链接依赖 libc” 的问题,ROPgadget
能从程序自身提取到完整的 ROP 组件(如pop edx; ret
、mov [edx], eax; ret
、int 0x80
等),足以构造出调用execve("/bin/sh")
的完整 ROP 链;- 若程序为动态编译(依赖外部 libc),且未泄露 libc 基址,
ROPgadget
无法获取 libc 内部的 gadget(如system
地址、/bin/sh
字符串地址),自动生成的 ROP 链会因 “缺少关键组件” 失效,无法直接使用 “一把梭”。
one_gadget攻击
PWN_074
题目:噢?好像到现在为止还没有了解到one_gadget? PWN_074
checksec:
64位保护全开….我都懵了。
相对于现阶段,看到这种保护全开的题可能会有点迷茫,但是实际上在堆阶段后面基本上都是保护全开的题比较多一点,这里也是为了让大家提前了解一些攻击手法。
IDA64分析:
开局直接泄露了pringtf的地址我们,但因为地址动态变化,所以我们可以泄露libc库地址,算出这个程序的函数偏移量。
通过 __isoc99_scanf 函数从用户输入中读取一个长整数,并将其存储在 v4 数组的第一个元素中。
再将 v4 数组的第一个元素的值赋给了数组的第二个元素。继续通过函数指针调用了 v4 数组的第一个元素所指向的函数。这个部分利用函数指针的特性,将其转换为函数并进行调用。
使用用户输入来获取函数指针,并通过函数指针调用相应的函数。需要注意的是,这种通过用户输入来获取函数指针并调用函数的做法极有可能会带来安全隐患,因为恶意用户可以输入不安全的函数指针,导致程序出现问题。
到这里引出下一个攻击手段——one gadget
one_gadget是libc中存在的一些执行execve("/bin/sh", NULL, NULL)的片段,当可以泄露libc地址,并且可以知道libc版本的时候,可以使用此方法来快速控制指令寄存器开启shell。
相比于system("/bin/sh"),这种方式更加方便,不用控制RDI、RSI、RDX等参数。运用于不利构造参数的情况。
|
|
one_gadget并不总是可以获取shell,它首先要满足一些条件才能执行成功,后面提示就是在调用one_gadget前需要满足的条件。
约束条件满足:one - gadget 是 libc 中执行
execve("/bin/sh", NULL, NULL)
的代码片段,但需要满足特定约束条件才能执行成功。0x4f2c5
的约束条件为rsp & 0xf == 0
且rcx == NULL
,0x4f322
的约束条件为[rsp + 0x40] == NULL
,0x10a2fc
的约束条件为[rsp + 0x70] == NULL
。在 PWN074 场景中,程序栈布局使得[rsp + 0x70]
天然为 NULL(因为该地址超出当前函数栈帧范围,属于未使用的空闲栈区域,在 Linux 系统中默认填充为 0) ,满足0x10a2fc
的约束条件,而其他两个地址的约束条件难以满足。例如,rsp & 0xf == 0
要求栈指针低 4 位为 0,在程序未提供调整栈指针对齐的可控途径时,很难实现;同时程序未对rcx
寄存器进行控制,rcx
大概率不为 NULL,不满足0x4f2c5
的条件;[rsp + 0x40]
在程序运行时存储有临时数据,无法通过用户输入清空,不满足0x4f322
的条件。
本题仅仅是一个工具引进使用,具体可以自行调试一下便知。省了很大一部分时间和精力,对于初学者来说,libc版本是个坑。因此在训练题型上平台一般弄了几个比较相对固定的libc版本,实际比赛时libc的版本更加多样复杂。
当然,在这里如果不是使用的附带虚拟机,则需要自行去泄漏libc,当然前面经过这么多题的练习,相信这些也是没难度了。
exp:
|
|
one_gadget + libc_base
:计算得到 one_gadget 在内存中的真实地址
*str(one_gadget+libc_base)
:one_gadget + libc_base
是十进制整数),main
函数明确使用 __isoc99_scanf("%ld", v4)
读取用户输入,也就是说要的是用户输入的字符串,不要数值本身的意义。(one_gadget 真实地址为 0x7f8a9b12d95c
(十进制数值为 140239165128028
),程序要求输入的是字符串 "140239165128028"
,而非数值140239165128028
本身)大概就这么理解,这是python的自身规则。
栈迁移
栈迁移是在二进制漏洞利用(尤其是栈溢出漏洞场景)中常用的一种技术手段,目的是改变程序执行时栈的布局和执行流程,绕过一些安全防护机制, 实现攻击者想要的代码执行。
在存在栈溢出漏洞的程序中,攻击者原本可以通过覆盖返回地址来劫持程序执行流,让程序跳转到攻击者指定的地址(如 shellcode 地址)执行恶意代码。但随着系统安全机制(如 NX 保护,使栈上数据不可执行 )的引入或者栈空间过小,直接在栈上执行注入的 shellcode 变得困难。同时,一些程序还可能存在栈金丝雀保护(Stack Canary)来检测栈是否被非法篡改,这也增加了传统栈溢出利用的难度。因此,栈迁移技术应运而生,以绕过这些安全防护。
原理
- 改变栈的位置:正常情况下,程序按照默认的栈布局执行,栈迁移的核心是将栈的位置移动到一个对攻击者有利的地方,比如具有可写可执行权限的内存区域(如
bss
段) 。在栈溢出漏洞发生时,攻击者可以通过覆盖某些关键寄存器(如esp
,在 32 位程序中;rsp
,在 64 位程序中 )的值,使程序在后续执行中,将新的栈帧设置在攻击者指定的内存位置。 - 构造新栈帧:在新的栈位置,攻击者精心构造栈帧,放置需要执行的指令地址、函数参数等数据。例如,攻击者可以将
execve("/bin/sh", NULL, NULL)
系统调用的相关参数和指令地址按照正确的顺序放置在新栈帧中,当程序执行到相应指令时,就会按照攻击者的意图执行系统调用,获取 shell。
PWN_075
题目:栈空间不够怎么办? PWN_075
checksec一下:
32位开启NX保护,部分开启RELRO。
IDA分析,查看main中的ctfshow函数:
其实在连靶机进行交互时,无论你输入什么,都是招原样给你打印出来的,主要还是看read、memset函数,况且没开canary保护有站处理漏洞可以利用。
«««< HEAD 在海曙目录还看到了system函数。
=======
191b05fa45e02dc8995178e6a8d2c0c238003afb 跟进hackerout函数:
看到有system函数,不过里边不是binsh,问题不大可以构造出来。
«««< HEAD
看回ctfshow函数的栈溢出漏洞,可以发现s容量是36,但s到ebp距离有0x28=40,加上覆盖ebp本身的4字节,仅 8 字节(44-36=8)溢出空间,无法直接布置system("/bin/sh");
所需的 ROP 链(需覆盖返回地址 + 传递参数,至少 12 字节以上),那么这里就应该要想到栈迁移
了。
栈迁移的本质是通过 leave
指令改变栈顶指针(esp)的位置,将原本狭小的栈空间转移到我们可控的大空间。那我们就得调用levae汇编指令
看回ctfshow函数的栈溢出漏洞,可以发现s容量是36,但s到ebp距离有0x28=40,加上覆盖ebp本身的4字节,也就是说只有8字给到我们放东西,无法进行我们的ROP链构造,那么这里就应该要想到栈迁移
了
191b05fa45e02dc8995178e6a8d2c0c238003afb