这题考察的还是对子进程与父进程之间堆栈的关系
debug部分参考的这位师傅
依照程式启动时输出的leave your name
查看string调用找到004019e9()

再追下,就到了main()中

看下调用顺序
应当是
1 |
|
但神奇的是它在程式执行过程中,首先输出的是004019e9的leave_your_name

而后才是00401931的Wanna_return

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

而后执行到这里会call 44EDF0

eax给了0x3a这里是
1 | #syscall |
1 | 58 common vfork sys_vfork |
所以这里是syscall_vfork创建一个子进程
在其中他会sys_vfork,创建一个新的子进程。

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

这让我有点懵了,我能想到可能是子进程去执行了401931(),而父进程是在子进程之后才执行的自己的输出。
但我感觉这明显是不合理,子进程正常是会从父进程继续往下走,也就是说子进程也会去先输出父进程401931()之后的内容。
但现在执行的现象就是如此,没办法只能从先输出内容的func.004019e9来看
先观察004019e9的逻辑

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

而后004019e9进程继续执行的话就会执行到sys_exit,整个主进程的逻辑就结束了
那既然vfork的子进程会和主进程共用一个栈,那问题可能会发生在主进程派生子进程的位置,也就是刚刚func.00401931()的sys_vfork位置,就是子进程开始执行的位置。
让我们返回func.00401931()仔细看下,会发现在子进程执行回来的路径上有一个cmp 1他会跳向ret的位子,以及还有一个cmp dword [var_2ch], 0也可以做到。


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


那这里为什么又要单独写一个cmp 1来做je跳转到ret
再进入子进程看下

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

这里是cmp 1没成立,然后再跑了几下就到nop了,就error了,所以他存在意义就是为了让我们能通过cmp 1来到达ret,可以想到这就是出题人给我们的route.
那就要看下子进程的这个ret返回的位置,不同于主进程,这里被放置了一个0x040186b

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

第一阶段基本就这样了。
这里有个0x44edf0,会再派生一个子进程

每个子线程在之后走到
00401a55
进入这里的
0044ee30
会执行
sys_exit退出当前进程
所以他的目的就很明确就是为了让
0x44edf0派生子线程于main这一段进行基于子进程派生之间的伪循环
这里有个read,这里会触发一次读入

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

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

此时进入到这个函数中

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

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

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

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

1.先把rsi写入到xmm2

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

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

不过这里并没有对地址进行写入 所以不是这里
3.这里他将xmm2的内容也就是我们输入的那部分,写入到了rdi的地址

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

再往下走这个进程就结束了
那我们现在的疑问就是在他下一个子进程中栈会发生什么变化
继续往下走看接下来的子进程(我这个子线程开时间长了gdb会崩溃,所以重新又开了个进程,被写入的栈地址和上面的对不上)
走了两步发现这里从rbp - 0x118取了个值

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

不过这个覆盖了又该做什么用处呢,带着这个疑问接着往下走
可以看到他这里下面进入了0x44f810

0x44f810其中会syscall调用read

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

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

然后就写入了,这里如果能利用xmm2的写入覆盖掉0x100也就是写入长度,那就可以在第二轮once写入的时候实现溢出
有点好奇,返回来看了下xmm2写入的地址rdi是怎么确定的,看下来发现是

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

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

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

0x100在0x7ffce77cd040
1 | pwndbg> p/d 0x7ffce77cd040-0x7ffce77ccfd0 |
也就说明只要初次once时允许写入'A' * 0x70就可以覆盖到之后,其他子进程read时rdx的长度取值
那我们就要回到初次写入时,看read的数量是否>=0x70

可以看到最初这里允许read0x100个那妥妥是够了
所以我们直接输入0x70个字符,当然这里为了方便看我在最后的8位替换为1
又回到xmm2二次写入这里(不知道为啥这一遍调试时候xmm2不见了,为了方便记忆还是叫这部分xmm2把)

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

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

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

看一下它比对的位置

看了下其实就是第一个输入时候的头8字节,只要头8个字节的’A’替换为0x11111111
就可以跳出让其成功ret
那我们现在拿到了两个可利用的条件,让我们思考一下怎么实现栈溢出
1 | 1.首先我们在第一轮输入`"0x70"`个`'A'`或者别的字符,让其派生的子进程中的read可以溢出 |
我们来看下派生的子进程中的cmp 0x1111111,这里我们需要用第一轮覆盖同样0x100的方式,让第二次xmm2来让其覆盖到这里。
我在派生的子进程中用0x22222222来填充

可以看到这里正好在这次2的填充之前,那也就知道了,需要用之后的xmm2来覆盖这里
淦啦这里因为gdb又崩了所以地址都对不上了,这里rdi指向的xmm2最初写向的地址0x7ffdce429f58

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

距离100也就是0x64
payload修改为

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

成功被修改为0x11111111!
这样在派生的子进程-》再派生的子进程-》派生的子进程,因为共用一个堆栈,导致就成功过了cmp 0x11111111
我们试一下

成功嘞
再看下$rbp的地址距离

再重写payload,再执行

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

我本以为这里往下走走就结束了准备直接execve一条龙了,结果他到ret之前又call了个函数,直接给我进程扬了
进来之后是这样的,是个检测的样子,我懒得分析了..所以采用肉眼观察法(

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

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

这里覆盖的话我一开始准备用上一个进程的xmm2来覆盖rbp-0x8,不过我发现他好像有数量限制
为了验证我修改了payload,来做区分

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

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

偏移152也就是0x98
改一下payload

rbp和rbp+0x8加俩占位
可以看到canary成啦

rbp也ok了

这样就可以直接打execve了 syscall_read + syscall_execve取值一把嗦!
1 | from pwn import * |
整个流程




