一、解題情況

沒有去專門繞過殼的反調試,完全使用frida和IDA靜態分析,完成了第一個任務成功獲取flag。

加密算法共4種,第二個任務注冊機,缺一個算法的解密算法,其他三個算法均已寫好C實現的解密算法。

Flag截圖

二、大概解題思路

frida默認會被檢測,經過修改源碼中的幾個特征(端口號、臨時目錄、maps中相關字符串等),自行編譯后獲得一個可以hook除了libsec2023.so之外的所有so的frida。

隨后hook系統函數dlopen,當加載libil2cpp.so之后從內存dump下來解密后的so文件,通過比較apk中的so來修復一些結構體信息,此時得到了一個完整的可被IDA正常解析的libil2cpp.so,因此可以用IL2CPPDumper恢復IDA中符號和結構體。

找到CollectCoin函數,使用frida去patch其中金幣增加的邏輯,改為每次增加1000,這樣我們就可以一次性達到獲取flag的分數,從而輕易獲取flag。

之后發現有兩個被混淆了函數名稱的C#類,經過足夠的分析后,可以判斷其中一個是VM,包含一些字段用來模擬棧、操作碼等,總共有21個方法,除了構造方法外均為VM的Handler。隨后在xxx函數通過frida分析找到XTEA加密,然后用frida在內存中找到并提取了密鑰(第一處加密)。

(PS:這里的加密多少處是指我找的順序,不是邏輯上的順序)

Hook SmallKeyboard__iI1Ii_4610736函數后發現函數在開頭跳轉到了g_sec2023_p_array的函數中,隨后在libsec2023.so分析該函數,發現被混淆。模式基本上都是將常規的分支、直接跳轉改為用寄存器尋址跳轉,寄存器中的跳轉地址由一個常數+一個偏移來得到,常數和取偏移的基址在一個函數中是固定的,在跳轉前,使用條件選擇指令等來獲取相應的偏移,從而間接實現分支跳轉,使用Unicorn模擬執行+IDAPython修改匯編+遇到特殊情況手動改的方式,我們可以輕易地去除這個混淆。

去除混淆后可以找到兩個加密算法(第三、四處加密),其中第三處可以直接分析,第四處加密通過frida hook得知是調用了Sec2023.Encrypt方法,該類由殼加載,JADX / JEB等工具無法直接分析,需要使用frida-dump工具在運行時dump下來,然后分析。隨后可以發現一個加了BlackObfuscator的dex中就找到了該方法,由于沒有去除混淆的方案,這里選擇使用復制所有真實指令,結合第三處加密,通過frida觀察加密結果來分析加密算法實現的是否正確了,得到了想要的結果之后就可以寫這兩處加密的解密函數。

最后回頭去分析用VM實現的加密算法(第二處加密),我的解決方案是在frida中hook所有handler,通過輸出所有被執行過的指令為C代碼,將棧在輸出的源碼中處理成寄存器變量(register關鍵字),所有通過opcode來取出的值處理為常量,這樣重新編譯后就可以得到較為精簡的加密算法,該過程需要結合frida多次觀察和分析。

分析完這四處算法,逆向編寫出這4處加密算法的解密算法,就可以成功實現注冊機。

三、具體解題過程

*由于實際解題過程有點復雜和混亂,如果有地方沒說清楚請諒解

整一個假的Frida

下載frida源碼,將其中的/server/server.vala中的re.frida_server改為其他字符串,再把src/linux/linux-host-session.vala中的so文件特征字符串更改,然后用一個27042之外的端口啟動修改好的frida,隨后啟動游戲,發現啟動成功,并不會被檢測。

Dump && Recover IL2CPP

雖然用修改后的frida去hook libsec2023.so仍然會被檢測,但是hook其他庫沒有出現問題。而so在被dlopen加載之后有可能就解密好了,因此這里選擇用frida去嘗試hook dlopen去dump libil2cpp.so。

