某車聯網App 通訊協議加密分析(四) Trace Code

語言: CN / TW / HK

一、目標

之前我們已經通過Trace Block 來比對了Unidbg和App跑的結果。現在他們執行的流程都差不多了,但是結果還是不對,今天我們就要通過Trace Code進行更細緻的對比。

v6.1.0

二、步驟

縮小Trace的範圍

Trace Code那麼好使,我們為什麼不一上來就Trace一遍?

因為Trace Code的粒度太細了,一上來就搞,跑出幾百萬行結果,根本沒法看。

我們通過Trace Block已經在逐步縮小範圍了。

JNIEnv->GetStringUtfChars("FlK6XicivmCwPSE3sk6b71m9WbWd/gYZtlajqGXhEXXjmWEZziR51rVWSEDwUUi4UN9RnoCGbLNmqI80Fiog4Sw==") was called from RX@0x4002b744[libencrypt.so]0x2b744
  sub_2b8b8
  sub_2b800
  sub_2b800
  sub_2b800
  sub_2b800
  sub_2b828
  sub_ab6c
  sub_a528
  sub_ab9c
  sub_a848
  sub_a7e8
  sub_a7c4
  sub_8ee4
  sub_7334
  sub_8f40
  sub_95e0
 ........
  sub_a7c4
  sub_8ee4
  sub_7334
  sub_8f40
  sub_96bc
  sub_9a2c
  sub_9268
  sub_a818
  sub_a90c
  sub_a928
  sub_2b854
JNIEnv->ReleaseStringUTFChars("FlK6XicivmCwPSE3sk6b71m9WbWd/gYZtlajqGXhEXXjmWEZziR51rVWSEDwUUi4UN9RnoCGbLNmqI80Fiog4Sw==") was called from RX@0x4002b86c[libencrypt.so]0x2b86c

我們的目標大概率是在 0x7000 → 0xa000 這個地址範圍之類。

定位Trace Code目標

翻一翻IDA裡面的匯出表

1:ida

比較閤眼緣的就這三個了,0x7184 ,0x77A4 貌似都沒有被Trace Block命中,感覺幕後大boss應該就是這個 0x8EE4

Tip:

可以在懷疑的幾個函式上加個hook,看看是否命中。

Unidbg Trace Code

emulator.attach().addBreakPoint(module.base + 0x8EE4  , new BreakPointCallback() {
    @Override
    public boolean onHit(Emulator<?> emulator, long address) {
        System.out.println(" ====== traceCode ====== ");

        UnidbgPointer pX = UnidbgPointer.register(emulator, Arm64Const.UC_ARM64_REG_X1);

        byte[] bData = pX.getByteArray(0,16);
        String strLabel = String.format("x0值 0x%08x", pX.peer);
        Inspector.inspect(bData,strLabel);

        try {
            emulator.traceCode(module.base + 0x8EE4, module.base + 0x9C0C).setRedirect(new PrintStream(new File("traceCodeCar.log")));
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }

        return true;
    });

}

先在 0x8EE4 加個斷點,然後開始Trace Code。 引數是Trace的起始地址和結束地址,已經儲存的結果檔案。

從這個Trace結果來看,每16個位元組去做解密。典型的AES。

Frida Trace Code

Stalker的好處很多,最大的功能是可以過反除錯,所以基於Stalker去寫Trace Code,寫了好幾個版本,一直不大滿意。

前幾天發現了

http://github.com/IIIImmmyyy/frida-trace

比較帥,拿來改了改。效果很好。(只支援Arm64)

let moduleBase;
let isFirstIn = true;
let pre_regs;
let infoMap = new Map();
let detailInsMap = new Map();

function parserNextAddr(ins) {
    let s = JSON.stringify(ins);
    let address = ins.address;
    // log("address:"+address)
    let offset = address - moduleBase;
    let s1 = (offset).toString(16);
    let entity = {};
    entity.address = offset;
    return s1;
}

