【2025春节】解题领红包之六(安卓版)——Writeup

查看 71|回复 9
作者:jackyyue_cn   
各位坛友们新年快乐!
相信大家都在紧张刺激的抢红包吧
前面的二三四题目我跟着论坛中大佬们的教学,花点时间基本上都能出来。
到第五题就蒙了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。
感谢!

指令, 地址

抱薪风雪雾   

这题 Windows 版套了个原版的 upx,直接用 upx -d 就能脱掉了,没有坑;Win 和安卓版理论上都是一样的难度。
opcode 0 其实是未定义,被编译器优化了。实际的 opcode 要加 1。
文中的 opcode12 对应的其实是虚拟机的 19 (十进制) 求模指令,不知道为什么安卓端给优化成这个鬼样子了 32 位 arm 处理器不一定有求模指令,所以编译器自己补了一段代码…
// 定义
constexpr uint8_t SMVM_MOD = 19;
// 实现
        case SMVM_MOD: {
            const auto a = POP(); // divisor
            const auto b = POP(); // dividend
            if (a == 0)
                vm->halt = true; // division by zero
            else
                PUSH(b % a);
            break;
        }
对比了下 aarch64 和 arm 的反编译代码,可以发现 aarch64 下生成的伪码是正常的:
      case 0x12u:
        v17 = *(_WORD *)(a1 + 65538);
        v18 = v17 - 4;
        v19 = v17 - 8;
        v20 = (int *)(a1 + v18);
        *(_WORD *)(a1 + 65538) = v19;
        v21 = v20[1];
        if ( !v21 )
          goto LABEL_34;
        *(_WORD *)(a1 + 65538) = v18;
        *v20 = *(_DWORD *)(v5 + v19) % v21;
        continue;

将a乘以36再加上这个值 ,重复5次(后来发现,这个像是36进制?)

是的,base36 编码的数字。码表打乱过。
const char vm_chars_table[37] = "KEA7WGUN01S6DJB28O5I3LQXMVH9YTPC4ZFR";
void encode_base36(char *str, uint32_t value)
{
    char buffer[20] = {};
    int i = 0;
    do {
        buffer[i++] = vm_chars_table[value % 36];
        value /= 36;
    } while (value);
    int j = 0;
    for (; i > 0; j++, i--)
        str[j] = buffer[i - 1];
    str[j] = '\0';
}

第五题就蒙了EXE加壳 还运行不了 (可能是要WIN10以上)

第五题因为 Win7 下不太好勾反调试要用的函数,所以屏蔽了下。本来应该有弹窗提示的,但是在那之前就崩了  
WangWhereGo   

那红宝,还是不容易
Miracle0927   

6666学习一下
jackyyue_cn
OP
  

6666  过年干这个才是正事啊
twl288   


爱飞的猫 发表于 2025-2-13 19:26
[md]这题 Windows 版套了个原版的 upx,直接用 `upx -d` 就能脱掉了,没有坑;Win 和安卓版理论上都是一样 ...

感谢版主答疑
看了一下 还真是64位libso的可读性更好一些
当时觉得32位应该要好处理一些 就没去看64位了 没想到被ARM/THUMB编译器给绕了好大一圈的路
love657902   

这技术可以啊
伤城幻化   

东西不错
89507982   

很强,看到那么大一串switch 我都头皮发麻了,下不去嘴
抱薪风雪雾   

虽然不会,但是给大佬们点赞
您需要登录后才可以回帖 登录 | 立即注册