function WriteMemToFile(addr, size, file_path) {
  Java.perform(function() {
    var prefix = '/data/data/com.com.sec2023.rocketmouse.mouse/files/'
    var mkdir = Module.findExportByName('libc.so', 'mkdir');
    var chmod = Module.findExportByName('libc.so', 'chmod');
    var fopen = Module.findExportByName('libc.so', 'fopen');
    var fwrite = Module.findExportByName('libc.so', 'fwrite');
    var fclose = Module.findExportByName('libc.so', 'fclose');
    var call_mkdir = new NativeFunction(mkdir, 'int', ['pointer', 'int']);
    var call_chmod = new NativeFunction(chmod, 'int', ['pointer', 'int']);
    var call_fopen =
        new NativeFunction(fopen, 'pointer', ['pointer', 'pointer']);
    var call_fwrite =
        new NativeFunction(fwrite, 'int', ['pointer', 'int', 'int', 'pointer']);
    var call_fclose = new NativeFunction(fclose, 'int', ['pointer']);
    call_mkdir(Memory.allocUtf8String(prefix), 0x1FF);
    call_chmod(Memory.allocUtf8String(prefix), 0x1FF);
    var fp = call_fopen(
        Memory.allocUtf8String(prefix + file_path),
        Memory.allocUtf8String('wb'));
    if (call_fwrite(addr, 1, size, fp)) {
      console.log('[+] Write file success, file path: ' + prefix + file_path);
    } else {
      console.log('[x] Write file failed');
    }
    call_fclose(fp);
  });
}
function HookLibWithCallback(name, callback) {
  var dlopen = Module.findExportByName('libdl.so', 'dlopen');
  var detach_listener = Interceptor.attach(dlopen, {
    onEnter: function(args) {
      var cur = args[0].readCString();
      console.log('[+] dlopen called, name: ' + cur);
      if (cur.indexOf(name) != -1) {
        this.hook = true;
      }
    },
    onLeave: function() {
      if (this.hook) {
        console.log('[+] Hook Lib success, name:', name);
        callback();
        detach_listener.detach();
      }
    }
  });
}
function DumpIL2CPP() {
  var libil2cpp = TraverseModules('single', {name: 'libil2cpp.so'});
  WriteMemToFile(libil2cpp.base, libil2cpp.size, 'libil2cpp.so');
}
function main() {
  HookLibWithCallback('libil2cpp.so', DumpIL2CPP);
}
main();

從手機里把pull出來so,發現函數已經解密,IDA能正常解析,但是還有部分函數會爆紅。因此用010editor對apk中的so和dump出來的so進行比對,補上尾部的重定位表,重新IDA打開,此時已經可以正常解析。

此時的libil2cpp.so非常完整,嘗試使用IL2CPPDumper恢復符號,發現可以成功Dump。

Patch游戲邏輯 && 獲得Flag

在dump.cs中搜索金幣相關方法以及字段,發現了CollectCoin這個函數。

IDA不斷跟進,可以找到一處自增邏輯,那么我們可以利用frida去修改這里的匯編來使我們的金幣獲取更快。

function PatchIncrease() {
  var libil2cpp = TraverseModules('single', {name: 'libil2cpp.so'});
  var insn = libil2cpp.base.add(0x465674);
  console.log('[+] Patching..');
  Memory.protect(insn, 4, 'rwx');
  insn.writeByteArray([0x01, 0xA0, 0x0F, 0x11]);
}

重新啟動游戲,隨便吃幾粒金幣,即可獲取flag。

分析IL2CPP

題目要求實現外掛的注冊機,在dnspy中找到SmallKeyboard類,可以分析到輸入的小鍵盤一些相關字段和方法。

在IDA中跟進get_input_il1li函數,可以發現是一個判斷按鍵類型進入不同函數的邏輯。

結合frida輔助分析可以知道,當KeyType<2時為數字輸入,當KeyType=2時為確認,觸發關鍵邏輯。

分析SmallKeyboard__iI1Ii_4610736函數

其中SmallKeyboard__iI1Ii_4610736為關鍵函數,將我們輸入的key轉為UInt64類型傳入,跟進可以發現有一處跳轉會跳到g_sec2023_p_array中的一個函數,此處我已經patch(為了IDA能正常反編譯該函數)。

使用frida hook首部的跳轉指令(此處已經是patch成NOP)后一條指令,可以發現我們輸入的key發生了變化。

