2023腾讯游戏安全mobile端初赛wp
绕过一些简单的检测
-
绕过端口检测。
app
对 27042 与23946端口有检测。frida
换个启动端口即可。./frida-server-16.1.10-android-arm64 -l 0.0.0.0:1234
adb forward tcp:1234 tcp:1234 IDA
也换个调试端口./android_server64 -p 23947
adb forward tcp:23947 tcp:23947 -
绕过
ida_server
的检测。app
会检测/data/local/tmp
目录下有无android_server64
等文件。我们直接把
android_server64
换个位置即可。
Il2CppDumper dump
https://github.com/Perfare/Il2CppDumper
正常情况下 运行下面命令即可
Il2CppDumper.exe .\libil2cpp.so .\global-metadata.dat <output-directory>
但对于这道题目,我们没有dump到相关文件
查看 global-metadata.dat
的16进制内容,可以发现,内容很正常,没用被加密的迹象
再看一下 libil2cpp.so
的反汇编,可以发现libil2cpp.so
中大部分逻辑被加密了,这也是为什么我们用 Il2CppDumper.exe
dump不出来 相关文件的原因。
libil2cpp.so
文件势必会在app运行时被解密,我们用frida在内存中抓取 libil2cpp.so
即可。
frida抓取脚本:
let dump_so_name = "libil2cpp.so" function dumpso(){ try{ let modu=Process.getModuleByName(dump_so_name) var base_addr = modu.base var base_size = modu.size if (modu){ console.log("name: " +modu.name); console.log("base: " +base_addr); console.log("size: " +"0x"+base_size.toString(16)); // filter unmaping range // filter addr between base,base+size var file_path = "/data/data/com.com.sec2023.rocketmouse.mouse/" + modu.name + "_" + modu.base + ".so"; var all_size = 0 var range_arry = Process.enumerateRanges("r") var file_handle = new File(file_path, "wb"); range_arry.forEach(range => { if ( (parseInt(range.base,16) >= parseInt(base_addr)) && (parseInt(range.base,16) <= parseInt(base_addr) + base_size )){ // console.log all_size += range.size var libso_buffer = ptr(range.base).readByteArray(range.size); file_handle.write(libso_buffer); file_handle.flush(); console.log(`[+] ${range.base}-${"0x" + range.size.toString(16)}-${range.protection}`) } }) file_handle.close(); console.log("[dump_size]:", "0x" + all_size.toString(16)); console.log("[dump]:", file_path); }else{ console.log('[x] ' + dump_so_name +'not found'); } }catch(e){ console.log("dump so error \t" + e) } } function hook_dlopen() { var dlopen = Module.findExportByName(null, "dlopen"); Interceptor.attach(dlopen, { onEnter: function (args) { this.call_hook = false; var so_name = ptr(args[0]).readCString(); if (so_name.indexOf(dump_so_name) >= 0) { console.log("dlopen:", ptr(args[0]).readCString()); this.call_hook = true; } }, onLeave: function (retval) { if (this.call_hook) { dumpso(); } } }); } hook_dlopen();
抓取之后,可以看到.so文件已经被解密了。
不过由于dump出来的.so文件没有分段,我们直接把 dump出来的.so patch到被加密的 .so文件中。
idapython脚本如下:
# import idc
# begin = 0x0000000002B6850 # end = 0x0000000013CB778 # data = idc.get_bytes(begin,end-begin) # fp = open(r"D:\tlsn\BTools\RE\APK Easy Tool v1.60 Portable\1-Decompiled APKs\mouse_pre.aligned.signed\assets\bin\Data\Managed\Metadata\get_data","wb") # fp.write(data) # fp.close() # print("finish") import idc import ida_bytes begin = 0x0000000002B6850 end = 0x0000000013CB778 fp = open(r"D:\tlsn\BTools\RE\APK Easy Tool v1.60 Portable\1-Decompiled APKs\mouse_pre.aligned.signed\assets\bin\Data\Managed\Metadata\get_data","rb") data = fp.read() # for i in range(begin,end): # idc.patch_byte(i,data[i-begin]) ida_bytes.patch_bytes(begin,data) print("finish") import idc import idaapi def upc(begin,end): for i in range(begin,end): idc.del_items(i) for i in range(begin,end): idc.create_insn(i) for i in range(begin,end): idaapi.add_func(i) print("Finish!!!") begin = 0x0000000002B6850 end = 0x000000000C4E42C upc(begin,end)
接下来,使用Il2CppDumper
可以直接dump出相关文件
Il2CppDumper.exe .\libil2cpp.so .\global-metadata.dat <output-directory>
全部导入ida中,即可恢复libil2cpp.so
的大部分符号
逆向分析 libil2cpp.so
文件
在dump.cs
中搜索 Assembly-CSharp.dll
,即可找到所有关键逻辑的类与相关地址偏移。
dump.cs
中直接搜 coin
,可以直接定位到这个函数:
一看就是在比较金币的数量
我们直接 hook 内存,修改以下金币数目,即可那道flag:
frda脚本如下:
// 0x0000000004653CC: cmp 1000
var offset = 0x0000000004653C8 var baseAddr = Module.findBaseAddress("libil2cpp.so") console.log(baseAddr) Interceptor.attach(ptr(baseAddr).add(offset), { onEnter: function(args){ var TssSdtInt = this.context.x0 // TssSdtInt Coins; var m_slot = TssSdtInt.add(0x10).readPointer() // private TssSdtIntSlot m_slot; // 0x10 var m_value = m_slot.add(0x10).readPointer() // private Int32[] m_value; // 0x10 // System_Int32_array var m_xor_key = m_slot.add(0x10+8).readU32() // private Int32 m_xor_key; // 0x18 var m_index = m_slot.add(0x10+0xc).readU32() // private Int32 m_index; // 0x1c console.log("````````````````````````````````````````````````") // console.log(m_value) // console.log(m_xor_key) // console.log(m_index) // return this->fields.m_xor_key ^ m_value->m_Items[m_index]; var m_idx_val = m_value.add(0x20 + m_index * 4).readU32() var real_val = m_idx_val ^ m_xor_key console.log(real_val) // change m_value->m_Items[m_index]; var m_idx_val_addr = m_value.add(0x20 + m_index * 4) var new_val = 1000 var enc_new_val = new_val ^ m_xor_key ptr(m_idx_val_addr).writeU32(enc_new_val) }, onLeave: function(retval){ } });

继续逆向,可以找到一个叫做 SmallKeyboard
的类:
用frida hook SmallKeyboard
类的相关函数,通过解析结构体参数,可以得到以下相关信息:
iI1Ii
函数会在按键抬起的时候触发。iI1Ii
函数会在按键按下的时候触发。iI1Ii
函数会在我们按下 ok 后触发,猜测是check函数。oO0oOo0
函数功能是生成token。oO0oOoO
函数的功能是获取token的值。oO0o0o0
字段保存的是token的值。iIIIi
字段保存的是我们所有的输入。
frida hook 脚本如下:
let dump_so_name = "libil2cpp.so"
var baseAddr = Module.findBaseAddress("libil2cpp.so") console.log("libil2cpp_base_addr: " + baseAddr) /* struct System_String_o { System_String_c *klass; void *monitor; System_String_Fields fields; }; struct System_String_Fields { int32_t _stringLength; uint16_t _firstChar; }; */ function parseSystemString(addr){ try{ var _stringLength = ptr(addr).add(0x10).readU32(); var string_real_addr = ptr(addr).add(0x10+4) var string_val = string_real_addr.readUtf16String(_stringLength) return string_val }catch(e){ console.log("parseSystemString throw:\t" + e) } } function parse_iII1i(addr){ var KeyType = ptr(addr).add(0x18).readPointer() var SValue = ptr(addr).add(0x20).readPointer() // console.log(SValue) var RSvalue = parseSystemString(SValue) return RSvalue } function hook_SmallKeyboard(){ try{ // hook_list[0]处理up、[1]处理press、[2]处理 ok var hook_list = [0x465880,0x465fdc,0x465e90] // 0 => up // 1=>press // 2 -> ok for(var i = 0;i<hook_list.length;i++){ var addr = hook_list[i] const idx = i Interceptor.attach(ptr(addr).add(baseAddr), { onEnter: function(args){ // console.log(idx) if (idx == 0){ // key board console.log("###############################") // var _info = args[1] // iII1i _info // var RSvalue = parse_iII1i(_info) // console.log("iII1i\t" + RSvalue) var SmallKeyboard_this = args[0] var strInput = SmallKeyboard_this.add(0x18).readPointer() var oO0o0o0 = SmallKeyboard_this.add(0x38).readPointer() var iIIIi = SmallKeyboard_this.add(0x40).readPointer() var real_strInput = parseSystemString(strInput) var real_oO0o0o0 = parseSystemString(oO0o0o0) console.log(iIIIi) var real_iIIIi = parseSystemString(iIIIi) console.log("strInput:\t" + real_strInput) console.log("oO0o0o0:\t" + real_oO0o0o0) // 这个实际上就是生成的token值 console.log("iIIIi:\t" + real_iIIIi) // 这个实际上是我们已经输入的值 }else if(idx == 2){ // 这个函数是我们键入 ok 后调用的 var SmallKeyboard_this = args[0] var uint64_t_i1I = args[1] console.log(uint64_t_i1I) } }, onLeave: function(retval){ } }); } }catch(e){ console.log("hook_SmallKeyboard throw:\t" + e) } } function parse_Int32(addr){ var m_value = ptr(addr).add(0x10).readU32() return m_value } function hook_generate_token(){ try{ // var offset = 0x000000000465F58 // hook x0 // Interceptor.attach(ptr(baseAddr).add(offset), { // onEnter: function(args){ // var token_int = this.context.x0 // TssSdtInt Coins; // var m_value = parse_Int32(token_int) // console.log("````````````````````````````````````````````````") // console.log(m_value) // }, // onLeave: function(retval){ // } // }); /* var offset = 0x000000000831950 // 挂钩System_String__Concat_8591696函数 Interceptor.attach(ptr(baseAddr).add(offset), { onEnter: function(args){ this.token = args[0] this.str1 = args[1] }, onLeave: function(retval){ console.log(`${parseSystemString(this.token)} +Connect+ ${parseSystemString(this.str1)} ==> ${parseSystemString(retval)}`) } }); // sResult = +Connect+ 1253 ==> sResult = 1253 // +Connect+ 2 ==> 2 // 2 +Connect+ 2 ==> 22 // 22 +Connect+ 7 ==> 227 // 227 +Connect+ 3 ==> 2273 // 2273 +Connect+ 7 ==> 22737 // 22737 +Connect+ 7 ==> 227377 // 227377 +Connect+ 2 ==> 2273772 // 2273772 +Connect+ 4 ==> 22737724 // TOKEN: +Connect+ 22737724 ==> TOKEN: 22737724 */ }catch(e){ console.log(e) } } hook_SmallKeyboard() hook_generate_token()
挂钩 0x000000000465F4C处的地址,hook x9寄存器,可以得到 generate token的核心代码,实际上是 0x934510 地址处的随机数生成函数。
function hook_6_Next_vatble_addr(){
try{ var offset = 0x000000000465F4C // 挂钩 v5->klass->vtable._6_Next.methodPtr ==> 0x6f3654d510 - 0x6f35c19000 => 0x934510 Interceptor.attach(ptr(baseAddr).add(offset), { onEnter: function(args){ var addr_ = this.context.x9 console.log(addr_) }, onLeave: function(retval){ } }); }catch(e){ console.log("hook_6_Next_vatble_addr throw:\t" + e) } }
在分析 SmallKeyboard
的check函数的时候,可以分析,其跳转到了代码数据0的地方,很神奇,猜测是程序在运行时才动态填充这个代码段数据。暂时不管。
下面hook这个类
钩住其所有函数,然后栈回溯,事实上能跟到 这个函数:0x000000000465AB4
/* struct System_UInt16_array { Il2CppObject obj; Il2CppArrayBounds *bounds; il2cpp_array_size_t max_length; uint16_t m_Items[65535]; }; */ function parse_System_UInt16_array(addr){ var m_item_addr = ptr(addr).add(0x20) console.log(hexdump(m_item_addr, { offset: 0, length: 0x50, header: true, ansi: true })); } function parse_System_UInt32_array(addr){ var m_item_addr = ptr(addr).add(0x20) console.log(hexdump(m_item_addr, { offset: 0, length: 0x50, header: true, ansi: true })); } function stack_backstace(your_this){ console.log('stack backtrace:\n' + Thread.backtrace(your_this.context, Backtracer.ACCURATE) .map(DebugSymbol.fromAddress).join('\n') + '\n'); } function hook_Oo0(){ var hook_list = [ 0x46a55c,0x4660e8,0x466178,0x46ad44,0x46ae50,0x46aecc,0x46af44,0x46afc4,0x46b040,0x46b0bc,0x46b13c,0x46b1c4, 0x46b228,0x46b28c,0x46b2c4,0x46b32c,0x46b394,0x46b3f8,0x46b478,0x46b4e8,0x46b568,0x46b578,0x46b5dc,0x46b664 ] for(var i = 0;i<hook_list.length;i++){ const addr = hook_list[i] const idx = i Interceptor.attach(ptr(baseAddr).add(addr),{ onEnter: function(args){ // console.log("``````````````````````````````````````````````````````") // console.log(`Enter: ${idx} ==> ${"0x" + addr.toString(16)}`) // hook all fields // var this_Oo0 = args[0] // var OoOOO00_Uint16_array = this_Oo0.add(0x10).readPointer() // var OOoOO0_UInt32_array11111 = this_Oo0.add(0x18).readPointer() // var Ooooo_Uint32_array222222 = this_Oo0.add(0x20).readPointer() // var Uint32_arr2_idx = this_Oo0.add(0x28).readU32() // var Uint16_arr_idx = this_Oo0.add(0x2c).readU32() // var Oooo0Oo_Int32 = this_Oo0.add(0x30).readU32() // var oOOO0O0O_Int32 = this_Oo0.add(0x34).readU32() stack_backstace(this) // if (addr == 0x4660e8){ // var OoOOO00_Uint16 = args[1] // var what1 = args[2] // var OOoOO0_UInt32_array1 = args[3] // parse_System_UInt16_array(OoOOO00_Uint16) // console.log(what1) // parse_System_UInt32_array(OOoOO0_UInt32_array1) // fa 17 d8 10 e4 17 af 93 // } // if(addr == 0x000000000465998){ // var our_inp = this.context.x1 // var X0_this = this.context.x0 // console.log("our_inp:\t" + our_inp) // console.log("X0_this:\t" + X0_this) // } // if(addr == 0x000000000465AB4){ // var arg1 = args[0] // var arg2 = args[1] // // guess string and uint64 // // var str1 = parseSystemString(arg1); // var big_int = arg2 // console.log("arg1:\t"+ arg1) // 这是this指针,x0寄存器 // console.log("arg2:\t"+ big_int) // 这是 加密后的big_int,x1寄存器 // var x1_val = this.context.x1 // console.log("x1_val:\t" + x1_val) // } }, onLeave: function(retval){ // console.log(`Leave : ${idx} ==> ${"0x" + addr.toString(16)}`) } }) } }
再栈回溯这个函数 (0x000000000465AB4) 可以跟到这样的调用链:
libil2cpp.so+0x46599C
==> libsec2023.so+0x0311A0 ==> libil2cpp.so+0x0465AB4
这其实就是check函数的执行流程,如果你能手动改一下参数类型,就能在 libil2cpp.so+0x0465AB4 的函数中看到最后的比较
逆向分析 libil2cpp.so
的算法
vm算法
前面hook了 Oo0
类中的函数,实际上 Oo0
类自己实现了一个虚拟机,用来加密输入。
就像这样:
事实上通过分析可以发现,这些不过都是些指令混淆
下面给出 frida hook vm虚拟机的脚本:
function hook_vm(){ var offset = 0x000000000465AB4 var vm_list = [ 0x00000000046AEA4,0x00000000046AF20,0x00000000046AF94,0x00000000046B01C,0x00000000046B088,0x00000000046B110, 0x00000000046B18C,0x00000000046B20C,0x00000000046B270 ] for(var i = 0;i<vm_list.length;i++){ const addr = vm_list[i] const idx = i Interceptor.attach(ptr(baseAddr).add(addr),{ onEnter: function(args){ if(addr == 0x00000000046AEA4){ var pop = this.context.x11.and(0xffffffff) var rle = this.context.x10.and(0xffffffff) console.log("rle:\t" + rle) console.log("pop:\t" + pop) var new_rle = ptr(rle).add(pop) console.log("new_\t" + new_rle) console.log(`new_rel = rel + pop;`) } if(addr == 0x00000000046AF20){ var pop = this.context.x11.and(0xffffffff) var rle = this.context.x10.and(0xffffffff) var new_rle = ptr(rle).sub(pop) console.log("rle:\t" + rle) console.log("pop:\t" + pop) console.log("new_\t" + new_rle) console.log(`new_rel = rel - pop;`) } if(addr == 0x00000000046AF94){ var pop = this.context.x11.and(0xffffffff) var rle = this.context.x10.and(0xffffffff) var new_rle = ptr(pop.toInt32() * rle.toInt32()) console.log("rle:\t" + rle) console.log("pop:\t" + pop) console.log("new_\t" + new_rle) console.log(`new_rel = rel * pop;`) } if(addr == 0x00000000046B01C){ var pop = this.context.x11.and(0xffffffff) var rle = this.context.x10.and(0xffffffff) var new_rle = ptr(rle).shl(pop) console.log("rle:\t" + rle) console.log("pop:\t" + pop) console.log("new_\t" + new_rle) console.log(`new_rel = rel << pop;`) } if(addr == 0x00000000046B088){ var pop = this.context.x11.and(0xffffffff) var rle = this.context.x10.and(0xffffffff) var new_rle = ptr(rle).shr(pop) console.log("rle:\t" + rle) console.log("pop:\t" + pop) console.log("new_\t" + new_rle) console.log(`new_rel = rel >> pop;`) } if(addr == 0x00000000046B110){ var pop = this.context.x11.and(0xffffffff) var rle = this.context.x10.and(0xffffffff) var new_rle = ptr(rle).and(pop) console.log("rle:\t" + rle) console.log("pop:\t" + pop) console.log("new_\t" + new_rle) console.log(`new_rel = rel & pop;`) } if(addr == 0x00000000046B18C){ var pop = this.context.x10.and(0xffffffff) var rle = this.context.x12.and(0xffffffff) var new_rle = ptr(rle).xor(pop) console.log("rle:\t" + rle) console.log("pop:\t" + pop) console.log("new_\t" + new_rle) console.log(`new_rel = rel ^ pop;`) } if(addr == 0x00000000046B20C){ var pop = this.context.x11.and(0xffffffff) var rle = this.context.x10.and(0xffffffff) console.log("pop:\t" + pop) console.log("rle:\t" + rle) console.log(`new_rel = rle < pop;`) } if(addr == 0x00000000046B270){ var pop = this.context.x11.and(0xffffffff) var rle = this.context.x10.and(0xffffffff) console.log("pop:\t" + pop) console.log("rle:\t" + rle) console.log(`new_rel = rle == pop;`) } }, onLeave: function(retval){ // console.log(`Leave : ${idx} ==> ${"0x" + addr.toString(16)}`) } }) } }
可以得到这些数据:
ps:我在游戏中的输入是 "1433223",在进入 libil2cpp.so+0x0465AB4处的函数的时候,1433223就已经被加密成 0x93af17e410d817fa 了,这里的vm也是从 0x93af17e410d817fa 开始加密的。(从1433223到0x93af17e410d817fa 这段加密操作在后面我们会讲)
1433223: 0x93af17e410d817fa
part1 = 0x10d817fa part2 = 0x93af17e4 rle: 0x10d817fa pop: 0x18 new_ 0x10 new_rel = rel >> pop // 高8位 ,inp4 = part2 >> 24 rle: 0x10 pop: 0xff new_ 0x10 new_rel = rel & pop // inp4 &= 0xff =====> 0x10 rle: 0x18 pop: 0x8 new_ 0x10 new_rel = rel - pop // pop: 0x0 rle: 0x10 new_rel = rle < pop // rle: 0x10d817fa pop: 0x10 new_ 0x10d8 new_rel = rel >> pop // inp3 = part2 >> 16 rle: 0x10d8 pop: 0xff new_ 0xd8 new_rel = rel & pop // inp3 &= 0xff =====> 0xd8 rle: 0x10 pop: 0x8 new_ 0x8 new_rel = rel - pop // pop: 0x0 rle: 0x8 new_rel = rle < pop rle: 0x10d817fa pop: 0x8 new_ 0x10d817 new_rel = rel >> pop // inp2 = part2 >> 8 rle: 0x10d817 pop: 0xff new_ 0x17 new_rel = rel & pop // inp2 &= 0xff ====> 0x17 rle: 0x8 pop: 0x8 new_ 0x0 new_rel = rel - pop pop: 0x0 rle: 0x0 new_rel = rle < pop rle: 0x10d817fa pop: 0x0 new_ 0x10d817fa new_rel = rel >> pop // inp1 = part2 >> 0 rle: 0x10d817fa pop: 0xff new_ 0xfa new_rel = rel & pop // inp1 &= 0xff ====> 0xfa rle: 0x0 pop: 0x8 new_ 0xfffffffffffffff8 new_rel = rel - pop pop: 0x0 rle: 0xfffffff8 new_rel = rle < pop rle: 0xfa pop: 0x1b new_ 0xdf new_rel = rel - pop // inp1 - 0x1b --> 0xdf rle: 0x17 pop: 0xc2 new_ 0xd5 new_rel = rel ^ pop // inp2 ^ 0xc2 ---> 0xd5 rle: 0xd8 pop: 0xa8 new_ 0x180 new_rel = rel + pop // inp3 + 0xa8 ---> 0x180 rle: 0x10 pop: 0x36 new_ 0x26 new_rel = rel ^ pop // inp4 ^ 0x36 ---> 0x26 rle: 0xdf pop: 0x0 new_ 0xdf new_rel = rel ^ pop // new_inp1 ^ 0 --> 0xdf rle: 0xdf pop: 0x0 new_ 0xdf new_rel = rel << pop // new_inp1 <<0 = --> 0xdf rle: 0xff pop: 0x0 new_ 0xff new_rel = rel << pop // rle: 0xdf pop: 0xff new_ 0xdf new_rel = rel & pop // new_inp1 & 0xff --> 0xdf rle: 0xdf pop: 0x0 new_ 0xdf new_rel = rel + pop // new_inp1 + 0 --> 0xdf rle: 0x4 pop: 0x1 new_ 0x5 new_rel = rel + pop // 4+1 --> 5 rle: 0x0 pop: 0x8 new_ 0x8 new_rel = rel + pop pop: 0x19 rle: 0x8 new_rel = rle < pop rle: 0xd5 pop: 0x8 new_ 0xdd new_rel = rel ^ pop // new_inp2 ^= 0x8 --> 0xdd rle: 0xdd pop: 0x8 new_ 0xdd00 new_rel = rel << pop // new_inp2 <<= 0x8 --> 0xdd00 rle: 0xff pop: 0x8 new_ 0xff00 new_rel = rel << pop rle: 0xdd00 pop: 0xff00 new_ 0xdd00 new_rel = rel & pop // new_inp2 &= 0xff00 rle: 0xdd00 pop: 0xdf new_ 0xdddf new_rel = rel + pop // sum += new_inp2 rle: 0x5 pop: 0x1 new_ 0x6 new_rel = rel + pop rle: 0x8 pop: 0x8 new_ 0x10 new_rel = rel + pop pop: 0x19 rle: 0x10 new_rel = rle < pop rle: 0x180 pop: 0x10 new_ 0x190 new_rel = rel ^ pop // new_inp3 ^= 0x10 --> 0x190 rle: 0x190 pop: 0x10 new_ 0x1900000 new_rel = rel << pop // new_inp3 <<= 0x10 rle: 0xff pop: 0x10 new_ 0xff0000 new_rel = rel << pop rle: 0x1900000 pop: 0xff0000 new_ 0x900000 new_rel = rel & pop // new_inp3 &= 0xff0000 rle: 0x900000 pop: 0xdddf new_ 0x90dddf new_rel = rel + pop // sum += new_inp3 rle: 0x6 pop: 0x1 new_ 0x7 new_rel = rel + pop rle: 0x10 pop: 0x8 new_ 0x18 new_rel = rel + pop pop: 0x19 rle: 0x18 new_rel = rle < pop rle: 0x26 pop: 0x18 new_ 0x3e new_rel = rel ^ pop // new_inp4 ^= 0x18 rle: 0x3e pop: 0x18 new_ 0x3e000000 new_rel = rel << pop // new_inp4 <<= 0x18 rle: 0xff pop: 0x18 new_ 0xff000000 new_rel = rel << pop rle: 0x3e000000 pop: 0xff000000 new_ 0x3e000000 new_rel = rel & pop // new_inp4 &= 0xff000000 rle: 0x3e000000 pop: 0x90dddf new_ 0x3e90dddf new_rel = rel + pop // sum += new_inp4 rle: 0x7 pop: 0x1 new_ 0x8 new_rel = rel + pop rle: 0x18 pop: 0x8 new_ 0x20 new_rel = rel + pop pop: 0x19 rle: 0x20 new_rel = rle < pop rle: 0x93af17e4 pop: 0x18 new_ 0x93 new_rel = rel >> pop // inp4 = part2 >> 0x18 --> 0x93 rle: 0x93 pop: 0xff new_ 0x93 new_rel = rel & pop rle: 0x18 pop: 0x8 new_ 0x10 new_rel = rel - pop pop: 0x0 rle: 0x10 new_rel = rle < pop rle: 0x93af17e4 pop: 0x10 new_ 0x93af new_rel = rel >> pop rle: 0x93af pop: 0xff new_ 0xaf new_rel = rel & pop // inp3 = (part2 >> 0x10) & 0xff --> 0xaf rle: 0x10 pop: 0x8 new_ 0x8 new_rel = rel - pop pop: 0x0 rle: 0x8 new_rel = rle < pop rle: 0x93af17e4 pop: 0x8 new_ 0x93af17 new_rel = rel >> pop rle: 0x93af17 pop: 0xff new_ 0x17 new_rel = rel & pop // inp2 = (part2 >> 0x8) & 0xff --> 0x17 rle: 0x8 pop: 0x8 new_ 0x0 new_rel = rel - pop pop: 0x0 rle: 0x0 new_rel = rle < pop rle: 0x93af17e4 pop: 0x0 new_ 0x93af17e4 new_rel = rel >> pop rle: 0x93af17e4 pop: 0xff new_ 0xe4 new_rel = rel & pop // inp1 = (part2 >> 0) & 0xff --> 0xe4 rle: 0x0 pop: 0x8 new_ 0xfffffffffffffff8 new_rel = rel - pop pop: 0x0 rle: 0xfffffff8 new_rel = rle < pop rle: 0xe4 pop: 0x2f new_ 0xb5 new_rel = rel - pop // inp1 -= 0x2f rle: 0x17 pop: 0xb6 new_ 0xa1 new_rel = rel ^ pop // inp2 ^= 0xb6 rle: 0xaf pop: 0x37 new_ 0xe6 new_rel = rel + pop // inp3 += 0x37 rle: 0x93 pop: 0x98 new_ 0xb new_rel = rel ^ pop // inp4 ^= 0x98 rle: 0xb5 pop: 0x0 new_ 0xb5 new_rel = rel + pop // inp1 += 0 rle: 0xb5 pop: 0x0 new_ 0xb5 new_rel = rel << pop // inp1 << 0 rle: 0xff pop: 0x0 new_ 0xff new_rel = rel << pop rle: 0xb5 pop: 0xff new_ 0xb5 new_rel = rel & pop // inp1 & 0xff rle: 0xb5 pop: 0x0 new_ 0xb5 new_rel = rel + pop // sum += inp1 rle: 0x4 pop: 0x1 new_ 0x5 new_rel = rel + pop rle: 0x0 pop: 0x8 new_ 0x8 new_rel = rel + pop pop: 0x19 rle: 0x8 new_rel = rle < pop rle: 0xa1 pop: 0x8 new_ 0xa9 new_rel = rel + pop // inp2 += 0x8 rle: 0xa9 pop: 0x8 new_ 0xa900 new_rel = rel << pop // inp2 << 0x8 rle: 0xff pop: 0x8 new_ 0xff00 new_rel = rel << pop rle: 0xa900 pop: 0xff00 new_ 0xa900 new_rel = rel & pop rle: 0xa900 pop: 0xb5 new_ 0xa9b5 new_rel = rel + pop rle: 0x5 pop: 0x1 new_ 0x6 new_rel = rel + pop rle: 0x8 pop: 0x8 new_ 0x10 new_rel = rel + pop pop: 0x19 rle: 0x10 new_rel = rle < pop rle: 0xe6 pop: 0x10 new_ 0xf6 new_rel = rel + pop // rle: 0xf6 pop: 0x10 new_ 0xf60000 new_rel = rel << pop rle: 0xff pop: 0x10 new_ 0xff0000 new_rel = rel << pop rle: 0xf60000 pop: 0xff0000 new_ 0xf60000 new_rel = rel & pop rle: 0xf60000 pop: 0xa9b5 new_ 0xf6a9b5 new_rel = rel + pop rle: 0x6 pop: 0x1 new_ 0x7 new_rel = rel + pop rle: 0x10 pop: 0x8 new_ 0x18 new_rel = rel + pop pop: 0x19 rle: 0x18 new_rel = rle < pop rle: 0xb pop: 0x18 new_ 0x23 new_rel = rel + pop rle: 0x23 pop: 0x18 new_ 0x23000000 new_rel = rel << pop rle: 0xff pop: 0x18 new_ 0xff000000 new_rel = rel << pop rle: 0x23000000 pop: 0xff000000 new_ 0x23000000 new_rel = rel & pop rle: 0x23000000 pop: 0xf6a9b5 new_ 0x23f6a9b5 new_rel = rel + pop rle: 0x7 pop: 0x1 new_ 0x8 new_rel = rel + pop rle: 0x18 pop: 0x8 new_ 0x20 new_rel = rel + pop pop: 0x19 rle: 0x20 new_rel = rle < pop low_4val: 0x3e90dddf high_4val: 0x23f6a9b5
还原算法:
def vmenc(inp): part1 = inp & 0xffffffff part2 = (inp >> 32) & 0xffffffff p1_inp1 = part1 & 0xff p1_inp2 = (part1 >> 8) & 0xff p1_inp3 = (part1 >> 16) & 0xff p1_inp4 = (part1 >> 24) & 0xff p1_inp1 -= 0x1b p1_inp2 ^= 0xc2 p1_inp3 += 0xa8 p1_inp4 ^= 0x36 p1_inp1 = ((p1_inp1 ^ 0) << 0) & 0xff p1_inp2 = ((p1_inp2 ^ 0x8) << 0x8) &0xff00 p1_inp3 = ((p1_inp3 ^ 0x10) << 0x10) & 0xff0000 p1_inp4 = ((p1_inp4 ^ 0x18) << 0x18) & 0xff000000 p1_sum = p1_inp1 + p1_inp2 + p1_inp3 + p1_inp4 p2_inp1 = part2 & 0xff p2_inp2 = (part2 >> 8) & 0xff p2_inp3 = (part2 >> 16) & 0xff p2_inp4 = (part2 >> 24) & 0xff p2_inp1 -= 0x2f p2_inp2 ^= 0xb6 p2_inp3 += 0x37 p2_inp4 ^= 0x98 p2_inp1 = ((p2_inp1 + 0) << 0) & 0xff p2_inp2 = ((p2_inp2 + 0x8) << 0x8) & 0xff00 p2_inp3 = ((p2_inp3 + 0x10) << 0x10) & 0xff0000 p2_inp4 = ((p2_inp4 + 0x18) << 0x18) & 0xff000000 p2_sum = p2_inp1 + p2_inp2 + p2_inp3 + p2_inp4 return p1_sum,p2_sum def conver(m): if m < 0: return 0x100 + m return m def vmdec(p1_sum,p2_sum): p2_inp1 = (p2_sum & 0xff) p2_inp2 = ((p2_sum & 0xff00) >> 0x8) - 0x8 p2_inp3 = ((p2_sum & 0xff0000) >> 0x10) - 0x10 p2_inp4 = ((p2_sum & 0xff000000) >> 0x18) - 0x18 p2_inp1 += 0x2f p2_inp2 ^= 0xb6 p2_inp3 -= 0x37 p2_inp4 ^= 0x98 p2_inp1 = conver(p2_inp1) p2_inp2 = conver(p2_inp2) p2_inp3 = conver(p2_inp3) p2_inp4 = conver(p2_inp4) part2 = (p2_inp1 & 0xff) + ((p2_inp2 & 0xff) << 8) + ((p2_inp3 & 0xff) << 0x10) + ((p2_inp4 & 0xff) << 0x18) p1_inp1 = (p1_sum & 0xff) ^ 0x0 p1_inp2 = ((p1_sum & 0xff00) >> 0x8) ^ 0x8 p1_inp3 = ((p1_sum & 0xff0000) >> 0x10) ^ 0x10 p1_inp4 = ((p1_sum & 0xff000000) >> 0x18) ^ 0x18 p1_inp1 += 0x1b p1_inp2 ^= 0xc2 p1_inp3 -= 0xa8 p1_inp4 ^= 0x36 p1_inp1 = conver(p1_inp1) p1_inp2 = conver(p1_inp2) p1_inp3 = conver(p1_inp3) p1_inp4 = conver(p1_inp4) part1 = (p1_inp1 & 0xff) + ((p1_inp2 & 0xff) << 8) + ((p1_inp3 & 0xff) << 0x10) + ((p1_inp4 & 0xff) << 0x18) return part1 + (part2 << 32) enc = 0x93af17e410d817fa enc1,enc2 = vmenc(enc) assert enc1 == 0x3e90dddf and enc2 == 0x23f6a9b5 dec = vmdec(enc1,enc2) assert dec == 0x93af17e410d817fa
这是四段加密中其中一段的加密。算法还原进度 1/4
魔改xtea算法
继续向下分析可以看到一个魔改 xtea加密算法,算法还原如下
# magical xtea # vmlater: low_4val: 0x3e90dddf high_4val: 0x23f6a9b5 # ===> # tealater: low_4val: 0x7dd89904 high_4val: 0xc6ff408e from ctypes import * from struct import pack,unpack def encrypt(v, key): v0, v1 = c_uint32(v[0]), c_uint32(v[1]) delta = 0x21524111 total1 = c_uint32(0xBEEFBEEF) total2 = c_uint32(0x9D9D7DDE) for i in range(64): v0.value += (((v1.value << 7) ^ (v1.value >> 8)) + v1.value) ^ (total1.value - key[total1.value & 3]) total1.value -= delta v1.value += (((v0.value << 8) ^ (v0.value >> 7)) - v0.value) ^ (total2.value + key[(total2.value>>13) & 3]) total2.value -= delta return v0.value, v1.value def decrypt(v, key): v0, v1 = c_uint32(v[0]), c_uint32(v[1]) delta = 0x21524111 total1 = c_uint32(0xBEEFBEEF - 64 * delta) total2 = c_uint32(0x9D9D7DDE - 64 * delta) for i in range(64): total2.value += delta v1.value -= (((v0.value << 8) ^ (v0.value >> 7)) - v0.value) ^ (total2.value + key[(total2.value>>13) & 3]) total1.value += delta v0.value -= (((v1.value << 7) ^ (v1.value >> 8)) + v1.value) ^ (total1.value - key[total1.value & 3]) return v0.value, v1.value def enxtea(inp:bytes,key:bytes): from struct import pack,unpack k = unpack("<4I",key) inp_len = len(inp) // 4 value = unpack(f"<{inp_len}I",inp) res = b"" for i in range(0,inp_len,2): v = [value[i],value[i+1]] x = encrypt(v,k) # x = decrypt(v,k) res += pack("<2I",*x) return res def dextea(inp:bytes,key:bytes): from struct import pack,unpack k = unpack("<4I",key) inp_len = len(inp) // 4 value = unpack(f"<{inp_len}I",inp) res = b"" for i in range(0,inp_len,2): v = [value[i],value[i+1]] x = decrypt(v,k) res += pack("<2I",*x) return res def xtea_enc(low_4val =0x3e90dddf,high_4val = 0x23f6a9b5 ): # enc # low_4val = 0x3e90dddf # high_4val = 0x23f6a9b5 inp = pack("<I",low_4val) + pack("<I",high_4val) key = [2071428195,3312413682,728170800,1990973438] key = pack("<I",key[0]) + pack("<I",key[1]) + pack("<I",key[2]) + pack("<I",key[3]) ret = enxtea(inp,key) xtea_enc1 = unpack("<I",ret[0:4])[0] xtea_enc2 = unpack("<I",ret[4:8])[0] # assert xtea_enc1 == 0x7dd89904 and xtea_enc2 == 0xc6ff408e return xtea_enc1,xtea_enc2 def xtea_dec(xtea_enc1 = 0x7dd89904,xtea_enc2 = 0xc6ff408e): # dec key = [2071428195,3312413682,728170800,1990973438] key = pack("<I",key[0]) + pack("<I",key[1]) + pack("<I",key[2]) + pack("<I",key[3]) # xtea_enc1 = 0x7dd89904 # xtea_enc2 = 0xc6ff408e inp = pack("<I",xtea_enc1) + pack("<I",xtea_enc2) ret = dextea(inp,key) xtea_dec1 = unpack("<I",ret[0:4])[0] xtea_dec2 = unpack("<I",ret[4:8])[0] # assert xtea_dec1 == 0x3e90dddf and xtea_dec2 == 0x23f6a9b5 return xtea_dec1,xtea_dec2
算法还原进度 2/4
下面就剩 libsec2023.so
中的算法了。
逆向分析 libsec2023.so
的算法
splited 片段算法
在 0x00000000003B8FC偏移处是 libsec2023.so中的 第一个加密算法。
但是这个函数内部全是一个一个BR跳转形成的小片段,静态完全不能逆。
我们尝试用frida 勾住所有的 BR跳转的目的地址,分析规律:
function hook_libsec2023(){ var libsec2023_base = Module.findBaseAddress("libsec2023.so") // GetStaticMethodID console.log("libsec2023_base: \t" + libsec2023_base) var hook_list = [ // enc1 0x00000000003BA00,0x00000000003BA30,0x00000000003BA70,0x00000000003BADC,0x00000000003BB28,0x00000000003BB4C, 0x00000000003BB54, // enc2 // 0x00000000003A08C,0x00000000003A0C8,0x00000000003A0F8, // enc3 // fin //0x00000000003B950,0x00000000003B990,0x00000000003B9CC, //0x00000000003B9B0 ] for(var i =0;i<hook_list.length;i++){ const haddr = hook_list[i] const idx = i Interceptor.attach(ptr(libsec2023_base).add(haddr),{ onEnter: function(args){ if(haddr == 0x00000000003B9CC){ var next_addr = this.context.lr console.log(`${idx}:\t${"0x" + haddr.toString(16)} =>\t${ptr(next_addr).sub(libsec2023_base)}(${ptr(next_addr)})`) } // 这里 hook到了: low_32: 0x10d817fa ; high_32: 0x93af17e4, 也就是分析 .so文件的最后一个地方 if(haddr == 0x00000000003B9B0){ var low_32 = this.context.x19 var high_32 = this.context.x8 console.log("low_32:\t" + low_32) console.log("high_32:\t" + high_32) } if(haddr == 0x00000000003B950){ var next_addr = this.context.x8 console.log(`${idx}:\t${"0x" + haddr.toString(16)} =>\t${ptr(next_addr).sub(libsec2023_base)}(${ptr(next_addr)})`) } if(haddr == 0x00000000003B990){ var next_addr = this.context.x8 console.log(`${idx}:\t${"0x" + haddr.toString(16)} =>\t${ptr(next_addr).sub(libsec2023_base)}(${ptr(next_addr)})`) } if(haddr == 0x00000000003BA30 || haddr ==0x00000000003BA70 ){ var next_addr = this.context.x12 console.log(`${idx}:\t${"0x" + haddr.toString(16)} =>\t${ptr(next_addr).sub(libsec2023_base)}(${ptr(next_addr)})`) } if(haddr == 0x00000000003BADC || haddr == 0x00000000003BB28){ var next_addr = this.context.x13 console.log(`${idx}:\t${"0x" + haddr.toString(16)} =>\t${ptr(next_addr).sub(libsec2023_base)}(${ptr(next_addr)})`) } if(haddr == 0x00000000003BB4C || haddr == 0x00000000003BA00){ var next_addr = this.context.x10 console.log(`${idx}:\t${"0x" + haddr.toString(16)} =>\t${ptr(next_addr).sub(libsec2023_base)}(${ptr(next_addr)})`) } if(haddr == 0x00000000003BB54){ // stack_backstace(this) var next_addr = this.context.lr console.log(`${idx}:\t${"0x" + haddr.toString(16)} =>\t${ptr(next_addr).sub(libsec2023_base)}(${ptr(next_addr)})`) } }, onLeave: function(retval){ } }) } }
大致如下:
0: 0x3ba00 => 0x3ba04(0x6d8d506a04)
1: 0x3ba30 => 0x3ba34(0x6d8d506a34) 2: 0x3ba70 => 0x3ba34(0x6d8d506a34) 2: 0x3ba70 => 0x3ba34(0x6d8d506a34) 2: 0x3ba70 => 0x3ba34(0x6d8d506a34) 2: 0x3ba70 => 0x3ba74(0x6d8d506a74) 5: 0x3bb4c => 0x3ba04(0x6d8d506a04) 1: 0x3ba30 => 0x3ba34(0x6d8d506a34) 2: 0x3ba70 => 0x3ba34(0x6d8d506a34) 2: 0x3ba70 => 0x3ba34(0x6d8d506a34) 2: 0x3ba70 => 0x3ba34(0x6d8d506a34) 2: 0x3ba70 => 0x3ba74(0x6d8d506a74) 3: 0x3badc => 0x3bae0(0x6d8d506ae0) 4: 0x3bb28 => 0x3bae0(0x6d8d506ae0) 4: 0x3bb28 => 0x3bae0(0x6d8d506ae0) 4: 0x3bb28 => 0x3bae0(0x6d8d506ae0) 4: 0x3bb28 => 0x3bb2c(0x6d8d506b2c) 5: 0x3bb4c => 0x3bb50(0x6d8d506b50) 6: 0x3bb54 => 0xfffffffffad3650c(0x6d8820150c)
可以发现大部分跳转都是四个四个出现的。
盲猜是对输入的四字节数据的逐字节加密。这样一想,关键函数片段其实也就三四个,完全能逆。
我这里结合unidbg来进行的分析,边调边逆,大致log如下:
0: 0x3ba00 => 0x3ba04(0x6d8d506a04)
1: 0x3ba30 => 0x3ba34(0x6d8d506a34) 2: 0x3ba70 => 0x3ba34(0x6d8d506a34) 2: 0x3ba70 => 0x3ba34(0x6d8d506a34) 2: 0x3ba70 => 0x3ba34(0x6d8d506a34) 2: 0x3ba70 => 0x3ba74(0x6d8d506a74) part2_ori 为高四位 :0 part1_ori 为低四位 part2 = part2_ori idx = 3 part2 >>= 0x18 part2 ^= idx *0xbffff6af = part2 & 0xff idx -=1 part2 = part2_ori part2 >>= 0x10 part2 ^= idx idx -=1 *0xbffff6ae = part2 & 0xff part2 = part2_ori part2 >>= 0x8 part2 ^= idx idx -=1 *0xbffff6ad = part2 & 0xff part2 = part2_ori part2 >>= 0 part2 ^= idx(0) idx -=1 *0xbffff6ac = part2 & 0xff ===========> *0xbffff6ac ==> 00 01 02 03 0x3ba74: 3: 0x3badc => 0x3bae0(0x6d8d506ae0) // 以后 0xbffff6ac 简称 mem mem[0] -= 0x1c mem[1] ^= 0xd3 mem[2] -= 0x5e mem[3] ^= 0x86 ===========> 0xbffff6ac ==> E4 D2 A4 85 !!!! 4: 0x3bb28 => 0x3bae0(0x6d8d506ae0) 4: 0x3bb28 => 0x3bae0(0x6d8d506ae0) 4: 0x3bb28 => 0x3bae0(0x6d8d506ae0) 4: 0x3bb28 => 0x3bb2c(0x6d8d506b2c) r1: W14 = mem[3] (0x85) w14 -= 0x18 (0x6d) mem[3] = w14 w14 &= 0xff w14 <<= 0x18 ==> 0x6d000000 w10 = 0 w10 += w14 #define 0xbffff6b8 Dmem *(DWORD)0xbffff6b8 = w10 r2: W14 = mem[2] (0xa4) W14 -= 0x10 mem[2] = w14 w14 &= 0xff w14 <<= 0x10 ==> 0x940000 w10 += w14 *(DWORD)0xbffff6b8 = w10 (0x6d940000) 得到: 0xbffff6b8: ===> E4 CA 94 6D 和: 0xbffff6ac: ===> E4 CA 94 6D 不过后面 0xbffff6ac被清空了,用于下一轮的工作 都是指向这俩地方 5: 0x3bb4c => 0x3ba04(0x6d8d506a04) 1: 0x3ba30 => 0x3ba34(0x6d8d506a34) 2: 0x3ba70 => 0x3ba34(0x6d8d506a34) 2: 0x3ba70 => 0x3ba34(0x6d8d506a34) 2: 0x3ba70 => 0x3ba34(0x6d8d506a34) 2: 0x3ba70 => 0x3ba74(0x6d8d506a74) 3: 0x3badc => 0x3bae0(0x6d8d506ae0) 4: 0x3bb28 => 0x3bae0(0x6d8d506ae0) 4: 0x3bb28 => 0x3bae0(0x6d8d506ae0) 4: 0x3bb28 => 0x3bae0(0x6d8d506ae0) 4: 0x3bb28 => 0x3bb2c(0x6d8d506b2c) 5: 0x3bb4c => 0x3bb50(0x6d8d506b50) 6: 0x3bb54 => 0xfffffffffad3650c(0x6d8820150c) // 这次是 0x15de87走这一遭 得到了: 0xbffff6bc: 6B 04 A9 6D ------> 总结: 从: 0x00000000 0x0015de89 ===> E4 CA 94 6D 6B 04 A9 6D ===> 0x6d94cae4 0x6da9046b ----> swap()---> 6d 94 ca e4 与 6d a9 04 6b
unidbg脚本如下;
package com.Tencent_game;
import com.github.unidbg.Emulator; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Module; import com.github.unidbg.debugger.BreakPointCallback; import com.github.unidbg.debugger.DebuggerType; import com.github.unidbg.linux.android.AndroidEmulatorBuilder; import com.github.unidbg.linux.android.AndroidResolver; import com.github.unidbg.linux.android.dvm.*; import com.github.unidbg.memory.Memory; import com.github.unidbg.virtualmodule.android.AndroidModule; import java.io.File; import com.github.unidbg.arm.context.Arm64RegisterContext; public class qua_2023 { public final AndroidEmulator emulator; public final VM vm; public final Memory memory; public final Module module; DvmClass cNative; public qua_2023(){ emulator = AndroidEmulatorBuilder.for64Bit().build(); memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); emulator.getSyscallHandler().setEnableThreadDispatcher(true); vm = emulator.createDalvikVM(); // vm.setVerbose(true); new AndroidModule(emulator,vm).register(memory); DalvikModule dalvikModule = vm.loadLibrary(new File("D:\\tlsn\\BTools\\RE\\APK Easy Tool v1.60 Portable\\1-Decompiled APKs\\mouse_pre.aligned.signed\\lib\\arm64-v8a\\libsec2023.so"), true); module = dalvikModule.getModule(); vm.callJNI_OnLoad(emulator,module); } public static void main(String[] args){ qua_2023 mainActivity = new qua_2023(); mainActivity.debugger(); } private void debugger() { emulator.attach(DebuggerType.CONSOLE).addBreakPoint(module.base+ 0x00000000003B8CC); // emulator.traceRead(0xbffff6bc,0xbffff6c0); module.callFunction(emulator,0x00000000003B8CC,0x15de87); } }
算法还原:
# .so enc1 def so_enc1(inp): part1 = inp & 0xffffffff part2 = (inp >> 32) & 0xffffffff p1_inp1 = (part1 & 0xff) ^ 0 p1_inp2 = ((part1 >> 0x8) & 0xff) ^ 1 p1_inp3 = ((part1 >> 0x10) & 0xff) ^ 2 p1_inp4 = ((part1 >> 0x18) & 0xff) ^ 3 p1_inp1 -= 0x1c p1_inp2 ^= 0xd3 p1_inp3 -= 0x5e p1_inp4 ^= 0x86 p1_inp1 -= 0 p1_inp2 -= 0x8 p1_inp3 -= 0x10 p1_inp4 -= 0x18 p1_inp1 = conver(p1_inp1) p1_inp2 = conver(p1_inp2) p1_inp3 = conver(p1_inp3) p1_inp4 = conver(p1_inp4) p1_sum = p1_inp1 + (p1_inp2 << 8) + (p1_inp3 << 16) + (p1_inp4 << 24) p2_inp1 = (part2 & 0xff) ^ 0 p2_inp2 = ((part2 >> 0x8) & 0xff) ^ 1 p2_inp3 = ((part2 >> 0x10) & 0xff) ^ 2 p2_inp4 = ((part2 >> 0x18) & 0xff) ^ 3 p2_inp1 -= 0x1c p2_inp2 ^= 0xd3 p2_inp3 -= 0x5e p2_inp4 ^= 0x86 p2_inp1 -= 0 p2_inp2 -= 0x8 p2_inp3 -= 0x10 p2_inp4 -= 0x18 p2_inp1 = conver(p2_inp1) p2_inp2 = conver(p2_inp2) p2_inp3 = conver(p2_inp3) p2_inp4 = conver(p2_inp4) p2_sum = p2_inp1 + (p2_inp2 << 8) + (p2_inp3 << 0x10) + (p2_inp4 << 0x18) return p1_sum,p2_sum def so_dec1(p1_sum,p2_sum): p1_inp1 = p1_sum & 0xff p1_inp2 = (p1_sum >> 8) & 0xff p1_inp3 = (p1_sum >> 0x10) & 0xff p1_inp4 = (p1_sum >> 0x18) & 0xff p1_inp1 += 0 p1_inp2 += 0x8 p1_inp3 += 0x10 p1_inp4 += 0x18 p1_inp1 += 0x1c p1_inp2 ^= 0xd3 p1_inp3 += 0x5e p1_inp4 ^= 0x86 p1_inp1 &= 0xff p1_inp2 &= 0xff p1_inp3 &= 0xff p1_inp4 &= 0xff p1_inp1 ^= 0 p1_inp2 ^= 1 p1_inp3 ^= 2 p1_inp4 ^= 3 part1 = p1_inp1 + (p1_inp2 << 8) + (p1_inp3 << 0x10) + (p1_inp4 << 0x18) p2_inp1 = p2_sum & 0xff p2_inp2 = (p2_sum >> 8) & 0xff p2_inp3 = (p2_sum >> 0x10) & 0xff p2_inp4 = (p2_sum >> 0x18) & 0xff p2_inp1 += 0 p2_inp2 += 0x8 p2_inp3 += 0x10 p2_inp4 += 0x18 p2_inp1 += 0x1c p2_inp2 ^= 0xd3 p2_inp3 += 0x5e p2_inp4 ^= 0x86 p2_inp1 &= 0xff p2_inp2 &= 0xff p2_inp3 &= 0xff p2_inp4 &= 0xff p2_inp1 ^= 0 p2_inp2 ^= 1 p2_inp3 ^= 2 p2_inp4 ^= 3 part2 = p2_inp1 + (p2_inp2 << 8) + (p2_inp3 << 0x10) + (p2_inp4 << 0x18) return part1 + (part2 << 32) ming = 0x15de87 enc1,enc2 = so_enc1(ming) assert enc1 == 0x6da9046b and enc2 == 0x6d94cae4 enc1 = 0x6da9046b enc2 = 0x6d94cae4 ming = so_dec1(enc1,enc2) assert ming == 0x15de87
算法还原进度 3/4
loadclass 加载外部类中的算法
下面继续逆向
hook这个 GetStaticMethodID
jni函数,可以发现其调用了java层的 encry函数,但是我们目前反编译app的java层代码中是不包含encry函数的,猜测app在运行的时候 主动调用了 loadclass函数来加载外部 类,事实上也确实如此。
hook住loadclass函数是能发现程序调用了这样一个外部类的:
猜测 GetStaticMethodID
函数调用的 encry
方法也来自这个类中。
后面我尝试了多种方法去 找sec2023.Encrypt
这个类,比如 hook open函数,找dex的位置,也确实找到了一些蛛丝马迹,比如程序在运行的时候会在 /data/user/0/com.com.sec2023.rocketmouse.mouse/files
目录下生成一个 encry.dex
,并删除掉。我尝试在encry.dex
落地的时候 抓取到它,但没成功,不知道是不是分析有误。
后来,我尝试使用frida-dexdump
成功dump出了所有的dex(回头可以学一下它的hook方法)
用jadx打开后看到了 控制流平坦化,不急,高版本 jeb自带反混淆
大体加密逻辑如下:
还原算法:
def ror(m,round): return (m >> round ) | (m << (32-round)) def so_enc2(inp): inp_int = (inp[0] << 24) + (inp[1] << 16) + (inp[2] << 8) + inp[3] v1 = ror(inp_int,7) new_arr = [(v1 >> 24) & 0xff,(v1 >> 16) & 0xff,(v1 >> 8) & 0xff,v1 & 0xff ] key = [0x32, 0xCD, 0xFF, 0x98, 0x19, 0xB2, 0x7C, 0x9A] for i in range(4): new_arr[i] = new_arr[i] ^ key[i % 8] new_arr[i] = new_arr[i] + i new_arr[i] &= 0xff return new_arr def so_dec2(new_arr): key = [0x32, 0xCD, 0xFF, 0x98, 0x19, 0xB2, 0x7C, 0x9A] for i in range(4): new_arr[i] = new_arr[i] - i new_arr[i] = conver(new_arr[i]) new_arr[i] = new_arr[i] ^ key[i % 8] v1 = (new_arr[0] << 24) + (new_arr[1] << 16) + (new_arr[2] << 8) + new_arr[3] inp_int = ror(v1,32-7) inp = [(inp_int >> 24) & 0xff, (inp_int >> 16) & 0xff, (inp_int >> 8) & 0xff, inp_int & 0xff ] return inp ming1 = [0x6d,0x94, 0xca, 0xe4] ming2 = [0x6d,0xa9, 0x04, 0x6b] res1 = so_enc2(ming1) res2 = so_enc2(ming2) res1_int = res1[0] + (res1[1] << 8) + (res1[2] << 16) + (res1[3] << 24) res2_int = res2[0] + (res2[1] << 8) + (res2[2] << 16) + (res2[3] << 24) assert res1_int == 0x10d817fa and res2_int == 0x93af17e4 m1 = so_dec2(res1) m2 = so_dec2(res2) assert m1 == ming1 and m2 == ming2
算法还原进度 4/4
完整注册机
def vmenc(inp): part1 = inp & 0xffffffff part2 = (inp >> 32) & 0xffffffff p1_inp1 = part1 & 0xff p1_inp2 = (part1 >> 8) & 0xff p1_inp3 = (part1 >> 16) & 0xff p1_inp4 = (part1 >> 24) & 0xff p1_inp1 -= 0x1b p1_inp2 ^= 0xc2 p1_inp3 += 0xa8 p1_inp4 ^= 0x36 p1_inp1 = ((p1_inp1 ^ 0) << 0) & 0xff p1_inp2 = ((p1_inp2 ^ 0x8) << 0x8) &0xff00 p1_inp3 = ((p1_inp3 ^ 0x10) << 0x10) & 0xff0000 p1_inp4 = ((p1_inp4 ^ 0x18) << 0x18) & 0xff000000 p1_sum = p1_inp1 + p1_inp2 + p1_inp3 + p1_inp4 p2_inp1 = part2 & 0xff p2_inp2 = (part2 >> 8) & 0xff p2_inp3 = (part2 >> 16) & 0xff p2_inp4 = (part2 >> 24) & 0xff p2_inp1 -= 0x2f p2_inp2 ^= 0xb6 p2_inp3 += 0x37 p2_inp4 ^= 0x98 p2_inp1 = ((p2_inp1 + 0) << 0) & 0xff p2_inp2 = ((p2_inp2 + 0x8) << 0x8) & 0xff00 p2_inp3 = ((p2_inp3 + 0x10) << 0x10) & 0xff0000 p2_inp4 = ((p2_inp4 + 0x18) << 0x18) & 0xff000000 p2_sum = p2_inp1 + p2_inp2 + p2_inp3 + p2_inp4 return p1_sum,p2_sum def conver(m): if m < 0: return 0x100 + m return m def vmdec(p1_sum,p2_sum): p2_inp1 = (p2_sum & 0xff) p2_inp2 = ((p2_sum & 0xff00) >> 0x8) - 0x8 p2_inp3 = ((p2_sum & 0xff0000) >> 0x10) - 0x10 p2_inp4 = ((p2_sum & 0xff000000) >> 0x18) - 0x18 p2_inp1 += 0x2f p2_inp2 ^= 0xb6 p2_inp3 -= 0x37 p2_inp4 ^= 0x98 p2_inp1 = conver(p2_inp1) p2_inp2 = conver(p2_inp2) p2_inp3 = conver(p2_inp3) p2_inp4 = conver(p2_inp4) part2 = (p2_inp1 & 0xff) + ((p2_inp2 & 0xff) << 8) + ((p2_inp3 & 0xff) << 0x10) + ((p2_inp4 & 0xff) << 0x18) p1_inp1 = (p1_sum & 0xff) ^ 0x0 p1_inp2 = ((p1_sum & 0xff00) >> 0x8) ^ 0x8 p1_inp3 = ((p1_sum & 0xff0000) >> 0x10) ^ 0x10 p1_inp4 = ((p1_sum & 0xff000000) >> 0x18) ^ 0x18 p1_inp1 += 0x1b p1_inp2 ^= 0xc2 p1_inp3 -= 0xa8 p1_inp4 ^= 0x36 p1_inp1 = conver(p1_inp1) p1_inp2 = conver(p1_inp2) p1_inp3 = conver(p1_inp3) p1_inp4 = conver(p1_inp4) part1 = (p1_inp1 & 0xff) + ((p1_inp2 & 0xff) << 8) + ((p1_inp3 & 0xff) << 0x10) + ((p1_inp4 & 0xff) << 0x18) return part1 + (part2 << 32) enc = 0x93af17e410d817fa enc1,enc2 = vmenc(enc) assert enc1 == 0x3e90dddf and enc2 == 0x23f6a9b5 dec = vmdec(enc1,enc2) assert dec == 0x93af17e410d817fa # magical xtea # vmlater: low_4val: 0x3e90dddf high_4val: 0x23f6a9b5 # ===> # tealater: low_4val: 0x7dd89904 high_4val: 0xc6ff408e from ctypes import * from struct import pack,unpack def encrypt(v, key): v0, v1 = c_uint32(v[0]), c_uint32(v[1]) delta = 0x21524111 total1 = c_uint32(0xBEEFBEEF) total2 = c_uint32(0x9D9D7DDE) for i in range(64): v0.value += (((v1.value << 7) ^ (v1.value >> 8)) + v1.value) ^ (total1.value - key[total1.value & 3]) total1.value -= delta v1.value += (((v0.value << 8) ^ (v0.value >> 7)) - v0.value) ^ (total2.value + key[(total2.value>>13) & 3]) total2.value -= delta return v0.value, v1.value def decrypt(v, key): v0, v1 = c_uint32(v[0]), c_uint32(v[1]) delta = 0x21524111 total1 = c_uint32(0xBEEFBEEF - 64 * delta) total2 = c_uint32(0x9D9D7DDE - 64 * delta) for i in range(64): total2.value += delta v1.value -= (((v0.value << 8) ^ (v0.value >> 7)) - v0.value) ^ (total2.value + key[(total2.value>>13) & 3]) total1.value += delta v0.value -= (((v1.value << 7) ^ (v1.value >> 8)) + v1.value) ^ (total1.value - key[total1.value & 3]) return v0.value, v1.value def enxtea(inp:bytes,key:bytes): from struct import pack,unpack k = unpack("<4I",key) inp_len = len(inp) // 4 value = unpack(f"<{inp_len}I",inp) res = b"" for i in range(0,inp_len,2): v = [value[i],value[i+1]] x = encrypt(v,k) # x = decrypt(v,k) res += pack("<2I",*x) return res def dextea(inp:bytes,key:bytes): from struct import pack,unpack k = unpack("<4I",key) inp_len = len(inp) // 4 value = unpack(f"<{inp_len}I",inp) res = b"" for i in range(0,inp_len,2): v = [value[i],value[i+1]] x = decrypt(v,k) res += pack("<2I",*x) return res def xtea_enc(low_4val =0x3e90dddf,high_4val = 0x23f6a9b5 ): # enc # low_4val = 0x3e90dddf # high_4val = 0x23f6a9b5 inp = pack("<I",low_4val) + pack("<I",high_4val) key = [2071428195,3312413682,728170800,1990973438] key = pack("<I",key[0]) + pack("<I",key[1]) + pack("<I",key[2]) + pack("<I",key[3]) ret = enxtea(inp,key) xtea_enc1 = unpack("<I",ret[0:4])[0] xtea_enc2 = unpack("<I",ret[4:8])[0] # assert xtea_enc1 == 0x7dd89904 and xtea_enc2 == 0xc6ff408e return xtea_enc1,xtea_enc2 def xtea_dec(xtea_enc1 = 0x7dd89904,xtea_enc2 = 0xc6ff408e): # dec key = [2071428195,3312413682,728170800,1990973438] key = pack("<I",key[0]) + pack("<I",key[1]) + pack("<I",key[2]) + pack("<I",key[3]) # xtea_enc1 = 0x7dd89904 # xtea_enc2 = 0xc6ff408e inp = pack("<I",xtea_enc1) + pack("<I",xtea_enc2) ret = dextea(inp,key) xtea_dec1 = unpack("<I",ret[0:4])[0] xtea_dec2 = unpack("<I",ret[4:8])[0] # assert xtea_dec1 == 0x3e90dddf and xtea_dec2 == 0x23f6a9b5 return xtea_dec1,xtea_dec2 # .so enc1 def so_enc1(inp): part1 = inp & 0xffffffff part2 = (inp >> 32) & 0xffffffff p1_inp1 = (part1 & 0xff) ^ 0 p1_inp2 = ((part1 >> 0x8) & 0xff) ^ 1 p1_inp3 = ((part1 >> 0x10) & 0xff) ^ 2 p1_inp4 = ((part1 >> 0x18) & 0xff) ^ 3 p1_inp1 -= 0x1c p1_inp2 ^= 0xd3 p1_inp3 -= 0x5e p1_inp4 ^= 0x86 p1_inp1 -= 0 p1_inp2 -= 0x8 p1_inp3 -= 0x10 p1_inp4 -= 0x18 p1_inp1 = conver(p1_inp1) p1_inp2 = conver(p1_inp2) p1_inp3 = conver(p1_inp3) p1_inp4 = conver(p1_inp4) p1_sum = p1_inp1 + (p1_inp2 << 8) + (p1_inp3 << 16) + (p1_inp4 << 24) p2_inp1 = (part2 & 0xff) ^ 0 p2_inp2 = ((part2 >> 0x8) & 0xff) ^ 1 p2_inp3 = ((part2 >> 0x10) & 0xff) ^ 2 p2_inp4 = ((part2 >> 0x18) & 0xff) ^ 3 p2_inp1 -= 0x1c p2_inp2 ^= 0xd3 p2_inp3 -= 0x5e p2_inp4 ^= 0x86 p2_inp1 -= 0 p2_inp2 -= 0x8 p2_inp3 -= 0x10 p2_inp4 -= 0x18 p2_inp1 = conver(p2_inp1) p2_inp2 = conver(p2_inp2) p2_inp3 = conver(p2_inp3) p2_inp4 = conver(p2_inp4) p2_sum = p2_inp1 + (p2_inp2 << 8) + (p2_inp3 << 0x10) + (p2_inp4 << 0x18) return p1_sum,p2_sum def so_dec1(p1_sum,p2_sum): p1_inp1 = p1_sum & 0xff p1_inp2 = (p1_sum >> 8) & 0xff p1_inp3 = (p1_sum >> 0x10) & 0xff p1_inp4 = (p1_sum >> 0x18) & 0xff p1_inp1 += 0 p1_inp2 += 0x8 p1_inp3 += 0x10 p1_inp4 += 0x18 p1_inp1 += 0x1c p1_inp2 ^= 0xd3 p1_inp3 += 0x5e p1_inp4 ^= 0x86 p1_inp1 &= 0xff p1_inp2 &= 0xff p1_inp3 &= 0xff p1_inp4 &= 0xff p1_inp1 ^= 0 p1_inp2 ^= 1 p1_inp3 ^= 2 p1_inp4 ^= 3 part1 = p1_inp1 + (p1_inp2 << 8) + (p1_inp3 << 0x10) + (p1_inp4 << 0x18) p2_inp1 = p2_sum & 0xff p2_inp2 = (p2_sum >> 8) & 0xff p2_inp3 = (p2_sum >> 0x10) & 0xff p2_inp4 = (p2_sum >> 0x18) & 0xff p2_inp1 += 0 p2_inp2 += 0x8 p2_inp3 += 0x10 p2_inp4 += 0x18 p2_inp1 += 0x1c p2_inp2 ^= 0xd3 p2_inp3 += 0x5e p2_inp4 ^= 0x86 p2_inp1 &= 0xff p2_inp2 &= 0xff p2_inp3 &= 0xff p2_inp4 &= 0xff p2_inp1 ^= 0 p2_inp2 ^= 1 p2_inp3 ^= 2 p2_inp4 ^= 3 part2 = p2_inp1 + (p2_inp2 << 8) + (p2_inp3 << 0x10) + (p2_inp4 << 0x18) return part1 + (part2 << 32) ming = 0x15de87 enc1,enc2 = so_enc1(ming) assert enc1 == 0x6da9046b and enc2 == 0x6d94cae4 enc1 = 0x6da9046b enc2 = 0x6d94cae4 ming = so_dec1(enc1,enc2) assert ming == 0x15de87 def ror(m,round): return (m >> round ) | (m << (32-round)) def so_enc2(inp): inp_int = (inp[0] << 24) + (inp[1] << 16) + (inp[2] << 8) + inp[3] v1 = ror(inp_int,7) new_arr = [(v1 >> 24) & 0xff,(v1 >> 16) & 0xff,(v1 >> 8) & 0xff,v1 & 0xff ] key = [0x32, 0xCD, 0xFF, 0x98, 0x19, 0xB2, 0x7C, 0x9A] for i in range(4): new_arr[i] = new_arr[i] ^ key[i % 8] new_arr[i] = new_arr[i] + i new_arr[i] &= 0xff return new_arr def so_dec2(new_arr): key = [0x32, 0xCD, 0xFF, 0x98, 0x19, 0xB2, 0x7C, 0x9A] for i in range(4): new_arr[i] = new_arr[i] - i new_arr[i] = conver(new_arr[i]) new_arr[i] = new_arr[i] ^ key[i % 8] v1 = (new_arr[0] << 24) + (new_arr[1] << 16) + (new_arr[2] << 8) + new_arr[3] inp_int = ror(v1,32-7) inp = [(inp_int >> 24) & 0xff, (inp_int >> 16) & 0xff, (inp_int >> 8) & 0xff, inp_int & 0xff ] return inp ming1 = [0x6d,0x94, 0xca, 0xe4] ming2 = [0x6d,0xa9, 0x04, 0x6b] res1 = so_enc2(ming1) res2 = so_enc2(ming2) res1_int = res1[0] + (res1[1] << 8) + (res1[2] << 16) + (res1[3] << 24) res2_int = res2[0] + (res2[1] << 8) + (res2[2] << 16) + (res2[3] << 24) assert res1_int == 0x10d817fa and res2_int == 0x93af17e4 m1 = so_dec2(res1) m2 = so_dec2(res2) assert m1 == ming1 and m2 == ming2 # generate cheat by token def all_enc(token:int): enc1_part1,enc1_part2 = so_enc1(token) enc1_part1_list = [(enc1_part1 >> 0x18) & 0xff,(enc1_part1 >> 0x10) & 0xff,(enc1_part1 >> 8) & 0xff,enc1_part1 & 0xff] enc1_part2_list = [(enc1_part2 >> 0x18) & 0xff,(enc1_part2 >> 0x10) & 0xff,(enc1_part2 >> 8) & 0xff,enc1_part2 & 0xff] enc2_part1_list = so_enc2(enc1_part1_list) enc2_part2_list = so_enc2(enc1_part2_list) enc2_part1 = enc2_part1_list[0] + (enc2_part1_list[1] << 8) + (enc2_part1_list[2] << 16) + (enc2_part1_list[3] << 24) enc2_part2 = enc2_part2_list[0] + (enc2_part2_list[1] << 8) + (enc2_part2_list[2] << 16) + (enc2_part2_list[3] << 24) enc2 = enc2_part2 + (enc2_part1 << 32) # vmenc3_part1,vmenc3_part2 = vmenc(enc2) enc4_low4,enc4_high4 = xtea_enc(vmenc3_part1,vmenc3_part2) return enc4_low4,enc4_high4 input_ = 1433223 cheat1,cheat2 = all_enc(input_) assert cheat1 == 0x7dd89904 and cheat2 == 0xc6ff408e def generate_cheat_by_token(enc4_low4,enc4_high4=0): vmenc3_part1,vmenc3_part2 = xtea_dec(enc4_low4,enc4_high4) enc2 = vmdec(vmenc3_part1,vmenc3_part2) enc2_part2 = enc2 & 0xffffffff enc2_part1 = (enc2 >> 32) & 0xffffffff enc2_part1_list = [enc2_part1 & 0xff,(enc2_part1 >> 8) & 0xff , (enc2_part1 >> 0x10) & 0xff,(enc2_part1 >> 0x18) & 0xff] enc2_part2_list = [enc2_part2 & 0xff,(enc2_part2 >> 8) & 0xff , (enc2_part2 >> 0x10) & 0xff,(enc2_part2 >> 0x18) & 0xff] enc1_part1_list = so_dec2(enc2_part1_list) enc1_part2_list = so_dec2(enc2_part2_list) enc1_part1 = (enc1_part1_list[0] << 0x18) + (enc1_part1_list[1] << 0x10) + (enc1_part1_list[2] << 0x8) + enc1_part1_list[3] enc1_part2 = (enc1_part2_list[0] << 0x18) + (enc1_part2_list[1] << 0x10) + (enc1_part2_list[2] << 0x8) + enc1_part2_list[3] cheat = so_dec1(enc1_part1,enc1_part2) return cheat input_ = generate_cheat_by_token(cheat1,cheat2) assert input_ == 1433223 token = 80820328 # 这里输入token cheat = generate_cheat_by_token(token) print(f"token: {token} ===> cheat: {cheat}") # 小键盘中输入 生成的cheat码即可实现作弊(加速、无敌、吸金币)
__EOF__

本文链接:https://www.cnblogs.com/lordtianqiyi/p/18622721.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现