相信大家都在紧张刺激的抢红包吧
前面的二三四题目我跟着论坛中大佬们的教学,花点时间基本上都能出来。
到第五题就蒙了EXE加壳 还运行不了 (可能是要WIN10以上)
第六题是我解的最费精力的一道题
2/3上线,2/10才解出 前后花了一星期 所以主要来说说这个过程
我的基础不足 解题过程比较费力, 期待大佬们的更简洁明了的解法
【01】入手
拿到题目,分为Win版和安卓版。看了一下Win版,好像是加壳了的(最害怕这种),安卓只是加混淆和so,
所以选择从安卓版入手
【02】APK调试准备
首先,使用NP或MP管理器,将AndroidManifest.xml中加上调试标志
[Asm] 纯文本查看 复制代码
其次,是我踩到的一个坑,在IDA调试so的时候 apk始终没有加载出so 后来发现是xml里禁用了解包出so,还要改xml,找到extractNativeLibs,改成true
[Asm] 纯文本查看 复制代码
第三,将ida里的dbgsvr\android_server上传到安卓设备,设置好端口转发
[color=]adb forward tcp:23946 tcp:23946
用adb启动调试程序 adb shell am start -D -n cn.afdm_52pojie.cm2025_1/cn.afdm_52pojie.cm2025_1.MainActivity
选择ida里的ARM android/linux调试服务器,附加到进程,记住进程id
第四,启动jdb。先进行端口转发 adb forward tcp:8700 jwdp:
再启动 jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
在ida里点击运行,安卓上的程序就能运行了
【03】代码的初探索
1.首先用jadx查看了一下apk,发现有个NativeLib类,里面有个check函数,参数是一个整数和一个字符串,猜测应该是uid和flag 比较可疑

06-1-jadx.jpg (69.7 KB, 下载次数: 1)
下载附件
2025-2-13 13:03 上传
2.进ida,打开从apk中lib/armeabi-v7a下面解压出来的libnativelib.so,直接到Exports里面找
按Ctrl+F搜索check ,果然找到了这个函数

06-2-exports.jpg (36.04 KB, 下载次数: 1)
下载附件
2025-2-13 13:06 上传
3.双击进去,按一下Tab键,出现伪C代码,不过还是不怎么清晰

06-3-pseudo.jpg (24.63 KB, 下载次数: 1)
下载附件
2025-2-13 13:08 上传
4.按照教程,将函数的参数改成JNIEnv *env\ jobject \jint \jstring,可以看到是进入函数sub_BB10进行验证的