const byteToHex = [];
for (let n = 0; n <= 0xff; ++n) {
    const hexOctet = n.toString(16).padStart(2, "0");
    byteToHex.push(hexOctet);
}

function hex(arrayBuffer) {
    const buff = new Uint8Array(arrayBuffer);
    const hexOctets = [];
    for (let i = 0; i < buff.length; ++i)
        hexOctets.push(byteToHex[buff[i]]);
    return hexOctets.join("");
}

function formatArm64Regs(context) {
    let regs = [];
    regs.push(context.x0);
    regs.push(context.x1);
    regs.push(context.x2);
    regs.push(context.x3);
    regs.push(context.x4);
    regs.push(context.x5);
    regs.push(context.x6);
    regs.push(context.x7);
    regs.push(context.x8);
    regs.push(context.x9);
    regs.push(context.x10);
    regs.push(context.x11);
    regs.push(context.x12);
    regs.push(context.x13);
    regs.push(context.x14);
    regs.push(context.x15);
    regs.push(context.x16);
    regs.push(context.x17);
    regs.push(context.x18);
    regs.push(context.x19);
    regs.push(context.x20);
    regs.push(context.x21);
    regs.push(context.x22);
    regs.push(context.x23);
    regs.push(context.x24);
    regs.push(context.x25);
    regs.push(context.x26);
    regs.push(context.x27);
    regs.push(context.x28);
    regs.push(context.fp);
    regs.push(context.lr);
    regs.push(context.sp);
    regs.push(context.pc);
    return regs;
}
function getPcReg(regs) {
    return regs[32];
}
function isRegsChange(context, ins) {
    let currentRegs = formatArm64Regs(context);
    let logInfo = "";
    for (let i = 0; i < 32; i++) {
        if (i === 30) {
            continue;
        }
        let preReg = pre_regs[i];
        let currentReg = currentRegs[i];
        if (Number(preReg) !== Number(currentReg)) {
            if (logInfo === "") {
                //嘗試讀取string
                let changeString = "";
                try {
                    let nativePointer = new NativePointer(currentReg);
                    changeString = nativePointer.readCString();
                }
                catch (e) {
                    changeString = "";
                }
                if (changeString !== "") {
                    currentReg = currentReg + "   (" + changeString + ")";
                }
                logInfo = "\t " + getRegsString(i) + " = " + preReg + " --> " + currentReg;
            }
            else {
                logInfo = logInfo + "\t " + getRegsString(i) + " = " + preReg + " --> " + currentReg;
            }
        }
    }
    //列印PC暫存器
    let parse = JSON.parse(ins);
    let mnemonic = parse.mnemonic; //補充str
    if (mnemonic === "str") {
        let strParams = getStrParams(parse, currentRegs);
        logInfo = logInfo + strParams;
    }
    else if (mnemonic === "cmp") {
        let cmpParams = getCmpParams(parse, currentRegs);
        logInfo = logInfo + cmpParams;
    }
    else if (mnemonic === "b.gt" || mnemonic === "b.le" || mnemonic === "b.eq" || mnemonic === "b.ne" || mnemonic === "b") {
        // log(ins)
        let bgtAddr = getbgtAddr(parse, currentRegs);
        logInfo = logInfo + bgtAddr;
    }
    let entity = {};
    entity.info = logInfo;
    let address = parse.address;
    if (lastAddr === undefined) {
        entity.color = getColor();
        lastAddr = address;
    }
    else {
        let number = address - lastAddr;
        if (number === 0x4) {
            entity.color = getColor();
        }
        else {
            currentIndex++;
            entity.color = getColor();
        }
        lastAddr = address;
    }
    pre_regs = currentRegs;
    return entity;
}
let lastAddr = undefined;
let currentIndex = 0;
function getColor() {
        return "";

    if (currentIndex > 1) {
        currentIndex = 0;
    }
    if (currentIndex === 0) {
        return "C35";  // logger_1.LogColor.C35;
    }
    else if (currentIndex === 1) {
        return "C97";  // logger_1.LogColor.C97;
    }
    else if (currentIndex === 2) {
        return "C97";  // logger_1.LogColor.C97;
    }
}
function getRegsString(index) {
    let reg;
    if (index === 31) {
        reg = "sp";
    }
    else {
        reg = "x" + index;
    }
    return reg;
}
function getbgtAddr(parser, currentRegs) {
    let bgtAddr = "";
    let operands = parser.operands;
    for (let i = 0; i < operands.length; i++) {
        let operand = operands[i];
        if (operand.type === "imm") {
            let value = operand.value;
            let number = value - moduleBase;
            bgtAddr = "\t block addr:" + number.toString(16);
            break;
        }
    }
    return bgtAddr;
}
function getStrParams(parser, currentRegs) {
    let operands = parser.operands;
    for (let i = 0; i < operands.length; i++) {
        let operand = operands[i];
        if (operand.type === "reg") {
            //獲取value
            let value = operand.value;
            if (value === "wzr") {
                return "\t " + "str = 0";
            }
            else {
                let replace = value.replace("w", "");
                let index = replace.replace("x", "");
                let index_reg = currentRegs[index];
                let changeString = "";
                try {
                    let nativePointer = new NativePointer(index_reg);
                    changeString = nativePointer.readCString();
                }
                catch (e) {
                    changeString = "";
                }
                //讀取值
                if (changeString !== "") {
                    index_reg = index_reg + "   (" + changeString + ")";
                }
                return "\t " + "str = " + index_reg;
            }
        }
    }
}
function getCmpParams(parser, currentRegs) {
    let operands = parser.operands;
    let cmpInfo = "";
    for (let i = 0; i < operands.length; i++) {
        let operand = operands[i];
        if (operand.type === "reg") {
            let value = operand.value;
            let replace = value.replace("w", "");
            let index = replace.replace("x", "");
            let index_reg = currentRegs[index];
            let changeString = "";
            try {
                let nativePointer = new NativePointer(index_reg);
                changeString = nativePointer.readCString();
            }
            catch (e) {
                changeString = "";
            }
            //讀取值
            if (changeString !== "") {
                index_reg = index_reg + "   (" + changeString + ")";
            }
            cmpInfo = cmpInfo + "\t " + value + " = " + index_reg;
        }
    }
    return cmpInfo;
}

