-
-
[原创]对某某漫画的算法还原测试
-
发表于: 2天前 451
-
前言:文章仅供学习 严禁用于非法用途
关键点:360脱壳修复 字节系列抓包 dump so修复 idapython使用 MD5 unidbg Frida
查壳修复
MT查壳可以看到是360加固,直接使用在线网站可以进行脱壳,也可以找到360加固的脱壳点进行手动dump,都可以,只是这篇文章更偏向于算法还原,所以直接使用在线网站进行脱壳了
97eK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1j5#2y4W2)9J5k6h3q4D9i4K6u0r3
可以得到脱下来的dex文件

放到MT里面进行修复就行,MT没有会员也可以使用NP进行修复
然后将这些dex全部放到原始的APK里面,把原来dex的删掉就行,这样只是可以让我们把他放大jadx进行分析了,因为他还有一个oncreate被抽取了,以及许多stub的特征没有删除,这里要具体往下分析的话,大家使用Android Studio进行分析就行,看他的log日志哪里报错了,不过会有许多,建议大家找到一处,然后直接进行MT正则匹配全部替换为null,有的地方也许不能为null,尽量看就行,这里不再具体说明
抓包分析:
抓包配置:
我采用的方式是Reqable转发Burpsuit的方式,具体就是


然后在bp里面开一个端口转发监听

这样就可以抓到了,还是蛮好用的
抓包检测:
这个APP是没有检测的,只不过我最开始没有抓到,最后还是重启手机抓到的,也可以具体分析,因为我之前在抓了几个字节系列的包,也是不行,看了一些网上的教程手动过了一下,这里大家可以参考一下
字节就是会魔改这个libsscronet.so,我是在内存种dump的,因为他的lib里面没有,那么其实大概率也是没有魔改,这里只是给大家分享一下

dump_so的脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | function dump_so(so_name) { Java.perform(function () { let currentApplication = Java.use('android.app.ActivityThread').currentApplication() let dir = currentApplication.getApplicationContext().getFilesDir().getPath() let libso = Process.getModuleByName(so_name) console.log('[name]:', libso.name) console.log('[base]:', libso.base) console.log('[size]:', ptr(libso.size)) console.log('[path]:', libso.path) let file_path = dir + '/' + libso.name + '_' + libso.base + '_' + ptr(libso.size) + '.so' let file_handle = new File(file_path, 'wb') if (file_handle && file_handle != null) { Memory.protect(ptr(libso.base), libso.size, 'rwx') let libso_buffer = ptr(libso.base).readByteArray(libso.size) file_handle.write(libso_buffer) file_handle.flush() file_handle.close() console.log('[dump]:', file_path) } })} |
有两种把,第二种就是遍历内存了
大家搜索一下这个函数SSL_CTX_set_custom_verify()

交叉引用一下

其实它是有参数的,只不过没有显示出来,大家可以网上搜索一下,找到他的回调,也可以看汇编找

也就是这一个函数,大家写一个frida脚本,把他返回值写成0x0就行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | function ssl_pass2() { var offest = 0x301184; var soName = 'libsscronet.so'; console.log("==") const module = Process.findModuleByName(soName) Interceptor.attach(module.base.add(offest), { onEnter: function (args) { }, onLeave: function (retval) { retval.replace(0x0) console.log("返回值", retval) return retval } })} |
这样就可以过掉了,一般会有两个地方出现这个函数,大家可以都测试一下,有的是第一个交叉引用,有的是第二个交叉引用出现
Java请求位置1分析:
因为我也是第一次进行这样的抓包分析,我就把能抓到的包的值全部进行加密解密了
先看这个吧,这两个参数先还原了一下:
Java层定位

搜索关键词就行,可以定位到基本的


基本就是这个,只不过就是对请求类型进行判断了一下罢了

Java层其实就是这些,一个jmd,调用了So层的函数,不过so层函数有点不同寻常
S层md函数分析


这个其实隐藏起来了,看汇编找到这个注册函数表


继续看
我其实没看到有什么像加密的地方,不过这里是重点

加载了一些文件什么的,我这里没分析清楚,不过我用其他方法,主动调用这个函数,然后进行测试
1 2 3 4 5 | let JNISecurity = Java.use("com.kanman.JNISecurity");var str1 = "1"var str2 = "2"var ret1 = JNISecurity.jmd(str1, str2)console.log("jmd加密:", ret1) |

