某車聯網App 通訊協議加密分析(四) Trace Code
一、目標
之前我們已經通過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
縱浪大化中 不喜亦不懼 應盡便須盡 無復獨多慮
- 設計模式之狀態模式
- 如何實現資料庫讀一致性
- 我是怎麼入行做風控的
- C 11精要:部分語言特性
- 吳恩達來信:人工智慧領域的求職小 tips
- EasyCV帶你復現更好更快的自監督演算法-FastConvMAE
- 某車聯網App 通訊協議加密分析(四) Trace Code
- 帶你瞭解CANN的目標檢測與識別一站式方案
- EasyNLP玩轉文字摘要(新聞標題)生成
- PostgreSQL邏輯複製解密
- 基於 CoreDNS 和 K8s 構建雲原生場景下的企業級 DNS
- 迴圈神經網路(RNN)可是在語音識別、自然語言處理等其他領域中引起了變革!
- 技術分享| 分散式系統中服務註冊發現元件的原理及比較
- 利用谷歌地圖採集外貿客戶的電話和手機號碼
- 跟我學Python影象處理丨關於影象金字塔的影象向下取樣和向上取樣
- 帶你掌握如何使用CANN 運算元ST測試工具msopst
- 一招教你如何高效批量匯入與更新資料
- 一步步搞懂MySQL元資料鎖(MDL)
- 你知道如何用 PHP 實現多程序嗎?
- KubeSphere 閘道器的設計與實現(解讀)