function HookSmallKeyboard$$parse_input_iI1Ii_4610736() {
  var libil2cpp = TraverseModules('single', {name: 'libil2cpp.so'});
  var HookSmallKeyboard$$parse_input_iI1Ii_4610736 =
      libil2cpp.base.add(0x465AB4);
  Interceptor.attach(HookSmallKeyboard$$parse_input_iI1Ii_4610736, {
    onEnter: function(args) {
      PrintNativeBackTraceFuzzy(this.context, TraverseModules('all', {}));
      console.log('[+] HookSmallKeyboard$$parse_input_iI1Ii_4610736 called');
      console.log('input_key: ', args[1]);
      Dump_g_sec2023_o_array();
    }
  });
}
// 假設輸入了1111,則key會變成0x7d8174410d817fa
// [+] HookSmallKeyboard$$parse_input_iI1Ii_4610736 called
// input_key:  0x7d8174410d817fa

該函數中,我們可以找到一個XTEA加密算法,通過frida hook我們可以獲取到key。

function get_reg_code() {
  var libil2cpp = TraverseModules('single', {name: 'libil2cpp.so'});
  var ctor = libil2cpp.base.add(0x4660E8);
  Interceptor.attach(ctor, {
    onEnter: function(args) {
      console.log('[+] vm ctor called');
      console.log('ipt_enc_high: ', this.context.x22);
    },
    onLeave: function(retval) {
      console.log('[+] vm ctor returned');
    }
  });
  var loc_465D34 = libil2cpp.base.add(0x465D34);
  Interceptor.attach(loc_465D34, {
    onEnter: function(args) {
      console.log('[+] get_reg_code called');
      console.log('key_array: ', parseSystemArrayObjectU32(this.context.x20));
      console.log('ipt_enc_high: ', this.context.x22);
      console.log('ipt_enc_low: ', this.context.x21);
      console.log('reg_code_i32: ', this.context.x0);
    }
  });
}

下面是對該加密算法的實現,經過frida對輸入輸出進行驗證,是正確的。

uint32_t key[4] = {0x7B777C63, 0xC56F6BF2, 0x2B670130, 0x76ABD7FE};
void EncryptXTEA(uint32_t *msg, uint32_t *key, ssize_t round) {
  uint32_t v0 = msg[0], v1 = msg[1];
  uint32_t delta = DELTA, sum = 0xBEEFBEEF;
  for (ssize_t idx = 0; idx < round; idx++) {
    v0 += (((v1 << 7) ^ (v1 >> 8)) + v1) ^ (sum - key[sum & 3]);
    sum += delta;
    v1 += (((v0 << 8) ^ (v0 >> 7)) - v0) ^ (sum + key[(sum >> 13) & 3]);
  }
  msg[0] = v0;
  msg[1] = v1;
}

分析libsec2023.so中g_sec2023_p_array的函數

上面的情況說明跳轉到的那個地方有對input進行加密的函數,打開libsec2023.so跟進那個函數分析,其中v5函數指針調用的是一個打log的函數,不用理會。主要是sub_3B8CC。

跟進可以發現函數被混淆了,因為分支跳轉指令利用寄存器尋址。

分析混淆機制以及使用Unicorn去混淆

分析可以發現,是將常規的分支、直接跳轉改為用寄存器尋址跳轉,寄存器中的跳轉地址由一個常數+一個偏移來得到,常數和取偏移的基址在一個函數中是固定的,在跳轉前,使用條件選擇指令等來獲取相應的偏移,從而間接實現分支跳轉。

現在的情況是,我沒有去干掉殼的反調試,因此沒辦法調試libsec2023.so,而這個殼frida也hook不上。但我又不想費力去手動patch掉所有混淆的匯編指令,此時的最佳選擇顯然是利用模擬執行相關的工具去幫我們完成。

去混淆腳本

通過腳本+人工半自動分析,可以很快的去除掉所有需要去除的花指令。

from emu_utils import *
from unicorn import *
from unicorn.arm64_const import *
# from ida_bytes import *
def trace_back_insn_with_target(insn_queue, target_reg):
    for insn in insn_queue:
        if (target_reg in insn.op_str):
            if (insn.mnemonic == 'add'):
                print(insn.mnemonic + '\t' + insn.op_str)
        if (insn.mnemonic == 'ldr'):
            print(insn.mnemonic + '\t' + insn.op_str)
        if (insn.mnemonic == 'csel'):
            print(insn.mnemonic + '\t' + insn.op_str)
def log_hook(emu, addr, size, user_data):
    disasm = get_disasm(emu, addr, size)
    print(hex(addr) + '\t' + disasm.mnemonic + '\t' + disasm.op_str)
