【Android安全-某短视频平台最新sig3字段算法分析(一)】此文章归类为:Android安全。
最近看到手里的某短视频APP来了兴致,特意拿来分析记录下,整个系列文章大概分为抓包,java层分析,so定位,so去花,unidbg,算法还原等几个部分,这几篇文章会记录下我的整个调试过程,前面的文章会比较基础,入门级玩家基本可以略过了,因为考虑到文章的连续性我这边还是会记录发表下。
因为需要对关键字段的算法进行分析,所以个人习惯还是要先抓下协议看一下,先不管别的用BurpSuite抓个包出来看下
抓到后总体感觉进包速度跟APP的流量不太匹配,感觉大概率是走了其他协议。既然抓到的包里面有sign字段,就先从sign字段入手看下,
用frida hook看下调用栈
var ins_C82880ba = Java.use("com.kuaishou.weapon.ks.ba"); ins_C82880ba.a.overload('java.lang.String', 'java.lang.String').implementation = function(a, b) { var ret = this.a(a, b) show_stacks() console.log(ret) return ret }
后面根据调用栈顺藤摸瓜分析就好,最后找到了发送函数发现跟okhttp有关
试着将okhttp的接口发送与接收接口打印一下看看
var ins_okhttp = Java.use("okhttp3.OkHttpClient") ins_okhttp.newCall.overload('okhttp3.Request').implementation = function(a) { console.log(a); //show_stacks() return this.newCall(a); } var ins_RealCall = Java.use("okhttp3.RealCall"); ins_RealCall.execute.overload().implementation = function() { //show_stacks() var ret = this.execute(); console.log(ret.toString()) return ret }
找到了__NS_sig3字段,这个是我们需要分析算法的字段,之前Burp Suite抓不到包的原因也出来了,走的是quic协议。
直接在代码搜索__NS_sig3进行定位
一直往里面进
调用接口找到了
直接搜索C0526k开始
至此,so与调用so的接口都确定下来了。
将定位到的lib文件导入IDA,f5之后发现 JNI_OnLoad出现jumpout了
直接汇编先分析一下
.text:0000000000045854 ; jint JNI_OnLoad(JavaVM *vm, void *reserved) .text:0000000000045854 EXPORT JNI_OnLoad .text:0000000000045854 JNI_OnLoad ; DATA XREF: LOAD:0000000000003AD0↑o .text:0000000000045854 .text:0000000000045854 var_20 = -0x20 .text:0000000000045854 var_10 = -0x10 .text:0000000000045854 var_8 = -8 .text:0000000000045854 .text:0000000000045854 STP X0, X1, [SP,#-32]! #sp开辟32个字节的空间,x0,x1入栈 .text:0000000000045858 STP X2, X30, [SP,#16] #x2,x30入栈 栈从下往上依次是x0,x1,x2,x30 .text:000000000004585C ADR X1, dword_4587C # x1 = 0x4587c .text:0000000000045860 SUBS X1, X1, #4 # x1 = 0x4587c-4->x1 = 0x45878 .text:0000000000045864 MOV X0, X1 # x0' = x1 -> x0' = 0x45877c .text:0000000000045868 ADDS X0, X0, #0x34 ; '4' # x0' = x0' + 0x34 -> x0' = 0x458ac .text:000000000004586C STR X0, [SP,#24] # [SP,#24] = x0' -> 栈从下往上依次是x0,x1,x2,x0' .text:0000000000045870 LDP X2, X9, [SP,#16] # x2 = [SP,#16],x9=[SP,#24] -> x2 = x2, x9=x0' .text:0000000000045874 LDP X0, X1, [SP],#0x20 # x0 = [SP],x1=[SP,#8] -> x0 = x0,x1=x1 sp恢复栈平衡 .text:0000000000045878 BR X9 # br x0' -> br 0x4669c
从注释基本都可以看出来,前期开栈到恢复栈,就弄了一堆花里胡哨的算了下绝对跳转地址,x0与x1未变化,也比较符合花指令的特性。只要把前面的垃圾指令nop掉,绝对跳转改成相对跳转即可。
先手动使用IDA插件Keypatch试一下,先nop掉垃圾指令。
在修改跳转指令为相对跳转
修改完需要导入patch到so中
重新用IDA打开按F5即可生效,但是一个个修改比较麻烦,还是需要用脚本根据特征码定位进行修改会比较方便,对比JNI_OnLoad与JNI_UnLoad就会发现特征码比较明显
直接上脚本
import keystone from keystone import * import ida_bytes import idaapi import idc def pattern_search(pattern): match_list = [] addr = 0 while True: addr = ida_bytes.bin_search(addr, idc.BADADDR, bytes.fromhex(pattern), None, idaapi.BIN_SEARCH_FORWARD, idaapi.BIN_SEARCH_NOCASE) if addr == idc.BADADDR: break else: match_list.append(addr) addr = addr + 1 return match_list def get_jumpout_addr(addr): data1 = idc.get_operand_value(addr + 8, 1) data2 = idc.get_operand_value(addr + 12, 2) data3 = idc.get_operand_value(addr + 20, 2) return data1 - data2 + data3 def generate_asm(code, addr): ks = Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN) encode, count = ks.asm(code, addr) return encode def main(): match_list = pattern_search("E0 07 BE A9 E2 7B 01 A9") print(len(match_list)) for i in range(len(match_list)): encode_b = generate_asm("B " + str(hex(get_jumpout_addr(match_list[i]))), match_list[i]) encode_nop = generate_asm("nop", 0) ida_bytes.patch_bytes(match_list[i], bytes(encode_b)) ida_bytes.patch_bytes(match_list[i] + 4, bytes(encode_nop) * 9) if __name__ == "__main__": main()
先整个基本框架跑一下JNI_OnLoad函数,没问题后再调用so函数,调用so的参数直接hook java层接口结合java源码就能获取,这里就不多说了
package com.ks.run; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Module; import com.github.unidbg.arm.backend.Unicorn2Factory; 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.linux.android.dvm.array.ArrayObject; import com.github.unidbg.linux.android.dvm.wrapper.DvmBoolean; import com.github.unidbg.linux.android.dvm.wrapper.DvmInteger; import com.github.unidbg.memory.Memory; import com.github.unidbg.virtualmodule.android.AndroidModule; import java.io.File; import java.util.ArrayList; import java.util.List; public class KSEmulator extends AbstractJni { private final AndroidEmulator emulator; private final Module module; private final VM vm; public KSEmulator() { emulator = AndroidEmulatorBuilder .for64Bit() .addBackendFactory(new Unicorn2Factory(true)) .setProcessName("com.smile.gifmaker") .build(); Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/kuaishou/ks_12.10.30.39327.apk")); vm.setJni(this); vm.setVerbose(true); new AndroidModule(emulator, vm).register(memory); DalvikModule dm = vm.loadLibrary("kwsgmain", true); module = dm.getModule(); System.out.print("base :" + module.base + "\n"); System.out.print("size :" + module.size + "\n"); dm.callJNI_OnLoad(emulator); } private void call_doCommandNative_sig3(String text) { List<Object> params = new ArrayList<>(); params.add(vm.getJNIEnv()); params.add(0); params.add(10418); StringObject str = new StringObject(vm, text); vm.addLocalObject(str); ArrayObject strArray = new ArrayObject(str); StringObject key1 = new StringObject(vm, "d7b7d042-d4f2-4012-be60-d97ff2429c17"); vm.addLocalObject(key1); DvmInteger dInt = DvmInteger.valueOf(vm, -1); vm.addLocalObject(dInt); DvmBoolean dBoolean = DvmBoolean.valueOf(vm, false); vm.addLocalObject(dBoolean); DvmObject<?> dClass = vm.resolveClass("com/yxcorp/gifshow/App").newObject(null); vm.addLocalObject(dClass); StringObject key2 = new StringObject(vm, ""); vm.addLocalObject(key2); ArrayObject paramArray = new ArrayObject(strArray, key1, dInt, dBoolean, dClass, null, dBoolean, key2); params.add(vm.addLocalObject(paramArray)); Number number = module.callFunction(emulator, 0x40cd4, params.toArray()); DvmObject<?> object = vm.getObject(number.intValue()); String result = (String)object.getValue(); System.out.println("result:"+ result); } public static void main(String[] args) { KSEmulator emulator = new KSEmulator(); emulator.call_doCommandNative_sig3("HandsomeBro"); } }
运行后会出现如下错误
[23:29:06 547] WARN [com.github.unidbg.AbstractEmulator] (AbstractEmulator:417) - emulate RX@0x40040cd4[libkwsgmain.so]0x40cd4 exception sp=unidbg@0xbffff020, msg=unicorn.UnicornException: Invalid memory read (UC_ERR_READ_UNMAPPED), offset=13ms @ Runnable|Function64 address=0x40040cd4, arguments=[unidbg@0xfffe1640[libandroid.so]0x640, 0, 10418, 846492085] Exception in thread "main" java.lang.NullPointerException at com.ks.run.KSEmulator.call_doCommandNative_sig3(KSEmulator.java:64) at com.ks.run.KSEmulator.main(KSEmulator.java:70) Process finished with exit code 1
第一反应大概率是访问了无效内存导致崩溃了,这种情形一般是调用so接口前没有初始化导致的,先hook找一下初始化的流程
function hook_so() { var base = Module.findBaseAddress("libkwsgmain.so"); if (base) { var addr_doCommandNative = base.add(0x40cd4); Interceptor.attach(addr_doCommandNative, { onEnter: function (args) { console.log("doCommandNative() args[2] = " + args[2]) }, onLeave: function (retval) { } }) var addr_gdbf = base.add(0x408a4); Interceptor.attach(addr_gdbf, { onEnter: function (args) { console.log("gdbf() enter") }, onLeave: function (retval) { } }) var addr_dcabk = base.add(0x40948); Interceptor.attach(addr_dcabk, { onEnter: function (args) { console.log("dcabk() enter") }, onLeave: function (retval) { } }) var addr_gdgi = base.add(0x403bc); Interceptor.attach(addr_gdgi, { onEnter: function (args) { console.log("gdgi() enter") }, onLeave: function (retval) { } }) var addr_gksf = base.add(0x407f0); Interceptor.attach(addr_gksf, { onEnter: function (args) { console.log("gksf() enter") }, onLeave: function (retval) { } }) } }
发现JNI接口其他函数并没有调用,但是每次都会调用doCommandNative的0x28ac也就是10412
unidbg文件加上10412的函数
private void call_doCommandNative_init() { List<Object> params = new ArrayList<>(); params.add(vm.getJNIEnv()); params.add(0); params.add(10412); StringObject key1 = new StringObject(vm, "d7b7d042-d4f2-4012-be60-d97ff2429c17"); vm.addLocalObject(key1); DvmInteger dInt = DvmInteger.valueOf(vm, 0); vm.addLocalObject(dInt); DvmObject<?> dClass = vm.resolveClass("com/yxcorp/gifshow/App").newObject(null); vm.addLocalObject(dClass); ArrayObject paramArray = new ArrayObject(dInt, key1, dInt, dInt, dClass, dInt, dInt); params.add(vm.addLocalObject(paramArray)); Number number = module.callFunction(emulator, 0x40cd4, params.toArray()); System.out.println("numbers:" + number); DvmObject<?> object = vm.getObject(number.intValue()); String result = (String)object.getValue(); System.out.println("result:"+ result); }
运行后发现会报缺少类的错误,挨个解决一下
public boolean callBooleanMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { switch (signature) { case "java/lang/Boolean->booleanValue()Z": return ((DvmBoolean)dvmObject).getValue(); } return super.callBooleanMethodV(vm, dvmObject, signature, vaList); } @Override public void callStaticVoidMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) { switch (signature) { case "com/kuaishou/android/security/internal/common/ExceptionProxy->nativeReport(ILjava/lang/String;)V": return; } } @Override public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) { switch (signature) { case "com/kuaishou/android/security/internal/common/ExceptionProxy->getProcessName(Landroid/content/Context;)Ljava/lang/String;": return new StringObject(vm, "com.smile.gifmaker"); } return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList); } @Override public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { switch (signature) { case "com/yxcorp/gifshow/App->getPackageCodePath()Ljava/lang/String;": { return new StringObject(vm, "/data/app/com.smile.gifmaker-VZhzinzcefoqqzJZ47EE0A==/base.apk"); } case "com/yxcorp/gifshow/App->getPackageName()Ljava/lang/String;": { return new StringObject(vm, "com.smile.gifmaker"); } case "com/yxcorp/gifshow/App->getAssets()Landroid/content/res/AssetManager;": { return new AssetManager(vm, signature); } case "com/yxcorp/gifshow/App->getPackageManager()Landroid/content/pm/PackageManager;": { return vm.resolveClass("android.content.pm.PackageManager").newObject(null); } } return super.callObjectMethodV(vm, dvmObject, signature, vaList); }
再次运行结果能正常显示,但是每次结果都会不一样,这里就需要找一下随机因子了,一般时间戳会用的比较多一点,也有用系统函数获取随机数的,可以到IDA里面搜下关键字,先搜索下random
发现很多调用都没有用到,在搜索下time
这里IDA里面很多函数都有用到,直接到unidbg工程里面固定下参数
固定后运行发现数据每次都一样了
如果不想再ida中搜索或者搜索不到关键字,其实也可以直接在unidbg工程里面加日志或者固定参数进行测试,unidbg工程有实现标准的linux系统调用接口
今天就记录到这里,剩下的算法还原我们留到第二篇在写。
警告:本文章相关代码与分析流程仅用于技术学习与提升,切勿用于非法用途,否则后果自负。
更多【Android安全-某短视频平台最新sig3字段算法分析(一)】相关视频教程:www.yxfzedu.com