安全学习记录——逆向篇(三)栈溢出

前面我们对一个程序进行了破解,整个过程似乎只需要一定的汇编知识,具体的只看逻辑。那么我们最开始学汇编基础时学的那么多计算机原理是用来干嘛的呢。今天的栈溢出就要结合这两个东西来看看如何通过内存来欺骗CPU让它以为我们输入了正确密码,以及如何通过栈溢出来操纵计算机。

过程

为了创造一个方便观察的条件,我们人为地添加了一个栈溢出漏洞:

char buffer[8];  #限制输入字符长度0-7
authenticated = strcmp(password, PASSWORD);  #比较
strcpy(buffer, password);  #将上面字符拷贝到password中

此时这段代码在栈中的结构是:

 由于我们人为地限定了输入字符的长度,一旦我们输入的字符超过了数组的限制,那么我们多出来的字符就会进入到后一个内存地址中,也就是会覆盖掉int authenticated。换句话说,我们可以通过超长的变量去修改掉验证函数,从而绕过密码验证。

首先我们看看正常情况下这个内存应该是什么样子,由于需要给密码赋值,所以我们需要在赋值这个地方添加一个断点,此时我们输入密码qqqqqqq,也就是7个q,看看右下角高亮部分内存里的数值,能看到7个71,因为q的值就是0x71。注意看,此时高位字节的值是00000001。

 然后我们放行进程,毫不意外,结果失败了。

然后我们看看,假设我多输入了一个q,内存数值会发生什么变化:

此时,高位字节变成了00000000,原本的01被71前面的00给挤掉了,也就是说原本用来比对的eax的值变成了0,那么自然的就能通过后面的00比较,于是,我们就能得到密码正确的结果。

原理

验证过程修改

首先我们要知道,值是怎么被放进栈中的。比如我们将1234输入到栈中,每一行从左往右压,呈现的结果是:

21
43

读取时就反着移动回去就行。像祖玛里管道中的球一样,1就是入口,也是出口。

所以,当我们输入7q时,得到的结果如下:

buffer71717171006C14E0
auth0000000100717171

 这里有两个问题:为什么7q是从上一行的高字节开始压入直到占据下一行的低字节;结尾的那个00是个什么东西。

第一个问题,这个问题和我们想要研究的东西关系不大,我也不知道为什么但是我们可以猜一下,这一行的低字节部分是一个命令或者函数所代表的值,正确读法应该是E0146C00。

第二个问题,结尾的这个00是什么东西。这是个换行符占位,我们在编程时,虽然每一行最后看起来没什么东西但实际上,回车也是要占据一个位置的,部分语言中需要用/n来替代否则无法换行。换行符没有数值,所以是null,表现为00。因此对于第一个问题,我们可以说低字节位置大概率是个对7q进行操作的函数的编译结果,因为它结尾也是00。

此时我们知道了换行符以00的形式占据了一位,第一行后半又是个固定值,由于char函数限定了输入值在内存中占据空间,因此当我们输入了8个q将高低位全部占完后,换行符该放哪去呢?当然是被压到了第二行的高位的最后一个里面了。所以结果就是:

buffer71717171006C14E0
auth0000000071717171

溢出的换行符占据了验证的结果,但是读取验证结果并放入eax时仍然是使用的这个位置的值,别的命令又不会变,因此无论验证结果是多少,这里都会变成0,于是在主函数中进行00比较时就会得出密码正确的结论。

假设我们再多输一位q呢,此时验证的结果会变成71,所以结果一定不正确:

而只要我们输入一个8位字符串,一定可以通过验证:

这就是栈溢出的基本原理,但是这样的话我们必须要知道对方的验证方法是什么样子,万一下次不是0而是1呢,有没有一种方法可以帮我们直接跳过验证来出结果呢?

验证过程跳过

根据上面的栈结构来看在我们输入的验证部分的下面就是rbp,而在rbp下方是返回地址,这里写了个return。所以004015E2是什么呢?我们能看到是将验证结果通过eax输入到指定位置,接下来就是正常的验证流程。结果为0就成功,不为0就失败。

00000000004015E2 | 8985 AC030000            | mov dword ptr ss:[rbp+3AC],eax          |
00000000004015E8 | 83BD AC030000 00         | cmp dword ptr ss:[rbp+3AC],0            |
00000000004015EF | 74 0E                    | je c2.4015FF                            |
00000000004015F1 | 48:8D0D 2B2A0000         | lea rcx,qword ptr ds:[404023]           | 0000000000404023:"incorrect password, try again"
00000000004015F8 | E8 0B150000              | call <JMP.&puts>                        |
00000000004015FD | EB B8                    | jmp c2.4015B7                           |
00000000004015FF | 48:8D0D 422A0000         | lea rcx,qword ptr ds:[404048]           | 0000000000404048:"Congratulations, you have entered the correct password!"

