观前提示:
本文章仅供学习交流,切勿用于非法通途,如有侵犯贵司请及时联系删除
样本:aHR0cHM6Ly9wYW4uYmFpZHUuY29tL3MvMUFrMms4M1BjTlRjLVRSbWJDQWlRRkE/cHdkPTgyd3Y=
0x1 准备工作
- 下载并安装样本App
- 脱壳
- 跳转至加密位置
0x2 Unidbg实现
- 初始化
通过使用Frida hooknativeSign
和nativeInit
发现在加载完so后会调用一次nativeInit
接着才是正常调用nativeSign
方法 所以在unidbg中也需要主动初始化
- 补环境
复制粘贴好基础框架后运行
报了下面的错误
INFO [com.github.unidbg.linux.AndroidElfLoader] (AndroidElfLoader:464) - libsign.so load dependency libandroid.so failed exit status=0
按照报错给出的信息就是缺少libandroid.so
这个东东 那我们给上就好了
new AndroidModule(emulator,vm).register(memory);
继续执行 发现已经不提示缺少libandroid.so
了 但还是退出了 也没给出具体原因
那我们改改src/test/resources/log4j.properties
将里面的INFO
改成DEBUG
接着再次运行
能够看到里面读取了/proc/self/maps
这个文件
虽然unidbg会自动补上一个maps 但是这个maps和真机环境上的maps差距甚远 所以我们还得从真机上copy一个正确的maps文件 并且重定向到我们自己的maps上
@Override public FileResult resolve(Emulator emulator, String pathname, int oflags) { System.out.println(pathname); if (pathname.equals("/proc/self/maps")){ return FileResult.success(new SimpleFileIO(oflags,new File("unidbg-android/src/test/java/com/meiriyouxian/maps"),pathname)); } return null; }
此时此刻 这个so文件已经能正常加载了
- 主动调用
由之前hook的结果可知需要先初始化 所以在unidbg中同样需要进行初始化
并且由java层代码可知传入了俩个参数
private static native int nativeInit(Context context, String str);
unidbg主动调用
public void CallnativeInit(){ List<Object> args = new ArrayList<>(10); args.add(vm.getJNIEnv()); args.add(0); DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null); args.add(vm.addLocalObject(context)); args.add(vm.addLocalObject(new StringObject(vm,"01000002"))); module.callFunction(emulator,0x38bb4+1,args.toArray()); }
此时出现报错
补上即可
@Override public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { switch (signature){ case "android/content/Context->getAssets()Landroid/content/res/AssetManager;":{ return new AssetManager(vm,signature); } } return super.callObjectMethodV(vm, dvmObject, signature, vaList); }
初始化完后根据java代码实现一下nativeSign
private static native String nativeSign(Context context, long j, byte[] bArr);
unidbg主动调用
public void CallnativeSign(){ List<Object> args = new ArrayList<>(10); args.add(vm.getJNIEnv()); args.add(0); DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null); args.add(vm.addLocalObject(context)); args.add(1648631301174L); byte[] input = "1234567890".getBytes(StandardCharsets.UTF_8); args.add(vm.addLocalObject(new ByteArray(vm,input))); Number number = module.callFunction(emulator, 0x38bf4 + 1, args.toArray()); System.out.printf(vm.getObject(number.intValue()).getValue().toString()); }
看看效果 这个时候已经能够正常出值了 并且多次主动调用发现返回值没有发生改变
0x3 算法还原
使用IDA Pro打开so文件发现so文件被加壳 这里使用yang神的dump_so脚本
IDA Pro打开dump出来的so文件 进来后可以在导出表中找到nativeSign
和nativeInit
都是静态注册的
直接打开nativeSign
进行分析
从伪代码的逻辑上看 最终指向了sub_36FC0
而且返回值为v18
这里用unidbg来hook辅助分析so还原算法
emulator.attach().addBreakPoint(module.base+0x36FC0+1);
hook住后 记住mr0的地址 输入blr下断 接着跳到下一个断点
输入刚刚mr0的地址
看到00 70 2E 40 也就是存放数据的地址 通过读取验证确定就是返回的加密值了
上traceWrite
emulator.traceWrite(0x402e7000L, 0x402e7000L + 32L);
通过hook可知在0x37f64
存在写操作
上ida跳转过去 突然看到了熟悉的东西 这不就是base64吗
双击一下 发现 这码表好像不对 可以确定是改了码表的base64
先hook一下sub_37F3C
看看输入和输出结果 输入值
输出值
上CyberChef验证 可以见到结果一模一样
接着找找是谁调用了sub_37F3C
跳转过来后看到v7
和a2
有关
继续找是谁调用了sub_37E5C
跳过来看到a2
对应v116
而v116
与sub_2F8F6
有关 所以hook一下sub_2F8F6
这里注意一下方法一共被调用了3次 后2次才是我们需要的 第2次输出值和之前base64传入值对上了 但是还差了点
第3次输出值就和之前base64传入值一模一样了
看一下传入值 第2次
第3次
和输出值对比了一下可以发现 就是取我们传入的时间戳1648631301174
分成了俩半164863130
和1174
中间插入了第2次的传入值
接着找找这段值的由来
还是使用traceWrite
emulator.traceWrite(0x402a10f0L, 0x402a10f0L + 32L);
得到结果 跳转过去0x36488
先hooksub_363DC
的传入值
emulator.attach().addBreakPoint(module.base+0x363DC+1);
a2
a3
知道了传入值后看看代码流程
根据代码不难看出 主要操作就是循环累加
for ( i = 0; i != v11; ++i ) { sub_815D8(i, v28); v14 = v3 + 1; if ( *v3 << 31 ) v14 = *(v3 + 2); v15 = v14[v13]; sub_815D8(i, v27); v17 = v26 + 1; if ( *v26 << 31 ) v17 = *(v3 + 5); v18 = v31; v19 = *(a3 + 2); v20 = v17[v16]; if ( (v29 & 1) == 0 ) v18 = &v29 + 1; if ( (*a3 & 1) == 0 ) v19 = a3 + 1; v18[i] = v20 + v15 + v19[i]; }
v19就是a3 v20是v17[v16]对应a2+12+1 v15是v14[v13]对应a2+1
而v16和v13在ida中并没有正确识别到 那就继续hook看是什么情况
先在0x3647C
处下个断点看v13
r0=0xbfffee85(-1073746299) r1=0x0 ldrb.w fp, [r0, r1]" [0xbfffee85] => 0x31 //1 r0=0xbfffee85(-1073746299) r1=0x1 ldrb.w fp, [r0, r1]" [0xbfffee86] => 0x31 //1 r0=0xbfffee85(-1073746299) r1=0x2 ldrb.w fp, [r0, r1]" [0xbfffee87] => 0x37 //7 r0=0xbfffee85(-1073746299) r1=0x3 ldrb.w fp, [r0, r1]" [0xbfffee87] => 0x34 //4 r0=0xbfffee85(-1073746299) r1=0x4 ldrb.w fp, [r0, r1]" [0xbfffee85] => 0x31 //1 r0=0xbfffee85(-1073746299) r1=0x5 ldrb.w fp, [r0, r1]" [0xbfffee86] => 0x31 //1 r0=0xbfffee85(-1073746299) r1=0x6 ldrb.w fp, [r0, r1]" [0xbfffee87] => 0x37 //7 r0=0xbfffee85(-1073746299) r1=0x7 ldrb.w fp, [r0, r1]" [0xbfffee87] => 0x34 //4 .......
然后在0x364A4
处下个断点看v16
r0=0xbfffee91(-1073746287) r1=0x0 0x400364a4:*"ldrb r0, [r0, r1]" [0xbfffee91] => 0x41 //A r0=0xbfffee91(-1073746287) r1=0x1 0x400364a4:*"ldrb r0, [r0, r1]" [0xbfffee91] => 0x42 //B r0=0xbfffee91(-1073746287) r1=0x2 0x400364a4:*"ldrb r0, [r0, r1]" [0xbfffee91] => 0x43 //C r0=0xbfffee91(-1073746287) r1=0x3 0x400364a4:*"ldrb r0, [r0, r1]" [0xbfffee91] => 0x44 //D r0=0xbfffee91(-1073746287) r1=0x4 0x400364a4:*"ldrb r0, [r0, r1]" [0xbfffee91] => 0x45 //E r0=0xbfffee91(-1073746287) r1=0x5 0x400364a4:*"ldrb r0, [r0, r1]" [0xbfffee91] => 0x46 //F r0=0xbfffee91(-1073746287) r1=0x6 0x400364a4:*"ldrb r0, [r0, r1]" [0xbfffee91] => 0x47 //G r0=0xbfffee91(-1073746287) r1=0x7 0x400364a4:*"ldrb r0, [r0, r1]" [0xbfffee91] => 0x47 //H r0=0xbfffee91(-1073746287) r1=0x8 0x400364a4:*"ldrb r0, [r0, r1]" [0xbfffee91] => 0x41 //A r0=0xbfffee91(-1073746287) r1=0x9 0x400364a4:*"ldrb r0, [r0, r1]" [0xbfffee91] => 0x42 //B .......
能看到v13和v16其实就是i
相当于1174
和ABCDEFGH
在循环往复取值
所以这段代码主要的作用就加加加
用python还原就是
def sub_363DC(v14,v19): v17="ABCDEFGH".encode() result=[] for i in range(len(v19)): result.append(v17[i%8]+v14[i%4]+v19[i]) return bytes(result).hex()
查找谁调用了sub_363DC
找找v162的由来
跳转过来后看到下面的报错信息出现了protobuf
带着猜想 复制数据测试 发现能成功反序列化
1: 16777218 2: 1 3: "29FE8A6E707C1697A7DB626B9880CC8DF6BD9AFA6C72BDE90F4CA76D39EA3A77" 6: 1
其中1就是0x1000002 3为未知字符串 2和6不变都是1 验证成功就可以开始写protoc文件了
syntax = "proto3"; message meiriyouxian{ uint64 i1=1; uint64 i2=2; string s3=3; uint64 i6=6; }
写完生成python文件就可以在python里面引用生成我们需要的结果
生成结果和hook结果能够对上 现在需要找出里面s3的来源
hooksub_348B4
r0=0x402a1000 r1=0xbffff070 r2=0x1000002 r3=0x1 r4=0xbfffefc8 r5=0x1000002 r6=0x1
看看v144
所以v144也就是我们自己定义的s3
查找一下v144的引用
跳转过来 没发现什么特别的地方 先hook了再说
sub_2E5A4
有2处调用 第2次调用才是我们需要的
此时能看到 值已经生成了 所以我们还得找v178的来源
此时重点来到sub_37D8C
进去观摩观摩 每个方法都看看瞧瞧
sub_37D8C->sub_2FB14->sub_367F6->sub_36558
里面发现了v28 = dword_9E030[v14++];
眼尖的朋友们应该发现这是什么东西了吧 不认识不要紧 随便复制一个去百度搜搜看
结果很明显 就是一个sha256的k表 那这就是一个sha256算法吗?
不一定 也有可能是hmac sha256
继续分析 回到sub_37D8C
找v201的引用位置 (这里我把v201改成了sha)
这里可以看到上面俩处引用
先看看 sub_37D80(&sha, a3, a4)
通过对比sha256的c代码发现sub_37D80
就是SHA256update
接着看sub_37D5C(&sha, v13)
进入sub_37D5C中的sub_2FA30方法发现0x36
和0x5C
这不就是HMAC SHA256中的INNER_PAD和OUTER_PAD吗
HMAC-SHA256 C实现
https://blog.csdn.net/miniphoenix/article/details/110135164
确定完是HMAC SHA256 那就是找key以及查看是否加盐了
hooksub_37D5C
emulator.attach().addBreakPoint(module.base+0x37D5C+1);
所以key就是PwwGKgCqZAc2PPb31TLnnqPNVFAAdq/X
hooksub_37D80
emulator.attach().addBreakPoint(module.base+0x37D80+1);
结果显示没有加盐之类的操作
知道明文,加密方式和KEY后在CyberChef
验证结果
结果一模一样
0x4 mfsig加密流程总结
- HMAC SHA256
- protobuf
- sub_363DC
- base64
- 头部拼接
mfsn
感谢各位大佬观看
感谢大佬们的文章分享
如有错误 还请海涵
共同进步
[完]