def step_over_hook(emu, addr, size, none):
    disasm = get_disasm(emu, addr, size)
    if (disasm.mnemonic == 'bl' or disasm.mnemonic == 'blr'):
        emu.reg_write(UC_ARM64_REG_PC, addr + size)
    if (disasm.mnemonic == 'ret'):
        print('function returned')
        emu.emu_stop()
    if (addr == 0x3ac68):
        emu.reg_write(UC_ARM64_REG_W10, 0xEECF7326)
def normal_hook(emu, addr, size, insn_queue):
    global const_value, offset_value, cond, cond_value, uncond_value
    disasm = get_disasm(emu, addr, size)
    reg_maps = get_reg_maps()
    insn_queue.insert(0, disasm)
    if (len(insn_queue) > 8):
        insn_queue.pop()
    if (disasm.mnemonic == 'csel'):
        cond_value = emu.reg_read(reg_maps[disasm.op_str.split(', ')[1]])
        uncond_value = emu.reg_read(reg_maps[disasm.op_str.split(', ')[2]])
        cond = disasm.op_str.split(', ')[3]
    if (disasm.mnemonic == 'cset'):
        cond_value = 1
        uncond_value = 0
        cond = disasm.op_str.split(', ')[1]
    if (disasm.mnemonic == 'ldr'):
        if (len(disasm.op_str.split(', ')) == 3):
            offset_value = emu.reg_read(
                reg_maps[disasm.op_str.split(', ')[1].split('[')[1]])
        elif (len(disasm.op_str.split(', ')) == 4):
            offset_value = emu.reg_read(
                reg_maps[disasm.op_str.split(', ')[1].split('[')[1]])
            cond_value *= 8
    if (disasm.mnemonic == 'add' and '#' not in disasm.op_str and 'w' not in disasm.op_str):
        const_value = emu.reg_read(reg_maps[disasm.op_str.split(', ')[2]])
    if (disasm.mnemonic == 'br'):
        print('on br insn')
        target_reg = disasm.op_str
        trace_back_insn_with_target(insn_queue, target_reg)
        print(hex(const_value), hex(offset_value),
              cond, cond_value, uncond_value)
        cond_addr = emu.mem_read(offset_value + cond_value, 4)
        cond_addr = (int.from_bytes(
            cond_addr, byteorder='little') + const_value) & 0xffffffff
        uncond_addr = emu.mem_read(offset_value + uncond_value, 4)
        uncond_addr = (int.from_bytes(
            uncond_addr, byteorder='little') + const_value) & 0xffffffff
        patch_asm = b''
        patch_asm += get_asm('b' + cond + ' ' + hex(cond_addr), addr - 4)
        patch_asm += get_asm('b ' + hex(uncond_addr), addr)
        # patch_bytes(addr - 4, patch_asm)
        emu.reg_write(UC_ARM64_REG_PC, addr + size)
