Attack Lab

内容纲要

ICS还是比较有特色的,布置的作业大题到目前为止都很好玩,第一个大题叫做Datalab,要求你使用很简单的运算(位运算,逻辑运算等)实现一系列功能。第二个大题是Bomblab,你需要在虚拟环境中通过反向工程为“炸弹程序”构造拆除密码,炸弹爆炸还会扣分(紧张刺激)。第三个大题是Archlab,要求通过优化一个简单的流水线处理器结构以及汇编代码来对六行C代码进行极限优化(真·极限·优化)。

而这次的作业叫做Attacklab,实际上这是我完成得最快的lab,不过它的确很好玩,写一篇博客记录一下。

第一题

顾名思义,Attacklab是一个渗透类型的作业大题。实际上,Attacklab要求你使用简单的方法(缓冲区溢出攻击)对一个程序进行攻击,改变其行为,让它执行你想要执行的代码。

缓冲区溢出攻击的原理很简单,通过程序缺陷,构造输入数据让程序在栈上产生缓冲区溢出,将栈上的返回地址覆盖成攻击者想要执行的代码地址,来改变程序的行为。如果攻击得当,威力强大。比若一个内核程序出现了内存溢出漏洞,那么成功的攻击就相当于攻击者以最高的内核权限执行它想要的代码...╮(╯▽╰)╭唉嘿嘿。

Attacklab相对其它lab友善很多,坡度更缓,第一个Attack要求你攻击一个有漏洞的函数:

//汇编翻译为c代码如下
void getbuf () {
    char buf[BUFSIZE];
    gets(buf);
}

你需要构造输入,攻击它,让这个函数返回时跳转到一个从未被执行的touch1()函数内,即可通关,拿到10分。

通过objdump -d反汇编,阅读代码可以了解到touch1()的地址与buf长度,构造一个长度略长于缓冲区的字符串,在字符串末尾添上小端表示法的touch1()函数地址,字符串在内存溢出后会覆盖掉栈上的返回地址,控制程序跳转到我们想要执行的函数。顺利通关。

第二题

第二个Attack仍然要求你攻击相同的函数,但是要求返回到函数touch2(int code),其中code必须为某一个正确的常数T,才能通关。

那么就需要代码注入(Code Injection)了。

首先我们依然通过类似的方法改掉输入函数的返回地址,不过这一次,我们将返回地址修改为栈顶。那么函数在返回后,处理器会从栈上继续读取指令,而栈的内容是我们可以注入的——构造输入数据,将我们想执行的代码转换为机器码,再转换为字符串输入,就成功的把代码注入到了栈上。

于是搞一段汇编,将%rdi也就是第一参数寄存器修改为T。由于x86-64体系的jmp指令格式复杂,于是将touch2(int)的地址推入栈内,再一次使用ret跳转到touch2(int)函数,就能完成正确的调用,顺利通关。

第三题

如果栈没有被有效地保护,攻击者是可以肆意妄为的——任何代码都能被轻松注入。第三个Attack要求攻击同样的输入函数,来达到调用touch3(char*s),且字符串s必须与T输出为16进制数字的样子一样。

这也很简单,不过需要注入更多的代码。T是32位整数,那么其十六进制表示的字符串最多有8个字符,占用64个二进制位。我们决定将字符串储存到栈上,先使用一条注入的movabsq指令在栈上写入字符串,然后获取地址并填写到第一参数寄存器内,使用推栈-返回跳转到touch3(char*)实现调用,总体来说也很简单,于是又拿到十分,通关。

第四题

从第四题开始,事情变得有意思了。现代计算机往往对栈空间有硬件级别的保护——栈内(较高地址内存区域)的数据是“不可执行”的。CPU会在硬件级别实现对这段内存的保护,软件级别的简单的向栈注入代码的攻击不可能实现。同时编译器也会将栈的开始地址进行随机偏移,让攻击者难以绝对定位在栈上的数据。

那么攻击就不可能实现了吗?还是能。

系统内存中往往有大量可执行的代码,动态链接库,系统函数,成百上千。而且对于一条指令,如果从不同的起点开始阅读,则可以有不同的解释方法,比如这条指令:

