3※ [SHCTF 2024] No stack overflow2
-
考点:ret2libc,libc 库查询,整数溢出
-
在 linux 下使用
checksec
查看该程序开启的保护,发现Arch
为amd64-64-little
,这说明这是一个 64位 的程序,并且采用了 小端 存储,即低位对应低地址,高位对应高地址。 -
下方的
RELRO
,这是一种通过设置 重定位相关表 的权限为 只读 来防止其被修改的安全机制,我们只关注其对 got 表的影响。Partial RELRO
指的是部分开启,此时 got 表被设置为:每个表项只有在未解析过该函数地址前是可写,载入地址后改为只读: -
不过我们暂时不关心,把 vuln 文件拖进 IDA64 打开,点击左侧
main
,按 F5 反编译,发现程序主要功能是先读入一个长度,接着检测长度小于等于 256 时再读入最多这么长的字节: -
我们看 IDA64 提示的存储地址,发现
nbytes_4
长度也是0x100
即 256 个字节,难道无法溢出吗: -
我们发现关键在于传入参数时,将长度转为了
unsigned int
无符号整数,这就使得首位的符号位被当做了数据,那么如果原先输入长度为 -1,二进制对应的0xFFFFFFFF
,那么转为无符号整数后就变为了 2147483647,就可以绕过上方的长度检测了: -
那么接下来就是要构造我们的 payload1 了,但是一个个查看左侧函数发现没有
system
相关的,按 Shift+F12 也没有发现/bin/sh
字符串。 -
但是发现左侧有
puts
函数,那么我们可以考虑使用和上一题相同的方法:将puts
函数的 got 表地址泄露出来,然后查询其在动态库中的位置,二者相减得到偏移量。 接着就可以先查询动态库中的system
函数和字符串/bin/sh
的位置,来计算出它们在程序中的位置,然后便可以构造 payload 模拟执行system("/bin/sh")
了。 -
那么我们先打开 ELF 文件,查询
puts
函数 plt 表,got表所在的地址,以及puts
结束后返回的main
函数的地址: -
这里的
process('./vuln')
指的是先不连接服务器,而是用本地文件来模拟,方便我们调试: -
然后在第一处输入 -1 绕过长度检测:
-
但是我们忽然发现一个问题,本题与上一题不同的是,上一题是 32位 程序,所有参数均通过栈来传递,我们只需要将函数的传参压入栈中即可模拟函数执行。但是这题是 64位 程序,函数的前 6个 参数是依次通过 6个 寄存器来传递:
rdi
,rsi
,rdx
,rcx
,r8
,r9
,之后的更多的参数才用栈来传递。这样使得程序运行的速度提升了不少,但是对于我们栈溢出攻击就不能直接通过栈来传参了,需要修改寄存器的值。
-
我们可以想到,
pop rdi
指令可以将栈内写入的内容传给rdi
这样就可以修改它了,那么如果我们将pop rdi
指令所在的地址,放在函数返回的ret
处,就可以修改rip
下一条指令执行pop rdi
了! -
可是仅仅是这样,
rip
跳转之后就回不来了,程序流程整个被打乱,所以我们需要找一个后面紧跟着ret
指令的pop rdi
,这样下一句还会执行ret
,将此时rsp
自加过后的栈顶的内容给rip
,不就仍然等同于继续进行栈溢出攻击了吗。 -
同理,如果想要修改
rsi
,rdx
,也需要在程序中找到pop rsi
随后ret
的代码片段的地址,以此来传递参数模拟函数执行。这个过程就是所谓的构造 ROP 链。
-
我们可以通过使用
ropper
工具或是ROPgadget
工具,在 linux 下快速查找一个文件中出现指定字符串的位置,我们通过使用管道符来查找所有pop
开头到ret
结尾的字符串,再要求其中含有rdi
,写出以下命令查询:ropper --file vuln --search "pop|ret" | grep "rdi"
,发现成功找到一个: -
记录下这段程序所在的地址
pop_rdi_ret = 0x401223
,接下来就可以使用了。由于puts
函数只需要一个参数,即输出的字符串的地址,我们只需要rdi
来传参,所以现在可以开始构造 payload1 了: -
先填充
0x100
即 256个字节 给nbytes_4
,然后因为这是 64位 程序,要填充 8个字节 给s
即rbp
,接下来在r
处填入我们的pop rdi;ret
程序的地址:p64(pop_rdi_ret)
,然后填入要修改的rid
数据,即puts
的传参,也就是要输出的字符串的地址:puts_got
。 -
然后填充
pop rdi;ret
返回回来后要执行的程序的地址,我们要输出puts
函数的 got 表里的内容,所以这里填调用的输出函数puts
的地址:puts_plt
。 -
最后填充
puts
函数输出完返回回来后要执行的下一个程序的地址,由于我们需要再次利用这里的栈溢出来执行system("/bin/sh")
,所以填main
函数的首地址。 -
可以看到此时已经将地址输出出来了,不过都是
\x
开头的 16 进制bytes
数据。由于 64位 程序的地址都是以\x7f
开头的,并且由于这是个 小端程序,字符串地位置存储在低位置,所以输出出来是倒序的,所以我们可以用.recvuntil(b'\x7f')
读到\x7f
为止。 -
又由于虽然我们是 64位 程序虽然应该有 8 位,但使用时的编码都是以
\x7f
开头的 6位 编码地址,所以我们只要读进来的最后 6个字节: -
然后我们要对这个 16进制 的
bytes
数据用u64()
进行解包,但是如果直接使用的话程序会报错: -
这是因为
u64()
每次解包需要输入 \(8个字节\),而刚才的地址不足 8位,我们需要在左侧用.ljust(8,b'\x00')
补\x00
将其补满 8位: -
此时再输出发现就是正确的一个整数地址了:
-
那么拿到了一个
puts
函数的 got 表的所在地址,接下来我们就可以通过查询动态库中puts
函数的地址然后计算出偏移量啦。 -
不过这道题题目并没有把动态库文件直接给我们,我们需要根据泄露出来的
puts
函数的 got 表的所在地址来查询到系统所使用的动态库版本。
-
我们可以下载使用
LibcSearcher
这个 python 库来打开对应的动态库,只需要提供某个已知函数的具体地址即可: -
接下来就和上一题一样了,查询
puts
函数在动态库的地址,计算出偏移量,然后查询system
函数和/bin/sh
字符串在动态库的地址,计算出在程序内的地址。不过要注意的是此时使用LibcSearcher
指令,需要用.dump("xxx")
来查询某个函数的地址,用.dump("str_bin_sh")
来查询字符串/bin/sh
的位置: -
此时运行时我们会发现,查找到多个匹配的动态库,程序询问我们要使用哪一个版本的,这是因为先前我们用的是
process('./vuln')
在本地调试。而如果连接到服务器上的时候就不需要我们进行选择了。 -
不过现在我们需要根据自己的 ubuntu 版本来选择对应的动态库,我们可以先按 Ctrl+C 退出程序,在 linux 中输入
ldd --version
来查询版本: -
可以看到第二行,我的是
2.39-0ubuntu8.3
,那么再次运行程序,这次就选择这个版本的动态库,填入程序提示的版本前方的编号 0 按回车,可以看到下方有一句该版本be choosed
就成功选择了: -
那么完事具备,我们现在已经执行到第二次
main
函数要求我们输入长度的位置了,再次输入-1
,然后开始构造我们第二次的 payload2: -
先填充前面
0x108
个字符到r
处与 payload1 一样,然后通过pop di;ret
来传递system
的参数:bin_sh_addr
, -
然后填充要执行的
system
函数的地址:system_addr
-
最后的返回地址在哪里都无所谓,因为马上要得到系统权限进入交互模式了,并不会返回回来用上,直接不填。
-
但是!此时运行会发现并没有得到系统权限,反而报错退出了。这是因为这是采用了新的高版本的 gcc 编译器的 64位 系统,其在调用动态库中的
system
函数时,对rsp
有额外的要求: -
在准备进入
system
函数时,会对此时的rsp
也就是栈顶进行一次检验,要求此时指向的地址必须能被 16 整除,也就是必须以 0 结尾,否则报错退出不予调用。 -
我们进入 IDA64 的
main
函数,点击nbytes_4
查看栈空间,发现我们填充到r
的位置以 8 结尾: -
所以此时放在
r
中的pop rdi;ret
的地址以 8 结尾,接下来/bin/sh
字符串的地址以 0 结尾,而system
函数的地址以 8 结尾,就无法通过高版本的rsp
检验。 -
那么我们需要再调用
system
函数之前额外填充一个 某段程序的地址,这样在执行system
函数时rsp
就以 0 结尾了。 -
最简单的就是找一个只有一句
ret
指令的地址,rip
执行原先函数的ret
跳转到这里后,下一句还是将执行ret
,没有区别,但是此时rsp
已经自加了一次。 -
所以我们用
ropper
指令寻找一个只有一句ret
的程序,在 linux 下输入ropper --file vuln --search "ret"
查找: -
记录下程序的位置
ret = 0x40101a
,接下来只需要在调用system
之前多填充一个ret
的地址即可: -
此时运行完程序,在选择动态库版本输入 0 后,我们发现已经进入了交互模式,输入
ls
可以看到当前目录下的文件,大功告成: -
最后调整为远程连接服务器,
ls
一下发现有flag
,cat flag
获取 flag: -
最后放上完整 exp(调整了一下顺序):
-
除了使用 LibcSearcher 在线查询动态库之外,我们还可以使用一个在线网站将服务器所使用的动态库下载下来:
https://libc.rip/
,使用的时候只需要输入,泄露的函数的名称,和泄露出来的函数的地址的后三位(16进制)即可: -
当然,如果用
puts
函数查不到对应的版本的话,可以试着用别的函数查询,网站的内容有时候明没有更新到最新(这里就是,我换成了read
函数 ): -
然后可以下载下来,本地进行调试(当然我们不知道服务器用的是哪一个,这只限于本地调试代码用的下载)。
3※ No stack overflow2 pro
-
考点:libc 静态链接
-
这题题目首先提示了,使用了静态链接,也就是将动态链接直接写入了程序中,这样就没有 plt 表和 got 表供我们使用了。
-
首先在 linux 下用
checksec vuln
查看文件保护情况: -
发现是 64位 小端程序,开了
Partial RELRO
,开了NX
保护,这个就是不允许执行存放在数据段的代码,也就是为什么我们之前,都要费尽心思往栈里面写别的程序的地址的原因:代码直接放在栈里面不允许执行。 -
接着是
Stack:Canary found
,这是指开启了Canary
保护:在进入函数前生成一个校验码压入栈中,在函数返回时检测校验码是否被修改,若被修改则判断栈发生了改变收到了溢出攻击,自动结束程序。这是对栈溢出攻击的防护。 -
那么接下来我们拖入 IDA64 中,发现左边乱七八糟一大堆,这正是因为静态链接引起的,将所有动态库里的函数全写进来了,如果查看过这个文件的大小的话,会发现它远远大于我们之前使用动态库连接技术的文件的大小:
-
我们找到加黑了的
main
,点击进入,F5 反编译,发现和上一题的代码一模一样,都是输入一个长度,然后转化为有符号的int
来进行判断大小,接着往 v9 中存入不超过先前读入的长度的字节。很明显这里存在着和前几题一样的栈溢出: -
那么我们记得先前有提到
Canary
保护,点开 v9 查看栈结构找找 校验值 存在哪里,但是发现 v9 下面直接就是s
和r
了,并没有找到Canary
保护的校验值存储的位置,那么就不需要理会了,直接正常溢出即可执行我们想执行的程序,也就是所谓的劫持程序。 -
我们需要模拟
system("/bin/sh")
,这在动态库里本质是输入指令syscall
,所以我们就需要一个写着syscall
指令的地址,用ropper --file vuln --search "syscall"
进行查找: -
发现很多个,我们随便选哪个地址都可以,因为执行完
syscall
指令后我们会获得系统权限进入交互模式,就不用管syscall
指令之后还有什么了,可以选最后一个syscall_addr = 0x41cbf6
: -
接下来我们要找字符串
/bin/sh
,按 Shift+F12,按 alt+T 查找字符串/bin/sh
,发现并没有跳转,不存在现成的字符串: -
所以我们只能自己找一个地址,往里面写入字符串
/bin/sh
。首先我们需要找一个有读和写权限的段,因为既要写进去也要读出来使用。我们按 Shift+F7 打开段视图,一般使用.bss
段,BSS
段通常是指用来存放程序中 未初始化 的或者 \(初始化为0\) 的 全局变量 和 静态变量 也就是说,只要初始值为 0 的类型,都会先放在这里,等到再次赋值时才会被取出。所以写在这里面可以全局使用。 -
我们点开
.bss
段,随便复制一个起始位置,bss_addr = 0x4E72C0
: -
那么接下来我们要往里面写数据,可以调用
read
函数,在左侧下方输入read
查找函数位置: -
点进去,复制函数入口位置,
read_addr = 0x44FD90
: -
我们发现
read
函数需要三个参数,由于这是 64位 程序通过寄存器传参,所以和上一题一样我们要去寻找pop rdi;ret
,pop rsi;ret
,pop rdx;ret
的程序的存放位置,来改变寄存器的值为read
函数传参: -
在 linux 下用
ropper --file vuln --search "pop|ret" | grep "rdi"
来找与rdi
相关的指令,在一大堆结果中找到紧挨着ret
的程序,pop_rdi_ret = 0x4022bf
: -
在 linux 下用
ropper --file vuln --search "pop|ret" | grep "rsi"
来找与rsi
相关的指令,同理找紧挨着ret
的程序,pop_rsi_ret = 0x40a32e
: -
在 linux 下用
ropper --file vuln --search "pop|ret" | grep "rdx"
来找与rsi
相关的指令时,发现没有紧挨着ret
的程序,我们找一个离ret
最近的程序,中间仍然多了一个pop rbx
,不过也可以用,每次多传一个 0 给rbx
即可,pop_rdx_rbx_ret = 0x49D06B
: -
最后在程序内找到
main
的起始地址,因为第一次溢出后我们输入字符串/bin/sh
还需要第二次溢出来执行system("/bin/sh")
,main_addr = 0x401B7A
: -
那么我们可以开始构造第一次溢出的 payload1 了,需要注意的是,这一个程序输入长度的时候用
(unsigned int)
输入,判断的时候转为(int)
判断,所以我们需要输入2147483679
,对应的二进制转化为(int)
就是 -1: -
然后构造 payload1,先用 0x100 + 0x08 个字节填充到
r
处,然后用寄存器为read
函数传参: -
第一个参数表示读取的文件,为 0 表示从控制台读入,我们
pop_rdi_ret
后传 0 -
第二个参数为存放的地址,我们
pop_rsi_ret
后传bss_addr
, -
第三个参数为最大的写入长度,可以大一点,我们
pop_rdx_rbx_ret
后传0x100
,然后传 0 给多的pop rbx
-
最后填充函数结束后返回的地址
main_addr
: -
且慢,这是 64 程序,需要检验一下调用
main
函数之前,rsp
是否指向的地址末尾为 0,可以简单数一下main
函数是第 9 条指令,第一条指令以 8 结尾,此时main
也以 8 结尾,无法通过检验。我们需要再填充一条指令进去,这就是所谓的平衡栈操作。 -
同上一题,我们再用
ropper --file vuln --search "ret"
找一下ret
指令的位置,取单独的指令,ret = 0x454257
: -
那么我们此时在进入
main
之前加一个ret
指令的地址来平衡栈,构造出 payload1: -
运行可以看到此时程序成功执行了
read
函数,在输入一串字符后重新开始执行main
函数了: -
接下来我们可以往这个
.bss
段里面写入/bin/sh
字符串了,需要注意的是字符串需要一个结束标识符\x00
,所以我们往里面写的应该是b'/bin/sh\x00'
: -
接着重新再来一遍
main
函数,还是先输入2147483649
绕过长度判断,然后开始构造 payload2 来执行我们的system("/bin/sh")
:
-
syscall
的本质是系统通过调用这条指令时rax
里面的值,来执行不同的函数,我们执行system("/bin/sh")
时需要令rax = 0x3b
来执行execve
语句。(在 32位 系统中,是用int 80h
代替syscall
,同时令eax = 0x0b
),所以我们需要找到修改rax
寄存器的代码段pop rax;ret
,和之前的那些一样,都是所谓的 gadget。 -
在 linux 中用
ropper --file vuln --search "pop|ret" | grep "rax"
来查找,pop_rax_ret = 0x4507f7
: -
那么接下来就可以继续构造我们的 payload2 了,先填充
0x108
个字符到r
,然后在pop_rax_ret
后传0x3b
修改rax
, -
接着为
syscall
传参数,一共有三个参数:第一个参数是字符串地址,
pop_rdi_ret
后传bss_addr
;第二和第三个参数涉及系统内核取参方式,系统空间与用户空间之间的协议,都设为 0 默认即可。
-
pop_rsi_ret
后传 0,pop_rdx_rbx_ret
后传 0 ,再传 0 给rbx
。 -
最后填入
syscall_addr
的地址,来调用系统的syscall
功能。 -
此时算一下是否栈平衡,我们数一下
syscall
的地址为第 10 个指令,此时rsp
末位为 0,无需调整。 -
运行成功获得系统权限,进入交互模式,输入
ls
成功输出当前目录下的内容: -
转为远程连接服务器再次运行,输入
cat flag
获得 flag: -
最后附上完整 exp(调整了下顺序):