function ZY_unTraceAddrEnd(){
    var tid = Process.getCurrentThreadId();
    Stalker.unfollow(tid);
    Stalker.garbageCollect();
    console.log(TAG + " ======== unTraceAddr_End");
}

function ZY_trace_Stalker_begin(soname, addr, size) {
    let module = Process.findModuleByName(soname);
    moduleBase = module.base;
    console.log(TAG + JSON.stringify(module));
        console.log(TAG + "addr = " + addr);
        console.log(TAG + "size = " + size);

    Interceptor.attach(moduleBase.add(addr), {
        onEnter: function (args) {
            this.pid = Process.getCurrentThreadId();
                        // console.log(TAG + " ==== ZY_trace_Stalker_begin ==== ");

            //看下結構體的值
            Stalker.follow(this.pid, {
                events:{
                    call:false,
                    ret:false,
                    exec:false,

                    block:false,
                    compile:false
                },
                onReceive:function(events){
                },

                transform: function (iterator) {
                    let lastInfo;
                    const instruction = iterator.next();
                    let startAddress = instruction.address;
                    // console.log(TAG + "startAddress:" + startAddress + " base:" + module.base );

                    if (size === 0) {
                        size = module.size;
                        addr = 0;
                    }

                    const isModuleCode = startAddress.compare(moduleBase.add(addr)) >= 0 &&
                                                             startAddress.compare(moduleBase.add(addr).add(size)) < 0;
                    do {
                        if (isModuleCode) {
                                                        // console.log(TAG + instruction.address + ":" + instruction);

                            let s = parserNextAddr(instruction);
                            let address = instruction.address;
                            let offset = address - moduleBase;
                            let lastInfo = s.toString(16) + "\t\t" + instruction;
                            detailInsMap.set(offset, JSON.stringify(instruction));
                            infoMap.set(offset, lastInfo);
                            iterator.putCallout(function (context) {
                                let regs = JSON.stringify(context);
                                if (isFirstIn) {
                                    isFirstIn = false;
                                    //儲存暫存器
                                    pre_regs = formatArm64Regs(context);
                                }
                                else {
                                    //列印的實際是上一次的 這樣延遲一次可以打印出暫存器變化
                                    let pcReg = getPcReg(pre_regs);
                                    let offset = Number(pcReg) - moduleBase;
                                    let logInfo = infoMap.get(offset);
                                    let detailIns = detailInsMap.get(offset);
                                    // log("detailIns:"+detailIns)
                                    let entity = isRegsChange(context, detailIns);
                                    console.log(TAG + logInfo + " ; " + entity.info, entity.color);
                                }
                            });
                        }
                        iterator.keep();
                    } while (iterator.next() != null);
                },
            });
        },
        onLeave: function (ret) {
            // libtprt.saveStringMapTofile();
            Stalker.unfollow(this.pid);
            console.log(TAG + "ret:" + ret);
        }
    });
}