根据反编译结果我们知道执行成功的命令在004015FF上,所以我们要做的就是通过更多的输入淹没掉栈中上面所有的部分,最终只去执行一个返回行为,并且修改掉返回位置使程序直接成功。

所以,最终这块内存的样子应该是:

buffer71

71

717100A614E0
auth7171717171717171
rbp7171717171717171
return00000000004015FF

可能会注意到buffer前面这段00A614E0怎么和前面不一样,这一段并不属于我们的程序部分,而是别的东西,每一次重启计算机,内存清空再填入,会导致地址结构等发生变化,因为我的记录一般会连续写好久。这并不重要,我们需要关注的是程序本身。但是程序本身运行的位置不会变化,因为这里显示的是计算机为程序分配的虚拟地址,从它被创建开始就固定了,虚拟地址要映射到实际的物理地址上,换句话说,映射的物理地址也会因为运行情况而变化。我们也不用去纠结这个问题,怎么映射是计算机的问题,我们需要找的只是有用的地址。

在知道了这个地方原本应该是什么样子后我们需要对应的字符输入进去来完成压栈,这里我不推荐去网上找ASCII码转换,因为有部分字符无法被识别会导致出错,比如这个FF,对应的码是ÿ,但是从程序中输入进去会得出E3。不只这个,几乎所有转换表中靠后的不常见字符都只能得出E3的结果。而且4015会被当作一个字符,所以这里我推荐用一些工具来写,比如我用的UEStudio,可以直接在编辑中开启16进制模式。这个软件要收费的,但是都学逆向了,收费使用还是问题吗(*^_^*)。我们输入我们期望的内存结构,得到如下图

把结果复制到记事本里,我们就知道输入的密码应该是:qqqqqqqqqqqqqqqqqqqq@。千万不要按照顺序直接复制,栈是从后往前压的,所以每一行要反过来,但不要整个全反过来。复制到程序中结果是:qqqqqqqqqqqqqqqqqqqq^U@。最后我们就得到了:

此时,无论返回上方的过程有多荒谬,程序都会直接跳转到成功结果。 

可是这种办法似乎也需要我们去分析程序结构,并没有办法靠一个简单且固定的流程来利用这个漏洞,而且我们甚至需要知道程序本身有哪些功能,还要去一个一个找,这是很麻烦的。哪怕我们可以通过这种方式去调用本地功能和代码,仍然需要我们去看反编译结构。并且在实际环境中,程序加载的地址往往是动态的,写死位置后能不能运行全看运气,因此我们需要一个简单的方法来定位我们需要执行的代码。

Shellcode

shellcode属于用机器语言编写的payload,其目的是为了获取控制计算机的某些功能的权限。我们可以通过调用动态链接库中的某些功能来实现某些操作,比如弹窗病毒,今天我们就来写个弹窗试试。

首先我们要知道,弹窗是从哪来的,弹窗来自于user32.dll的messagebox文件,一个Windows执行程序并不包含完整的代码,部分通用元素会被放在dll中,等程序运行到一定程度时被调用,就像一个个固定的积木模块,这样的好处是可以极大程度地减小程序的体积,并且在更新时只更新特定部分,而不需要整个程序重下一遍。我们要做的事情就是控制这个由积木堆起来的程序的其中一个功能模块。

所以我们要修改一下代码,添加user32.dll,目的是为了我们能找到这个功能的的实际地址,当然也可以使用depends随便加载一个有窗口的pe文件来得到。

#include <windows.h>//准备调用dll

LoadLibrary("user32.dll");//循环前加载dll

然后,我们需要将C输出成为一个32位的exe文件,因为我们的64位地址中包含了大量的00,这会导致我们的字符从00处直接截断,而且我们也没有办法直接向内存中输入null来占位。这需要大量的寄存器操作,没有必要。另外根据ASCII码的对照表,0-1开头的16进制都是某种指令,无法通过字符直接输入,因此我们需要去寻找除这以外的合适的地址。而转码本身又有太多问题,不同工具不同设备不同架构转出来的结果可能完全不一样甚至根本无法互相识别,有时候甚至需要字节拼接来实现完整指令,这需要灵感和反复尝试。因此shellcode的构造本身存在一定运气成分。学逆向就要做好长时间卡死无任何正反馈的准备。下面是32位程序的输出方式:

gcc -o c.exe -m32 test.c

此处补充个很简单的规则,当我们输入了某个东西的时候,内存会在原本的栈上压入我输入的内容,也就是添加一块区域来运行程序,运行结束后就释放掉。压进来的这个123456会被视作一个整体,哪怕它并没有完全填充这个栈段,整个段仍然会被当作一个整体完全执行,然后直接开始执行verify。

