先感谢40K0
师傅的指点,要不然本人真的做不出这道题来。
目录
文件清单 confused.zip
原题文件
confused.app.zip 就是原题文件。
源文件
- main.c
- vm.c
- code.h
- vm.h
原题文件核心程序: xia0Crackme
xia0Crackme.i64 为它的分析文件,IDA
分析文件来源于40K0
师傅。
模拟程序流文件
由于大部分人都没有
Mac
,所以本人基于原题的程序流,编译了两个版本的可执行文件,其行为应该基本与原题一致。
crackme 为Linux下编译的64为程序,带了debug Symbols。gcc 7.1编译。
crackme.exe 为Windows下编译的64程序,带了debug Symbols。编译器为 Microsoft (R) C/C++ Optimizing Compiler Version 19.16.27027.1 for x64。(MSVC),crackme.ilk、crackme.pdb为生成的对应的调试文件。
分析
刚拿到题目是有点搓手不急的,第一是因为是mac
程序,环境就满足不了,后面经大佬指点后才知道了,做这种题目不需要把全部代码都看懂,只要分析核心代码就好了,
[ViewController checkCode:]
if ((unsigned int)check(*((__int64 *)&v14 + 1)) == 1)
objc_msgSend(v17, "onSuccess");
else
objc_msgSend(v17, "onFailed");
主要是为了通过check
函数。
后面模拟的是虚拟器的运行,主要作用任然是为了混淆
我们。具体原理可以了解这篇文章:https://www.52pojie.cn/forum.php?mod=viewthread&tid=860237&page=1
。
虚拟机运行
__int64 __fastcall check(__int64 a1)
{
char vm; // [rsp+20h] [rbp-C0h]
__int64 v3; // [rsp+D8h] [rbp-8h]
v3 = a1;
memset(&vm, 0, 0xB8uLL);
init_vm((vm_struc *)&vm, (char *)a1);
return (unsigned int)sub_100001F00((vm_struc *)&vm);
}
__int64 __fastcall sub_100001F00(vm_struc *a1)
{
a1->pc = (__int64)&loc_100001980 + 4;
while ( *(unsigned __int8 *)a1->pc != 0xF3 )
run(a1);
free(buffer);
return (unsigned int)a1->buffer;
}
从上面可以看出,验证是否真确,主要取决于a1->buffer
。而a1->buffer
的值由下面的指令来决定。
__int64 __fastcall mov_buf_imm(vm_struc *a1)
{
__int64 result; // rax
result = *(unsigned int *)(a1->pc + 1);
a1->buffer = result;
a1->pc += 5LL;
return result;
}
首先将(__int64)&loc_100001980 + 4
内存的内容dump
下来,后面要分析的。dump
下来之后,数据如下:
code.h
unsigned char code_bin[] = {
0xf0, 0x10, 0x66, 0x00, 0x00, 0x00, 0xf8, 0xf2, 0x30, 0xf6, 0xc1, 0xf0,
0x10, 0x63, 0x00, 0x00, 0x00, 0xf8, 0xf2, 0x31, 0xf6, 0xb6, 0xf0, 0x10,
0x6a, 0x00, 0x00, 0x00, 0xf8, 0xf2, 0x32, 0xf6, 0xab, 0xf0, 0x10, 0x6a,
0x00, 0x00, 0x00, 0xf8, 0xf2, 0x33, 0xf6, 0xa0, 0xf0, 0x10, 0x6d, 0x00,
0x00, 0x00, 0xf8, 0xf2, 0x34, 0xf6, 0x95, 0xf0, 0x10, 0x57, 0x00, 0x00,
0x00, 0xf8, 0xf2, 0x35, 0xf6, 0x8a, 0xf0, 0x10, 0x6d, 0x00, 0x00, 0x00,
0xf8, 0xf2, 0x36, 0xf6, 0x7f, 0xf0, 0x10, 0x73, 0x00, 0x00, 0x00, 0xf8,
0xf2, 0x37, 0xf6, 0x74, 0xf0, 0x10, 0x45, 0x00, 0x00, 0x00, 0xf8, 0xf2,
0x38, 0xf6, 0x69, 0xf0, 0x10, 0x6d, 0x00, 0x00, 0x00, 0xf8, 0xf2, 0x39,
0xf6, 0x5e, 0xf0, 0x10, 0x72, 0x00, 0x00, 0x00, 0xf8, 0xf2, 0x3a, 0xf6,
0x53, 0xf0, 0x10, 0x52, 0x00, 0x00, 0x00, 0xf8, 0xf2, 0x3b, 0xf6, 0x48,
0xf0, 0x10, 0x66, 0x00, 0x00, 0x00, 0xf8, 0xf2, 0x3c, 0xf6, 0x3d, 0xf0,
0x10, 0x63, 0x00, 0x00, 0x00, 0xf8, 0xf2, 0x3d, 0xf6, 0x32, 0xf0, 0x10,
0x44, 0x00, 0x00, 0x00, 0xf8, 0xf2, 0x3e, 0xf6, 0x27, 0xf0, 0x10, 0x6a,
0x00, 0x00, 0x00, 0xf8, 0xf2, 0x3f, 0xf6, 0x1c, 0xf0, 0x10, 0x79, 0x00,
0x00, 0x00, 0xf8, 0xf2, 0x40, 0xf6, 0x11, 0xf0, 0x10, 0x65, 0x00, 0x00,
0x00, 0xf8, 0xf2, 0x41, 0xf6, 0x06, 0xf7, 0x01, 0x00, 0x00, 0x00, 0xf3,
0xf7, 0x00, 0x00, 0x00, 0x00, 0xf3, 0x5d, 0xc3, 0x0f, 0x1f, 0x84, 0x00,
0x00, 0x00, 0x00, 0x00
};
unsigned int code_bin_len = 220;
然后本人把整个虚拟机的实现的代码全部移植到vm.c
中,vm.c
可以从上面下载。然后写出对应的vm.h
文件。
vm.h
#define LOBYTE(arg) (*(char *)(&(arg)))
#define __int8 char
char *buffer;
typedef struct vm_struc
{
int eax_;
int ebx_;
int ecx_;
int edx_;
int flag;
int field_14;
unsigned char *pc;
long long code_F0;
unsigned char *mov_reg_imm;
long long code_F1;
unsigned char *xor_eax_ebx;
long long code_F2;
unsigned char *cmp_eax_imm;
long long code_F4;
unsigned char *add_eax_ebx;
long long code_F5;
unsigned char *sub_eax_ebx;
long long code_F3;
unsigned char *nop;
long long code_F6;
unsigned char *jz_imm;
long long code_F7;
unsigned char *mov_buf_imm;
long long code_F8;
unsigned char *enc_eax_2;
int buffer;
} vm_struc;
int init_vm(vm_struc *a1, char *input);
int xor_eax_ebx(vm_struc *a1);
vm_struc *cmp_eax_imm(vm_struc *a1);
vm_struc *jz_imm(vm_struc *a1);
void nop();
int sub_100001B80(char a1, int a2);
int enc_eax_2(vm_struc *a1);
int add_eax_ebx(vm_struc *a1);
int sub_eax_ebx(vm_struc *a1);
// 最主要是这条指令
int mov_buf_imm(vm_struc *a1);
vm_struc *mov_reg_imm(vm_struc *a1);
int run(vm_struc *a1);
上述文件只能在64位的Linux
上编译行为才会正常,否则会因为字节偏移错误导致行为异常。
下面这段代码模拟虚拟机运行,当操作码为0xF3
,则结束运行。
while (*(unsigned __int8 *)a1->pc != 0xF3)
run(a1);
free(buffer);
经过分析操作码发现,程序的行为如下:
mov_reg_imm(a1);
for (i = 0; i < 18; i++)
{
enc_eax_2(a1);
cmp_eax_imm(a1);
jz_imm(a1);
mov_reg_imm(a1);
}
enc_eax_2(a1);
cmp_eax_imm(a1);
jz_imm(a1);
mov_buf_imm(a1);
mov_reg_imm
vm_struc *mov_reg_imm(vm_struc *a1)
{
vm_struc *result; // rax
int *v2; // [rsp+Ch] [rbp-18h]
v2 = (int *)(a1->pc + 2);
switch (*(unsigned __int8 *)(a1->pc + 1))
{
case 0x10u:
a1->eax_ = *v2;
break;
case 0x11u:
a1->ebx_ = *v2;
break;
case 0x12u:
a1->ecx_ = *v2;
break;
case 0x13u:
a1->edx_ = *v2;
break;
case 0x14u:
a1->eax_ = buffer[*v2];
break;
default:
break;
}
result = a1;
a1->pc += 6LL;
return result;
}
经过操作码分析之后你会发现,在mov_reg_imm
中,只有case 0x10u
这种情况,其他都是用来混淆
的。也就是这个函数的功能就是将操作码后面的一个字节赋值给eax。
enc_eax_2
int enc_eax_2(vm_struc *a1)
{
int result; // rax
result = sub_100001B80(a1->eax_, 2); // 凯撒密码,key=2
a1->eax_ = (char)result;
++a1->pc;
return result;
}
对eax进行加密变换。
cmp_eax_imm
vm_struc *cmp_eax_imm(vm_struc *a1)
{
vm_struc *result; // rax
a1->flag = (a1->eax_ == buffer[*(unsigned __int8 *)(a1->pc + 1)]);
result = a1;
a1->pc += 2LL;
return result;
}
注意
:这里的flag
一定要为1,否则后面就会执行错误,所以我们的任务就是构建输入使得每次进行cmp_eax_imm
时,得到的flag
都要是1,这里用的(unsigned int8 )(a1->pc + 1)
进行buf
偏移,所有的偏移路径通过操作码得到:{0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41}
,到时候下面的脚本有会有代码进行计算。这刚好对应了int init_vm(vm_struc a1, char input)
中的这行代码memcpy_chk((buffer + 48), input, 18LL, -1LL);
。
jz_imm
vm_struc *jz_imm(vm_struc *a1)
{
vm_struc *result; // rax
// 要求flag尽可能为 1 ,不发生跳转
if (a1->flag)
a1->flag = 0;
else
a1->pc += *(unsigned __int8 *)(a1->pc + 1);
result = a1;
a1->pc += 2LL;
return result;
}
这里会用到上面cmp_eax_imm
函数设置的flag
值,要求flag尽可能为 1 ,才不会发生跳转。
上面的步骤会重复判断19
次,然后进行mov_buf_imm
。
mov_buf_imm
// 最主要是这条指令
int mov_buf_imm(vm_struc *a1)
{
int result; // rax
result = *(unsigned int *)(a1->pc + 1);
a1->buffer = result;
a1->pc += 5LL;
return result;
}
这里会把a1->buffer
的值赋值为操作码后面4字节组成的一个int型数据。而a1->buffer
决定了check
的返回值,所以只要执行mov_buf_imm
时,pc的操作码后面4字节组成的一个int型数据不为0
的话,就能check成功。
由于分析时,发现除了最前面要填充5个字节意外,其他的指令都是11个字节的。也就是下面的指定序列是11字节的。这对用我们后面的脚本分析有很大的用处。
enc_eax_2(a1); # 1 字节
cmp_eax_imm(a1); # 2 字节
jz_imm(a1); # 2 字节
mov_reg_imm(a1); # 6 字节
具体脚本
我们只需要写脚本来模拟判断正确的情况,然后就能得到输入的值。
#!/usr/bin/python3
# -*- coding:utf-8 -*-
import binascii
# 操作码
s = "F01066000000F8F230F6C1F0" + \
"1063000000F8F231F6B6F0106A000000" + \
"F8F232F6ABF0106A000000F8F233F6A0" + \
"F0106D000000F8F234F695F010570000" + \
"00F8F235F68AF0106D000000F8F236F6" + \
"7FF01073000000F8F237F674F0104500" + \
"0000F8F238F669F0106D000000F8F239" + \
"F65EF01072000000F8F23AF653F01052" + \
"000000F8F23BF648F01066000000F8F2" + \
"3CF63DF01063000000F8F23DF632F010" + \
"44000000F8F23EF627F0106A000000F8" + \
"F23FF61CF01079000000F8F240F611F0" + \
"1065000000F8F241F606F701000000F3" + \
"F700000000F35DC30F1F840000000000"
# 头部的另外计算 F0 10 66 00 00 00
s = '0' * 5 * 2 + s
l = int(len(s)/22)
# 每11个字节的操作进行打印
print('操作码整理')
for i in range(l):
print('%2d' % (i+1), s[22*i:22*(i+1)])
print()
print('打印偏移的值')
output = '{'
for i in range(1, 19):
data = s[22*i:22*(i+1)]
output += '0x%s, ' % (data[4:6])
output = output[:-2] + '}'
print(output)
print()
print('打印对应的秘钥')
key = []
output = '{'
for i in range(18):
data = s[22*i:22*(i+1)]
output += '0x%s, ' % (data[14:16])
key += [int(data[14:16], 16)]
output = output[:-2] + '}'
print(output)
def sub_100001B80(a1, a2):
v4 = 0
v5 = -1
if (a1 >= 65):
v4 = (a1 <= 90)
if (v4):
v5 = (a2 + a1 - ord('A')) % 26 + ord('A')
else:
v3 = 0
if (a1 >= ord('a')):
v3 = a1 <= ord('z')
if (v3):
v5 = (a2 + a1 - ord('a')) % 26 + ord('a')
else:
v5 = a1
return int(v5)
print()
# 解密
plaintext = ''
for v in key:
result = sub_100001B80(v, 2)
plaintext += chr(result)
print('flag:')
print(plaintext)
运行实例:
ex@Ex:~/test/confused$ python3 main.py
操作码整理
1 0000000000F01066000000
2 F8F230F6C1F01063000000
3 F8F231F6B6F0106A000000
4 F8F232F6ABF0106A000000
5 F8F233F6A0F0106D000000
6 F8F234F695F01057000000
7 F8F235F68AF0106D000000
8 F8F236F67FF01073000000
9 F8F237F674F01045000000
10 F8F238F669F0106D000000
11 F8F239F65EF01072000000
12 F8F23AF653F01052000000
13 F8F23BF648F01066000000
14 F8F23CF63DF01063000000
15 F8F23DF632F01044000000
16 F8F23EF627F0106A000000
17 F8F23FF61CF01079000000
18 F8F240F611F01065000000
19 F8F241F606F701000000F3
20 F700000000F35DC30F1F84
打印偏移的值
{0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41}
打印对应的秘钥
{0x66, 0x63, 0x6A, 0x6A, 0x6D, 0x57, 0x6D, 0x73, 0x45, 0x6D, 0x72, 0x52, 0x66, 0x63, 0x44, 0x6A, 0x79, 0x65}
flag:
helloYouGotTheFlag
总结
学习要看到核心,抓住本质。