这题考察的还是对子进程与父进程之间堆栈的关系
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 * |
整个流程