def emulate_execution(filename, start_addr, hook_func, user_data):
    emu = Uc(UC_ARCH_ARM64, UC_MODE_LITTLE_ENDIAN)
    textSec = get_section(filename, '.text')
    dataSec = get_section(filename, '.data')
    textSec_entry = textSec.header['sh_addr']
    textSec_size = textSec.header['sh_size']
    textSec_raw = textSec.header['sh_offset']
    TEXT_BASE = textSec_entry >> 12 << 12
    TEXT_SIZE = (textSec_size + 0x1000) >> 12 << 12
    TEXT_RBASE = textSec_raw >> 12 << 12
    dataSec_entry = dataSec.header['sh_addr']
    dataSec_size = dataSec.header['sh_size']
    dataSec_raw = dataSec.header['sh_offset']
    DATA_BASE = dataSec_entry >> 12 << 12
    DATA_SIZE = (dataSec_size + 0x1000) >> 12 << 12
    DATA_RBASE = dataSec_raw >> 12 << 12
    VOID_1_BASE = 0x00000000
    VOID_1_SIZE = TEXT_BASE
    VOID_2_BASE = TEXT_BASE + TEXT_SIZE
    VOID_2_SIZE = DATA_BASE - VOID_2_BASE
    STACK_BASE = DATA_BASE + DATA_SIZE
    STACK_SIZE = 0xFFFFFFFF - STACK_BASE >> 12 << 12
    emu.mem_map(VOID_1_BASE, VOID_1_SIZE)
    emu.mem_map(TEXT_BASE, TEXT_SIZE)
    emu.mem_map(DATA_BASE, DATA_SIZE)
    emu.mem_map(VOID_2_BASE, VOID_2_SIZE)
    emu.mem_map(STACK_BASE, STACK_SIZE)
    emu.mem_write(TEXT_BASE, read(filename)[TEXT_RBASE:TEXT_RBASE+TEXT_SIZE])
    emu.mem_write(DATA_BASE, read(filename)[DATA_RBASE:DATA_RBASE+DATA_SIZE])
    emu.reg_write(UC_ARM64_REG_FP, STACK_BASE + 0x1000)
    emu.reg_write(UC_ARM64_REG_SP, STACK_BASE + STACK_SIZE // 2)
    emu.hook_add(UC_HOOK_CODE, log_hook)
    emu.hook_add(UC_HOOK_CODE, step_over_hook, user_data)
    emu.hook_add(UC_HOOK_CODE, hook_func, user_data)
    emu.emu_start(start_addr, 0x0)
if __name__ == '__main__':
    filename = './libsec2023.so'
    start_addr = 0x3ac68
    insn_queue = []
    emulate_execution(filename, start_addr, normal_hook, insn_queue)

分析加密函數

去混淆后即可分析函數,經過分析,可以知道是對input以4字節為一組分別進行加密。

其中enc_1邏輯較為明顯,顯然是可逆的。

下面這個函數實現了上面的加密算法,經過frida驗證輸入輸出是正確的。

void EncryptObfusedRoundI(uint32_t ipt[]) {
  for (ssize_t idx = 0; idx < 2; ++idx) {
    uint8_t buf[4] = {0}, tmp[4] = {0};
    for (ssize_t i = 0; i < 4; i++) {
      buf[i] = ((uint8_t *)&ipt[idx])[i] ^ i;
    }
    buf[3] ^= 0x86;
    buf[2] -= 0x5E;
    buf[1] ^= 0xD3;
    buf[0] -= 0x1C;
    for (ssize_t i = 0; i < 4; i++) {
      tmp[i] = buf[i] - i * 8;
    }
    ipt[idx] = *(uint32_t *)&tmp;
  }
}

而enc_2中調用了一個類中的方法,我們只知道這個方法叫什么,但是并不知道是哪個類下的方法,因此我們需要hook GetStaticMethodID這個JNI函數。

[圖片]

通過hook JNI函數找到調用解密函數的類

通過hook,我們可以知道調用的是Sec2023.Encrypt方法,但是JADX中并沒有找到,不能直接分析。說明這個類是由殼動態加載的,因此我們需要找辦法dump下來dex。

function HookGetStaticMethodID() {
  var libart = TraverseModules('single', {name: 'libart.so'});
  var GetStaticMethodID = libart.base.add(0x3A87B4);
  Interceptor.attach(GetStaticMethodID, {
    onEnter: function(args) {
      var clazz = args[1];
      var name = args[2].readUtf8String();
      var sig = args[3].readUtf8String();
      // get clazz name
      var clazz_name = Java.vm.getEnv().getClassName(clazz);
      console.log(`[*] GetStaticMethodID called: ${clazz_name} ${name} ${sig}`);
    }
  });
}

使用frida-dexdump獲取殼加載的Dex && 分析Sec2023.Encrypt方法

通過這個工具我們可以很輕松地在內存里找到相關的dex,dump下來就可以接著分析了。

打開分析,可以發現加了BlackObfuscator,而我并沒有現成的解決方案可以參考和使用,那就只能推理和猜猜看了。

分析被BlackObfuscator混淆的算法

這里我用一種很簡單粗暴的辦法,就是把每個case中的真實指令直接復制出來,然后可以發現變量之間的運算有明顯的邏輯關系,經過推理和結合frida分析驗證(上面的frida腳本log了加密完的key),可以推理出正確的加密算法。

uint32_t EncryptObfusedRoundII(uint32_t ipt) {
  uint32_t n1 = 0, n2 = 0;
  uint8_t result[4] = {0}, *buf = (uint8_t *)&ipt;
  uint8_t key[8] = {0x32, 0xCD, 0xFF, 0x98, 0x19, 0xB2, 0x7C, 0x9A};
  n1 = __builtin_bswap32(*(uint32_t *)buf);
  n2 = (n1 >> 7) | (n1 << 25);
  *(uint32_t *)result = __builtin_bswap32(n2);
  for (int idx = 0; idx < 8; idx++) {
    result[idx] = (result[idx] ^ key[idx % 8]);
    result[idx] = (result[idx] + idx);
  }
  return *(uint32_t *)result;
}

使用frida分析VM && 使用frida解釋VM

分析完上面提到的算法之后,我們可以在此處找到一個函數的調用,它改變了input的值,所以應為加密算法。

對這兩個類進行分析,可以知道上面那個類中的都是opcode dict的key,下面那個類是VM的Handler和一些相關的字段。

VM直接分析不好分析,我們需要側面去分析。兩種常用的方法:一是把指令數組和handler自己實現在程序外部自行分析,二是直接hook來分析。這里選擇直接hook通過打log來還原VM邏輯。

首先需要解析VM類的this指針。

function ParseVMClazz(thiz) {
  var fields = thiz.add(0x10);
  var opcode = fields.add(0x00);
  var input = fields.add(0x08);
  var buf = fields.add(0x10);
  var x = fields.add(0x18);
  var cnt = fields.add(0x1C)
  var maybe_idx = fields.add(0x20);
  var num = fields.add(0x24);
  var opdict = fields.add(0x28);
  var vm_context = {
    opcode: opcode.readPointer(),
    input: input.readPointer(),
    buf: buf.readPointer(),
    x: x.readS32(),
    cnt: cnt.readS32(),
    maybe_idx: maybe_idx.readS32(),
    num: num.readS32(),
    opdict: opdict.readPointer(),
  };
  return vm_context;
}

然后通過手動hook一個個handler來實現VM解釋器。

function VMparser() {
  var libil2cpp = TraverseModules('single', {name: 'libil2cpp.so'});
  var array_op_1_O00O0000O0o = libil2cpp.base.add(0x46B480);
  Interceptor.attach(array_op_1_O00O0000O0o, {
    onEnter: function(args) {
      var x = ParseVMClazz(args[0]).x;
      // console.log('[*] array_op_1_O00O0000O0o called');
      console.log(`    input[buf${x}] = buf${x - 1};`);
    }
  });
  var array_op_2_O00O00000oo = libil2cpp.base.add(0x46B4E8);
  Interceptor.attach(array_op_2_O00O00000oo, {
    onEnter: function(args) {
      var x = ParseVMClazz(args[0]).x;
      var opcode = ParseVMClazz(args[0]).opcode.add(32);
      var cnt = ParseVMClazz(args[0]).cnt;
      // console.log('[*] array_op_2_O00O00000oo called');
      console.log(`    input[${opcode.add(cnt * 2).readU16()}] = buf${x};`);
    }
  });
  var array_op_3_O00O0000OO = libil2cpp.base.add(0x46B3F8);
  Interceptor.attach(array_op_3_O00O0000OO, {
    onEnter: function(args) {
      var x = ParseVMClazz(args[0]).x;
      var opcode = ParseVMClazz(args[0]).opcode.add(32);
      var cnt = ParseVMClazz(args[0]).cnt;
      // console.log('[*] array_op_3_O00O0000OO called');
      console.log(`    buf${x + 1} = input[${opcode.add(cnt * 2).readU16()}];`);
    }
  });
  var array_op_4_O0O0000Ooo = libil2cpp.base.add(0x46B28C);
  Interceptor.attach(array_op_4_O0O0000Ooo, {
    onEnter: function(args) {
      var opcode = ParseVMClazz(args[0]).opcode.add(32);
      var cnt = ParseVMClazz(args[0]).cnt;
      // console.log('[*] array_op_4_O0O0000Ooo called');
      console.log(`    cnt = ${opcode.add(cnt * 2).readU16()};`);
    }
  });
  .....
  ...
}

隨便輸入一個字符串,點擊確認按鈕,我們就可以得到一個輸出的形式為C代碼的VM邏輯。

輸出如下,因為后面我放到直接把他作為加密函數來驗證VM邏輯是否dump正確,所以有一些改動。

  register uint32_t buf0, buf1, buf2, buf3, buf4, buf5, cnt;
  uint32_t input[8] = {0};
  input[0] = ipt[0];
  input[1] = ipt[1];
  buf0 = 24;
  input[2] = buf0;
  buf0 = input[0];
  buf1 = input[2];
  buf0 = ((~(buf0 + 0x100) | 0xFFFFFEFF) + 2 * buf0 +
          (~(buf0 + 0x100) & 0xFFFFFEFF) + 0x202) >>
         buf1;
  buf1 = 255;
  buf0 = (buf1 + (buf1 ^ ~buf0) + (~buf0 & ~buf1) + (buf0 & ~buf1) + 1) & buf0;
  buf1 = input[2];
  buf2 = 8;
  buf1 = buf1 - buf2 + (buf1 ^ buf2) + (buf1 & buf2) - (buf1 | buf2);
  input[2] = buf1;
  buf1 = input[2];
  buf2 = 0;
  buf1 = buf1 < buf2;
  if (buf1) {
    cnt = 26;
  } else {
    cnt = 4;
  }
  .....
  ...

由于我們上面其他加密算法都已經找到了,再加上這個,我們可以對輸入輸出做完整的驗證。經過frida hook輸入輸出可以知道dump下來的VM邏輯沒問題。至此,最后一個問題就是如何去得到這個算法的逆算法。

重編譯VM指令,恢復算法邏輯

對于這種VM,我們可以直接重新開優化編譯,并把其中的一些變量用register關鍵字定義為寄存器變量,這樣IDA的反編譯效果會有極大的提升。

重新編譯后,IDA反編譯出的算法邏輯如下,總共一百多行,不多也不少。難就難在有指令替換。打算直接動調猜每一條指令的作用(因為變量不會被重復賦值,指令也并沒有像原本VM這種難以接受的分析,所以我覺得這是可行的)。

四、梳理加密過程,開整注冊機

題目要求對于任意TOKEN,我們的注冊機都能生成一個KEY來注冊外掛。那就意味著隨機生成的TOKEN經過解密之后就是KEY。那注冊機其實就是上述所有算法的解密算法實現。我們知道,首先執行的是殼內兩個算法加密,然后是VM內的算法加密,最后是XTEA算法加密,那么注冊機就是逆過來實現就完事了。

XTEA解密算法

void DecryptXTEA(uint32_t *msg, uint32_t *key, ssize_t round) {
  uint32_t v0 = msg[0], v1 = msg[1];
  uint32_t delta = DELTA, sum = 0xBEEFBEEF + delta * round;
  for (ssize_t idx = 0; idx < round; idx++) {
    v1 -= (((v0 << 8) ^ (v0 >> 7)) - v0) ^ (sum + key[(sum >> 13) & 3]);
    sum -= delta;
    v0 -= (((v1 << 7) ^ (v1 >> 8)) + v1) ^ (sum - key[sum & 3]);
  }
  msg[0] = v0;
  msg[1] = v1;
}

VM解密算法

暫未實現

殼內第二個算法的解密算法

uint32_t DecryptObfusedRoundII(uint32_t enc) {
  uint32_t n1 = 0, n2 = 0;
  uint8_t result[4] = {0}, *buf = (uint8_t *)&enc;
  uint8_t key[8] = {0x32, 0xCD, 0xFF, 0x98, 0x19, 0xB2, 0x7C, 0x9A};
  for (int idx = 0; idx < 8; idx++) {
    result[idx] = (buf[idx] - idx);
    result[idx] = (result[idx] ^ key[idx % 8]);
  }
  n2 = __builtin_bswap32(*(uint32_t *)result);
  n1 = (n2 << 7) | (n2 >> 25);
  return __builtin_bswap32(n1);
}

殼內第一個算法的解密算法

void DecryptObfusedRoundI(uint32_t enc[]) {
  for (ssize_t idx = 0; idx < 2; ++idx) {
    uint8_t buf[4] = {0}, tmp[4] = {0};
    *(uint32_t *)&tmp = enc[idx];
    for (ssize_t i = 0; i < 4; i++) {
      buf[i] = tmp[i] + i * 8;
    }
    buf[3] ^= 0x86;
    buf[2] += 0x5E;
    buf[1] ^= 0xD3;
    buf[0] += 0x1C;
    for (ssize_t i = 0; i < 4; i++) {
      tmp[i] = buf[i] ^ i;
    }
    enc[idx] = *(uint32_t *)&tmp;
  }
}