zuiyou登录算法逆向
Created At : 2024-06-09 00:07
Views 👀 :
打开app登录,使用HttpCanary抓包
1 2 3 4 5 6 7 8 9 10 11 POST /stat/action?sign=v2-161b1aa20aa4c8f5b5f97007e77f75a7 HTTP/1.1 ZYP: mid=304623228 X-Xc-Agent: av=5.7.3,dt=0 User-Agent: okhttp/3.12.2 Zuiyou/5.7.3 (Android/30) X-Xc-Proto-Req: duck-1717933249-4grdwokVrZzGxu+FnZp7Ar5XtPZgg+anpDOIMIeiHOldgtTMZIzxFlfhdGnDjRYkUfWrC+O3oX/GPXoIKaROH+z9Ny+mCLTRe3+gKlAUCWM= Request-Type: text/json Content-Type: application/xcp Content-Length: 912 Host: stat.izuiyou.com Connection: Keep-Alive Accept-Encoding: gzip
jadx直接搜索“sign”,定位到关键函数
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 package com.izuiyou.network;import android.app.ContextProvider;import com.meituan.robust.ChangeQuickRedirect;import com.meituan.robust.PatchProxy;import com.meituan.robust.PatchProxyResult;public class NetCrypto { public static ChangeQuickRedirect changeQuickRedirect; static { we5.a(ContextProvider.get(), "net_crypto" ); native_init(); } public static String a (String str, byte [] bArr) { String str2; PatchProxyResult proxy = PatchProxy.proxy(new Object []{str, bArr}, null , changeQuickRedirect, true , 47387 , new Class []{String.class, byte [].class}, String.class); if (proxy.isSupported) { return (String) proxy.result; } String sign = sign(str, bArr); if (str.contains("?" )) { str2 = "&sign=" + sign; } else { str2 = "?sign=" + sign; } return str + str2; } public static native byte [] decodeAES(byte [] bArr, boolean z); public static native byte [] encodeAES(byte [] bArr); public static native String generateSign (byte [] bArr) ; public static native String getProtocolKey () ; public static native void native_init () ; public static native boolean registerDID (byte [] bArr) ; public static native void setProtocolKey (String str) ; public static native String sign (String str, byte [] bArr) ; }
使用fridahook一下a方法看
1 2 3 4 5 6 7 8 9 10 11 function callsign ( ){ Java .perform (function ( ){ var NetCry = Java .use ("com.izuiyou.network.NetCrypto" ); var JStr = Java .use ("java.lang.String" ); var arg1 = "n1ng" ; var arg1Bytes = JStr .$new(arg1).getBytes ("UTF-8" ); var retval = NetCry .a ("12345" ,arg1Bytes); console .log ("result :" + retval); }); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 //result (frida) C:\Users\n1ng>frida -UF -l E:\Learn\Unidbg\最右app\callsign.js ____ / _ | Frida 16.1.4 - A world-class dynamic instrumentation toolkit | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit/quit -> Exit . . . . . . . . More info at https://frida.re/docs/home/ . . . . . . . . Connected to MI 9 (id=21ce24db) [MI 9::最右 ]-> callsign() result :12345?sign=v2-c4a2c9fbbcad24427a76f999fe406f61 [MI 9::最右 ]-> callsign() result :12345?sign=v2-c24557ab0d6fcdc08d5edac4dd76b899 [MI 9::最右 ]-> callsign() result :123245?sign=v2-c24557ab0d6fcdc08d5edac4dd76b899 [MI 9::最右 ]->
可以看到sign的值由两个传入的参数共同决定,由三部分第一个参数+固定字符+第二个参数生成的32位字符(猜测可能是哈希)
接下来使用unidbg尝试进行模拟执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class zuiyou extends AbstractJni { private final VM vm; private final AndroidEmulator emulator; private final Module module ; zuiyou(){ emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.xiaochuankeji.tieba" ).build(); final Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver (23 )); vm = emulator.createDalvikVM(new File ("unidbg-android\\src\\test\\java\\com\\test2\\right573.apk" )); DalvikModule dm = vm.loadLibrary(new File ("unidbg-android\\src\\test\\java\\com\\test2\\libnet_crypto.so" ),true ); module = dm.getModule(); vm.setJni(this ); vm.setVerbose(true ); dm.callJNI_OnLoad(emulator); } public static void main (String[] args) throws Exception{ zuiyou test = new zuiyou (); } }
1 2 3 4 5 6 7 8 9 10 11 //日志输出 JNIEnv->FindClass(com/izuiyou/network/NetCrypto) was called from RX@0x40049fc5[libnet_crypto.so]0x49fc5 JNIEnv->RegisterNatives(com/izuiyou/network/NetCrypto, RW@0x401c9010[libnet_crypto.so]0x1c9010, 8) was called from RX@0x40049fd7[libnet_crypto.so]0x49fd7 RegisterNative(com/izuiyou/network/NetCrypto, native_init()V, RX@0x4004a069[libnet_crypto.so]0x4a069) RegisterNative(com/izuiyou/network/NetCrypto, encodeAES([B)[B, RX@0x4004a0b9[libnet_crypto.so]0x4a0b9) RegisterNative(com/izuiyou/network/NetCrypto, decodeAES([BZ)[B, RX@0x4004a14d[libnet_crypto.so]0x4a14d) RegisterNative(com/izuiyou/network/NetCrypto, sign(Ljava/lang/String;[B)Ljava/lang/String;, RX@0x4004a28d[libnet_crypto.so]0x4a28d) RegisterNative(com/izuiyou/network/NetCrypto, getProtocolKey()Ljava/lang/String;, RX@0x4004a419[libnet_crypto.so]0x4a419) RegisterNative(com/izuiyou/network/NetCrypto, setProtocolKey(Ljava/lang/String;)V, RX@0x4004a479[libnet_crypto.so]0x4a479) RegisterNative(com/izuiyou/network/NetCrypto, registerDID([B)Z, RX@0x4004a4f5[libnet_crypto.so]0x4a4f5) RegisterNative(com/izuiyou/network/NetCrypto, generateSign([B)Ljava/lang/String;, RX@0x4004a587[libnet_crypto.so]0x4a587)
可以看到动态注册了函数
接下来看native_init和sign函数
1 2 3 4 5 6 7 public void native_init () { List<Object> list = new ArrayList <>(10 ); list.add(vm.getJNIEnv()); list.add(0 ); module .callFunction(emulator, 0x4a096 , list.toArray()); }
产生了报错
1 java.lang.UnsupportedOperationException: com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;
这是因为调用callStaticObjectiMethodV方法时遇到了无法处理的函数,此时可以查看AbstractJni.java中看看Unidbg开发者是如何处理的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 case "java/lang/CharSequence->toString()Ljava/lang/String;": { return new StringObject(vm, dvmObject.value.toString()); } case "java/lang/String->toLowerCase()Ljava/lang/String;": { return new StringObject(vm, dvmObject.value.toString().toLowerCase()); } case "android/content/pm/PackageManager->getApplicationInfo(Ljava/lang/String;I)Landroid/content/pm/ApplicationInfo;": StringObject packageName = vaList.getObjectArg(0); if(packageName.value.equals(vm.getPackageName())){ return new ApplicationInfo(vm); } else { throw new UnsupportedOperationException(signature); } case "java/lang/String->trim()Ljava/lang/String;":{ StringObject stringObject = (StringObject) dvmObject; return new StringObject(vm, stringObject.value.trim()); } } throw new UnsupportedOperationException(signature);
可见开发者对一些常见的类和方法已经做了一些处理,因此我们可以类似的在AbstractJni中或者我们自己的类(这里是zuiyou,因为zuiyou类继承了AbstractJni类)加上我们的代码进行补环境
1 2 3 4 5 6 7 8 public DvmObject<?> callStaticObjectMethodV(BaseVM vm , DvmClass dvmClass,String signature, VaList valist){ switch (signature){ case "com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;" : System.out.println("TODO" ); } return super .callStaticObjectMethodV(vm, dvmClass, signature, valist); }
从上面打印的报错日志的签名信息中可以看出,返回的类型是Landroid/content/Context,因此我们也返回一个Context即可
1 2 3 4 5 6 7 8 public DvmObject<?> callStaticObjectMethodV(BaseVM vm , DvmClass dvmClass,String signature, VaList valist){ switch (signature){ case "com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;" : System.out.println("TODO" ); return vm.resolveClass("android/content/Context" ).newObject(null ); } return super .callObjectMethodV(vm, dvmClass, signature, valist); }
重新运行,发现不会报错了,接下来看sign方法
1 2 3 4 5 6 7 8 9 10 private String callSign () { List<Object> list = new ArrayList <>(10 ); list.add(vm.getJNIEnv()); list.add(0 ); list.add(vm.addLocalObject(new StringObject (vm,"12345" ))); ByteArray byteArray = new ByteArray (vm, "n1ng" .getBytes(StandardCharsets.UTF_8)); list.add(vm.addLocalObject(byteArray)); Number number = module .callFunction(emulator, 0x4a28d , list.toArray()); return vm.getObject(number.intValue()).getValue().toString(); }
1 2 3 4 5 //报错 java.lang.UnsupportedOperationException: android/content/Context->getClass()Ljava/lang/Class; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417) at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262) at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)
可以看到是我们上面传入的Context的getClass方法报错,根据报错,现在重写一下callObjectMethodV
1 2 3 4 5 6 7 8 public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList){ switch (signature){ case "android/content/Context->getClass()Ljava/lang/Class;" :{ return dvmObject.getObjectType(); } } return super .callObjectMethodV(vm, dvmObject, signature, vaList); }
1 2 3 java.lang.UnsupportedOperationException: java/lang/Class->getSimpleName()Ljava/lang/String; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
可以看到这次是找不到getSimpleName
sign方法先在com/izuiyou/common/base/BaseApplication中调用getAppContext方法,获取了Context,再用getClass方法获取他的类,现在需要查看类名,前面两步其实在为这一步服务,获取类名才是他的目的,那么类名是什么呢,可以用objection的插件wallbreaker来查看
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 (frida) C:\Users\n1ng>objection -g cn.xiaochuankeji.tieba explore -P ~/.objection/plugins/WallBreaker Using USB device `MI 9` Agent injected and responds ok! Loaded plugin: wallbreaker _ _ _ _ ___| |_|_|___ ___| |_|_|___ ___ | . | . | | -_| _| _| | . | | |___|___| |___|___|_| |_|___|_|_| |___|(object)inject(ion) v1.11.0 Runtime Mobile Exploration by: @leonjza from @sensepost [tab] for command suggestions cn.xiaochuankeji.tieba on (Xiaomi: 11) [usb] # plugin wallbreaker classdump com.izuiyou.common.base.BaseApplication package com.izuiyou.common.base class BaseApplication { /* static fields */ static ChangeQuickRedirect changeQuickRedirect; => null static ax5 globalLifecycleCallbacks; => [0x3e5a]: ax5@7dd9267 static Application ___APPLICATION; => [0x3e3a]: cn.xiaochuankeji.tieba.AppController@94e6829 /* instance fields */ /* constructor methods */ BaseApplication(); /* static methods */ static Context getAppContext(); static Application __getApplication(); /* instance methods */ void attachBaseContext(Context); void onCreate(); }
可以看到完整的类名是cn.xiaochuankeji.AppController,继续重写callObjectMethodV
1 2 3 4 5 6 7 8 9 10 11 12 public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList){ switch (signature){ case "android/content/Context->getClass()Ljava/lang/Class;" :{ return dvmObject.getObjectType(); } case "java/lang/Class->getSimpleName()Ljava/lang/String;" :{ return new StringObject (vm, "AppController" ); } } return super .callObjectMethodV(vm, dvmObject, signature, vaList); }
再次报错
1 2 java.lang.UnsupportedOperationException: android/content/Context->getFilesDir()Ljava/io/File; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
这里是在获取类的路径,继续重写callOBjectMethodV
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList){ switch (signature){ case "android/content/Context->getClass()Ljava/lang/Class;" :{ return dvmObject.getObjectType(); } case "java/lang/Class->getSimpleName()Ljava/lang/String;" :{ return new StringObject (vm, "AppController" ); } case "android/content/Context->getFilesDir()Ljava/io/File;" : case "java/lang/String->getAbsolutePath()Ljava/lang/String;" :{ return new StringObject (vm, "/data/user/0/cn.xiaochuankeji.tieba/files" ); } } return super .callObjectMethodV(vm, dvmObject, signature, vaList); }
1 2 3 java.lang.UnsupportedOperationException: android/os/Debug->isDebuggerConnected()Z at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticBooleanMethodV(AbstractJni.java:154)
检测是否有调试,重写callStaticBooleanMethodV,
1 2 3 4 5 6 7 8 public boolean callStaticBooleanMethodV (BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) { switch (signature){ case "android/os/Debug->isDebuggerConnected()Z" :{ return false ; } } throw new UnsupportedOperationException (signature); }
1 2 3 4 //报错 java.lang.UnsupportedOperationException: android/os/Process->myPid()I at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticIntMethodV(AbstractJni.java:174)
获取pid,可以用unidbg自带的api重写callStaticIntMethodV
1 2 3 4 5 6 7 8 public int callStaticIntMethodV (BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) { switch (signature){ case "android/os/Process->myPid()I" :{ return emulator.getPid(); } } throw new UnsupportedOperationException (signature); }
再次运行
1 2 3 [11:10:45 334] INFO [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:1898) - openat dirfd=-100, pathname=/proc/11724/status, oflags=0x20000, mode=0 JNIEnv->NewStringUTF("v2-c4a2c9fbbcad24427a76f999fe406f61") was called from RX@0x4004a373[libnet_crypto.so]0x4a373 v2-c4a2c9fbbcad24427a76f999fe406f61
现在得到的sign值和前面fridahook的值一致
算法分析 上面提到这个sign的值可能是hash算法得到的,使用ida的findhash找一下看
1 2 3 4 生成对应的hook脚本如下: frida -UF -l E:\Learn\Unidbg\最右app\unidbg\right\libnet_crypto_findhash_1717654864.js *********************************************************************************** 花费 375.0495550632477 秒,因为会对全部函数反编译,所以比较耗时间哈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 //frida注入生成的hook脚本 (frida) C:\Users\n1ng>frida -UF -l E:\Learn\Unidbg\最右app\unidbg\right\libnet_crypto_findhash_1717654864.js ____ / _ | Frida 16.1.4 - A world-class dynamic instrumentation toolkit | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit/quit -> Exit . . . . . . . . More info at https://frida.re/docs/home/ . . . . . . . . Connected to MI 9 (id=21ce24db) [MI 9::最右 ]-> 函数sub_65540疑似哈希函数,包含初始化魔数的代码。 0xb95285fd libnet_crypto.so!0x655fd 0xb950d2db libnet_crypto.so!0x4a2db 0xbcfbd1cd base.odex!0x1381cd
ida查看sub_65540
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int __fastcall sub_65540 (_BYTE *a1, int a2, int *a3) { char v5[24 ]; int v6[25 ]; v6[0 ] = 1733632769 ; v6[1 ] = -305288311 ; v6[2 ] = -1732583682 ; v6[3 ] = 372397174 ; v6[4 ] = 0 ; v6[5 ] = 0 ; sub_4B368((int )v5, a1, a2); sub_63394((int )v6, (int )v5); sub_4ABB8((int )v5); sub_6533A(a3, v6); return _stack_chk_guard - v6[22 ]; }
使用HookZz框架hook该方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public void hook65540 () { IHookZz hookZz = HookZz.getInstance(emulator); hookZz.wrap(module .base + 0x65540 + 1 , new WrapCallback <RegisterContext>() { @Override public void preCall (Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) { System.out.println(ctx.getLongArg(0 )); System.out.println(ctx.getLongArg(1 )); System.out.println(ctx.getLongArg(2 )); } public void postCall (Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) { } }); } JNIEnv->GetArrayLength([B@0x6e316e67 => 4 ) was called from RX@0x400671e1 [libnet_crypto.so]0x671e1 3221223148 4 3221223020
三个参数,第一个和第三个很可能是一个地址,第一个可能就是加密前的sign,很多加密都习惯将结果放在参数中,因此猜测第三个参数是加密后的sign
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void hook65540 () { IHookZz hookZz = HookZz.getInstance(emulator); hookZz.wrap(module .base + 0x65540 + 1 , new WrapCallback <HookZzArm32RegisterContext>() { @Override public void preCall (Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { System.out.println(ctx.getLongArg(0 )); System.out.println(ctx.getLongArg(1 )); System.out.println(ctx.getLongArg(2 )); Inspector.inspect(ctx.getPointerArg(0 ).getByteArray(0 ,0x10 ),"Arg1" ); Inspector.inspect(ctx.getPointerArg(2 ).getByteArray(0 ,0x10 ),"Arg3" ); } public void postCall (Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { } }); }
1 2 3 4 5 6 7 8 9 10 11 12 //result [20:42:57 481]Arg1, md5=bfffab558dd8ec67b6327ed6e40e9929, hex=6e316e6700f7ffbf0000000000000000 size: 16 0000: 6E 31 6E 67 00 F7 FF BF 00 00 00 00 00 00 00 00 n1ng............ ^-----------------------------------------------------------------------------^ >-----------------------------------------------------------------------------< [20:42:57 482]Arg3, md5=d525d6782a32c113f3c398d91c7afbcf, hex=ecf6ffbf00000000745e175788f6ffbf size: 16 0000: EC F6 FF BF 00 00 00 00 74 5E 17 57 88 F6 FF BF ........t^.W.... ^-----------------------------------------------------------------------------^
可以看到第一个参数是我们输入的字符,长度刚刚好等于第二个参数,猜测第二个就是长度
现在hook一下方法结束后的retval
使用push和pop来获取执行后的arg(类似frida中的this.xxx=args[x]的方法来保存)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 >-----------------------------------------------------------------------------< [21:05:48 136]Arg1, md5=bfffab558dd8ec67b6327ed6e40e9929, hex=6e316e6700f7ffbf0000000000000000 size: 16 0000: 6E 31 6E 67 00 F7 FF BF 00 00 00 00 00 00 00 00 n1ng............ ^-----------------------------------------------------------------------------^ >-----------------------------------------------------------------------------< [21:05:48 137]Arg3, md5=d525d6782a32c113f3c398d91c7afbcf, hex=ecf6ffbf00000000745e175788f6ffbf size: 16 0000: EC F6 FF BF 00 00 00 00 74 5E 17 57 88 F6 FF BF ........t^.W.... ^-----------------------------------------------------------------------------^ >-----------------------------------------------------------------------------< [21:05:48 140]retval, md5=a2e9ffee40f1d90a756834b1137fde3e, hex=c4a2c9fbbcad24427a76f999fe406f61 size: 16 0000: C4 A2 C9 FB BC AD 24 42 7A 76 F9 99 FE 40 6F 61 ......$Bzv...@oa ^-----------------------------------------------------------------------------^
可以看到方法结束后的第三个参数和一开始fridahook得到的结果一致,猜测正确,第一个是输入,第二个是长度,第三个是结果
分析函数
1 2 3 4 v6[0] = 0x67552301; v6[1] = 0xEDCDAB89; v6[2] = 0x98BADEFE; v6[3] = 0x16325476;
发现四个值,猜测是md5的四个iv,但是并不是标准的,我们的输入md5加密后也和输出不同,可能存在魔改,修改一下iv加密一下看
1 2 plainText: b'n1ng' result: c4a2c9fbbcad24427a76f999fe406f61
和hook到的值相同,加密算法就是修改iv的md5
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 3049155267@qq.com