得到这个,md大家可以用cmd解一下发现可以解密出来,但是我没次数,只能用别的方法,就是算法自吐脚本
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 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | function hook_tess() { Java.perform(function () { //Base64 var base64 = Java.use('android.util.Base64'); var string = Java.use('java.lang.String'); /*base64.encode.overload('[B', 'int', 'int', 'int').implementation = function(){ send("=================base64 encode===================="); send(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new())); send(arguments[0]); send(arguments[1]); send(arguments[2]); send(arguments[3]); var data=this.encode(arguments[0],arguments[1],arguments[2],arguments[3]) send("base64:"+string.$new(data)); return data; }*/ /*base64.decode.overload('[B', 'int', 'int', 'int').implementation = function(){ send("=================base64 decode===================="); send(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new())); send(arguments[0]); send(arguments[1]); send(arguments[2]); send(arguments[3]); var data=this.decode(arguments[0],arguments[1],arguments[2],arguments[3]) send("base64:"+string.$new(data)); return data; }*/ // MD SHA var messageDigest = Java.use('java.security.MessageDigest'); // update for (var i = 0; i < messageDigest.update.overloads.length; i++) { messageDigest.update.overloads[i].implementation = function () { var name = this.getAlgorithm() send("=================" + name + "===================="); send(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new())); if (arguments.length == 1) { send(arguments[0]); this.update(arguments[0]); } else if (arguments.length == 3) { send(arguments[0]); send(arguments[1]); send(arguments[2]); this.update(arguments[0], arguments[1], arguments[2]); } } } // digest for (var i = 0; i < messageDigest.digest.overloads.length; i++) { messageDigest.digest.overloads[i].implementation = function () { var name = this.getAlgorithm() send("=================" + name + "===================="); send(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new())); if (arguments.length == 0) { var data = this.digest(); send(data); return data; } else if (arguments.length == 1) { send(arguments[0]); var data = this.digest(arguments[0]); send(data); return data; } else if (arguments.length == 3) { send(arguments[0]); send(arguments[1]); send(arguments[2]); var data = this.digest(arguments[0], arguments[1], arguments[2]); send(data); return data; } } } //MAC // var mac = Java.use('javax.crypto.Mac'); // for (var i = 0; i < mac.doFinal.overloads.length; i++) { // mac.doFinal.overloads[i].implementation = function () { // var name = this.getAlgorithm() // send("=================" + name + "===================="); // send(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new())); // if (arguments.length == 0) { // var data = this.doFinal(); // send(data); // return data; // } else if (arguments.length == 1) { // send(arguments[0]); // var data = this.doFinal(arguments[0]); // send(data); // return data; // } else if (arguments.length == 2) { // send(arguments[0]); // send(arguments[1]); // var data = this.doFinal(arguments[0], arguments[1]); // send(data); // return data; // } // } // } // DES DESede AES PBE RSA var cipher = Java.use('javax.crypto.Cipher'); // for (var i = 0; i < cipher.doFinal.overloads.length; i++) { // cipher.doFinal.overloads[i].implementation = function () { // var name = this.getAlgorithm() // send("=================" + name + "===================="); // send(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new())); // if (arguments.length == 0) { // var data = this.doFinal(); // send(data); // return data; // } else if (arguments.length == 1) { // send(arguments[0]); // var data = this.doFinal(arguments[0]); // send(data); // return data; // } else if (arguments.length == 2) { // send(arguments[0]); // send(arguments[1]); // var data = this.doFinal(arguments[0], arguments[1]); // send(data); // return data; // } else if (arguments.length == 3) { // send(arguments[0]); // send(arguments[1]); // send(arguments[2]); // var data = this.doFinal(arguments[0], arguments[1], arguments[2]); // send(data); // return data; // } else if (arguments.length == 5) { // send(arguments[0]); // send(arguments[1]); // send(arguments[2]); // send(arguments[3]); // send(arguments[4]); // var data = this.doFinal(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4]); // send(data); // return data; // } else { // send(arguments[0]); // send(arguments[1]); // send(arguments[2]); // send(arguments[3]); // var data = this.doFinal(arguments[0], arguments[1], arguments[2], arguments[3]); // send(data); // return data; // } // } // } // //KEY // var secretKey = Java.use('javax.crypto.spec.SecretKeySpec'); // for (var i = 0; i < secretKey.$init.overloads.length; i++) { // secretKey.$init.overloads[i].implementation = function () { // var name = this.getAlgorithm() // send("=================KEY===================="); // //send(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new())); // if (arguments.length == 2) { // send(arguments[0]); // send(arguments[1]); // this.$init(arguments[0], arguments[1]); // } else if (arguments.length == 4) { // send(arguments[0]); // send(arguments[1]); // send(arguments[2]); // send(arguments[3]); // this.$init(arguments[0], arguments[1], arguments[2], arguments[3]); // } // } // } //IV //DES KEY //DESede KEY //PBE KEY salt });} |
可以根据需要来使用

可以发现有一个

这不就是我们的输入+一个字符串吗,这个字符串其实也在so层,大家搜索一下也可以看到,而且交叉引用也能定到md哪个函数里面,

这里就对上了
大家也可以写一个python代码来实现
Java请求位置2分析:
m-request-did参数的分析
也是根据这个jadx搜索

获取设备码加密

可以定位到这里
分析就完事了

这样的

还是蛮清楚的
分析还原代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | let Utils = Java.use("com.kanman.allfree.ext.utils.Utils"); Utils["getDeviceId"].implementation = function () { console.log('getDeviceId is called'); let ret = this.getDeviceId(); console.log('getDeviceId ret value is ' + ret); return ret; };let InfoUtils = Java.use("com.kanman.allfree.utils.InfoUtils"); InfoUtils["getDeviceId"].implementation = function () { console.log('getDeviceId is called'); let ret = this.getDeviceId(); console.log('getDeviceId ret value is ' + ret); return ret;}; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | from Cryptodome.Cipher import AESimport base64from Cryptodome.Util.Padding import pad, unpaddef encryaes(data,key): cipher=AES.new(key.encode('utf-8'),AES.MODE_ECB) # padded = cipher.pad(data,AES.block_size) encrypted=cipher.encrypt(data.encode('utf-8')) return base64.b64encode(encrypted).decode('utf-8')def decryaes(base_enc,key): enc=base64.b64decode(base_enc) cipher=AES.new(key.encode('utf-8'),AES.MODE_ECB) dec=cipher.decrypt(enc) return dec.decode('utf-8')s='S5aTaQe22BdwsExgr1ftHPPGb9Sxq69Pue/WdH1HcHI='key='xujikmlioksjoped'inp="4efa5850dab58086"print(decryaes(s,key))print(encryaes(inp,key)) |
就可以得到了
Java层请求位置3分析:
下面这个处理起来还是蛮好玩的
Java层定位

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 | function hook_sig1() { Java.perform(function () { let Orange = Java.use("com.yxcorp.kuaishou.addfp.android.Orange"); let KWEGIDDFP = Java.use("com.yxcorp.kuaishou.addfp.KWEGIDDFP"); KWEGIDDFP["doSign"].implementation = function (context, str) { console.log('doSign is called' + ', ' + 'context: ' + context + ', ' + 'str: ' + str); let ret = this.doSign(context, str); console.log('doSign ret value is ' + ret); return ret; }; //获取活动的 Context var current_application = Java.use('android.app.ActivityThread').currentApplication(); var context = current_application.getApplicationContext(); // 构造 byte[] 数组 let bArr = Java.array('byte', [0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38]); // 可以根据需要构造适当的 byte[] 数组 let i = 4; // 整数参数 // 使用 overload 来调用 getClock 函数 let result = Orange.getClock(context, bArr, i); console.log("[*] 参数:", "bArr", bArr, "i:", i) // 打印返回值 console.log('[*] 返回值: ' + result); console.log("=====") })}hook_sig1() |

这里继续搜索就行

可以找到这个函数

一层层分析下去,可以看到这个so,以及so的代码
So层代码分析:
他就是静态注册的,但是有许多花指令


存在许多的垃圾代码,死代码,这时候咱们就得nop修复了,这个花指令也挺简单,就是插入了一段无用代码,永远不会执行,影响静态看
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 | import ida_bytesimport idcimport idaapidef find_and_patch(start,end): pattern=['STP', 'STP', 'ADR', 'SUBS', 'MOV', 'ADDS', 'STR','LDP','LDP','BR'] res=[] while start<end: ea=start cnt=0 subs_offset=0 adds_offset=0 for i in range(len(pattern)): if idc.print_insn_mnem(ea) =="SUBS": subs_offset=get_offset(ea) if idc.print_insn_mnem(ea) =="ADDS": adds_offset=get_offset(ea) if pattern[i] != idc.print_insn_mnem(ea): break else: cnt+=1 ea=idc.next_head(ea,end) if cnt==10: res.append((start+8,start+0x24+adds_offset-subs_offset)) #print(hex(0x20+adds_offset-subs_offset)) start+=0x20+adds_offset-subs_offset else: start+=4 return resdef get_offset(addr): raw_ins=ida_bytes.get_bytes(addr,4) instr=int.from_bytes(raw_ins,byteorder='little') imm_val=(instr>>10)&0xff #print('===',hex(imm_val)) return imm_val def set_color(start,end): for addr in range(start,end): idc.set_color(addr,idc.CIC_ITEM,0xd0ffc0)def do_patch(start,end): nop_bytes = (0x1F2003D5).to_bytes(4, "big") while start<=end: ida_bytes.patch_bytes(start,nop_bytes) start+=4def 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=0x4360end=0x4A54patchs=find_and_patch(begin,end)print(patchs)for i in range(len(patchs)): set_color(patchs[i][0],patchs[i][1]) do_patch(patchs[i][0],patchs[i][1])upc(begin,end) |
所以我写了一个idapython来帮助我们静态分析
这个使用就是这个

然后run就行


上面的就是成果,可以看了
接下来就是静态了,我使用frida脚本辅助我们静态看
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 | function hook_so_sig1() { var offest = 0x3F48; var soName = 'libsgcore.so'; const module = Process.findModuleByName(soName) console.log(module.base.add(offest)) Interceptor.attach(module.base.add(offest), { onEnter: function (args) { console.log("原始值", (this.context.x0)) // this.context.x1 = 0; // console.log("修改成功", this.context.x1) console.log("开始调用参数") console.log(args[0]) console.log(args[1]) console.log(args[2]) console.log(args[3]) console.log(Memory.readByteArray(this.context.x1, 0x40)) console.log(Memory.readByteArray(this.context.x2, 0x40)) // console.log(Memory.readByteArray(this.context.x2, 0x40)) // console.log(this.context.x0) // console.log(this.context.x1) // console.log(this.context.x22) }, onLeave: function (retval) { // retval.replace(0x0) console.log("返回值", retval) // return retval } })}function hook_findso() { var offest = 0x22D0; var soName = 'libsgcore.so'; const module = Process.findModuleByName(soName) console.log("模块基址", module.base.add(offest)) Interceptor.attach(module.base.add(offest), { onEnter: function (args) { // console.log("原始值", (this.context.x8)) // this.context.x1 = 0; // console.log("修改成功", this.context.x1) console.log("开始调用参数==============================") console.log('[*] 参数一', (args[0])) console.log('[*] 参数二', args[1]) console.log('[*] 参数三', args[2]) //var s = Memory.readPointer(args[0]) this.keys = args[0] console.log('[*] 参数一数组', Memory.readByteArray(this.context.x0, 0x40)) console.log('[*] 参数二数组', Memory.readByteArray(this.context.x1, 0x40)) // console.log('[*] 参数三数组', Memory.readByteArray(this.context.x2, 0x40)) // console.log(Memory.readByteArray(this.context.x2, 0x20)) // console.log(this.context.x0) // console.log(this.context.x1) // console.log(this.context.x22) }, onLeave: function (retval) { // retval.replace(0x0) console.log('[*] 返回值2:', Memory.readByteArray(retval, 0x40)) console.log('[*] 返回值:', Memory.readByteArray(this.keys, 0x40)) // return retval } })}function hook_enctypart() { var offest = 0x24A0; var soName = 'libsgcore.so'; const module = Process.findModuleByName(soName) console.log("模块基址", module.base.add(offest)) Interceptor.attach(module.base.add(offest), { onEnter: function (args) { // console.log("原始值", (this.context.x8)) // this.context.x1 = 0; // console.log("修改成功", this.context.x1) console.log("开始调用参数==============================") // console.log('[*] 参数一', (args[0])) // console.log('[*] 参数二', args[1]) // console.log('[*] 参数三', args[2]) //var s = Memory.readPointer(args[0]) this.keys = args[0] console.log('[*] 中间值', this.context.x9, 0x40) //console.log('[*] 中间值', Memory.readByteArray(this.context.x9, 0x40)) // console.log('[*] 参数二数组', Memory.readByteArray(this.context.x1, 0x40)) // console.log('[*] 参数三数组', Memory.readByteArray(this.context.x2, 0x40)) // console.log(Memory.readByteArray(this.context.x2, 0x20)) // console.log(this.context.x0) // console.log(this.context.x1) // console.log(this.context.x22) }, onLeave: function (retval) { // retval.replace(0x0) // console.log('[*] 返回值2:', Memory.readByteArray(retval, 0x40)) // console.log('[*] 返回值:', Memory.readByteArray(this.keys, 0x40)) // return retval } })} |
下面附上我的分析的代码
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 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 | jstring __fastcall Java_com_kwai_sgcore_SGCore_getClock(JNIEnv *JNIEnv, __int64 a2, __int64 a3, void *a4){ int n10; // w8 int i; // w9 jstring result; // x0 jbyte *v10; // x0 jbyte *v11; // x21 jsize v13; // w0 int n10_1; // w8 unsigned int v15; // w23 int v16; // w9 __int64 s_1; // x0 __int64 s_2; // x0 __int64 ptr_1; // x0 _BOOL4 v20; // w8 const char *s_3; // x8 int j_1; // w22 int n10_2; // w8 int v24; // w9 __int64 j; // x23 bool v26; // w8 unsigned __int64 n0x10; // x9 bool v28; // w8 char v29; // w12 unsigned int n0xFF; // w9 unsigned __int64 n0xF; // x10 int v32; // w12 __int64 idx; // x8 int v34; // w23 __int64 v35; // x0 __int64 v36; // x0 unsigned __int8 *ptr; // x22 int n10_3; // w8 int v39; // w9 __int64 k; // x23 int m; // w2 int v42; // w9 size_t v43; // [xsp+0h] [xbp-160h] size_t v44; // [xsp+0h] [xbp-160h] __int128 v45; // [xsp+20h] [xbp-140h] BYREF __int128 v46; // [xsp+30h] [xbp-130h] __int128 v47; // [xsp+40h] [xbp-120h] char v48; // [xsp+50h] [xbp-110h] char v49[104]; // [xsp+60h] [xbp-100h] BYREF char s[8]; // [xsp+C8h] [xbp-98h] BYREF __int64 v51; // [xsp+D0h] [xbp-90h] __int64 v52; // [xsp+D8h] [xbp-88h] JNIEnv *JNIEnv_1; // [xsp+140h] [xbp-20h] __int64 v54; // [xsp+148h] [xbp-18h] __int64 v55; // [xsp+150h] [xbp-10h] JNIEnv_1 = JNIEnv; v54 = a2; v55 = a3; v52 = *(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40); n10 = ::n10; i = (HIDWORD(::n10) - 1) * HIDWORD(::n10); if ( !a4 ) { result = 0LL; goto LABEL_9; } while ( 1 ) { v10 = (*JNIEnv)->GetByteArrayElements(JNIEnv, a4, 0LL); n10 = ::n10; v11 = v10; if ( (::n10 & 0x80000000) != 0 || (((HIDWORD(::n10) - 1) * HIDWORD(::n10)) & 0x80000000) == 0 ) break; (*JNIEnv)->GetByteArrayElements(JNIEnv, a4, 0LL); } if ( !v10 ) { result = 0LL; i = (HIDWORD(::n10) - 1) * HIDWORD(::n10); if ( (::n10 & 0x80000000) == 0 ) goto LABEL_21; goto LABEL_9; } if ( (*JNIEnv)->ExceptionCheck(JNIEnv) ) { while ( 1 ) { (*JNIEnv)->ExceptionClear(JNIEnv); n10 = ::n10; result = 0LL; i = (HIDWORD(::n10) - 1) * HIDWORD(::n10); if ( (::n10 & 0x80000000) != 0 || (i & 0x80000000) == 0 ) break; (*JNIEnv)->ExceptionClear(JNIEnv); } goto LABEL_20; } if ( (::n10 & 0x80000000) == 0 && (HIDWORD(::n10) - 1) * HIDWORD(::n10) < 0 ) goto LABEL_25; while ( 1 ) { v13 = (*JNIEnv)->GetArrayLength(JNIEnv, a4); n10_1 = ::n10; v15 = v13; v16 = (HIDWORD(::n10) - 1) * HIDWORD(::n10); if ( (::n10 & 0x80000000) != 0 || (v16 & 0x80000000) == 0 ) break;LABEL_25: (*JNIEnv)->GetArrayLength(JNIEnv, a4); } if ( !::s ) { while ( 1 ) { s_1 = AES_encrypt_(byte_F8AB, 16LL); // 0xC3,0xD9,0x4,0x91,0x81,0xAF,0x94,0x9,0x9F,0x39,0xC2,0xFC,0xB,0xCA,0x33,0xC7有点像AES加密,但是应该是把表改了 n10_1 = ::n10; ::s = s_1; v16 = (HIDWORD(::n10) - 1) * HIDWORD(::n10); if ( (::n10 & 0x80000000) != 0 || (v16 & 0x80000000) == 0 ) break; ::s = AES_encrypt_(byte_F8AB, 16LL); } } while ( n10_1 >= 0 && v16 < 0 ) ; if ( !s_0 ) { s_2 = (sub_2710)(JNIEnv, a3); n10_1 = ::n10; s_0 = s_2; v16 = (HIDWORD(::n10) - 1) * HIDWORD(::n10); } if ( (n10_1 & 0x80000000) == 0 && v16 < 0 ) { while ( 1 ) ; } ptr_1 = ::ptr; if ( !::ptr ) { while ( 1 ) { ptr_1 = (sub_2A28)(JNIEnv, a3); // 一堆java层的读取操作 n10_1 = ::n10; ::ptr = ptr_1; v16 = (HIDWORD(::n10) - 1) * HIDWORD(::n10); if ( (::n10 & 0x80000000) != 0 || (v16 & 0x80000000) == 0 ) break; ::ptr = (sub_2A28)(JNIEnv, a3); } } v20 = n10_1 < 10 || (v16 & 1) == 0; while ( !v20 ) ; if ( !s_0 || (s_3 = ::s) == 0LL || !ptr_1 ) // 从这里开始执行了已经 { result = (JNIEnv[167])(JNIEnv, &byte_1046F); n10 = ::n10; for ( i = (HIDWORD(::n10) - 1) * HIDWORD(::n10); (::n10 & 0x80000000) == 0 && i < 0; i = (HIDWORD(::n10) - 1) * HIDWORD(::n10) ) { (JNIEnv[167])(JNIEnv, &byte_1046F); result = (JNIEnv[167])(JNIEnv, &byte_1046F); n10 = ::n10; } goto LABEL_49; } while ( 1 ) // 从这里执行,但是没找到哪里进行的输入,等会trace一下 { *s = 0LL; v51 = 0LL; j_1 = strlen(s_3); // 读取长度是0xc MD5_state(v49); // 这个可能是MD5 state初始化 MD5_update(v49, v11, v15); // MD5 update // 第二个参数就是我们输入的那个数组值 // 参数三是5 n10_2 = ::n10; v24 = (HIDWORD(::n10) - 1) * HIDWORD(::n10); if ( (::n10 & 0x80000000) != 0 || (v24 & 0x80000000) == 0 ) break; *s = 0LL; v51 = 0LL; MD5_state(v49); // 这里没有执行,前面那个MD5不知道有没有加盐 MD5_update(v49, v11, v15); // 这里拼接了一些数值ca8e86efb32e,应该是加盐了 s_3 = ::s; } for ( j = 0LL; ; j += 2LL ) { v26 = n10_2 >= 0 && v24 < 0; if ( j >= j_1 ) break; if ( v26 ) goto LABEL_60; while ( 1 ) { sprintf(s, "%c%c", *(::s + j), *(::s + j + 1)); MD5_update(v49, s, 2LL); n10_2 = ::n10; v24 = (HIDWORD(::n10) - 1) * HIDWORD(::n10); if ( (::n10 & 0x80000000) != 0 || (v24 & 0x80000000) == 0 ) break;LABEL_60: sprintf(s, "%c%c", *(::s + j), *(::s + j + 1)); MD5_update(v49, s, 2LL); } } if ( v26 ) (sub_6A2C)(v49); // 这里也会被调用 (sub_6A2C)(v49); n0x10 = 0LL; v28 = ::n10 >= 0 && (HIDWORD(::n10) - 1) * HIDWORD(::n10) < 0; do { v29 = v49[n0x10 + 88]; if ( v28 ) s[n0x10] = v29; s[n0x10++] = v29; } while ( n0x10 < 0x10 ); if ( v28 ) { while ( 1 )LABEL_72: ; } n0xFF = 0; n0xF = 0LL; while ( n0xF < 0xF ) // 这里是对MD5的值进行扰动,把前15个字节的值加起来 { v32 = s[n0xF++]; n0xFF += v32; if ( v28 ) goto LABEL_72; } idx = 0LL; if ( n0xFF <= 0xFF ) LOBYTE(v34) = n0xFF; else v34 = -n0xFF; do { s[idx] ^= v34 ^ idx; // 这里是一个扰动,原来的值^idx^sums ++idx; } while ( idx != 15 ); while ( 1 ) { HIBYTE(v51) = v34; v44 = strlen(s_0); // 这里也是一个加密的地方,而且应该也是16次 v36 = MD5_func2(0, s_0, v44); // 这里的第四个参数是0x12 LODWORD(v44) = MD5_func2(v36, ::ptr, 32); // 这个函数在主动调用的时候会执行,并且第二个参数还是一个定值22e875c50d9a3fda6c2898eaf0a4d314 ptr = encryfinal_xor(s, 16LL, v44); (*JNIEnv)->ReleaseByteArrayElements(JNIEnv, a4, v11, 2LL); n10_3 = ::n10; v39 = HIDWORD(::n10); v48 = 0; v46 = 0u; v47 = 0u; v45 = 0u; if ( (::n10 & 0x80000000) != 0 || (((HIDWORD(::n10) - 1) * HIDWORD(::n10)) & 0x80000000) == 0 ) break; HIBYTE(v51) = v34; v43 = strlen(s_0); v35 = MD5_func2(0, s_0, v43); LODWORD(v43) = MD5_func2(v35, ::ptr, 32); encryfinal_xor(s, 16LL, v43); // 最后的一个异或扰动加密,这里的参数三应该是上面那个定值的MD5 (*JNIEnv)->ReleaseByteArrayElements(JNIEnv, a4, v11, 2LL); v48 = 0; v46 = 0u; v47 = 0u; v45 = 0u; } for ( k = 0LL; ; ++k ) // 输出、环境处理 { v42 = (v39 - 1) * v39; while ( n10_3 >= 0 && v42 < 0 ) ; if ( k == 24 ) break; for ( m = ptr[k]; ; m = ptr[k] ) { sprintf(&v45 + 2 * k, "%02x", m); n10_3 = ::n10; v39 = HIDWORD(::n10); if ( (::n10 & 0x80000000) != 0 || (((HIDWORD(::n10) - 1) * HIDWORD(::n10)) & 0x80000000) == 0 ) break; sprintf(&v45 + 2 * k, "%02x", ptr[k]); } } free(ptr); result = (*JNIEnv)->NewStringUTF(JNIEnv, &v45); n10 = ::n10; i = (HIDWORD(::n10) - 1) * HIDWORD(::n10);LABEL_49: if ( (n10 & 0x80000000) == 0 && i < 0 ) { while ( 1 ) ; }LABEL_20: if ( (n10 & 0x80000000) == 0 ) {LABEL_21: if ( i < 0 ) { while ( 1 ) ; } }LABEL_9: if ( (n10 & 0x80000000) == 0 && i < 0 ) { while ( 1 ) ; } return result;} |

这里有一个输入,我的frida代码应该也有记录,就直接分析代码了
首先就是输入拼接了一下这个ca8e86efb32e字符串然后进行MD5操作

接下来就是一个异或扰动,具体就是先加和(前15字节)再异或扰动

异或扰动完,就是另外一个加密

也就是encryfinal_xor这个函数,代码如下:
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 | void __fastcall encryfinal_xor(const void *src, size_t num_16, int key){ _WORD *input; // x20 _BOOL4 v7; // w24 size_t v8; // x8 size_t v9; // x10 unsigned int n0xFF; // w9 int v11; // w11 __int64 v12; // x11 bool v13; // zf nullsub_7(); input = malloc(num_16 + 8); v7 = n10 >= 0 && (HIDWORD(n10) - 1) * HIDWORD(n10) < 0; if ( input ) { if ( n10 < 0 || (HIDWORD(n10) - 1) * HIDWORD(n10) >= 0 ) goto LABEL_6; do { memset(input, 0, num_16 + 8); *(input + 3) = key; *input = 0x101; *(input + 2) = 1; memcpy(input + 7, src, num_16);LABEL_6: memset(input, 0, num_16 + 8); // 其实是执行这里 *(input + 3) = key; *input = 257; *(input + 2) = 1; memcpy(input + 7, src, num_16); } while ( v7 ); v8 = num_16 + 7; if ( num_16 == -7LL ) { *(input + v8) = 0; } else { v9 = 0LL; n0xFF = 0; do { v11 = *(input + v9++); n0xFF += v11; } while ( v8 > v9 ); if ( n0xFF > 0xFF ) n0xFF = -n0xFF; *input = n0xFF ^ 1; if ( num_16 != -6LL ) { v12 = 0LL; do { v13 = num_16 + 6 == v12 + 1; *(input + v12 + 1) ^= n0xFF ^ (v12 + 1); ++v12; } while ( !v13 ); } *(input + v8) = n0xFF; if ( v7 ) { while ( 1 ) ; } } } else if ( n10 >= 0 && (HIDWORD(n10) - 1) * HIDWORD(n10) < 0 ) { while ( 1 ) ; }} |
这里我写了一个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 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 | package com.suanfahuanyuan.kanman;import com.github.unidbg.AndroidEmulator;import com.github.unidbg.Module;import com.github.unidbg.arm.backend.Unicorn2Factory;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.DalvikModule;import com.github.unidbg.linux.android.dvm.DvmClass;import com.github.unidbg.linux.android.dvm.VM;import com.github.unidbg.linux.android.dvm.array.ByteArray;import com.github.unidbg.memory.Memory;import com.github.unidbg.memory.MemoryBlock;import com.github.unidbg.pointer.UnidbgPointer;import com.github.unidbg.virtualmodule.android.AndroidModule;import com.qwb2025clw.whiteboxaes;import java.io.File;public class kanmanenc { public final AndroidEmulator emulator; public final VM vm; public final Memory memory; public final Module module; DvmClass cNative; public int hitCount = 0; public kanmanenc() { emulator = AndroidEmulatorBuilder.for64Bit() .setProcessName("com.kanman.allfree") .addBackendFactory(new Unicorn2Factory(true)) .build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分 memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); emulator.getSyscallHandler().setEnableThreadDispatcher(true); vm = emulator.createDalvikVM(new File("/aa.apk")); // 创建Android虚拟机 ///vm.setVerbose(true); new AndroidModule(emulator, vm).register(memory); // 加载并初始化动态库 DalvikModule dalvikModule = vm.loadLibrary(new File("unidbg-android/src/test/java/com/suanfahuanyuan/kanman/libsgcore.so"), true); module = dalvikModule.getModule(); vm.getJNIEnv(); //emulator.attach(DebuggerType.CONSOLE).addBreakPoint(module.base + 0xF7E8); //emulator.attach(DebuggerType.CONSOLE).addBreakPoint(module.base + 0xF43C); debugger(); vm.callJNI_OnLoad(emulator, module); byte[] in16 = new byte[]{ (byte)0x47,(byte)0xEB,(byte)0x2F,(byte)0xA6, (byte)0x47,(byte)0x33,(byte)0x98,(byte)0x86, (byte)0x02,(byte)0xA6,(byte)0x8A,(byte)0x38, (byte)0xCD,(byte)0xED,(byte)0x56,(byte)0x01 }; MemoryBlock pSrc = memory.malloc(in16.length, true); UnidbgPointer srcPtr = pSrc.getPointer(); srcPtr.write(0, in16, 0, in16.length); pSrc.getPointer(); long num_16 = 16L; // int num_2 = 0xb0da86aa; module.callFunction(emulator,0x22D0,srcPtr,num_16,num_2); //install(); } public static void main(String[] args) { kanmanenc mainActivity = new kanmanenc(); mainActivity.debugger(); } private void debugger() { emulator.attach(DebuggerType.CONSOLE).addBreakPoint(module.base + 0x22D0); module.callFunction(emulator, 0x22D0); }} |
然后就是通过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 | #sig1这个签名的还原import hashlibdef sig1(inp:str): inp+="ca8e86efb32e"#这个值就是那个MD5的盐 md5_str=hashlib.md5(inp.encode('utf-8')).hexdigest() # print('md5',md5_str) md5_byte=bytes.fromhex(md5_str) # print('md5_byte',md5_byte) s=list(md5_byte) #第一次异或扰动 tmp1=[] sums1 = sum(s[:15]) if (sum(s[:15]) < 0xff) else (-sum(s[:15])) & 0xff for i in range(16): if i < 15: tmp1.append(s[i] ^ i ^ sums1) # print(hex(s[i] ^ i ^ sums1), end=' ,') else: tmp1.append(sums1&0xff) # print(hex(sums1)) #第二次异或扰动 ''' 前八字节基本是固定的 input 0x1 1 1 key=0xb0da86aa inp[16] 前23个字符取和 sum(inp[:23]) imp[0]^=1 再次进行异或 ''' # key = 0xb0da86aa 这个经过hook应该是一个定值,就是参数三 key1 = [0x1, 0x1, 1, 0xaa, 0x86, 0xda, 0xb0] tmp2 = key1 + tmp1 # print(tmp2) for i in range(len(tmp2)): tmp2[i] &= 0xff # print(hex(tmp2[i]), end=' ,') print('============================sig1==========================') sums2 = sum(tmp2[:23]) & 0xff if (sum(tmp2[:23]) < 0xff) else (-sum(tmp2[:23])) & 0xffff # print(hex(sums2)) tmp2[0] = (sums2 ^ 1) & 0xff # print(hex(tmp2[0]), end=' ,') final=[] final.append(tmp2[0]) for i in range(1, 23): if i!=23: final.append((tmp2[i] ^ (i) ^ sums2) & 0xff) else: final.append(sums2 & 0xff) # print(hex((tmp2[i] ^ (i) ^ sums2) & 0xff), end=' ,') # print(hex(sums2 & 0xff), end=' ,') final_str="".join(f'{byte:02x}' for byte in final) print("sig1:",final_str) print('============================sig1==========================')inp=''sig1(inp) |
就是这样
登录分析请求还原

Java层定位:
搜索这个x_data就行,定位到下面的函数

So层算法分析:
主要是下面这个,但是不知道为什么上面的咱们的哪个erciyuan2020这个出现在了咱们的地方,可能也是AES加密吧



可以看到so层又调用了java层的加密,应该也是标准的只不过加了参数罢了
hook一下就行

最终定位到这里,一个AES cbc模式的
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 | function hook_enc() { Java.perform(function () { let JNISecurity = Java.use("com.kanman.JNISecurity"); var str1 = "112233445566778899" var ret1 = JNISecurity.jEnc(str1) console.log("加密值", str1, "加密返回值", ret1) let KanManAES = Java.use("com.kanman.KanManAES"); KanManAES["encryptString"].implementation = function (str, str2) { console.log('encryptString is called' + ', ' + 'str: ' + str + ', ' + 'str2: ' + str2); let ret = this.encryptString(str, str2); console.log('encryptString ret value is ' + ret); return ret; }; //let JNISecurity = Java.use("com.kanman.JNISecurity"); console.log("====================================") JNISecurity["jFileEncrypt"].implementation = function (str, str2) { console.log('jFileEncrypt is called' + ', ' + 'str: ' + str + ', ' + 'str2: ' + str2); let ret = this.jFileEncrypt(str, str2); console.log('jFileEncrypt ret value is ' + ret); return ret; }; console.log("====================================") JNISecurity["jEnc"].implementation = function (str) { console.log('jEnc is called' + ', ' + 'str: ' + str); let ret = this.jEnc(str); console.log('jEnc ret value is ' + ret); return ret; }; //let JNISecurity = Java.use("com.kanman.JNISecurity"); var str2 = "" var ret2 = JNISecurity.dec(str2) console.log("解密值", ret2) // JNISecurity["dec"].implementation = function (str) { // console.log('dec is called' + ', ' + 'str: ' + str); // let ret = this.dec(str); // console.log('dec ret value is ' + ret); // return ret; // }; //let KanManAES = Java.use("com.kanman.KanManAES"); KanManAES["getHash"].overload('java.lang.String', '[B').implementation = function (str, bArr) { console.log('getHash is called' + ', ' + 'str: ' + str + ', ' + 'bArr: ' + bArr); let ret = this.getHash(str, bArr); console.log('getHash ret value is ' + ret); return ret; }; })}function hook_key() { Java.perform(function () { // Hook SecretKeySpec 构造函数 var SecretKeySpec = Java.use("javax.crypto.spec.SecretKeySpec"); SecretKeySpec.$init.overload('[B', 'java.lang.String').implementation = function (keyBytes, algorithm) { console.log("SecretKeySpec constructor called:"); console.log("Algorithm: " + algorithm); console.log("Key bytes: " + bytesToHex(keyBytes)); // 调用原始构造函数 return this.$init(keyBytes, algorithm); }; // Hook IvParameterSpec 构造函数 var IvParameterSpec = Java.use("javax.crypto.spec.IvParameterSpec"); IvParameterSpec.$init.overload('[B').implementation = function (ivBytes) { console.log("IvParameterSpec constructor called:"); console.log("IV bytes: " + bytesToHex(ivBytes)); // 调用原始构造函数 return this.$init(ivBytes); }; // 辅助函数:将字节数组转换为十六进制字符串 function bytesToHex(byteArray) { var hexStr = ""; for (var i = 0; i < byteArray.length; i++) { hexStr += ("00" + (byteArray[i] & 0xFF).toString(16)).slice(-2); } return hexStr; } });}function hook_dec() { Java.perform(function () { let C2488a = Java.use("com.comic.common.b.a.p.a"); C2488a["c"].overload('android.widget.RelativeLayout').implementation = function (relativeLayout) { console.log('c is called' + ', ' + 'relativeLayout: ' + relativeLayout); let ret = this.c(relativeLayout); console.log('c ret value is ' + ret); return ret; }; });} |
通过上面的这个frida以及上面的算法自吐脚本可以得到key和iv,就是这样的
具体算法如下:(我没有进行对齐操作,需要进行填充,大家可以再完善一下)
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 | #登录验证加密#输入就是传递的参数,密钥就是第二个参数,是so里面的定值,iv是一个定值"1992360ee9bc4f8f"import base64from Cryptodome.Cipher import AESfrom Cryptodome.Util.Padding import unpadfrom Cryptodome.Protocol.KDF import PBKDF2def login_decry(inp:str): key="4548ded8c9e02690" iv="1992360ee9bc4f8f" enc_bytes=base64.b64decode(inp) cipher=AES.new(key.encode('utf-8'),AES.MODE_CBC, iv.encode('utf-8')) enc=cipher.decrypt(enc_bytes) print(enc)def login_encry(inp:str): key="4548ded8c9e02690" iv="1992360ee9bc4f8f" cipher=AES.new(key.encode('utf-8'),AES.MODE_CBC, iv.encode('utf-8')) enc=cipher.encrypt(inp.encode('utf-8')) enc_base64=base64.b64encode(enc) print(enc_base64)print()inp='0OFm2t/mYjPIhqlECGE+Hs+SYycvu+AgBVK3vgmV3QtL3pJ67TP0cSXyVnNQGs1Q82xyniZkG3agTPOfR5ARlHBmdLAA3h2w7UFhhBVVMJllD4TMg7f1cP+GQa3/nGzpQUtS56MMWCL65Y5rf01xhKVUzNS+FCnp9cXkpK3AjHtHIJDqQxGpSBJbF35CnepDvPZoUwrmJS1UksXRHj3aoB0xuu/G4bLNIf+GMCLd3Bk3vB/p6Wrr2ZMMyMY2Q5SlxFhOSGQ1cwgLa5XLedsj1w=='print(len(inp)%16)login_decry(inp)enc='{"client-channel":"qihoo","service":"qmmh","countryCode":"","mobile":"xxxxxxxxxxx","refresh":"0","productname":"qmmh","client-type":"android","imgCode":"","client-version":"1.5.4","platformname":"android"}\x03\x03\x03'login_encry(enc) |
就得了登录请求的所有参数,然后他的返回包就是一个png图片的base64,大家cyberchef搞一下就行
VIP分析:
这一块我没具体分析,只是找到了一些关键位置
定位位置


这里分析就行,具体的我也没做
最后:
这次在这个APP上面收获了不少东西,frida使用,unidbg使用,idapython去除花指令等等
后面需要加强的就是学习一个360加固的修复运行这种,提高一下自己对壳修复的能力
如果上面文章有侵权行为,请联系我删除
赞赏
- [原创]对某某漫画的算法还原测试 452
- [原创]签名校验攻防对抗基础学习 1086
- [原创]2023年腾讯游戏安全竞赛安卓决赛题解复现 618
- [原创]OLLVM学习 3920