呼叫方法

ZY_trace_Stalker_begin('libencrypt.so',0x8EF4, 0x9C0C - 0x8EF4);

Tip:

Trace之前可以先匹配入參,只Trace指定的密文。然後unidbg 去 Trace 同樣的密文,這樣有利於比對。

對比結果

前戲鋪墊的太長了,總算拿到Trace Code的結果了。

1:main

這裡有個很奇怪的地方,

9608: "ldr w8, [x9, x8, lsl #2]" x9=0x40147510 x8=0x9094 => w8=0xf54de125

0x40147510 這個地址是在so的資料段。 這段程式碼的意思是 從 0x40147510+(0x9094<<2) = 0x4016B760 這個地址取資料

m0x4016B760

>-----------------------------------------------------------------------------<
[11:57:20 551]RW@0x4016b760[libencrypt.so]0x16b760, md5=cc2b1f1f88429f40f84599a613cf3143, hex=25e14df5a6ea8219a148998cebd3d9b5e55522c3109f9bd0347f27854da2729b83f3a390ce0c318645a588f14b339626692918f7119819acb36b0f92cd0dc4d596a74b1bc8f1263f2841c72757b265b4b1692c56a24a9c8f528a41e0d1d95149e25a0120a9e884147d223f7323eb4ff2
size: 112
0000: 25 E1 4D F5 A6 EA 82 19 A1 48 99 8C EB D3 D9 B5    %.M......H......
0010: E5 55 22 C3 10 9F 9B D0 34 7F 27 85 4D A2 72 9B    .U".....4.'.M.r.

從unidbg裡面列印一下,沒毛病。 就是 25 E1 4D F5

但是對應的App的結果就有點意思了

