参考: xz.aliyun.com/t/3851 www.freebuf.com/column/1746…
虚拟机组成
vm_init
vm_start
vm_dispatcher
handle
opcodes
DDCTF2018黑盒题目题解
lebel:自构opcode / 类似handle表 / vm_dispatcher / 操作新开辟空间的某偏移的值 / 搞懂opcode和handler /for嵌套
程序流程
- 输入password,这里要输入解压出的那个txt文件名flag-xxx.txt的xxx的部分
- 构造opcode序列,调用相关handler,使程序 PaF0!&Prv}H{ojDQ#7v= 变为Binggo 输出Binggo字样
- 输出txt中的flag
main函数分析
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 result; // rax
__int64 v4; // rbx
int i; // [rsp+Ch] [rbp-84h]
FILE *stream; // [rsp+10h] [rbp-80h]
char s[8]; // [rsp+20h] [rbp-70h]
int v8; // [rsp+28h] [rbp-68h]
__int16 v9; // [rsp+2Ch] [rbp-64h]
char v10; // [rsp+2Eh] [rbp-62h]
char filename[8]; // [rsp+30h] [rbp-60h]
__int64 v12; // [rsp+38h] [rbp-58h]
__int64 v13; // [rsp+40h] [rbp-50h]
int v14; // [rsp+48h] [rbp-48h]
__int16 v15; // [rsp+4Ch] [rbp-44h]
__int64 ptr; // [rsp+50h] [rbp-40h]
__int64 v17; // [rsp+58h] [rbp-38h]
__int64 v18; // [rsp+60h] [rbp-30h]
__int64 v19; // [rsp+68h] [rbp-28h]
__int64 v20; // [rsp+70h] [rbp-20h]
unsigned __int64 v21; // [rsp+78h] [rbp-18h]
v21 = __readfsqword(0x28u);
*(_QWORD *)filename = 0LL;
v12 = 0LL;
v13 = 0LL;
v14 = 0;
v15 = 0;
*(_QWORD *)s = 0LL;
v8 = 0;
v9 = 0;
v10 = 0;
ptr = 0LL;
v17 = 0LL;
v18 = 0LL;
v19 = 0LL;
v20 = 0LL;
puts("Please input your password!");
fgets(s, 15, stdin);
BYTE2(v8) = 0;
for ( i = 0; i <= 9 && isalnum(s[i]); ++i ) // 检查前十位为数字或字母
;
if ( i == 10 )
{
sprintf(filename, "flag-%s.txt", s); // 存flag的文件
stream = fopen(filename, "r");
if ( stream )
{
fread(&ptr, 23uLL, 1uLL, stream); // 文件里读取23个字节
LOBYTE(v19) = 0;
fclose(stream);
puts("---------------------[Welcome To ReverseMe!]---------------------");
puts("\n\nPlease Input Your Passcode,If You See print the \"Binggo\" string,Congratulations,You Win. Good luck!");
v4 = operator new(0xAA0uLL);
sub_401E98(v4); // 初始化v4这块空间
if ( (unsigned int)sub_4016BD(v4) == 0 ) // 程序能正常输入,这步在输入前,所以函数返回值为1
// 看了下大致是一个数组的初始化
{
printf("Error code:%x\n", (unsigned int)dword_6038E0);
exit(0);
}
fgets((char *)(v4 + 16), 100, stdin); // 二次输入
if ( memcmp("exit", (const void *)(v4 + 16), 4uLL) )
{
sub_401A48(v4);
if ( byte_603F00 ) // flag,交叉引用定位关键点
printf("Success!\nYour flag is %s\n", &ptr);
else
puts("Failed!");
}
sub_401B8B(v4);
result = 0LL;
}
else
{
puts("Could not read the flag! Please check your password.");
result = 1LL;
}
}
else
{
puts("Invalid password!");
result = 1LL;
}
return result;
}
看见一个类似flag标志位的地方if(byte_603F00) printf(success)
交叉引用到off_603840,main没调用,看着像handler
off_603840交叉引用到sub_4016BD,下断,动态调试发现在次断了9次,应是init opcode和对应的handler
分析opcode和handler的具体值
sub_401A48的函数指针和循环结构引起了我的注意,很像vm_dispatcher的流程
- for ( j = 0; j <= 8; ++j ) 对应 for(i=0 ; i < OPCODE_N ; i++) ,OPCODE_N = 9
- ( byte_603900[v2] (替换) == *(_BYTE *)(a1 + *(int *)(a1 + 4 * (j + 0x48LL) + 8) + 0x198) ) 对应 if(*cpu->eip == cpu->op_list[i].opcode) ,后者存的是所有opcode,此处下断获得具体值
- *(_QWORD *)(a1 + 672) = *(_QWORD )(a1 + 8 * ((int *)(a1 + 4 * (j + 72LL) + 8) + 0x54LL) + 8);// 后者存的是所有handler,此处下断获得具体值
- (*(void (__fastcall **)(__int64))(a1 + 672))(a1) 对应 cpu->op_list[i].handle(cpu); 这里一定要调用sub_40133D修改flag
//vm_dispatcher
for(i=0 ; i < OPCODE_N ; i++)
{
if(*cpu->eip == cpu->op_list[i].opcode)
{
cpu->op_list[i].handle(cpu);
break;
}
}
__int64 __fastcall sub_401A48(__int64 a1)
{
char v2; // [rsp+13h] [rbp-1Dh]
int i; // [rsp+14h] [rbp-1Ch]
int j; // [rsp+18h] [rbp-18h]
if ( a1 && a1 != -16 )
{
for ( i = 0; i < strlen((const char *)(a1 + 16)); ++i )// 循环输入长度次,相当于取所有opcode输入
{
v2 = *(_BYTE *)(a1 + i + 16); // 第i个字符
if ( i + 1 != strlen((const char *)(a1 + 16)) )
*(_BYTE *)(a1 + 664) = *(_BYTE *)(a1 + i + 1 + 16);// i+1个字符
for ( j = 0; j <= 8; ++j ) // 9 = OPCODE_N
{
if ( byte_603900[v2] == *(_BYTE *)(a1 + *(int *)(a1 + 4 * (j + 0x48LL) + 8) + 0x198) )// 后者存的是opcode
{
*(_QWORD *)(a1 + 672) = *(_QWORD *)(a1 + 8 * (*(int *)(a1 + 4 * (j + 72LL) + 8) + 0x54LL) + 8);// 后者存的是handler
(*(void (__fastcall **)(__int64))(a1 + 672))(a1);// 函数指针,执行Handle,一定要执行到sub_40133D修改flag
}
}
}
}
return 0LL;
}
下断后opcode具体值:
# 2A 27 3E 5A 3F 4E 6A 2B 28
# *'>Z?Nj+(
因为存在一次表替换,所以可得opcode对应的输入:
import sys
begin = 0x603900
end = 0x603900+512 #数组大小
l = ['*','\'','>','Z','?','N','j','+','(']
for j in l:
for i in range(32,127):#可见字符串范围
if(chr(Byte(begin+i)) == j):
sys.stdout.write(chr(i))
# $8Ct0Eu#;
下断后handler的具体值:
func1 0x0000000000400DC1 $对应
func2 0x0000000000400E7A 8对应
func3 0x0000000000400F3A C对应
func4 0x0000000000401064 t对应
func5 0x00000000004011C9
func6 0x000000000040133D #改变了flag
func7 0x00000000004012F3
func8 0x00000000004014B9
func9 0x0000000000400CF1
分析handler的功能(分析opcode的功能)
静态分析,发现很多*(_BYTE )((_QWORD *)(a1 + 8) + *(int *)(a1 + 288))类似的变量,动态调试,确定其功能如下
- +655 临时变量var
- +664 下一位输入
- +8 str (PaF0!&Prv}H{ojDQ#7v=)
- +288 index
- +292 0xFF,变量不能超过char范围
- +280 0x110在func8中*(_BYTE )((_QWORD *)(a1 + 280) + *(int *)(a1 + 288) + *(_QWORD *)a1)实为str[index]
- +16 输入的字符串 func8中*(_BYTE *)(a1 + 16+ *(int )(a1 + 288) + (__int64)(char *)(a1 + 664) - 48)实为input[index+下一位输入-48]
分析功能
func1 var = str[index] func2 str[index] = var func3 var = var+下一位输入-33 func4 var = var-下一位输入+33 func5 index++ func6 下一位输入需要为s,修改flag func7 index-- func8 index++ ; index-- ; str[index] = input[index+下一位输入-48]-49 func9 for(i=0;i<下一位输入;i++) index++; str[index] = input[index+下一位输入-48]-49
构建opcode序列
$t/80表示: var = str[0] var = 80 var = var - input[2] + 33 = 66 str[0] = var index++
类似,可构建产生Binggo的opcode序列为 $t/80$C)80$CI80$CX80Cg80\Cj8
处理多余位数 0#J1
index-- uuuuuuu
调用func6 Es
其它一些点
嵌套for流程图