06-4-pseudo.jpg (21.3 KB, 下载次数: 0)
下载附件
2025-2-13 15:20 上传
5.进入sub_BB10函数,对相关变量进行推测并改名,大致了解函数的流程
[C++] 纯文本查看 复制代码int __fastcall sub_BB10(int uid, char *flag)
{
char *v4; // r0
_QWORD *v5; // r4
_BYTE *v6; // r8
char *v7; // r6
__int64 v8; // d16
__int64 v9; // d17
__int64 v10; // d19
int v11; // r5
int v12; // r6
int result; // r0
if ( strlen(flag) != 29 ) //flag长度29
return 1;
v4 = (char *)malloc(0x10006u); //v4申请了一块内存
if ( !v4 )
return 2;
v5 = v4; //v5也指向内存
v6 = v4 + 65540;
sub_B81C(v4); //处理v4 应该是初始化
j_memcpy(v5 + 1024, &unk_9088, 0x100u); //向内存(1024*8=8192)0x2000处拷贝数据unk_9088
v8 = *(_QWORD *)flag;
v9 = *((_QWORD *)flag + 1);
v7 = flag + 13;
v5[512] = v8; //512*8=4096, 0x1000处放flag
v5[513] = v9;
v10 = *((_QWORD *)v7 + 1);
*(_QWORD *)((char *)v5 + 4109) = *(_QWORD *)v7;
*(_QWORD *)((char *)v5 + 4117) = v10; //这几句都是向内存中拷贝flag
sub_B80A(v5, uid); //向内存复制uid?
sub_B80A(v5, 4096); //
sub_B80A(v5, 0x2000); //
v11 = sub_B858(v5); //验证flag,返回v11
if ( !*v6 ) //v6不能为0
{
free(v5);
return 4;
}
v12 = (unsigned __int8)v6[1];
free(v5);
if ( !v12 ) //v12不能为0
return 4;
result = v11 - 1051524100;
if ( v11 != 1051524100 ) //v11必须等于这个数 0x3EACFC04
return 3;
return result;
}
//几个关键函数
int __fastcall sub_B81C(int a1)
{
int result; // r0
sub_1F50C(a1, 0x10000u); //这里查看是一个调用memset全部置0的函数
j_memcpy((void *)(a1 + 49152), &unk_8E88, 0x200u);//又向内存0xC000处拷贝数据unk_8E88
*(_DWORD *)(a1 + 0x10000) = -2147434496; // 0x8000C000
result = a1 + 0x10000;
*(_WORD *)(a1 + 65540) = 0;
return result;
}
int __fastcall sub_B80A(int result, int a2) //此处result应该是v5 内存指针
{
unsigned __int16 v2; // r3
v2 = *(_WORD *)(result + 65538) + 4; //0x10002处增加4
*(_WORD *)(result + 65538) = v2;
*(_DWORD *)(result + v2) = a2; //再放数据a2 是不是很像 PUSH a2
return result;
}
最后是关键的sub_B858函数,流程基本上还算清楚,是一个select case分支结构,看上去像是根据读取的数据执行不同的语句
再根据作者的提示 S = Stack 怀疑是使用模拟栈操作 实现的虚拟机
[Java] 纯文本查看 复制代码int __fastcall sub_B858(char *p_mem)
{
char *p_m10000; // r9
unsigned __int16 *pint16; // r6
char *v4; // r8
unsigned __int16 v5; // r2
unsigned int v6; // r1
int v7; // r0
unsigned int v8; // r1
unsigned int v9; // r2
__int16 v10; // r0
unsigned __int16 v11; // r1
unsigned __int16 v12; // r2
unsigned __int16 v13; // r3
__int16 v14; // r0
unsigned __int16 v15; // r2
unsigned __int16 v16; // r0
char *v17; // r5
int v18; // r1
__int16 v19; // r1
unsigned __int16 v20; // r0
__int16 v21; // r0
int v22; // r1
char *v23; // r0
int v24; // r2
int v25; // r1
char *v26; // r2
__int16 v27; // r0
int v28; // r2
unsigned __int16 v29; // r1
__int16 v30; // r0
__int16 v31; // r0
__int16 v32; // r0
__int16 v33; // r0
__int16 v34; // r0
p_m10000 = p_mem + 0x10000;
if ( p_mem[65540] )
goto LABEL_37;
pint16 = (unsigned __int16 *)*(unsigned __int16 *)p_m10000;
v4 = p_mem + 4;
while ( 1 )
{
v5 = (_WORD)pint16 + 1;
v6 = (unsigned __int8)p_mem[(unsigned __int16)pint16];
v7 = v6 & 7;
if ( v7 != 7 )
{
pint16 = (unsigned __int16 *)((char *)pint16 + 1);
v8 = v6 >> 3;
v9 = v8 - 1;
goto LABEL_8;
}
++pint16;
v7 = (unsigned __int8)p_mem[v5];
v8 = v6 >> 3;
v9 = v8 - 1;
if ( v8 - 1 > 0x1E )
break;
LABEL_8:
switch ( v9 )
{
case 0u:
v21 = *((_WORD *)p_m10000 + 1);
v11 = v21 - 4;
v7 = *(_DWORD *)&v4[(unsigned __int16)(v21 - 8)] ^ *(_DWORD *)&p_mem[(unsigned __int16)(v21 - 4) + 4];
goto LABEL_3;
case 1u:
*(_DWORD *)&p_mem[*((unsigned __int16 *)p_m10000 + 1)] = -*(_DWORD *)&v4[(unsigned __int16)(*((_WORD *)p_m10000 + 1)
- 4)];
continue;
case 2u:
*((_WORD *)p_m10000 + 1) -= 4 * v7;
continue;
case 3u:
case 0x19u:
continue;
case 4u:
v10 = *((_WORD *)p_m10000 + 1);
v11 = v10 - 4;
v7 = *(_DWORD *)&v4[(unsigned __int16)(v10 - 8)] | *(_DWORD *)&p_mem[(unsigned __int16)(v10 - 4) + 4];
goto LABEL_3;
case 5u:
v19 = *((_WORD *)p_m10000 + 1);
v20 = v19 - 8 - 4 * v7 + 4;
pint16 = *(unsigned __int16 **)&v4[(unsigned __int16)(v19 - 8)];
*(_DWORD *)&p_mem[v20] = *(_DWORD *)&v4[(unsigned __int16)(v19 - 4)];
*((_WORD *)p_m10000 + 1) = v20;
continue;
case 6u:
v30 = *((_WORD *)p_m10000 + 1);
v11 = v30 - 4;
v7 = *(_DWORD *)&p_mem[(unsigned __int16)(v30 - 4) + 4] != *(_DWORD *)&v4[(unsigned __int16)(v30 - 8)];
goto LABEL_3;
case 7u:
v22 = *((unsigned __int16 *)p_m10000 + 1);
v23 = &p_mem[v22 + -4 * v7];
v24 = *(_DWORD *)v23;
*(_DWORD *)v23 = *(_DWORD *)&p_mem[v22];
*(_DWORD *)&p_mem[v22] = v24;
continue;
case 8u:
v31 = *((_WORD *)p_m10000 + 1);
v11 = v31 - 4;
v7 = *(_DWORD *)&v4[(unsigned __int16)(v31 - 8)] & *(_DWORD *)&p_mem[(unsigned __int16)(v31 - 4) + 4];
goto LABEL_3;
case 9u:
*(_DWORD *)&p_mem[*((unsigned __int16 *)p_m10000 + 1)] = *(_DWORD *)&v4[(unsigned __int16)(*((_WORD *)p_m10000 + 1)
- 4)] > v7;
continue;
case 0x12u:
v14 = *((_WORD *)p_m10000 + 1);
v15 = v14 - 4;
v16 = v14 - 8;
*((_WORD *)p_m10000 + 1) = v16;
v17 = &p_mem[v15];
if ( !*((_DWORD *)v17 + 1) )
goto LABEL_34;
*((_WORD *)p_m10000 + 1) = v15;
sub_1F518(*(_DWORD *)&v4[v16]);
*(_DWORD *)v17 = v18;
continue;
case 0x14u:
*(_DWORD *)&p_mem[*((unsigned __int16 *)p_m10000 + 1)] = (unsigned __int8)v4[(unsigned __int16)(*((_WORD *)p_m10000 + 1) - 4)];
continue;
case 0x15u:
v33 = *((_WORD *)p_m10000 + 1);
v11 = v33 - 4;
v7 = *(_DWORD *)&v4[(unsigned __int16)(v33 - 8)] * *(_DWORD *)&p_mem[(unsigned __int16)(v33 - 4) + 4];
goto LABEL_3;
case 0x16u:
v28 = (unsigned __int16)pint16;
pint16 = (unsigned __int16 *)((char *)pint16 + (char)v7);
v29 = *((_WORD *)p_m10000 + 1) + 4;
*((_WORD *)p_m10000 + 1) = v29;
*(_DWORD *)&p_mem[v29] = v28;
continue;
case 0x17u:
case 0x18u:
v12 = *((_WORD *)p_m10000 + 1);
v13 = v12 - 4;
v12 -= 8;
*((_WORD *)p_m10000 + 1) = v12;
if ( (v8 == 25) != (*(_DWORD *)&v4[v13] == *(_DWORD *)&v4[v12]) )
pint16 = (unsigned __int16 *)((char *)pint16 + (char)v7);
continue;
case 0x1Au:
v27 = *((_WORD *)p_m10000 + 1);
v11 = v27 - 4;
v7 = (unsigned __int8)p_mem[(unsigned __int16)(*(_DWORD *)&v4[(unsigned __int16)(v27 - 8)]
+ *(_DWORD *)&p_mem[(unsigned __int16)(v27 - 4) + 4])];
goto LABEL_3;
case 0x1Bu:
v25 = *((unsigned __int16 *)p_m10000 + 1);
v26 = &p_mem[v25];
v11 = v25 + 4;
v7 = *(_DWORD *)&v26[-4 * v7];
goto LABEL_3;
case 0x1Du:
v11 = *((_WORD *)p_m10000 + 1) + 4;
LABEL_3:
*(_DWORD *)&p_mem[v11] = v7;
*((_WORD *)p_m10000 + 1) = v11;
break;
case 0x1Eu:
*(_DWORD *)&p_mem[*((unsigned __int16 *)p_m10000 + 1)] = *(_DWORD *)&v4[(unsigned __int16)(*((_WORD *)p_m10000 + 1)
- 4)]
- 1;
continue;
default:
goto LABEL_34;
}
}
LABEL_34:
p_m10000[4] = 1;
LABEL_36:
*(_WORD *)p_m10000 = (_WORD)pint16;
LABEL_37:
v34 = *((_WORD *)p_m10000 + 1);
*((_WORD *)p_m10000 + 1) = v34 - 4;
return *(_DWORD *)&p_mem[(unsigned __int16)(v34 - 4) + 4];
}
【04】认清虚拟机
通过反复阅读代码、ida动态调试so,在几个函数处下断点跟进,确认是虚拟机模式(或者有更专业的名字?)
虚拟机的内存共0x10006字节,在0x1000处存放一个256字节的映射表(后面发现的,将0-9和A-Z分别映射到一个1-36的数字,其余为0)
虚拟机的指令代码就是unk_8E88的0x200字节,被存放在申请的内存0xC000处
虚拟机的堆栈SP在0x8000,[sp+4]=uid, [sp+8]=0x1000=映射表 [sp+C]=0x2000=flag
注意红框处在ASCII模式下 是不是有一些奇怪的字符?