movl   $0x90909058,(%rdi)

它的机器码是:

c7 07 58 90 90 90

而第三个字节0x58则是popq %rax的机器码,接下来是三个0x90nop指令。这意味着如果我们通过某种方法让PC跳到第三个字节开始执行,那么实际效果相当于一个popq %rax

而有意思的是,如果这段二进制码后面紧接着一个0xc3ret指令),那么我们只要通过传统的缓冲区溢出覆盖掉栈上的返回地址,让返回地址跳到上面第三个字节的位置,那么CPU会执行popq %rax并在几个nop之后执行ret,使得栈指针加8,并继续根据栈上的下一个返回地址进行过程跳转,执行下一条栈顶所存放的地址所指向的指令了。

我们所要做的,就是从程序的运行内存中找到足够多的“简单指令+返回值”的代码碎片,并把它们的地址注入到栈上,换掉一连串返回地址,就能达到正常的代码注入效果。向寄存器写任意常数的指令较难找到(因为常数比较长,难以匹配到),但是我们可以通过在栈上交替写入popq指令的代码碎片地址与我们想要的常数来达到mov $constant, %reg的效果。

这一次,硬件都保护不了你啦。

通过肉眼观察反汇编,找到了十几条我们能用到的代码碎片,于是一如既往地写爆buf,挑选代码碎片并将一串返回地址以合适的顺序注入栈内,第一遍爆炸了,通过gdb单步汇编发现了一个小错,改改输入后成功通关,攻击成功!单步的时候看这PC在我精心构建的攻击子串控制下到处乱跳,这一句那一句将所有的碎片拼成了我的hack程序,开心地拿到35分。

第五题

第五题是第四题的进阶版,得正确调用touch3(char*),需要注入的代码量翻三倍,写了一个指令碎片表,先利用指令碎片读取栈指针,然后通过指令碎片进行了一次栈指针加法,把第一参数寄存器设置为一个栈指针偏移值,而这个偏移值恰好指向栈上我存放的T的十六进制字符串表示数据片段,一通操作之后通关,+5 extra points。

第六题

为了防范缓冲区溢出攻击,编译器会在调用函数时,在栈上的函数返回地址与函数局部变量之间插入一个CPU产生的六十四位“金丝雀值”,并在返回时检验栈上金丝雀是否被破坏,如果金丝雀被破坏,那就说明存在一个栈上的顺序写入操作写爆了栈帧,践踏了金丝雀,并可能继续向高地址写入,破坏栈上的返回地址,那么程序在探测到这一点后,会立马异常终止,防范可能到来的攻击。

这是比较高明的防范措施,现代编译器会自动为“容易受到攻击”的函数加入金丝雀保护。但是,如果你的程序臭成这样:

void getbuf_canary () {
    int len = 0;
    char buf[128];
    char sbuf[128];
    gets(buf);
    memcpy(sbuf+len, buf, sizeof buf);
}

那天王老子都救不了你了...

可以看出该程序有严重的漏洞,分析反汇编代码可以发现,如果我们在写入buf时故意溢出128个字节之外,由于三个局部变量在栈上是连续存储的,溢出buf之后,溢出的开始四个字节会漫入len之内。这意味着我们能通过构造输入产生溢出来控制len的数值。假若金丝雀长度为8字节,通过观察反汇编代码得知栈帧的空填充长度为24字节,而sbuf首地址为栈指针偏移0x120,我们控制len为0x8+0x120,那么看似无伤大雅的memcpy函数会帮助我们跳跃写入,直接飞越栈上的金丝雀值,把整个掺杂了数据和代码碎片地址、充满恶意的buf字符串顺序拷贝到返回地址和返回地址之后的连续内存空间里。

金丝雀活得好好的,但是返回地址已经被我们漂亮地篡改了,在函数返回之后,这个程序的控制权将再次落入我们手中,故技重演,调用touch3(char*),拿到最后五分。

Attacklab全部通关,而现在,这些知识已经足够供给我们对一个现代编译器生成的程序发起攻击了。

此条目发表在有趣的作业分类目录,贴了, 标签。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注