本文是对计算机系统基础第三次实验的复现,以此为基础讲解栈溢出的相关知识。
基本逻辑逆向
main函数
这里比较引人注目的是signal函数,先执行了三个该函数,又引入标准输入的文件描述符为infile,以下链接比较深入讲解了signal函数,读者可详细了解。
1 |
|
以下是一个可能的输出
1 | Going to sleep for a second... |
总之,这段利用sign函数来实现了一些违规输入的提示。包括段溢出,进程繁忙和错误指令。例如下面是段溢出错误的提示。
接下来,程序会去走一个循环。
getopt()function in C to parse command line arguments
getopt()函数原型:
1 | getopt(int argc, char *const argv[], const char *optstring) |
如果选项带有一个值,那么该值将由
optarg指向当没有更多选项可以处理时,它将返回-1
返回“?”表明这是一个无法识别的选项,它将其存储到
optopt中。有时某些选项需要一些值,如果选项存在但值不存在,那么它也会返回“?”。
- 我们可以使用 ‘:’ 作为
optstring的第一个字符,这样,如果没有给出值,它将返回 ‘:’ 而不是 ‘?’。
1 |
|
以下是输出
1 | Given Option: i |
因此,很容易为这段循环设置下面这段注释
1 | while ( 1 ) // 无限循环 |
1 | void __usercall __noreturn usage(const char *a1@<eax>) |
最后一段,要求必须有userid,即执行bufbomb程序的时候必须添加-u参数。
后面初始化bomb炸弹,打印Userid和Cookie。重点看下下面这段的汇编,cookie放eax寄存器以后,放到栈顶,作为seed执行_srandom函数和_random函数,该函数的返回值与0xFF0按位与运算后将结果加上256,比较符合C伪代码,并将结果放到esp+18的内存地址,后面将4和edi里存的数据作为参数入栈,查看edi(v3)的引用信息,发现默认为1,在传入-n参数时修改为5。
使用_calloc函数分配好堆空间以后,把分配空间的内存地址放到eax寄存器中,并把堆空间中首元素置零,ebx寄存器赋1,跳转到loc_80491BB。后面分析下来基本和C伪代码保持一致。重点在于-n参数会把v10(默认为0)参数改为1,会把v3(默认为1)参数改为5,从而影响到分配到分配到的v5的大小,这里我将其改名为arry,为了便于理解,将其他参数也改名。
1 | launcher(launcher_flag, arry[j] + ran_cookie); |
这是修改后的各个参数的名字,后面我们重点分析launcher函数即可。看下launcher函数的C风格的伪代码
可以看到,launcher_flag实际上是开启nitro这一关的全局变量,arry[j] + ran_cookie随机数当作全局偏移量量出现。
1 | int __cdecl launcher(int a1, int a2) // 定义一个名为 launcher 的函数,它接受两个整数参数 a1 和 a2 |
重点看下mmap分配的空间,应该注意到,(int)&unk_55685FF8实际上是mmap()函数分配的内存空间的reserved的最后一段。下面是程序头表(Program Header Table,PHT)的条目,它是在可执行和链接格式(ELF)文件中找到的。这个条目提供了加载(LOAD)段到内存中的必要信息。注意这里的 _reserved 物理地址这刚好是0x5586000,分配的权限为6(写和读),大小是0x100000。
有关信息与上图所示,重点是对一些变量的赋值和分配堆空间。重点函数是launch,看下C风格的伪代码。下面是入栈信息,可以看到参数并未放栈上,而是放在了eax和edx寄存器中了。
看下函数本体。
C伪代码
1 | unsigned int __usercall launch@<eax>(int global_nitro@<eax>, int global_offset@<edx>) |
进去testn()函数和test()函数看一下,大体可以分辨这两个函数为漏洞函数。testn()需要-n参数触发,test()函数不需要。大体知道这些就够了,具体的漏洞原理流程在每个部分具体说明。
Smoke
可以看到,Gets函数最多能读取1024个字节,而数组大小仅仅为40个字节,因此,可以传入44个字节装满数组并覆盖rbp,4个字节覆盖返回地址,使得跳转到指定的返回地址,本题要求是跳转到Smoke函数,仅仅跳转过去就完成了目标。
原理主要是因为leave指令相当于
1 | mov esp, ebp |
ret指令相当于
1 | pop eip |
注意:这里的
eip是不能直接操作的,所以上述代码只是为了解释ret的功能,并不能直接在代码中使用。
返回点在如图所示的高亮部分。
Fizz
本题还要求cookie要与栈上一个值相等。
这是修改后的各个参数的名字,后面我们重点分析launcher函数即可。
这段除了launch以外都是分配内存与变量赋值的操作,重点看下launch函数,逻辑也很简单,开-n参数执行testn,不开-n执行test函数
核心原理
leave指令
1 | push esp,ebp |
ret指令
1 | pop eip |
Smoke
函数在执行
getbuf后不返回1,而是转向Smoke函数
容易知道,Gets函数最多能写入1024个字节,而v1仅仅开辟了40(0x28)个字节的空间。因此,输入可以用44个字节覆盖整个v1和ebp,再用4个字节覆盖返回地址,使函数跳转到Smoke执行。
实操:生成cookie
二进制文件Smkoe
验证
Fizz
跳转思路与Smoke一致,不过这里还需要与栈上数据比较一下
那就在后面再加上几位cookie即可
执行结果
Bang
要求与全局变量进行比较,修改下全局变量
在跳转bang之前需要执行的汇编指令如下:
1 | push 0x3962b26d,%eax |
Nitro
通过第一阶段的基本逻辑分析容易知道:只有在执行-n程序的时候添加-n参数,该阶段才会被开启。
这里testn函数如果想正常返回,需要满足两个条件
- 栈帧不被破坏
getbufn函数返回cookie
我们现在来寻找漏洞点来注入我们的攻击指令。
我们前面分析了Gets函数的实现,容易知道,这里是存在栈溢出漏洞的。依照前面的思路,我们用520个字符填满v1数组,用4个字符覆盖ebp,用四个字符覆盖getbufn的返回地址。由前面的基本逻辑逆向,我们知道,该函数实际上需要执行5次,每次执行时的栈状态不一致,因此,我们选择使用nop指令来填充输入中除了攻击指令其他位置,以此保证只要返回地址跳到任意一个nop指令上,程序都会执行我们的攻击指令。
以上的分析结束以后,是时候来生成攻击指令了。
先写出汇编代码
1 | lea 0x28(%ebp),%esp |