06-4-8e88.jpg (138.27 KB, 下载次数: 0)
下载附件
2025-2-13 16:11 上传
到这里又卡住了 看来只有把所有指令功能都搞清楚 再分析出代码流程,才能分析虚拟机的操作。
先分析出每一个case指令的功能,大概像这样子:
[Asm] 纯文本查看 复制代码指令 功能
----------------------------
0: xor [sp-4], [sp]; sp-=4
1: neg [sp]
2: sub [sp], [sp]-4*v7
3: nop
4: or [sp-4], [sp]; sp-=4
5: mov [sp-4-4*v7], [sp]; sp = sp-4-4*v7; jmp [sp-4]
6: setne [sp-4], [sp]; sp-=4
7: xchg [sp-4*v7], [sp]
8: and [sp-4], [sp]; sp-=4
9: shl [sp], v7
A: not [sp]
C: add [sp-4], [sp]; sp-=4
E: jmp v7
F: exit
11: shr [sp], v7
12: ??
14: mov [sp], byte ptr [sp]
15: multi [sp-4], [sp]; sp-=4
16: push pc, call func_v7
17: cmp [sp], [sp-4]; sp-=8; je v7
18: cmp [sp], [sp-4]; sp-=8; jne v7
19: nop
1A: mov [sp-4], [sp+word([sp-4]+[sp])]; sp-=4
1B: push [sp-4*v7]
1D: push v7
1E: sub [sp], 1
其中指令12的功能比较复杂 还调用了一个子程序 费了很大功夫才勉强弄懂
接下来,现学了一下idapython,在所有select case处下断点并用脚本记录运行日志。脚本如下:
[Python] 纯文本查看 复制代码import idaapi
import idc
import idautils
import ida_dbg
import ida_funcs
import datetime
class MyDbgHook(ida_dbg.DBG_Hooks):
# 是否正在进行指令跟踪
is_tracing = False
# 标记是否刚刚到达过结束地址
was_at_end_addr = False
def __init__(self, start_addr, end_addr):
super().__init__()
self.start_addr = start_addr
self.end_addr = end_addr
self.rva = start_addr & 0x00000FFF
now = datetime.datetime.now()
timestr = now.strftime("%Y%m%d_%H%M%S")
self.f = open('i:\\crack\\2025spring\\06\\tracevmlog4_'+timestr +'.txt','w')
self.rawdata = 0
self.opdata = 0
self.opcode = 0
self.opaddr = 0
self.sp = 0x800c
def get_byte(int a):
b=a&0xff
if b&0x80:
return b-256
else:
return b
def dbg_bpt(self, tid, ea):
"""处理断点事件"""
current_addr = idc.get_reg_value("pc") # ARM64 使用 PC 寄存器
# print(f"断点触发在: {hex(current_addr)}")
# 如果是在开始地址处的断点,开启指令跟踪
if current_addr == self.start_addr:
# 开启指令跟踪
self.enable_tracing()
# 单步跟踪
# 保存上下文
r0 = idc.get_reg_value("r0") #v7
print(f"Start trace vm at {hex(self.start_addr)}, a1={hex(r0)},flag={hex(r0+0x1000)},opcode={hex(r0+0x10000)},stack={hex(r0+0xC000)}\n",file=self.f)
self.step_and_trace(self.start_addr, self.end_addr)
# 如果是在结束地址处的断点,关闭指令跟踪
elif current_addr == self.end_addr:
print(f"Exit trace vm at {hex(current_addr)}\n",file=self.f)
# 关闭指令跟踪
self.disable_tracing()
# 单步跟踪
#self.step_and_trace(self.start_addr, self.end_addr)
# 恢复运行
idaapi.continue_process()
# 通过偏移确定断点功能
else:
rva = current_addr & 0x00000FFF
if rva == 0x8AC: # switch
r0 = idc.get_reg_value("r0") #v7
r2 = idc.get_reg_value("r2") #v9
self.opdata = r0
self.opcode = r2
print(f"****Case {hex(r2)}: SP:{hex(self.sp)} ADDR:{hex(self.opaddr)} {hex(self.rawdata)} OPDATA={hex(self.opdata)}\n",file=self.f)
elif rva == 0x888: # v3 addr
#r0 = idc.get_reg_value("r0") #v3
r1 = idc.get_reg_value("r1") #v6
r6 = idc.get_reg_value("r6") #v3
self.opaddr = r6
self.rawdata = r1
#print(f"[v3addr:{hex(r0)},opcode:{hex(r1)}] ",file=self.f)
elif rva == 0x9D8: # 0 v7 XOR
r0 = idc.get_reg_value("r0")
r1 = idc.get_reg_value("r1")
print(f" [0] SP:{hex(self.sp)} ADDR:{hex(self.opaddr)} OPCODE:{hex(self.opcode)} xor [sp-4], [sp]; sp-=4 // v7=*(a1+SP-4)^*(a1+*SP) [SP={hex(r0)}] ; *(a1+*SP-4) = v7; *SP=*SP-4\n",file=self.f)
self.sp = self.sp - 4
elif rva == 0x992: # 1 negative
r0 = idc.get_reg_value("r0")
r1 = idc.get_reg_value("r1")
print(f" [1] SP:{hex(self.sp)} ADDR:{hex(self.opaddr)} OPCODE:{hex(self.opcode)} neg [sp] //*(a1+*SP)= -*(a1+*SP) [SP={hex(r0)},val={hex(r1)}]\n",file=self.f)
elif rva == 0x980: # 2 *SP = *SP - 4 * v7
r0 = idc.get_reg_value("r0")
r1 = idc.get_reg_value("r1")
rr = (r1-r0)//4
print(f" [2] SP:{hex(self.sp)} ADDR:{hex(self.opaddr)} OPCODE:{hex(self.opcode)} sub [sp], [sp]-4*v7 //*SP=*SP-4*v7 [SP={hex(r1)},v7={hex(rr)}]\n",file=self.f)
elif rva == 0x8F2: # 4 v7 OR
r0 = idc.get_reg_value("r0")
print(f" [4] SP:{hex(self.sp)} ADDR:{hex(self.opaddr)} OPCODE:{hex(self.opcode)} or [sp-4], [sp]; sp-=4 //v7 = *(a1+*SP-4) | *(a1+*SP); [SP={hex(r0)}] *(a1+*SP-4) = v7; *SP=*SP-4\n",file=self.f)
self.sp = self.sp - 4
elif rva == 0x99C: # 5 *(a1+*SP-4-4*v7) = *(a1+*SP), *SP = *SP-4-4*v7
r0 = idc.get_reg_value("r0")
r1 = idc.get_reg_value("r1")
print(f" [5] SP:{hex(self.sp)} ADDR:{hex(self.opaddr)} OPCODE:{hex(self.opcode)} mov [sp-4-4*v7], [sp]; sp = sp-4-4*v7; jmp [sp-4] //v3 = *(a1+*SP-4); *(a1+*SP-4-4*v7) = *(a1+*SP) ; *SP = *SP-4-4*v7 [SP={hex(r1)},v7={hex(r0)}]\n",file=self.f)
elif rva == 0xA6C: # 6 v7= ( *(a1+*SP) != *(a1+*SP-4) )
r0 = idc.get_reg_value("r0")
print(f" [6] SP:{hex(self.sp)} ADDR:{hex(self.opaddr)} OPCODE:{hex(self.opcode)} setne [sp-4], [sp]; sp-=4 //v7= ( *(a1+*SP) != *(a1+*SP-4) ) ; [SP={hex(r0)}] *(a1+*SP-4) = v7; *SP=*SP-4\n",file=self.f)
self.sp = self.sp - 4
elif rva == 0xA00: # 7 SWAP
r0 = idc.get_reg_value("r0")
r1 = idc.get_reg_value("r1")
r2 = idc.get_reg_value("r2")
r3 = idc.get_reg_value("r3")
print(f" [7] SP:{hex(self.sp)} ADDR:{hex(self.opaddr)} OPCODE:{hex(self.opcode)} xchg [sp-4*v7], [sp] //*(a1+*SP-4*v7) = *(a1+*SP) ; *(a1+*SP) = *(a1+*SP-4*v7) [SWAP:a1+SP-4*v7({hex(r0)})={hex(r3)},a1+SP({hex(r1)})={hex(r2)}]\n",file=self.f)
elif rva == 0xA9A: # 8 v7 AND
r1 = idc.get_reg_value("r1")
print(f" [8] SP:{hex(self.sp)} ADDR:{hex(self.opaddr)} OPCODE:{hex(self.opcode)} and [sp-4], [sp]; sp-=4 //v7 = *(a1+*SP-4) & *(a1+*SP); [SP={hex(r1+4)}] *(a1+*SP-4) = v7; *SP=*SP-4\n",file=self.f)
self.sp = self.sp - 4
elif rva == 0x9C4: # 9 SHIFT LEFT
r0 = idc.get_reg_value("r0")
r1 = idc.get_reg_value("r1")
print(f" [9] SP:{hex(self.sp)} ADDR:{hex(self.opaddr)} OPCODE:{hex(self.opcode)} shl [sp], v7 //*(a1+*SP) = *(a1+*SP) >v7 [SP={hex(r1)},v7={hex(r0)}]\n",file=self.f)
elif rva == 0x93E: # 12
r0 = idc.get_reg_value("r0")
print(f" [12] SP:{hex(self.sp)} ADDR:{hex(self.opaddr)} OPCODE:{hex(self.opcode)} ???? //if [*(a1+*SP-4)==0] goto LABEL34; *SP=*SP-4; [SP={hex(r0)}] call sub_518(*(a1+*SP-8)) ",file=self.f)
elif rva == 0x538: # 12-sub_534
r0 = idc.get_reg_value("r0")
r1 = idc.get_reg_value("r1")
r2 = idc.get_reg_value("r2")
r3 = idc.get_reg_value("r3")
print(f" [12*] sub_534 [p1={hex(r0)},p2={hex(r1)},p3={hex(r2)},p4={hex(r3)}] ",file=self.f)
elif rva == 0x550: # 12-sub_534
r3 = idc.get_reg_value("r3")
r12 = idc.get_reg_value("r12")
print(f" [12**] sub_534 [_clz1={hex(r12)},_clz2={hex(r3)},final-funcaddr=6DC-{hex(12*(r3-r12))}] ",file=self.f)
elif rva == 0xA10: # 14 COMPRESS
r0 = idc.get_reg_value("r0")
r1 = idc.get_reg_value("r1")
print(f" [14] SP:{hex(self.sp)} ADDR:{hex(self.opaddr)} OPCODE:{hex(self.opcode)} mov [sp], byte ptr [sp] //*(a1+*SP)= byte*(a1+*SP) [SP={hex(r0)},val={hex(r1)}]\n",file=self.f)
elif rva == 0xAC0: # 15 v7 multiple
r0 = idc.get_reg_value("r0")
print(f" [15] SP:{hex(self.sp)} ADDR:{hex(self.opaddr)} OPCODE:{hex(self.opcode)} multi [sp-4], [sp]; sp-=4 //v7 = *(a1+*SP-4) * *(a1+*SP); [SP={hex(r0)}] *(a1+*SP-4) = v7; *SP=*SP-4\n",file=self.f)
self.sp = self.sp-4
elif rva == 0xA44: # 16
r0 = idc.get_reg_value("r0")
r1 = idc.get_reg_value("r1")
print(f" [16] SP:{hex(self.sp)} ADDR:{hex(self.opaddr)} OPCODE:{hex(self.opcode)} call func_{hex(self.opaddr)}+{hex(r0)}+2 //*SP=*SP+4 ; *(a1+*SP+4)=old_OPADDR_v3 ; OPADDR_v3+=v7 [SP={hex(r1)},v7={hex(r0)}]\n",file=self.f)
self.sp = self.sp + 4
elif rva == 0x90C: # 17 JE / 18 JNE
r0 = idc.get_reg_value("r0")
r2 = idc.get_reg_value("r2")
print(f" [{hex(self.opcode)}] SP:{hex(self.sp)} ADDR:{hex(self.opaddr)} OPCODE:{hex(self.opcode)} cmp [sp], [sp-4]; sp-=8; je {hex(self.opaddr)}+{hex(r0)} //*SP = *SP-8 ; [SP={hex(r2)},v7={hex(r0)}] if [v9==0x18 && (*(a1+*SP) != *(a1+*SP-4))] OPADDR += v7\n",file=self.f)
self.sp = self.sp - 8
elif rva == 0xA28: # 1A
r0 = idc.get_reg_value("r0")
print(f" [1A] SP:{hex(self.sp)} ADDR:{hex(self.opaddr)} OPCODE:{hex(self.opcode)} mov [sp-4], [sp+word([sp-4]+[sp])]; sp-=4 //v7=*(a1+word*(a1+*SP-4)+word*(a1+*SP)) ; [SP={hex(r0)}] *(a1+*SP-4) = v7; *SP=*SP-4\n",file=self.f)
self.sp = self.sp - 4
elif rva == 0xA1C: # 1B
r0 = idc.get_reg_value("r0")
r1 = idc.get_reg_value("r1")
r2 = idc.get_reg_value("r2")
print(f" [1B] SP:{hex(self.sp)} ADDR:{hex(self.opaddr)} OPCODE:{hex(self.opcode)} push [sp-4*v7] //v7=*(a1+*SP-4*v7) ; [SP={hex(r1)},v7={hex(r0)},addr={hex(r2)}] *(a1+*SP+4) = v7; *SP=*SP+4\n",file=self.f)
self.sp = self.sp + 4
elif rva == 0xA88: # 1D push v7
r1 = idc.get_reg_value("r1")
r0 = idc.get_reg_value("r0")
print(f" [1D] SP:{hex(self.sp)} ADDR:{hex(self.opaddr)} OPCODE:{hex(self.opcode)} push {hex(r0)} //*(a1+*SP+4) = v7; *SP=*SP+4 [push v7. SP={hex(r1)},v7={hex(r0)}]\n",file=self.f)
self.sp = self.sp + 4
elif rva == 0xAE2: # 1E
r0 = idc.get_reg_value("r0")
print(f" [1E] SP:{hex(self.sp)} ADDR:{hex(self.opaddr)} OPCODE:{hex(self.opcode)} sub [sp], 1 //*(a1+*SP) = *(a1+*SP) - 1 [SP={hex(r0)}]\n",file=self.f)
idaapi.continue_process()
return 0
def dbg_step_into(self) -> bool:
"""处理单步事件"""
self.step_and_trace(self.start_addr, self.end_addr)
return True
def dbg_step_over(self):
"""处理单步事件"""
self.step_and_trace(self.start_addr, self.end_addr)
return True
def enable_tracing(self):
"""开启指令跟踪"""
if not self.is_tracing:
self.is_tracing = True
# 清除所有 tracing 数据
ida_dbg.clear_trace()
# 开启指令跟踪
ida_dbg.enable_insn_trace()
# 挂起其他线程
#suspend_other_threads()
print("清除所有 tracing 数据,开启指令跟踪,挂起其他线程")
def disable_tracing(self):
"""关闭指令跟踪"""
self.is_tracing = False
# 关闭指令跟踪
ida_dbg.disable_insn_trace()
print("关闭指令跟踪")
# 移除开始和结束地址的断点
idaapi.del_bpt(self.start_addr)
idaapi.del_bpt(self.end_addr)
print(f"移除断点: {hex(self.start_addr)}, {hex(self.end_addr)}")
self.f.close()
# 解除 hook
self.unhook()
print("解除hook")
# 恢复线程
#resume_all_threads()
def step_and_trace(self, start_addr, end_addr):
"""单步跟踪指令,遇到函数调用时步入"""
if self.is_tracing:
current_addr = idc.get_reg_value("pc") # ARM64 用 pc 寄存器
# 检查地址
off = current_addr - self.start_addr
#if off == 0x00:
# 检查当前地址是否为结束地址
if current_addr == end_addr:
print(f"[{hex(start_addr)}] 已到达结束地址,执行结束指令")
ida_dbg.request_step_over() # 执行完结束地址指令
self.was_at_end_addr = True
return
# 检查是否刚刚执行完结束地址指令
if current_addr != end_addr and self.was_at_end_addr:
print(f"[{hex(start_addr)}] 已执行完结束地址指令,关闭 tracing")
self.disable_tracing()
idaapi.continue_process()
return
print(f"[{hex(current_addr)}] step over")
ida_dbg.request_step_over() # 单步跟踪
def get_module_by_addr(addr):
"""根据地址获取所属的模块(段)起始地址"""
for seg in idautils.Segments():
if seg
使用ida运行脚本 几分钟后得到一个txt日志文件(这个脚本不知道为什么 一个断点会记录两次……)
[Plain Text] 纯文本查看 复制代码Start trace vm at 0xcb458860, a1=0xca4bce40,flag=0xca4bde40,opcode=0xca4cce40,stack=0xca4c8e40
****Case 0x16: SP:0x800c ADDR:0xc000 0xbf OPDATA=0x7f
[16] SP:0x8010 ADDR:0xc000 OPCODE:0x16 call func_0xc000+0x7f //*SP=*SP+4 ; *(a1+*SP+4)=old_OPADDR_v3 ; OPADDR_v3+=v7 [SP=0x800c,v7=0x7f]
****Case 0x1b: SP:0x8014 ADDR:0xc081 0xe3 OPDATA=0x3
[1B] SP:0x8018 ADDR:0xc081 OPCODE:0x1b push [sp-4*v7] //v7=*(a1+*SP-4*v7) ; [SP=0x8014,v7=0x3,addr=0xca4c4e50] *(a1+*SP+4) = v7; *SP=*SP+4
****Case 0x16: SP:0x801c ADDR:0xc082 0xbf OPDATA=0x32
[16] SP:0x8020 ADDR:0xc082 OPCODE:0x16 call func_0xc082+0x32 //*SP=*SP+4 ; *(a1+*SP+4)=old_OPADDR_v3 ; OPADDR_v3+=v7 [SP=0x8014,v7=0x32]
…………此处省略几千行
****Case 0x5: SP:0x850c ADDR:0xc080 0x33 OPDATA=0x3
[5] SP:0x850c ADDR:0xc080 OPCODE:0x5 mov [sp-4-4*v7], [sp]; sp = sp-4-4*v7; jmp [sp-4] //v3 = *(a1+*SP-4); *(a1+*SP-4-4*v7) = *(a1+*SP) ; *SP = *SP-4-4*v7 [SP=0x8024,v7=0x3]
****Case 0xf: SP:0x850c ADDR:0xc088 0x80 OPDATA=0x0
[F] SP:0x850c ADDR:0xc088 OPCODE:0xf exit //!!!!EXIT HERE
Exit trace vm at 0xcb458b0e
所以txt经过去重最终得到三千多条指令的执行过程
然后导入到Excel,前面加上序号,再提取出每条记录的ADDR指令地址,最后再按指令地址排序,复制一份副本,去掉重复项,得到比较干净的虚拟机汇编代码
从C000到C151共一百多条 我用红色标出了有跳转的指令

