某软件ollvm混淆登录参数分析
这次案例使用的是最新某马拉雅直接豌豆荚即可抓包 首先就是进行抓包了下面是抓到的数据包123456789101112POST/mobile/login/pwd/v3 HTTP/2host: passportws.ximalaya.comcookie:1_deviceandroid386501be-0e5c-3773-8b4b-d2f40c257a9a9.4.52;channeland-d3;implcom.ximalaya.ting.android;osversion33;fp009317657x2222q22264v0500q00000220020000000000012000000000000;device_modelPixel7;XIM;c-oper%E6%9C%AA%E7%9F%A5;net-modeWIFI;res1080%2C2219;AIDMWFiYjhmN2U4OGM4MWY1Mg;manufacturerGoogle;XDSWVzJyp9dAqYNR2NV/TpWj32w93g1JQRPZp94Qh4YkOIDrTzHvhcg7DkwZddPJEEo8SpjypYaAptOHvZ34ujAtl7b57bnLDLTGMOQXm6dRZdGAIpQYNJaXMKDZ1dOR;umid4051f75d0861f70a9723a837b71ae290ia;xm_grade0;specialModeStatus0;cookie2: $version1accept:*/*user-agent: ting_9.4.52(Pixel7,Android33)x-tk: TACaa2BczhlAb4OXDdzi0vS9AwlepoCuKX6lSN7MhFEYXGnikMYJO-ycVZWJ81G8Ka6U0hO-anu4NP90d1yExsZuYTs68ljb20ueGltYWxheWEudGluZy5hbmRyb2lkITEuMy4yNyE5LjQuNTIuMyFiPXBhc3Nwb3J0JnM9bG9naW4mdT0wcontent-type: application/json; charsetutf-8content-length:1338accept-encoding: gzip{password:Tv97ethyYCbW1cu28wKT6LoYasOlozs1bW6XkMsdesPTbWrcUxP0OzDFsBK56J9V6NQCKCoxEpa\nsjbAd6P7rdGrUMVFQ9VFuJhucxTbZn3FtCT4DdxW0okyw1/7CHxVVo6SESazZqCvdYX6JduejQzg\nsHSx/uilKXMvMymhTmY\u003d\n,fdsOtp:{\captcha_id\:\3723312ce42a04b5c0b40e605a882037\,\lot_number\:\f4cc6449828648b4a7482fcb187e0fe7\,\pass_token\:\29e78908983de432362042dfa8709d798254c6a5788b046b598bb849a162bab3\,\gen_time\:\1772978512\,\captcha_output\:\u9reFROUhQ6l5CNk-Y40DVpFm0A5jhpxZAMu8kiKIQFHmCtbVGvr_4a-HewecbuH_vgp4hEw_THRvGykOEhBHfIDOK2mngR_rxYH5LSZFntK0h-JrAso3IZc_a06v3Roo7cw7GARksy9__4dkEt5yro5gqzZbcZBGD1ByeVUK2qxlCiTZXqacspeuZTwmJgbSGLUZt8tnLAcnutND4K1rlpmk6D4NkEqjwyGz_3O_Sosrapf9KCpJiT76IJPprj0sdbRpmHzaV3dbltgA7WmUSqU-GmZly9HWALit1_qljzgrHq6TGb6C60noDZJ82qq5Ainym8HU7Ug4CTO9AO51GliCpeyVckKGNxA0EKZAi9LpVUIW5HHZDUs8-dG87PP43h4RbsyfL3fNYGFh5eHgLNFgg7SKEY9iNhCghPU7PJwWJ6QvwpqDOEqRHZK_yMj_KS54z8drWeu38v5Zr-K0Os3ABsdHcOCKad7JrKl08bl6NsWAWIj5J8ek9I7SbnsF-OBspi0BHAdJLO2snYWJYfTp7CuHu95XRIHzfyZaOI\u003d\},signature:90cf350f2fd44f466325a7210cc34fa7553dc60d,nonce:0-8D0B9F3600C47d912f9545b45cf0ef55d34f40b311136eb6adf183382af41e,account:PJlxL0CidJteJXvLypUoVliyTsh1RjQQ7DBuTsmqBKUg4qcpMf85TSUjZYT1ChwVzjyBDkHcJ\ngBM0gSRznapMGVfAx3Q4ewy8GuvzdWKdfO8h30HZD4lr92cp6tcPGY9pER8VHdrKWVW8iyR9gbs\n4YINBi3EWDB//KWb4qI\u003d\n} 接下来需要定位到登录函数调用的位置可以通过关键字段的搜索、或者是hook函数打印调用堆栈。这里我直接hook了HashMap来打印堆栈找到登录点123456789101112131415Java.perform(function(){console.log([*] Script loaded. Waiting for signature to be put into a Map or JSONObject...);varlog Java.use(android.util.Log);varHashMap Java.use(java.util.HashMap);HashMap.put.implementation function(key, value){if(key !null){varkeyStr key.toString();if(keyStr signature){console.log([] 找到拼接 signature, 即将打印调用堆栈....);console.log(log.getStackTraceString(Java.use(java.lang.Throwable).$new()) \r\n);}}returnthis.put(key,value);}})定位请求体组装位置1234567891011121314151617181920[Pixel 7::喜马拉雅 ]- [] 找到拼接 signature, 即将打印调用堆栈....java.lang.Throwableat java.util.HashMap.put(Native Method)at com.ximalaya.ting.android.loginservice.LoginRequest$4$1.b(Unknown Source:386)at com.ximalaya.ting.android.loginservice.LoginRequest$4$1.a(Unknown Source:365)at com.ximalaya.ting.android.loginservice.d.b$2.onSuccess(Unknown Source:73)at com.geetest.captcha.q.run(Unknown Source:1)at android.os.Handler.handleCallback(Handler.java:942)at android.os.Handler.dispatchMessage(Handler.java:99)at android.os.Looper.loopOnce(Looper.java:201)at android.os.Looper.loop(Looper.java:288)at android.os.XimaCrashHandler$1.run(Unknown Source:232)at android.os.Handler.handleCallback(Handler.java:942)at android.os.Handler.dispatchMessage(Handler.java:99)at android.os.Looper.loopOnce(Looper.java:201)at android.os.Looper.loop(Looper.java:288)at android.app.ActivityThread.main(ActivityThread.java:7898)at java.lang.reflect.Method.invoke(Native Method)at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)这里重定义了两个a函数除了从调用链上判断使用的是第一个a函数外还可以通过frida的overload来判断使用的是哪个函数。这里就是signature进行拼接的位置需要通过LoginRequest.b进行跟进最后发现调用的是LoginEncryptUtil的a当中的WTWEctUfLf函数。 通过函数声明可以看到这是个native函数属于liblogin_encrypt.so这个库接下来就需要对这个库当中的这个native函数进行分析了。so层分析进入so当中的WTWEctUfLf通过CFG可以查看到这个函数是被控制流平坦化了。Unidbg模拟执行 这里想试试用Unidbg模拟执行一下看看能不能找到一些线索这里传入的参数需要注意一下通过jadx的反编译可以看到传入了3个参数分别是上下文context布尔值z以及拼接后的参数sb.toString。这里已经知道了参数一和参数三的形式了但是参数二是true还是false依旧不清楚。那么这里可以直接对这个a方法进行hook看看传入的是什么hook脚本如下1234567Java.perform(function(){varLoginEncryptUtil Java.use(com.ximalaya.ting.android.loginservice.LoginEncryptUtil);LoginEncryptUtil.a.overload(android.content.Context,boolean,java.util.Map).implementation function(ctx,z ,map){console.log( [param2] z : z \r\n);returnthis.a(ctx,z,map);}});最后打印的是false1[param2] z :false那么此时就知道了参数二。此外为了确保模拟执行WTWEctUfLf的逻辑没有错误这里将传入的sb.toString()以及返回值都hook出来用作模拟执行的校验使用Frida框架进行hook12345678910111213141516171819202122232425262728293031varWTWEctUfLf_offset 0x39BC;functionhook_login_enc(){varliblogin_encrypt Process.findModuleByName(liblogin_encrypt.so);console.log([] 找到liblogin_encrypt的基址: liblogin_encrypt.base);varWTWEctUfLf_addr liblogin_encrypt.base.add(WTWEctUfLf_offset);console.log([] 目标函数地址: WTWEctUfLf_addr);Interceptor.attach(WTWEctUfLf_addr,{onEnter:function(args){varjstringArg args[4];console.log(args_4 : args[4]);if(!jstringArg.isNull()){varjniEnv Java.vm.getEnv();varcString jniEnv.getStringUtfChars(jstringArg,null);varstrValue cString.readUtf8String();console.log([] 传入的明文 (sb.toString()):\n\n strValue \n);jniEnv.releaseStringUtfChars(jstringArg, cString);}},onLeave:function(retval) {if(!retval.isNull()) {varjniEnv Java.vm.getEnv();varcString jniEnv.getStringUtfChars(retval,null);console.log([] 返回的密文 (signature): cString.readUtf8String());jniEnv.releaseStringUtfChars(retval, cString);}console.log(\n);}}}hook_login_enc(); 最终得到的数据如下12345678910111213[] 传入的明文 (sb.toString()):accountB8vZmyyyzbNpd7bPvNYYYGL8Idvddn85ENtGFFxj5An9vCQjv3yXNcli6I24KyRBje/9bADfYpna4FywwajYUMmu6ef25tTPTwaDI1Jq7I5hBbS3PNi0E079ic8m/KPlD3GG8GXjF1woNcY0UphWEp4LXqBoZv1wa1FE9ZofdsOtp{captcha_id:3723312ce42a04b5c0b40e605a882037,lot_number:6ac21b439ee744cd86b7c462eddc3930,pass_token:4c82b272afc4b59df1057cf66838a7bd3327afdfefe1be2456580fbcb0de869d,gen_time:1772978707,captcha_output:2Z76G2CapbcJxb3z0NRv6IFLTIZJ15mHQYRlVlkywBquuBg9auDb78p_hyS26NaSWJddIYSKQ83ixvuXQfCJWeMiXfURka9GYWuWyS6TkZQEEszgpPITemgwA5BntSqWd54TlHATrcJLgCSPoh81t0rr3Wgz2rJiU4_eTqL7aekJxSCVJ6mADxUzOUMC4q8J_YuyefL7SJlKGx_RXe6PZZYkOt2LDMl-E7qRTMYQlp6r2GCVcGQNJheZovQ1ZzFgzSX_PDFuq3Fm2Ir3y-nI5By57Kar1ohGJv8jGZtE5WEIFV2hoIlC0brWStgKwrTrXmJjnxBh_3S3ak1Xs1Zfaeaov4hPlfGpwW-Lh8OdX6WgAtxb7TsN0XyTpX3j-r8HKz7D71M4UFxSuhgl3OdrCjR5s--JcFybjIy6ANxjR7VlsxWJjyyxJ1x8M331UFD6wksJZjJpKX7LoWgFS48v1_DVxDAWmE4xis6YQYfIumMXS0PicjaxMcSLFyBEreWO4IiYHafv_7GVj5Qns-KihnUBgFLeBsG4b3CvU9-zvzg}nonce0-CCFB9B6F4E55df4ea842b75758d257361290d8f504ec1a1633b85d3010803bpasswordQpHGc4OjScTQRP9PTdYxn7ABdCmwS1JqfsBD00gX5UJ0/CSZ8HtNB/K4GrERFODfqKttLqI6RkoiFn15KquI1pMytM8/D9yvvLbg8FpWr2E2JKoMWrZ/J/f3oH4XFFtBgfzWQXTNZe4dHrdqjjPOcIgrhSHyNDyZvz2zktdDo[] 返回的密文 (signature): 9d54907ba45d54eaed4bed58cd18de8cb30c7c05# 9d54907ba45d54eaed4bed58cd18de8cb30c7c05模拟执行时主动调用就可以直接传入这个明文作为字符串参数并在结束后打印返回值。 这里Unidbg代码篇幅比较长就不放在文章当中了。有需要的话可以到附件进行下载。总的来说就是补完环境之后能够通过JNI日志看到大部分信息了。下面展示一些JNI的日志 首先就是获取包名然后将输入的字符串转成字符指针方便后续调用接着就是newStringUTF通过这个可以发现这里的字符串后面拼接了一串新的东西1MOBILE-V1-PRODUCT-7D74899B338B4F348E2383970CC09991E8E8D8F2BC744EF0BEE94D76D718C089 接着就是使用toUpperCase将参数转成大写。接着就是获取了SHA-1哈希算法的实例接着调用update这里传入的参数就是拼接上特定字符串的新参数最后调用digest进行哈希运算。然后toHexString之后通过StringBuilder.append进行拼接最后拼接的就是明文字符串 最后可以观察到特征串和我们hook到的一模一样。说明这个模拟执行流程正确了。 其实通过这个Unidbg已经几乎把这个signature生成方式给看得差不多了首先就是拼接一串特定字符串接着转大写然后进行SHA-1。从而完成signature参数的生成。这个加密函数貌似并没有自己实现加密逻辑全都是通过JNI来调用java层的函数以及加密标准库的。 那么Unidbg分析完了接下来可以试试IDA来分析这个ollvm静态分析 进入大致浏览了一下好像只有1千多行试着分析一下。进入函数后开始先通过TLS读取了一个值然后就开始获取包名了分析之后得出。来看看这个get_package_name函数是怎么样的处理ollvm中BCF的方法方法一 静态分析可以看到这个函数也被ollvm混淆了一下不过代码量不多这里直接从头分析一下 首先进来就是给v12进行赋值用作下面状态机的一个流程跳转。在ollvm中很多这种永真或者永假的条件此时可以进行化简实在不会的丢给ai化简。这里可以看到实际上执行的是a×(a−)a×(a−1) 那么起始就是偶数乘奇数结果就是偶数最低位为0接着取反此时最低位为1或上0xFFFFFFFE运算得出0xFFFFFFFF那么这个判断条件就为真所以v12 1。 所以这里事实上就只是对x_149的最低位做判断。继续往下看并记录状态12v12 1;v3 0xB5E7D523;方法二 利用IDA的分析引擎 这里点击x_149然后给这个x_149一个值这里我使用的是IDA Python来进行赋值接着将.bss段的可写权限(W)关掉这样就能够利用IDA引擎自动将这些虚假控制流去掉了。x_150处理手法也差不多效果如下接着回到函数继续分析。另外两个先不管如果用到了再进行分析。 接着进入while(1)循环由于此时的v3 0xB5E7D523不满足while和if的条件直接走到了if-else这个位置经过化简之后这里的条件判断其实就是if(v12 || v13)。此时的v12 1所以这里v3被赋值成0xEA4F3F0B接着就重新进行while循环此时v3满足了while ( v3 (int)0xCF812BC6 )则进入循环此时会进入到这个else分支 这里通过调用JNI的函数来获取context接着获取pkgname如果获取成功的话v15 0。这里调用函数用到的字符串都是加密的这里先分析完整个大体流程之后再讲如何解决这个问题。 执行完成这个else分支之后v3 0xB19E5A69接着就会因为不满足循环条件而跳出循环。接着就是这里的if继续break接着给v3继续赋值:接着到下一个while循环先走else块然后v3 -8285439接着就是if块通过CallObjectMethod调用getPackageName获取pkgName存放到v16。然后v3被设置成1958226928接着往下给将v16也就是pkgName赋值给v11然后调整状态码进入下一轮循环后直接就从if ( v3 ! 1958226928 )处返回pkgName了。所以这个函数其实原本的逻辑就是获取pkgName后返回去掉混淆之后就是这样的12345678910jstring sub_154C(JNIEnv *env, jobject ctx){jclass cls (*env)-FindClass(env,android/content/ContextWrapper);jmethodID mid (*env)-GetMethodID(env, cls,getPackageName,()Ljava/lang/String;);(*env)-DeleteLocalRef(env, cls);if(mid NULL) {return(*env)-NewStringUTF(env,);}return(jstring)(*env)-CallObjectMethod(env, ctx, mid);} 接下来就来解决一下字符串加密的问题解决字符串加密的方式方法一 分析解密函数 可以看到做的操作就是获取JNI的方法ID方便后续调用。可以看到这里的本该传入的字符串好像都是被加密的点击xmmword_2C010进入看看情况很明显确实是被加密了那么要怎么还原回原本的字符串呢这里可以使用交叉引用发现只有这里是进行了写操作的直接跳转到这个位置接着就是一步步往上跟踪最后定位到这个位置这里其实就是一个简单的异或加密但是数据量太多这个就交给AI来帮忙了xmmword_2C010这里的字符串并不只是这个地方的还延伸到了0x2C02E这个位置遇到了ALIGN才算结束 按照上面的方法定位解密的位置最后能够写出下面的脚本12345678910datasets[[15,0,10,28,1,7,10,65,13,1,0,26,11,0,26,65,0x2D,0x01,0x00,0x1A,0x0B,0x16,0x1A,0x39,0x1C,0x0F,0x1E,0x1E,0x0B,0x1C,0x6E],]keys[0x6E]fori, (data, key)inenumerate(zip(datasets, keys)):outbytes(b ^ keyforbindata)print(f[{i}] hex:, out.hex())print( bytes:, out) 这里用到的加密字符串基本上都能按照这个方法进行解密。方法二 dump文件 使用上面的方法来获取解密函数效率会非常慢而且写注释看的不舒服。通过刚才的分析可以发现这些字符串解密之后都是存放到原来的位置上的那么我们如果可以直接在这些位置上修改成明文的话反编译当中也可以看到明文字符串了。爽歪歪~~ 那么来分析一下这个解密函数在什么时候会被调用以此来确定dump点:通过交叉引用可以看到是在init_array当中被调用的而这个so会在进行了一次登录之后加载。这样就很方便了直接先登录一次让so加载加载完成后字符串解密完成。接着就可以直接dump了连dump点都不需要找这里使用Frida进行dump123456789101112131415161718192021222324252627282930313233varTARGET_SO liblogin_encrypt.so;varAPP_PACKAGE_NAME com.ximalaya.ting.android;functiondump_login_enc_lib(moduleName) {varmodule Process.getModuleByName(moduleName);if(module null) {console.log([-] 找不到模块: moduleName);return;}console.log([] 找到模块: module.name);console.log([] 基址: module.base);console.log([] 大小: module.size);vardumpPath /data/data/ APP_PACKAGE_NAME /files/ module.name _dump.so;try{varfile newFile(dumpPath,wb);if(file) {// 读取整个模块的内存Memory.protect(module.base, module.size,rwx);varmemoryBuffer module.base.readByteArray(module.size);file.write(memoryBuffer);file.flush();file.close();console.log([*] Dump 成功文件已保存至: dumpPath);console.log([*] 请使用命令将文件 pull 到电脑: adb pull dumpPath);}}catch(e) {console.log([-] Dump 失败: e);}}dump_login_enc_lib(TARGET_SO); dump下来的so使用soFixed来修复一下由于在加载到内存之后got表会被污染这里将对应解密的字符串数据移植到原来的so上便于分析。移植的过程中要注意soFixed修复的so节的文件偏移和内存地址是一样的但是原来的so可能有区别需要注意。这里写个python脚本跑一下再拿来分析123456789101112131415161718192021222324252627282930deffixSo():original_so_pathliblogin_encrypt.so# 原始加密的SOdumped_so_pathliblogin_encrypt_fixed.so# Frida Dump出来的明文SOoutput_so_pathliblogin_encrypt_decrypted.so# 最终缝合产物data_vaddr_in_dump0x2c000data_offset_in_orig0x1c000data_size0x2d316-0x2c000print(fdata size : {data_size})print([*] 开始执行底层指令缝合手术...)# 读取 Dump 文件中的明文代码withopen(dumped_so_path,rb) as f_dump:f_dump.seek(data_vaddr_in_dump)decrypted_opcodesf_dump.read(data_size)# 读取 原 SO 并进行替换withopen(original_so_path,rb) as f_orig:orig_databytearray(f_orig.read())# 将明文代码直接覆盖到原 SO 的对应物理位置orig_data[data_offset_in_orig : data_offset_in_origdata_size]decrypted_opcodeswithopen(output_so_path,wb) as f_out:f_out.write(orig_data)print(f[] 缝合成功产物已保存至: {output_so_path})if__name____main__:fixSo()这样看起来就非常舒服了。ollvm分析 接下来继续进行ollvm的分析。这里一开始一步步分析ollvm也是通过上面get_package_name的方法进行分析。发现太牢了然后试着用交叉引用逃课。首先这里会获取包名正常来说这个包名一般来说就是app的包名 接着对这个pkg_nname进行交叉引用并搜索app的包名:发现了然后跳转过去这里就是校验获取的包名和app包名是否一致然后设置op状态码。然后继续跟踪大致三四个状态码切换之后就会来到这个位置可以看到这两个字符串就是我们Unidbg模拟的时候发现拼接到参数当中的特定字符串。接下来可以继续分析状态机这里我就直接交叉引用看调用了。首先对v160进行状态引用 可以发现除了一堆字符指针的赋值就是将v160赋值给了v193。那么此时就到这个地方看看有没有什么特别的。并没有发现什么特别之处这里再对193进行交叉引用发现有用到了memcpy这种东西经过简单分析发现了几个这种结构都是开辟空间然后进行参数拼接。这里猜测就是参数进行拼接的地方。如果需要更加准确的知道到底是调用了哪一个的话需要跟着状态机分析。最后这里会通过newStringUTF将拼接后的参数转成jstring。接着就是继续对v175进行交叉引用接着就是这个位置转大写然后调用MessageDigest.digest接着继续引用最后就是这个位置也就是进行SHA-1哈希然后toHexString。 至此整个signature就差不多完成了。到这里整个Signature生成应该是比较明确的了大致过程就是将登录参数按固定格式拼接后追加固定字符串MOBILE-V1-PRODUCT-7D74899B338B4F348E2383970CC09991E8E8D8F2BC744EF0BEE94D76D718C089转大写再做SHA-1并转十六进制输出。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2410474.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!