而在第二步之后,esp就会指向verify的栈前帧return到指定地址去执行完另一个函数,然后顺移一位开始verify。跟上面一样,我们要做的是覆盖掉这个return。在这里我们要覆盖掉的是第二个返回地址,不是第一个,原因上面说了,我们可输入的内容必须是可持续的,不可能跳过几行再输入内容,方法并不是没有,而是这需要我们完整还原出中间跳过的部分,根本没有必要,干脆全部覆盖掉更方便。

我们先构造一下code

xor ebx,ebx //清零作截断符
push ebx //压入截断部分,目的是为了截断下面两半文字和后面的部分
push 1953719671 //输入弹窗文字的前半
push 1818845542 //输入弹窗文字的后半
mov eax,esp //前面压入后esp转到了文字开头部分,也就是栈顶,将此时文字的地址储存到eax中,因为ebx截断EIP偏移会刚好自动涵盖全部文字内容

//将eax内部的地址放入栈中,利用ebx将弹窗文字地址和下方文字内容以及上方调用弹窗的部分隔开
push ebx
push eax
push eax //其实调用只需要一个就够,这里写了两个是为了后面的地址完整在一行,否则就会出错。
push ebx

mov eax,11816414780 //放入弹窗库地址
call eax //调用
push  ebx
mov eax,117212112208 //程序结束库地址
call eax //结束

对应的16进制是

33 DB
53
68 77 65 73 74
68 66 61 69 6C
8B C4

53
50
50
53

D8 50 93 A4 76
FF D0
53 
B8 D0 70 D4 75
FF D0

上面应该将得比较详细了,这里说明一下,我们在构造代码的时候不只是需要看代码和数字的对应关系,还需要看看内存里面的部分地址结构,上面之所以需要连续压两个eax是为了保证后面的地址完整:否则地址就会进位,程序就会出错:

这里说一下动态链接库的各个函数地址去哪里找,如果需要某个函数,我们直接写一个极简程序来调用需要的函数,运行它,然后直接在dbg中的symbol部分去搜就有了,此处我们已经调用了,所以直接能找到。

好了我们开始想办法把这串代码送到程序里并执行,我们的程序有一个输入口来让我们输入密码,但需要我们输入ASCII码来转换为16进制。同时,我们还需找一个程序本身自带的跳转函数来让我们可以通过跳转esp来直接定位shellcode,因此我们需要在栈头的return部分设定一个jmp esp指令的地址,直接在所有模块里搜索指令就行,选一个指令地址,还是老样子,不要选01开头的,也不要有00的,一个地址转码转不进去就换一个,总有一个行的。这里我们选择了这个。

然后就是老样子,通过二进制转码得到我们需要输入的ASCII码这个地方说明一下,并不是随便一个工具构造好了就可以随便用,我的末尾多了个41 42 43 44出来,原因在于我根本没法输入D0,因为计算机把D0对应的字符Ð当作了D来处理,必须让它和别的字符一起拼接才能在栈中转换成D0。41是A,但是我这里BCD有了没有A,说明和D0拼接了。这次是在末尾,而且并不影响弹窗,下一次很可能是在中间出现无法被识别的情况,届时,我们也需要去拼接构造code。

此时,有了这串字符,我们就可以通过向程序输入字符来使它出现弹窗。此时无论PE被如何重新转载,我们的这串字符就可以直接对它起作用。

自言自语

从上面的过程中可以看出,我们能借用栈溢出在不改变程序的情况下直接迫使程序本身发生变化,那么我们是否可以使用一个自动化程序,主动地获取必要信息,填充到shellcode的固定位置然后获得自动输入到指定位置从而达成破解程序甚至借用程序获取远程操控计算机的能力呢?比如一个简单的bat。因为这个程序实在有点太简陋了,但这里遇到一个问题,自动注入字符过长了会直接崩溃,但是在程序里输入又是正常的,我的理解是,从命令框直接输入的字符和通过记事本等脚本程序输入的字符是有区别的,编码错误导致了程序崩溃,我懒得看了,这个编码问题已经让我头大很久了,好不容易找到个方式解决,后面再说吧。

这里有个现实的例子,就是我们在转码时使用的UEStudio,它的破解教程中最开始是无法匹配程序,但是通过破解程序打开dll后过一会就直接破解成功了,原理应该是一样的,通过获取自带dll中某些程序地址,然后自动装载运行注入来破解程序。

后面应该会看看通用shellcode怎么开发,怎么更简单地完成攻击过程。会用工具的人很厉害,但是会做工具的人更厉害

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值