题目描述里提到c编译的时候有警告,但是没报错
先看c源代码
#include <stdio.h>
#include <stdlib.h>
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;
}
发现login函数里面两次调用scanf函数,
传进去的参数不是&passcode1和&passcode2
而不加&的的话,
传进去的不是变量passcode1和passcode2的地址,
而是他们的值本身,
这样scanf就可能写到奇怪的地方去
就会导致出错
同时login函数中并没有对passcode1和passcode2的值进行初始化,
意味着,
如果我们能够控制passcode1和passcode2所在的那一片内存的值,
就可以让这两个变量成为指向内存中某处的指针,
从而通过scanf对那片内存进行修改
首先要考虑如何控制passcode1和passcode2的值,
注意到main函数中连续调用了welcome和login函数,并且都没有传入参数,
意味着他们的栈底将会是同一个位置,
并且welcome在login函数之前,
welcome中还有一个长度100的局部变量字符串,
先写入这个字符串,
此时passcode1和passcode2将来所处的位置就会有初始值,
welcome函数返回后,
main函数再调用login函数,
而login函数没有对passcode1和passcode2变量进行初始化,
即意味着我们能够控制passcode1和passcode2的值
最理想的情况当然是同时控制passcode1和passcode2,
这样在login函数中的if判断就会成立,
直接读取到flag
使用objdump -d查看elf文件,
welcome函数在8048639处通过plt表跳转调用了scanf,
结合栈的结构,判断此前的两条mov指令是在把参数写到栈中,
那么之前的804862f处通过lea指令向edx寄存器装入的地址应该就是第二个参数,
即长度为100的char数组的首地址为ebp-0x70,
再看login函数
8048586和80485b4两次调用了scanf,
结合c源代码中看到的程序流程,
判断第一次跟passcode1有关,第二次跟passcode2有关,
看804857c处,passcode1的地址是ebp-0x10,
看80485aa处,passcode2的地址是ebp-0xc,
welcome和login在执行过程中的ebp应该是一样的,
name[100]的地址和passcode1相差96个byte,跟passcode2相差100个byte,
又因为passcode1是int型,四个byte,
所以name[100]的最后四个byte的位置刚好就是passcode1的位置,
因此通过name[100]只能控制到passcode1,对passcode2无能为力,
既然如此,想要让login中if判断为真的想法就泡汤了,
参考https://zhuanlan.zhihu.com/p/130271689,
简单复习一下plt表和got表,
plt和got用在动态链接过程中,
获取数据段存放函数地址的那一小段代码称为PLT(Procedure Linkage Table)过程链接表,
存放函数地址的数据段称为GOT(Global Offset Table)全局偏移表,
除了plt[0]和got[0]、got[1]、got[2]有特殊作用外,
plt表和got表一一对应,
例如plt[1]对应got[3];
除第一项外,plt中的每个表项都是三条指令,以现在这个passcode程序为例,
例如调用printf时,call printf@plt就会来到8048420,
8048420是jmp指令,跳转到804a000处存储的地址,
而804a000就是got[3]所在的位置,
当目标函数没被调用过时,
got中存储的是对应的plt表项的第二条指令,
以这个题目中的printf为例,
在还没调用过printf时,
got[3],即804a000处实际存的值是8048426,即plt[1]中第二条指令的位置,
在第一次调用时,
call printf@plt来到8048420,是jmp指令,
随后jmp到got[3]保存的地址,
相当于jmp到8048426,
接着往下执行,
第二条指令通过push压入所需的参数,
然后第三条指令jmp到plt[0]
plt[0]中两条指令,
第一条压栈,
第二条跳转到got[2]处所存的地址,即是动态链接函数的入口地址,
而动态链接函数会根据已经压入栈的参数,
找到共享库中对应函数的地址,写回到got[3],
此后再调用printf函数,
过程就是call printf@plt -->plt[1]第一条指令jmp *got[3] --> printf函数入口,
相比之下,
第一次调用时则是,
call printf@plt -->plt[1]第一条指令jmp *got[3] --> plt[1]第二条指令压栈 --> plt[1]第三条指令jmp plt[0] --> plt[0]第一条指令再压栈 --> plt[0]第二条指令jmp *got[2] --> 进入动态链接函数 --> 回写got表&调用目标函数
现在就有完整的思路了
通过welcome函数控制passcode1的初始值,使其值为got[3]的地址
->
利用login函数中第一次scanf函数,将got[3]的值改成login函数中调用system函数处的地址
->
login函数中第二次调用printf时,由于got[3]已经被我们篡改,就会跳转到login中system函数调用
落实到细节,
got[3]的地址是0x0804a000,
考虑小端法存储,
那么welcome中的name就应该是任意96个字符+\x00\xa0\x04\x08,
而在login函数中,
调用system函数的地址是80485ea,
但实际上应该考虑到前一条将参数压栈的指令,
也就是0x080485e3,
scanf用的%d,
需要转成十进制,即134514154