本文是对计算机系统基础第三次实验的复现,以此为基础讲解栈溢出的相关知识。
基本逻辑逆向
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 |