# 最新京东参数分析
分析目标为 jdgs 参数。一般这种键值对类型的参数可以通过 hook HashMap 得到。
在 com.jd.security.jdguard.core 的 main 函数中生成
hook 此方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 Java .perform (function ( ) { var ReflectArray = Java .use ("java.lang.reflect.Array" ); var Bridge = Java .use ("com.jd.security.jdguard.core.Bridge" ); function formatByteArray (byteArray ) { var text = "" ; var length = ReflectArray .getLength (byteArray); for (var i = 0 ; i < length; i++) { var value = ReflectArray .getByte (byteArray, i); if (value < 0 ) { value += 256 ; } if (value === 10 ) { text += "\\n" ; } else if (value === 13 ) { text += "\\r" ; } else if (value === 9 ) { text += "\\t" ; } else if (value >= 32 && value <= 126 ) { text += String .fromCharCode (value); } else { text += "%" + ("0" + value.toString (16 )).slice (-2 ); } } return text; } function formatJavaArray (arrayObj ) { var values = []; var length; try { length = arrayObj.length ; } catch (e) { length = ReflectArray .getLength (arrayObj); } for (var i = 0 ; i < length; i++) { try { values.push (formatValue (arrayObj[i])); } catch (e) { values.push (formatValue (ReflectArray .get (arrayObj, i))); } } return "[" + values.join ("," ) + "]" ; } function formatValue (value ) { if (value === null || value === undefined ) { return String (value); } try { var className = value.getClass ().getName (); if (className === "[B" ) { return formatByteArray (value); } if (className.charAt (0 ) === "[" ) { return formatJavaArray (value); } } catch (e) { } try { return value.toString (); } catch (e) { return String (value); } } function formatObjectArray (objArr ) { if (objArr === null || objArr === undefined ) { return String (objArr); } var values = []; var length = objArr.length ; for (var i = 0 ; i < length; i++) { values.push (formatValue (objArr[i])); } return "[" + values.join ("," ) + "]" ; } var main = Bridge .main .overload ("int" , "[Ljava.lang.Object;" ); main.implementation = function (i10, objArr ) { console .log ("Bridge.main is called: i10=" + i10 + ", objArr=" + formatObjectArray (objArr)); var result = main.call (this , i10, objArr); console .log ("Bridge.main result=" + formatValue (result)); return result; }; });
部分输出如下
1 2 3 4 5 6 7 8 9 10 11 12 Bridge.main is called: i10=103, objArr=[0] Bridge.main result=0 Bridge.main is called: i10=103, objArr=[1,1776764556324] Bridge.main result=0 Bridge.main is called: i10=105, objArr=[1] Bridge.main result= Bridge.main is called: i10=105, objArr=[1] Bridge.main is called: i10=107, objArr=[0] Bridge.main result= Bridge.main result=@A0+Bj4/vopGiA4hx5HnvQNmIrSYQmmGGewjXNBOhHi7FM=,-1,1 Bridge.main is called: i10=101, objArr=[POST /client.action 36c589eaf38d8a48a04d7a8c6a3a618893bd60f3da52be653678b695941374b9=&area=1_72_55674_0&build=102518&client=android&clientVersion=15.9.50&ef=1&eid=eidA0832412213l532d0093a178319ddabcfd8a8f4cdaeb69b86b8a4yl74W5p4pz9PZF4PMqrR2TJmkSMunAUdLbIAB9iGFgMxjdQYcqak6QAwGM9%2F&ep=%7B%22hdid%22%3A%22JM9F1ywUPwflvMIpYPok0tt5k9kW4ArJEU3lfLhxBqw%3D%22%2C%22ts%22%3A1776764556216%2C%22ridx%22%3A-1%2C%22cipher%22%3A%7B%22d_model%22%3A%22UQv4ZWm0%22%2C%22wifiBssid%22%3A%22dW5hbw93bq%3D%3D%22%2C%22osVersion%22%3A%22CJC%3D%22%2C%22d_brand%22%3A%22H29lZ2nv%22%2C%22screen%22%3A%22CtO0EMenCNqm%22%2C%22uuid%22%3A%22ZWPrEWPtDzZuZJrwYtrwCm%3D%3D%22%2C%22aid%22%3A%22ZWPrEWPtDzZuZJrwYtrwCm%3D%3D%22%2C%22openudid%22%3A%22ZWPrEWPtDzZuZJrwYtrwCm%3D%3D%22%7D%2C%22ciphertype%22%3A5%2C%22version%22%3A%221.2.1%22%2C%22appname%22%3A%22com.jingdong.app.mall%22%7D&ext=%7B%22prstate%22%3A%220%22%2C%22pvcStu%22%3A%221%22%2C%22cfgExt%22%3A%22%7B%5C%22privacyOffline%5C%22%3A%5C%220%5C%22%7D%22%7D&functionId=welcomeHome&harmonyOs=0&lang=zh_CN&ma_sid=1&networkType=wifi&partner=yybd01&pv_sid=1&sdkVersion=33&uemps=0-2-999&x-api-eid-token=eidA0832412213l532d0093a178319ddabcfd8a8f4cdaeb69b86b8a4yl74W5p4pz9PZF4PMqrR2TJmkSMunAUdLbIAB9iGFgMxjdQYcqak6QAwGM9%2F&xAPIClientLanguage=zh_CN&xAPIClientMode=0&xAPICurrency=CNY&xAPIDJPTransmittedData=%7B%22tnSDKVersion%22%3A%221.54.0%22%7D&xAPINeoType=999&xAPINqe=0&xAPIRegion=CN&xAPISysNqe=UNKNOWN&x_app_key=vygmwcxg14ui3jxw,-§§-§§-§§-§§-§§-§§-§§-,eidA0832412213l532d0093a178319ddabcfd8a8f4cdaeb69b86b8a4yl74W5p4pz9PZF4PMqrR2TJmkSMunAUdLbIAB9iGFgMxjdQYcqak6QAwGM9/,1.0,@A0+Bj4/vopGiA4hx5HnvQNmIrSYQmmGGewjXNBOhHi7FM=,B91CD017-1FF8-42BF-BE74-7CB26EF31A4A,null] Bridge.main result={"b1":"3345a325-9dc1-4f87-b54e-874eb01c426d","b2":"3.6.2_0","b3":"3.0","b4":"CC+zGKrbKWRfYcWcB8CDZiLdgWJ3pE9I/++8gf0S8W83kQmXaecMVg+4w2chmCcpFZVrIdHS681sYzWXcokiRcQPP+ZhkiScN7EBEibd2mAvioX+eYfVrz7M+r6XuNi5rC0DPPc9oHQe71xJg5l+6lSERxOHCL0RAmBoYK7nhr60pyV7s59A4BWKyWkUKuQerdgeaAWqjyuJdlDpzQiU3LV/Na7XQg0nANlSAeeukeEX8MTZscSP845c1g4qq6myxINw9eeV0tgOswNKNOpixALYNXwA5f+cblPzNbDSCR8L/zIgsIqQFxA63DCen3ds1/2fAYjdF6utAC6c/E5VOIie1WxJ8yqRrt5JROeL0Qo4seXjLRmbbJp1Ca+MHx1Tww==","b5":"d94aeabf3d8689fa303603bfea82da9216c13c215688a5dd92355de4290750cc","b7":"1776764556793","b6":"16cea2241e0a1b343143e8cf2c945e576077101c21bb738602b2151ed800c9f1"}
会输出多个结果,b4 b5 b6 一直在变
固定参数主动调用
b5 固定,为 d94aeabf3d8689fa303603bfea82da9216c13c215688a5dd92355de4290750cc
# 模拟执行
用 unidbg 模拟执行 main 函数
先搭框架
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 package com.example.jd;import com.github.unidbg.AndroidEmulator;import com.github.unidbg.Emulator;import com.github.unidbg.arm.backend.Unicorn2Factory;import com.github.unidbg.file.FileResult;import com.github.unidbg.file.IOResolver;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.array.ByteArray;import com.github.unidbg.memory.Memory;import java.io.File;public class CoreNew extends AbstractJni implements IOResolver { private final AndroidEmulator emulator; private final DvmClass Bridge; private final VM vm; public CoreNew () { emulator = AndroidEmulatorBuilder.for64Bit() .addBackendFactory(new Unicorn2Factory (true )) .setProcessName("com.jingdong.app.mall" ) .build(); emulator.getSyscallHandler().addIOResolver(this ); Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver (23 )); vm = emulator.createDalvikVM(new File ("unidbg-android/src/test/resources/example/jd.apk" )); vm.setJni(this ); vm.setVerbose(true ); Bridge = vm.resolveClass("com.jd.security.jdguard.core.Bridge" ); DalvikModule dm = vm.loadLibrary("jdg" ,true ); dm.callJNI_OnLoad(emulator); } public ArrayObject calls () { int arg1 = 101 ; byte [] str = "POST /client.action 36c589eaf38d8a48a04d7a8c6a3a618893bd60f3da52be653678b695941374b9=&area=1_72_55674_0&build=102518&client=android&clientVersion=15.9.50&ef=1&eid=eidA0832412213l532d0093a178319ddabcfd8a8f4cdaeb69b86b8a4yl74W5p4pz9PZF4PMqrR2TJmkSMunAUdLbIAB9iGFgMxjdQYcqak6QAwGM9%2F&ep=%7B%22hdid%22%3A%22JM9F1ywUPwflvMIpYPok0tt5k9kW4ArJEU3lfLhxBqw%3D%22%2C%22ts%22%3A1776764556216%2C%22ridx%22%3A-1%2C%22cipher%22%3A%7B%22d_model%22%3A%22UQv4ZWm0%22%2C%22wifiBssid%22%3A%22dW5hbw93bq%3D%3D%22%2C%22osVersion%22%3A%22CJC%3D%22%2C%22d_brand%22%3A%22H29lZ2nv%22%2C%22screen%22%3A%22CtO0EMenCNqm%22%2C%22uuid%22%3A%22ZWPrEWPtDzZuZJrwYtrwCm%3D%3D%22%2C%22aid%22%3A%22ZWPrEWPtDzZuZJrwYtrwCm%3D%3D%22%2C%22openudid%22%3A%22ZWPrEWPtDzZuZJrwYtrwCm%3D%3D%22%7D%2C%22ciphertype%22%3A5%2C%22version%22%3A%221.2.1%22%2C%22appname%22%3A%22com.jingdong.app.mall%22%7D&ext=%7B%22prstate%22%3A%220%22%2C%22pvcStu%22%3A%221%22%2C%22cfgExt%22%3A%22%7B%5C%22privacyOffline%5C%22%3A%5C%220%5C%22%7D%22%7D&functionId=welcomeHome&harmonyOs=0&lang=zh_CN&ma_sid=1&networkType=wifi&partner=yybd01&pv_sid=1&sdkVersion=33&uemps=0-2-999&x-api-eid-token=eidA0832412213l532d0093a178319ddabcfd8a8f4cdaeb69b86b8a4yl74W5p4pz9PZF4PMqrR2TJmkSMunAUdLbIAB9iGFgMxjdQYcqak6QAwGM9%2F&xAPIClientLanguage=zh_CN&xAPIClientMode=0&xAPICurrency=CNY&xAPIDJPTransmittedData=%7B%22tnSDKVersion%22%3A%221.54.0%22%7D&xAPINeoType=999&xAPINqe=0&xAPIRegion=CN&xAPISysNqe=UNKNOWN&x_app_key=vygmwcxg14ui3jxw" .getBytes(StandardCharsets.UTF_8); ByteArray Bstr = new ByteArray (vm,str); StringObject str2 = new StringObject (vm,"-§§-§§-§§-§§-§§-§§-§§-" ); StringObject str3 = new StringObject (vm,"eidA0832412213l532d0093a178319ddabcfd8a8f4cdaeb69b86b8a4yl74W5p4pz9PZF4PMqrR2TJmkSMunAUdLbIAB9iGFgMxjdQYcqak6QAwGM9/" ); StringObject str4 = new StringObject (vm,"1.0" ); StringObject str5 = new StringObject (vm,"@A0+Bj4/vopGiA4hx5HnvQNmIrSYQmmGGewjXNBOhHi7FM=" ); StringObject str6 = new StringObject (vm,"B91CD017-1FF8-42BF-BE74-7CB26EF31A4A" ); DvmObject<?> str7 = null ; ArrayObject arg2 = new ArrayObject (Bstr,str2,str3,str4,str5,str6,str7); ArrayObject res = Bridge.callStaticJniMethodObject(emulator,"main(I[Ljava/lang/Object;)[Ljava/lang/Object;" ,arg1,arg2); return res; } public static void main (String[] args) { CoreNew core = new CoreNew (); ArrayObject res = core.calls(); System.out.println("res : " + res.getValue()[0 ].getValue()); } @Override public FileResult resolve (Emulator emulator, String pathname, int oflags) { System.out.println("open : " + pathname); return null ; } }
运行,提示缺少 libandroid.so ,需要注册虚拟模块
1 new AndroidModule(emulator,vm).register(memory);
再次运行
未初始化的问题
可以回想一下刚刚 hook 的时候
在最开始先执行了五次,a1 均非 101。可以推断出 101 操作码代表的是正常调用,那么其他操作码很可能就是初始化。在 unidbg 里模拟这几次调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public ArrayObject callInit (int arg1,String arg2) { DvmObject<?> obj = arg1 == 107 ? DvmInteger.valueOf(vm, Integer.parseInt(arg2)) : new StringObject (vm, arg2); ArrayObject arg = new ArrayObject (obj); ArrayObject res = Bridge.callStaticJniMethodObject(emulator,"main(I[Ljava/lang/Object;)[Ljava/lang/Object;" ,arg1, arg); return res; } public ArrayObject callInit103WithTime (long ts) { ArrayObject arg = new ArrayObject ( new StringObject (vm, "1" ), DvmLong.valueOf(vm, ts) ); ArrayObject res = Bridge.callStaticJniMethodObject(emulator,"main(I[Ljava/lang/Object;)[Ljava/lang/Object;" ,103 , arg); return res; } public static void main (String[] args) { CoreNew core = new CoreNew (); ArrayObject res1 = core.callInit(103 ,"0" ); printResult("res1" , res1); ArrayObject res2 = core.callInit103WithTime(1776764556324L ); printResult("res2" , res2); ArrayObject res3 = core.callInit(105 ,"1" ); printResult("res3" , res3); ArrayObject res4 = core.callInit(105 ,"1" ); printResult("res4" , res4); ArrayObject res5 = core.callInit(107 ,"0" ); printResult("res5" , res5); ArrayObject res = core.calls(); printResult("res" , res); }
# 补环境
经典的补 context 环境
1 2 3 4 5 case "com/jd/security/jdguard/core/Bridge->getAppContext()Landroid/content/Context;" : { return vm.resolveClass("android/app/Application" ,vm.resolveClass("android/context/ContextWrapper" ), vm.resolveClass("android/context/Context" )); }
获取包名,找到对应实现复制过来
获取包管理器也类似
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { switch (signature) { case "java/lang/Class->getPackageName()Ljava/lang/String;" : { String packageName = vm.getPackageName(); if (packageName != null ) { return new StringObject (vm, packageName); } break ; } case "java/lang/Class->getPackageManager()Landroid/content/pm/PackageManager;" : { return vm.resolveClass("android/content/pm/PackageManager" ).newObject(null ); } case "java/lang/Class->getPackageCodePath()Ljava/lang/String;" : { return new StringObject (vm,"/data/app/~~FZNFb1Py7Esp2oGc8euR3A==/com.jingdong.app.mall-yYYRiSBJOF6Gef3viDFLiQ==/base.apk" ); } } return super .callObjectMethodV(vm,dvmObject,signature,vaList); }
下面这个报错的方法是应用里的方法,
hook 获取值即可
1 2 3 4 5 6 7 8 9 Java .perform (function ( ){ var Bridge = Java .use ("com.jd.security.jdguard.core.Bridge" ); Bridge ["getAppKey" ].implementation = function ( ) { console .log (`Bridge.getAppKey is called` ); let result = this ["getAppKey" ](); console .log (`Bridge.getAppKey result=${result} ` ); return result; }; })
其余环境也类似
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList){ switch (signature) { case "com/jd/security/jdguard/core/Bridge->getAppContext()Landroid/content/Context;" : { return vm.resolveClass("android/app/Application" , vm.resolveClass("android/content/ContextWrapper" ), vm.resolveClass("android/content/Context" )).newObject(null ); } case "com/jd/security/jdguard/core/Bridge->getAppKey()Ljava/lang/String;" : { return new StringObject (vm,"3345a325-9dc1-4f87-b54e-874eb01c426d" ); } case "com/jd/security/jdguard/core/Bridge->getJDGVN()Ljava/lang/String;" : { return new StringObject (vm,"3.6.2" ); } case "com/jd/security/jdguard/core/Bridge->getPicName()Ljava/lang/String;" : { return new StringObject (vm,"3345a325-9dc1-4f87-b54e-874eb01c426d.jdg.jpg" ); } case "com/jd/security/jdguard/core/Bridge->getSecName()Ljava/lang/String;" : { return new StringObject (vm, "3345a325-9dc1-4f87-b54e-874eb01c426d.jdg.xbt" ); } case "com/jd/security/jdguard/core/Bridge->androidId()Ljava/lang/String;" : { return new StringObject (vm,"726c839b0f0624df" ); } case "com/jd/security/jdguard/core/Bridge->getBizNum(Ljava/lang/String;)Ljava/lang/String;" : { StringObject key = vaList.getObjectArg(0 ); String value = "" ; if (key != null ) { switch (key.getValue()) { case "timeline" : value = "@A0+Bj4/vopGiA4hx5HnvQNmIrSYQmmGGewjXNBOhHi7FM=" ; break ; case "srvTk" : value = "B91CD017-1FF8-42BF-BE74-7CB26EF31A4A" ; break ; default : value = "83" ; break ; } } return new StringObject (vm, value); } } return super .callStaticObjectMethodV(vm,dvmClass,signature,vaList); } public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList){ switch (signature) { case "java/lang/Class->getPackageName()Ljava/lang/String;" : case "android/app/Application->getPackageName()Ljava/lang/String;" : case "android/content/ContextWrapper->getPackageName()Ljava/lang/String;" : case "android/content/Context->getPackageName()Ljava/lang/String;" : { String packageName = vm.getPackageName(); if (packageName != null ) { return new StringObject (vm, packageName); } return new StringObject (vm, PACKAGE_NAME); } case "java/lang/Class->getPackageManager()Landroid/content/pm/PackageManager;" : case "android/app/Application->getPackageManager()Landroid/content/pm/PackageManager;" : case "android/content/ContextWrapper->getPackageManager()Landroid/content/pm/PackageManager;" : case "android/content/Context->getPackageManager()Landroid/content/pm/PackageManager;" : { return vm.resolveClass("android/content/pm/PackageManager" ).newObject(null ); } case "java/lang/Class->getPackageCodePath()Ljava/lang/String;" : case "android/app/Application->getPackageCodePath()Ljava/lang/String;" : case "android/content/ContextWrapper->getPackageCodePath()Ljava/lang/String;" : case "android/content/Context->getPackageCodePath()Ljava/lang/String;" : { return new StringObject (vm, APK_PATH); } case "java/lang/Class->getFilesDir()Ljava/io/File;" : case "android/app/Application->getFilesDir()Ljava/io/File;" : case "android/content/ContextWrapper->getFilesDir()Ljava/io/File;" : case "android/content/Context->getFilesDir()Ljava/io/File;" : { return vm.resolveClass("java/io/File" ).newObject(FILES_DIR); } case "java/io/File->getAbsolutePath()Ljava/lang/String;" : case "java/io/File->getPath()Ljava/lang/String;" : case "java/io/File->toString()Ljava/lang/String;" : { Object value = dvmObject.getValue(); return new StringObject (vm, value == null ? "" : value.toString()); } case "java/io/File->getName()Ljava/lang/String;" : { Object value = dvmObject.getValue(); String path = value == null ? "" : value.toString(); int index = Math.max(path.lastIndexOf('/' ), path.lastIndexOf('\\' )); return new StringObject (vm, index >= 0 ? path.substring(index + 1 ) : path); } case "java/lang/Class->getApplicationInfo()Landroid/content/pm/ApplicationInfo;" : case "android/app/Application->getApplicationInfo()Landroid/content/pm/ApplicationInfo;" : case "android/content/ContextWrapper->getApplicationInfo()Landroid/content/pm/ApplicationInfo;" : case "android/content/Context->getApplicationInfo()Landroid/content/pm/ApplicationInfo;" : { return vm.resolveClass("android/content/pm/ApplicationInfo" ).newObject(null ); } case "android/content/pm/Signature->toByteArray()[B" : { System.out.println("[env] Signature.toByteArray -> real cert len=" + REAL_APK_SIGNATURE.length); return new ByteArray (vm, REAL_APK_SIGNATURE); } } return super .callObjectMethodV(vm,dvmObject,signature,vaList); } @Override public int getIntField (BaseVM vm, DvmObject<?> dvmObject, String signature) { switch (signature) { case "android/content/pm/PackageInfo->versionCode:I" : return 102518 ; } return super .getIntField(vm, dvmObject, signature); } @Override public DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature) { switch (signature) { case "android/content/pm/PackageInfo->versionName:Ljava/lang/String;" : return new StringObject (vm, "15.9.50" ); case "android/content/pm/PackageInfo->applicationInfo:Landroid/content/pm/ApplicationInfo;" : return vm.resolveClass("android/content/pm/ApplicationInfo" ).newObject(null ); case "android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;" : return new StringObject (vm, APK_PATH); case "android/content/pm/ApplicationInfo->publicSourceDir:Ljava/lang/String;" : return new StringObject (vm, APK_PATH); case "android/content/pm/ApplicationInfo->dataDir:Ljava/lang/String;" : return new StringObject (vm, DATA_DIR); } return super .getObjectField(vm, dvmObject, signature); } @Override public long callLongMethodV (BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { switch (signature) { case "java/lang/Long->longValue()J" : { Object value = dvmObject.getValue(); return value instanceof Number ? ((Number) value).longValue() : Long.parseLong(String.valueOf(value)); } } return super .callLongMethodV(vm, dvmObject, signature, vaList); }
有几处检测 unidbg 的地方,比如 103 字节码下会检测 uname ().release,unidbg 会返回 1.0.0-unidbg,要改成正常 Android kernel 样式。因为补环境不是本文重点所以不做详细讨论
b7 明显是时间戳,可通过修改 UnixSyscallHandler.java 中的 gettimeofday64 函数来固定时间戳
再次运行发现 b4 b5 b6 均为定值。
1 res : {"b1":"3345a325-9dc1-4f87-b54e-874eb01c426d","b2":"3.6.2_0","b3":"3.0","b4":"JOQBm1B/hgnN2+tpop3IhL6HJfe33Vqa2pYHzDdMdm0+kgKNbjkECmRQHzH/442QL7Kn+rzFaY9y7P8YfkTey2DfI7L4sxLVJY/vcEBgtDoUvAooY9oVbIdqCzYOdRf6lmgfof1sMJJoLJEmXZKBCOtFgyHG/tqGR7EL9GBudjDBi/6OmBAEYwMBy1uZ5MVTShtrH+d/gbRaW4G1NWHAGDIH4Qf+4IpKgAQX/fBIYOrqV94UGj+OZV2XrmAV8ePvRdFP0zNAb2dXKKqaPHYK0rMIHgaauBsPFshGouOx+1s+beBrza5argwjBO2V12rubwWnJwKkXU4xVDlEo91MOqzffCSXud3Q3M2DsIXaMpbEFsIOdBFuhd1VfPCwoeYMPL/bAw==","b5":"71ef31ac0d77c192a2502f2dc300022cf7705c3d1a5c376f525165cb0bbec666","b7":"1785168434783","b6":"a5e0f374399aa08421288797a3dcf28a8afea69f7cee1b6d2934c64e8ae3d200"}
# b5 算法分析
刚刚主动调用时,b5 的值为 e1fa66109582848ed7c7f45e54809088d53c529b ,和现在模拟执行的值有出入。也就是说 unidbg 和真机环境之间有差距,导致了结果的不同。现在我们需要排查出不同的地方。
b5 为 32 字节,可以想到 sha256 函数。检验算法可以通过 trace 代码找特征实现
trace 代码
1 2 3 4 5 6 7 8 9 public void trace () { File traceFile = new File ("target/jd/trace.txt" ); try { PrintStream traceStream = new PrintStream (new FileOutputStream (traceFile), true ); emulator.traceCode(module .base, module .base + module .size).setRedirect(traceStream); } catch (FileNotFoundException e) { throw new IllegalStateException (e); } }
搜索 sha256 魔数,ida 里可以直接找到
交叉引用定位到 sha256 函数(文件中有间接 call 混淆,我已经修复了一部分所以可以引用到)
unidbg 中下断观察参数
1 2 3 4 5 public void Debugger2 () { Debugger debugger = emulator.attach(); debugger.addBreakPoint(module .base + 0x8b408 ); }
密钥:
明文:
0x8b3f0 下断看加密后的值
密文:
是标准的 HMAC-SHA256 算法。但是这个值和我们的明文参数以及密文都毫无关系
可以追踪一下密文地址,观察后续是否有改变
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public void Debugger () { Debugger debugger = emulator.attach(); debugger.addBreakPoint(module .base + 0x8B408 , new BreakPointCallback () { int count = 0 ; @Override public boolean onHit (Emulator<?> emulator, long address) { count++; RegisterContext ctx = emulator.getContext(); UnidbgPointer key = ctx.getPointerArg(0 ); int keyLen = ctx.getIntArg(1 ); UnidbgPointer data = ctx.getPointerArg(2 ); int dataLen = ctx.getIntArg(3 ); UnidbgPointer out = ctx.getPointerArg(4 ); int outLen = ctx.getIntArg(5 ); System.out.printf("[HMAC enter #%d] out=%s outLen=%d%n" , count, ptr(out), outLen); if (out != null && outLen > 0 ) { System.out.printf("[traceWrite] 0x%x - 0x%x%n" , out.peer, out.peer + outLen); emulator.traceWrite(out.peer, out.peer + outLen); } return true ; } }); }
输出:
上面的是 sha256 后的密文,下面的是最终的密文。
通过观察还可以发现,密文之间不连续,同一个地址先写了 0x17,再覆盖成 0x71,这通常代表着 0x17 -> 0x71。
不过这些都只是我们的猜测,因为加密被 vmp 和间接跳转保护,接下来就需要通过 trace 分析了。
寻找 trace 是有技巧的。可以先搜索 0xff334,因为值是在这里生成的,明显它是一个 vmp 中写入的操作。定位到这附近以后再搜索值会比直接搜索值要好一点。
左下角有两个热点图,这很可能就是 vmp 计算时的部分。b6 长度和 b5 相同,算法也可能相似。所以这两个热点图就是两次加密运算。我们定位到热点图的最上方,开始搜索密文
定位之后搜索这行运算
第一列为刚才 unidbg 中首次写入的值,第二列为最终密文。也就是说,确实是从 0x17 -> 0x71。
继续找 0x17 的生成运算,没找到。可以改为搜索 0x117
圈起来的是 sha256 后的密文。那么现在得到了初步结论:密文 [i] = (sha256(data,key)+ add_data [i]) ^ xor_data [i]。
那么 sha256 的明文从哪里来?观察其长度为 16 字节,首先可以想到 md5 算法等,搜索常数
有很多。在 ida 对照,发现 md5 核心运算在 sub_8B52C。监控此地址查看参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public void Hook () { Debugger debugger = emulator.attach(); debugger.addBreakPoint(module .base + 0x8B52C , new BreakPointCallback () { RegisterContext context = emulator.getContext(); int num = 0 ; @Override public boolean onHit (Emulator<?> emulator, long address) { num+=1 ; UnidbgPointer src = context.getPointerArg(0x20 ); UnidbgPointer src2 = context.getPointerArg(1 ); Inspector.inspect(src2.getByteArray(0 ,112 ),"0x8B52C onEnter arg1 " +num); debugger.addBreakPoint(context.getLRPointer().peer, new BreakPointCallback () { @Override public boolean onHit (Emulator<?> emulator, long address) { Inspector.inspect(src.getByteArray(0 ,0x20 ),"0x8B52C onLeave arg0 " +num); return true ; } }); return true ; } }); }
输出
md5 明文为请求参数。md5 的结果也不是最终放到 sha256 的结果,和刚刚一样,在 trace.txt 继续找运算就可以了,不再赘述。
言归正传。我们注意到真机 hook 和 unidbg 模拟执行得到的 b5 不同。问题出在哪里呢,我 hook 了 sha256 的明文和 key,发现明文相同,key 不同。
key 又是怎么生成的呢,跟踪生成地址
在 0xff314 生成。ida 里可以看到还是被保护的代码不好分析,所以继续分析 trace.txt
这两个异或的值比较特殊,看起来是可见字符。
提取
1 2 "com.jingdong.app.mall1.9.50com.jing" "3345a35-9dc1-4f87-b54e-874eb01c426d"
一个是包名 + 版本号,另一个是应用自己的 Appkey。
hook 一下真机中的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 function call ( ){ Java .perform (function ( ){ hook (); let Bridge = Java .use ("com.jd.security.jdguard.core.Bridge" ); var str0 = "POST /client.action 36c589eaf38d8a48a04d7a8c6a3a618893bd60f3da52be653678b695941374b9=&area=1_72_55674_0&build=102518&client=android&clientVersion=15.9.50&ef=1&eid=eidA0832412213l532d0093a178319ddabcfd8a8f4cdaeb69b86b8a4yl74W5p4pz9PZF4PMqrR2TJmkSMunAUdLbIAB9iGFgMxjdQYcqak6QAwGM9%2F&ep=%7B%22hdid%22%3A%22JM9F1ywUPwflvMIpYPok0tt5k9kW4ArJEU3lfLhxBqw%3D%22%2C%22ts%22%3A1776764556216%2C%22ridx%22%3A-1%2C%22cipher%22%3A%7B%22d_model%22%3A%22UQv4ZWm0%22%2C%22wifiBssid%22%3A%22dW5hbw93bq%3D%3D%22%2C%22osVersion%22%3A%22CJC%3D%22%2C%22d_brand%22%3A%22H29lZ2nv%22%2C%22screen%22%3A%22CtO0EMenCNqm%22%2C%22uuid%22%3A%22ZWPrEWPtDzZuZJrwYtrwCm%3D%3D%22%2C%22aid%22%3A%22ZWPrEWPtDzZuZJrwYtrwCm%3D%3D%22%2C%22openudid%22%3A%22ZWPrEWPtDzZuZJrwYtrwCm%3D%3D%22%7D%2C%22ciphertype%22%3A5%2C%22version%22%3A%221.2.1%22%2C%22appname%22%3A%22com.jingdong.app.mall%22%7D&ext=%7B%22prstate%22%3A%220%22%2C%22pvcStu%22%3A%221%22%2C%22cfgExt%22%3A%22%7B%5C%22privacyOffline%5C%22%3A%5C%220%5C%22%7D%22%7D&functionId=welcomeHome&harmonyOs=0&lang=zh_CN&ma_sid=1&networkType=wifi&partner=yybd01&pv_sid=1&sdkVersion=33&uemps=0-2-999&x-api-eid-token=eidA0832412213l532d0093a178319ddabcfd8a8f4cdaeb69b86b8a4yl74W5p4pz9PZF4PMqrR2TJmkSMunAUdLbIAB9iGFgMxjdQYcqak6QAwGM9%2F&xAPIClientLanguage=zh_CN&xAPIClientMode=0&xAPICurrency=CNY&xAPIDJPTransmittedData=%7B%22tnSDKVersion%22%3A%221.54.0%22%7D&xAPINeoType=999&xAPINqe=0&xAPIRegion=CN&xAPISysNqe=UNKNOWN&x_app_key=vygmwcxg14ui3jxw" ; var StringClass = Java .use ('java.lang.String' ); var byteArray = StringClass .$new(str0).getBytes (); var objArr = Java .array ('Ljava.lang.Object;' ,[byteArray,'-§§-§§-§§-§§-§§-§§-§§-' ,'eidA0832412213l532d0093a178319ddabcfd8a8f4cdaeb69b86b8a4yl74W5p4pz9PZF4PMqrR2TJmkSMunAUdLbIAB9iGFgMxjdQYcqak6QAwGM9/' ,'1.0' ,',@A0+Bj4/vopGiA4hx5HnvQNmIrSYQmmGGewjXNBOhHi7FM=' ,'B91CD017-1FF8-42BF-BE74-7CB26EF31A4A' ,null ]) let result = Bridge ["main" ](101 , objArr); console .log ("res:" ,result) }) } function hook ( ){ const base = Module .findBaseAddress ("libjdg.so" ); var target = base.add (0xF753C ); Interceptor .attach (target,{ onEnter :function (args ) { const w8 = this .context .x8 .toUInt32 () & 0xFF ; const w9 = this .context .x9 .toUInt32 () & 0xFF ; const out = (w8 ^ w9) & 0xff ; console .log ("w8 : " + w8.toString (16 ) + " w9 : " + w9.toString (16 ) + " out : " + out.toString (16 )); }, }) } function main ( ){ var timer = setInterval (function ( ){ var module = Process .findModuleByName ("libjdg.so" ); if (module != null ) { clearInterval (timer); call (); } },10 ); } setImmediate (main);
得到两个异或的字符串
1 2 "3345a325-9dc1-4f87-b54e-874eb01c426d" ".501A70367C58D5D41C4678DFD05F84F15.9"
Appkey 是一样的,只是另一个值是 apk 签名证书的 md5。
那么现在算法就清晰了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 import hashlibimport hmacMD5_ADD_TABLE_HEX = "xxx" MD5_XOR_TABLE_HEX = "xxx" KEY_HEX = ( "xxx" "xxx" "xxx" ) ADD_TABLE_HEX = ( "xxx" "xxx" ) XOR_TABLE_HEX = ( "xxx" "xxx" ) def calc_md5_16 (plain: bytes ) -> tuple [bytes , bytes ]: digest = hashlib.md5(plain).digest() add_table = bytes .fromhex(MD5_ADD_TABLE_HEX) xor_table = bytes .fromhex(MD5_XOR_TABLE_HEX) value16 = bytes ( ((digest[i] + add_table[i]) & 0xff ) ^ xor_table[i] for i in range (16 ) ) return digest, value16 def calc_hmac (plain16: bytes ) -> bytes : key = bytes .fromhex(KEY_HEX) return hmac.new(key, plain16, hashlib.sha256).digest() def vm_transform (hmac_out: bytes ) -> bytes : add_table = bytes .fromhex(ADD_TABLE_HEX) xor_table = bytes .fromhex(XOR_TABLE_HEX) return bytes ( ((hmac_out[i] + add_table[i]) & 0xff ) ^ xor_table[i] for i in range (32 ) ) def calc_b5 (plain_hex: str ): plain = bytes .fromhex(plain_hex) if len (plain) != 16 : raise ValueError(f"plain must be 16 bytes, got {len (plain)} bytes" ) hmac_out = calc_hmac(plain) b5 = vm_transform(hmac_out) return hmac_out, b5 def calc_from_post (plain: bytes ): md5_digest, value16 = calc_md5_16(plain) hmac_out = calc_hmac(value16) b5 = vm_transform(hmac_out) return md5_digest, value16, hmac_out, b5 if __name__ == "__main__" : post_plain = b"""POST /client.action 36c589eaf38d8a48a04d7a8c6a3a618893bd60f3da52be653678b695941374b9=&area=1_72_55674_0&build=102518&client=android&clientVersion=15.9.50&ef=1&eid=eidA0832412213l532d0093a178319ddabcfd8a8f4cdaeb69b86b8a4yl74W5p4pz9PZF4PMqrR2TJmkSMunAUdLbIAB9iGFgMxjdQYcqak6QAwGM9%2F&ep=%7B%22hdid%22%3A%22JM9F1ywUPwflvMIpYPok0tt5k9kW4ArJEU3lfLhxBqw%3D%22%2C%22ts%22%3A1776764556216%2C%22ridx%22%3A-1%2C%22cipher%22%3A%7B%22d_model%22%3A%22UQv4ZWm0%22%2C%22wifiBssid%22%3A%22dW5hbw93bq%3D%3D%22%2C%22osVersion%22%3A%22CJC%3D%22%2C%22d_brand%22%3A%22H29lZ2nv%22%2C%22screen%22%3A%22CtO0EMenCNqm%22%2C%22uuid%22%3A%22ZWPrEWPtDzZuZJrwYtrwCm%3D%3D%22%2C%22aid%22%3A%22ZWPrEWPtDzZuZJrwYtrwCm%3D%3D%22%2C%22openudid%22%3A%22ZWPrEWPtDzZuZJrwYtrwCm%3D%3D%22%7D%2C%22ciphertype%22%3A5%2C%22version%22%3A%221.2.1%22%2C%22appname%22%3A%22com.jingdong.app.mall%22%7D&ext=%7B%22prstate%22%3A%220%22%2C%22pvcStu%22%3A%221%22%2C%22cfgExt%22%3A%22%7B%5C%22privacyOffline%5C%22%3A%5C%220%5C%22%7D%22%7D&functionId=welcomeHome&harmonyOs=0&lang=zh_CN&ma_sid=1&networkType=wifi&partner=yybd01&pv_sid=1&sdkVersion=33&uemps=0-2-999&x-api-eid-token=eidA0832412213l532d0093a178319ddabcfd8a8f4cdaeb69b86b8a4yl74W5p4pz9PZF4PMqrR2TJmkSMunAUdLbIAB9iGFgMxjdQYcqak6QAwGM9%2F&xAPIClientLanguage=zh_CN&xAPIClientMode=0&xAPICurrency=CNY&xAPIDJPTransmittedData=%7B%22tnSDKVersion%22%3A%221.54.0%22%7D&xAPINeoType=999&xAPINqe=0&xAPIRegion=CN&xAPISysNqe=UNKNOWN&x_app_key=vygmwcxg14ui3jxw""" md5_digest, value16, hmac_out, b5 = calc_from_post(post_plain) print ("plain_len:" , len (post_plain)) print ("md5 :" , md5_digest.hex ()) print ("value16 :" , value16.hex ()) print ("hmac :" , hmac_out.hex ()) print ("b5 :" , b5.hex ())
得到的值和 hook 出的完全相同
# b6 算法分析
b6 算法和 b5 相似。我们在分析 b5 的过程中就可以发现很多运算都出现了两次
b6 明文为除去 b6 的返回结果。
因为 unidbg 模拟的是不正确的 b6 值,我们可以找一个 hook 得到的结果验证
放入算法中的明文为:
1 {"b1":"3345a325-9dc1-4f87-b54e-874eb01c426d","b2":"3.6.2_0","b3":"3.0","b4":"2I/HUB3GlR4ujULQffbQEgcYKmAkFOlDCkABO8LAtfUtuW+GSFyeEGetwLuGSn2BYyKPPTqteXF/cFtqG4t+cTmvaACovEzyVVk0qapCPt6RVbz6H29/ewwyUFRlqsCxYEWmL28+3C8ey9xBy/h99E57eyMpYPI/2oPTPd3o58xfDWM/rV/YLGEtWm1+qdJTFfnvwSpZVkKeCZAnLqNa/YgvJp0NlDB5VXg/6gge85BIsDEb42W8isYGUFvkaKoHWk6fO2XeBOEN25XE2VrfF0quorj6oSFGz1T8/lQpRtIpvMk/mS4VyLm8TUS3Viy2hXLSDROgKmsm7sYkUJywfKlNRhcbsEWhSZfXcL34FggVzZr52LnviIfuGM0/IgIjuE7X8eVeG8Y=","b5":"ba6fd0a624efb2dfeb110fa35332f7b8cb5e277dfbb7480e82176d9a14104f30","b7":"1777271045186"}
运行结果如下
完全符合