9608                ldr w8, [x9, x8, lsl #2] ;          x8 = 0x90f7 --> 0x73b55372

0x40147510+(0x90f7<<2) = 0x4016B8EC

m0x4016B8EC

>-----------------------------------------------------------------------------<
[12:00:52 810]RW@0x4016b8ec[libencrypt.so]0x16b8ec, md5=cd71edc06f5f49b56a700c1f6e541610, hex=e35324c7bb9b61e3b06ee0f4f571fb062a44630bc501d369b49e3e2cee56eaeb463ab0c57e5c3b12a8abf8f81264047dd98a871bc1b84e9d807db83241a8ee1d0be8aeecd8e1328a7311b4a73e2c9b925f24946cc35eacb6953472a0b4cdc9baebce5c65701ab7afa0f48251f1e5afab
size: 112
0000: E3 53 24 C7 BB 9B 61 E3 B0 6E E0 F4 F5 71 FB 06    .S$...a..n...q..
0010: 2A 44 63 0B C5 01 D3 69 B4 9E 3E 2C EE 56 EA EB    *Dc....i..>,.V..

這個地址對應的結果不對。 難道App會在記憶體中變異?

m0x4017B8EC

>-----------------------------------------------------------------------------<
[12:01:51 113]RW@0x4017b8ec[libencrypt.so]0x17b8ec, md5=09b5524828f471a42a3e75beed7b23a8, hex=7253b573f596a2d945a40787a842b3a0e46eadcc761474b12000e2a1ec0ab7053cca47f537ed81c4b823dc9866cc99a7b5b4b4adb3fffa867aa45080ea698ddb04e771872ee075f999b9ba241908bc5ac29666eb5f6a7800abee8213d52a28f19321df19b077a1716dbf3ae72a10f33f
size: 112
0000: 72 53 B5 73 F5 96 A2 D9 45 A4 07 87 A8 42 B3 A0    rS.s....E....B..
0010: E4 6E AD CC 76 14 74 B1 20 00 E2 A1 EC 0A B7 05    .n..v.t. .......

在unidbg的記憶體裡面搜尋了一下,發現加上 0x10000 之後的值恰好是App裡顯示的一致。

真相只有一個

pc時代過來的老同學就很敏感了。這個so並不是原始so,而是我們在記憶體中dump出來的。

我們觀察下原始so的節表

fenfei$ greadelf -l libencrypt.so

Elf file type is DYN (Shared object file)
Entry point 0x3210
There are 5 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x000000000002f288 0x000000000002f288  R E    0x10000
  LOAD           0x000000000002fba0 0x000000000003fba0 0x000000000003fba0
                 0x0000000000194a08 0x0000000000194ab8  RW     0x10000
  DYNAMIC        0x00000000001c5b90 0x000000000003fc00 0x000000000003fc00
                 0x0000000000000210 0x0000000000000210  RW     0x8
readelf: Error: no .dynamic section in the dynamic segment
  GNU_EH_FRAME   0x000000000002cd10 0x000000000002cd10 0x000000000002cd10
                 0x000000000000048c 0x000000000000048c  R      0x4
  LOAD           0x00000000001c5078 0x00000000001d5078 0x00000000001d5078
                 0x0000000000000afc 0x0000000000000afc  R E    0x1000

檔案地址 0x2fba0 對映到了 記憶體地址  0x3fba0

所以我們dump出來的so要修復這個對映

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x000000000002f288 0x000000000002f288  R E    0x10000
  LOAD           0x000000000003fba0 0x000000000003fba0 0x000000000003fba0
                 0x0000000000194a08 0x0000000000194ab8  RW     0x10000
  DYNAMIC        0x00000000001c5b90 0x000000000003fc00 0x000000000003fc00
                 0x0000000000000210 0x0000000000000210  RW     0x8
readelf: Error: no .dynamic section in the dynamic segment
  GNU_EH_FRAME   0x000000000002cd10 0x000000000002cd10 0x000000000002cd10
                 0x000000000000048c 0x000000000000048c  R      0x4
  LOAD           0x00000000001c5078 0x00000000001d5078 0x00000000001d5078
                 0x0000000000000afc 0x0000000000000afc  R E    0x1000

把我們dump出的so頭裡面的 0x2fba0 改成 0x3fba0

這次終於可以成功解密了

call decheckcode: {"code":"0","message":"success","respondData":{"serverTime":1663643024410,"timeSpan":"5","loginFlag":0}}

三、總結

忙活了老半天,其實最後只做了一下修復so檔案頭。嚴格意義上只改了3個位元組。

改3個位元組很簡單,分析並知道如何改,再哪改,才是我們的重點。

Trace Function 、Trace Block 、Trace Code。逐漸縮小範圍來定位。

還可以通過不同的入參來TraceCode,對比一下更有助於分析演算法。

1:ffshow

縱浪大化中 不喜亦不懼 應盡便須盡 無復獨多慮