06-5-asm1.jpg (262.28 KB, 下载次数: 1)
下载附件
2025-2-13 16:35 上传

06-5-asm2.jpg (277.95 KB, 下载次数: 1)
下载附件
2025-2-13 16:35 上传

06-5-asm3.jpg (275.16 KB, 下载次数: 0)
下载附件
2025-2-13 16:35 上传

06-5-asm4.jpg (325.11 KB, 下载次数: 0)
下载附件
2025-2-13 16:35 上传

06-5-asm5.jpg (260.04 KB, 下载次数: 1)
下载附件
2025-2-13 16:36 上传

06-5-asm6.jpg (255.34 KB, 下载次数: 1)
下载附件
2025-2-13 16:36 上传

06-5-asm7.jpg (143.38 KB, 下载次数: 0)
下载附件
2025-2-13 16:36 上传
最后,就是梳理程序逻辑了。花了不少时间,逐条分析,得到程序逻辑大致如下:
1、将uid的十六进制(假设为0xAABBCCDD)与给定的字符串混合,得到 data = "2025AA52pojieBBafdmCC2025DD"的数据 (字符串就是在上面8E88ASCII中看到的那些)
2、利用CRC算法,得到上面数据的CRC值 crc = crc32(data)
3、将a置0,从{后面开始,读取flag的每个字符,到映射表中找对应的值,将a乘以36再加上这个值 ,重复5次(后来发现,这个像是36进制?)
4、取crc的低1字节乘以0x13541加上5,放在d中(flag第一段5字符的起点为5,后面依次为0xB\0x11\0x17)
5、对a、d进行指令12的操作 d = opcode12(a, d); (这个函数也分析了好久,静态分析只能到最后几条指令,动态分析才能跳到真正执行的地方)就是当a>=d时 a循环减去d 最后返回
6、将返回结果与上一次操作结果进行或操作
7、最后将结果减1,再异或0xC15303FB,返回
【05】挑战解密算法
既然知道了虚拟机执行的过程,后面的任务就是逆向出flag中的那些字符了。推理过程如下:
最终结果要等于 0x3EACFC04
0x3EACFC04 ^ 0xC15303FB = 0xFFFFFFFF
0xFFFFFFFF + 1 = 0
也就是说,每一次对flag字符串中5字符的运算结果必须全为0
设计出用C代码实现的虚拟机检查过程如下:
[C++] 纯文本查看 复制代码int main() {
char flag[0x1E] = "flag{QS3CF-1X9JG-YL7O4-LM3GU}";
char real_flag2[0x1E] = {0};
int i;
int pos = 0;
uint32_t v;
unsigned char ch;
uint32_t v8014, v8024, v8028, v802c, var1;
uint32_t a, b, d, tmp;
ch = 0x00;
//crc 处理结果
v8014 = 0xE6F0CBF7;
printf("v8014: %08X\n", v8014);
v8024 = var1 = 0;
//check flag
v8028 = 5;
while( v8028 > (pos * 8) ) & 0xFF; // crc 1 byte
a = b = 0;
for( i = 0; i > 0x19 != 0) ) { //SHOULD ENTER THIS
a = a + 1;
d = 0x13541 * v802c; // crc 1 byte
d = d + (v8028); // d = 5, B, 11, 17; d1 =
if( d ) {
d = opcode12(a, d); // a SHOULD == d
}
var1 = d; //var1 SHOULD BE 0
} else { //DON'T' ENTER THIS
a = a | 1;
var1 = a;
}
var1 = var1 | v8024; // V8024 SHOULD BE 0
tmp = var1; var1 = v8024; v8024 = tmp;
pos++;
v8028+=5;
if( flag[v8028] != '-' && flag[v8028] != '}' ) {
printf("Error flag string format!");
return -1;
}
v8028++;
}
v8024 = v8024 -1;
v8024 = v8024 ^ 0xc15303fb;
if(v8024 == 0x3EACFC04 )
printf("Success! Result is: %08X\n", v8024);
else
printf("Error flag str %s, result is: %08X\n", flag, v8024);
return 0;
}
最终得出结论,由5字符组成的36进制算出的数字a +1,要么等于d,要么必须是d的倍数,通过循环相减才能得到0
因此再次设计反向计算的代码,通过每一个crc字节和flag位置,算出d,再找到合适的a,再通过a去得到5个1-36的数字,并查映射表找到字符
代码如下:
[C++] 纯文本查看 复制代码void get_flag(uint32_t crc, char* dst)
{
uint32_t d[4], v, a, b;
uint8_t pos[4] = {5, 0xB, 0x11, 0x17};
int i, j, k;
unsigned char ch, f[7]={0};
char flag[0x1E]={0};
srand((uint32_t)time(NULL));
strcpy(flag, "flag{");
for (i = 0; i > (i*8)) & 0xFF;
d = 0x13541*v;
d += pos;
k=0;
b = d;
//while( b =0; j--) {
a = a + 1;
if(j > 0 ){
//v = 1+rand()%36;
v = a % 36;
if( v==0 ) v = 36;
f[j] = vtochar(v);
//printf("%02X:%c,",v,f[j]);
a = a - v;
a = a / 36;
}else{
v = a;
if( v >=1 && v
最终算出:
我的uid=1581363 的情况下
得到的flag= flag{XS1AC-YVBWO-QQOCL-LM3GG}
后来还发现,当a取d的更高倍数也可以,得到不同的flag也能通过。所以作者提示了答案可能不唯一。
以上就是我在任务六的解答过程,虽然解答出来了,但对其中的算法、实现原理等并不了解,期待大佬出更精准的WP。
感谢!