要点
连续调用
连续调用,这就隐含了它们的ebp是相同的(栈底相同);
GOT表:
概念:每一个外部定义的符号在全局偏移表(Global offset Table)中有相应的条目,GOT位于ELF的数据段中,叫做GOT段。
作用:把位置无关的地址计算重定位到一个绝对地址。程序首次调用某个库函数时,运行时连接编辑器(rtld)找到相应的符号,并将它重定位到GOT之后每次调用这个函数都会将控制权直接转向那个位置,而不再调用rtld。
PLT表:
过程连接表(Procedure Linkage Table),一个PLT条目对应一个GOT条目
当main()函数开始,会请求plt中这个函数的对应GOT地址,如果第一次调用那么GOT会重定位到plt,并向栈中压入一个偏移,程序的执行回到_init()函数,rtld得以调用就可以定位printf的符号地址,第二次运行程序再次调用这个函数时程序跳入plt,对应的GOT入口点就是真实的函数入口地址。
动态连接器并不会把动态库函数在编译的时候就包含到ELF文件中,仅仅是在这个ELF被加载的时候,才会把那些动态函库数代码加载进来,之前系统只会在ELF文件中的GOT中保留一个调用地址.
GOT覆写技术:
原理:由于GOT表是可写的,把其中的函数地址覆盖为我们shellcode地址,在程序进行调用这个函数时就会执行shellcode。
以上姿势来源: http://jing0107.lofter.com/post/1cbc869f_8b3d8a5
解题
连接过去1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void login(){
int passcode1;
int passcode2;
printf("enter passcode1 : ");
scanf("%d", passcode1); //注意这里没有取地址
fflush(stdin);
// ha! mommy told me that 32bit is vulnerable to bruteforcing :)
printf("enter passcode2 : ");
scanf("%d", passcode2); //同样没有取地址
printf("checking...\n");
if(passcode1==338150 && passcode2==13371337){
printf("Login OK!\n");
system("/bin/cat flag");
}
else{
printf("Login Failed!\n");
exit(0);
}
}
void welcome(){
char name[100];
printf("enter you name : ");
scanf("%100s", name);
printf("Welcome %s!\n", name);
}
int main(){
printf("Toddler's Secure Login System 1.0 beta.\n");
welcome();
login();
// something after login...
printf("Now I can safely trust you that you have credential :)\n");
return 0;
}
很明显我们要让passcode1和passcode为指定的值就行了。
但是我们尝试输点东西,又发现会报错。看到提示Segmentation Fault
段错误应该就是访问了不可访问的内存,这个内存要么是不存在的,要么是受系统保护的。
我们仔细再看下发现,输入passcode1和passcode2时,没有取地址符号,说明这不是平常的赋值。我们输入的是passcode的地址,那么
猜想1:我们要知道哪里存放着我们想要的值,把这个值的地址输进去就可以了
猜想2:或者有没有可能用什么溢出,我们自己在welcome()函数执行过程钟,把这两个特定值输进去,然后再指向他们?
看题解去
猜想都不对,我还是太菜了。
要查看他的保护,我们看下 有栈溢出保护
思路
利用got覆写技术:
将一个GOT表中的函数地址写到栈中,用来充当scanf()取的地址,然后把system(“/bin/cat flag”)这条指令的地址写到这个GOT表中的函数。
当这个函数被调用时,就会直接执行system(“/bin/cat flag”)
进一步探讨:
password1_addr=ebp-0x10,name和passcode1相差96个字节,所以name后4个字节正好可以覆盖到passcode1。因此可以把passcode1的地址覆盖成fflush或者printf或者exit的地址,然后利用scanf函数把system的地址覆写过去。这样等调用fflush或者printf或者exit的就调用成了system。
解题
我们看到scanf处没有加取地址符号(&),而且程序有没有对passcode1和passcode2 进行初始化;但看此时主函数中对welcome()和login()连续调用,这就隐含了它们的ebp是相同的;所以或许我们可以重这里利用;
我们用objdump -d passcode查看passcode这个可执行文件的汇编代码,这里我们主要看welcome()函数和login()函数的汇编
我们看到welcome()函数处的0x8048643处的代码,lea -0x70(%ebp),%edx;相当于edx=[ebp-0x70];冲这里我们知道name的地址是保存在ebp-0x70处;而在下图中0x8048586处的前3行处有mov -0x10(%ebp),%edx;可知passcod1在ebp-0x10处,因此在调用welcome处的字符串处我们输入(0x70-0x10)=96个字节,再覆盖四个字节就是passcode1的值,这里如果我们把passcode1的值覆盖为任意地址就可以实现任意地址写了;
这里我们将passcode1处的值写什么我们才能读到flag呢?
scanf(“%d”,passcode1)处的后两句又对printf调用,因此我们想能不能覆盖printf的地址呢,把printf的地址覆盖为
接下来我们用objdump -R passcode查看printf的地址
我们看到printf的指针保存在0x804a000处
可以将passcode1的值改为printf的地址(0x804a000),然后接下来会通过scanf将上面的关键系统命令的地址写进去,改变整个程序的执行过程,当程序调用
printf函数的时候,由于它的地址已经被改变了,所以会跳到关键系统命令的地方去。1
280485e3: c7 04 24 af 87 04 08 movl $0x80487af,(%esp)
80485ea: e8 71 fe ff ff call 8048460 <system@plt>
将它(printf)的地址改为 0x080485ea这个关键的地方
这里使用pwntools这个包来写利用脚本。因为scanf是要求%d输入,所以0x080485ea == 134514154
答案的分析:
1 | python -c "print ('a'*96+'\x00\xa0\x04\x08'+'\n'+'134514147\n')" | ./passcode |
这里主要分析’\x00\xa0\x04\x08’+’\n’+’134514147\n’.
0x0804a000是print函数的got表中的地址,在welcome中输入96个a+’\x00\xa0\x04\x08’.结果呢,login中的scanf函数在输入的时候,直接变量的地址就是:0x0804a000,输入的134514147(0x080485e3),将0x0804a000原有的地址给覆写了.这就是got表复写.在执行printf函数的时候,实际上执行的是system函数.
简单的说:
plt—->got—->真地址(printf函数的代码),经过我们覆写之后,
plt—->got—->假地址(system函数的代码).
这样我们就获得了shell.
为什么输入的内容刚好覆写printf函数的got表呢?
原因是:scanf(“%d”,a);无&符号.听别人讲:如果scanf没加&的话,程序会默认从栈中读取4个字节的数据当做scanf取的地址.这个栈应该是变量a的栈.就是说将a的值,16进制化作为地址,将你输入的内容写入这个地址中.如果这个地址不存在或不可写,将会报错.
参考
scanf忘记加’&’危害有多大? 详解GOT表覆写攻击技术
https://bbs.pediy.com/thread-247956.htm
https://www.cnblogs.com/binlmmhc/p/6189514.html
https://blog.csdn.net/qq_33528164/article/details/70505151
文章http://jing0107.lofter.com/post/1cbc869f_8b3d8a5