观前提示:
本文章仅供学习交流,切勿用于非法通途,如有侵犯贵司请及时联系删除
样本:aHR0cHM6Ly9wYW4uYmFpZHUuY29tL3MvMXl5N2Rtd1YtcEEtS1B3WmUtN3FRNlE/cHdkPWxpbm4=
0x1 抓包&定位
打开App可以直接抓包
可以看到链接的sign
以及request data
和response data
都被加密需要分析
直接jadx-gui搜索sign=
结果不多
跳转过来 还不清楚传入内容是什么 查找用例看调用处
从这里可知request data
是AES加密后的密文 sign
是对AES加密后的密文进行一个签名
从代码可知 具体sign
加密调用的是net_crypto.so
的一个native方法
同样的实现AES加解密的是encodeAES
和decodeAES
并且可知 在加载so的时候调用了一次初始化方法
0x2 Unidbg黑盒调用
简简单单造轮子 造完直接运行 目前还未有需要补的环境
加载完后 需要主动调用native_init
方法
public void native_init() { ArrayList<Object> args = new ArrayList<>(3); args.add(vm.getJNIEnv()); args.add(0); module.callFunction(emulator, 0x4a069, args.toArray()); }
开始报错 提示缺少一些环境
根据报错把缺少环境补上即可
vm.resolveClass("android/content/Context").newObject(null)
补完这个初始化方法就不报错了
接着主动调用sign
方法 这里图方便 直接将第二个传入参数改为了123456
只追求算法
public String sign() { ArrayList<Object> args = new ArrayList<>(3); args.add(vm.getJNIEnv()); args.add(0); String str = "http://api.xxxxx.com/account/login"; args.add(vm.addLocalObject(new StringObject(vm, str))); ByteArray byteArray = new ByteArray(vm, "123456".getBytes(StandardCharsets.UTF_8)); args.add(vm.addLocalObject(byteArray)); Number number = module.callFunction(emulator, 0x4a28d, args.toArray()); String result = vm.getObject(number.intValue()).getValue().toString(); return result; }
运行看报错接着补环境
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;": { return new StringObject(vm, "/data/user/0/cn.xxxx.tieba/files"); }
case "java/lang/String->getAbsolutePath()Ljava/lang/String;": { return new StringObject(vm, "/data/user/0/cn.xxxx.tieba/files"); }
case "android/os/Debug->isDebuggerConnected()Z": { return false; }
case "android/os/Process->myPid()I": { return emulator.getPid(); }
补到这里 成功出值 可以为所欲为了
算法分析
sign
根据Unidbg输出的动态注册偏移跳转到0x4a28d
看到方法很多 不知道都做了些什么
回到Unidbg 根据JNI输出可以得到很多信息
这里取了一些基本信息 又进行了MD5 那可以猜测这里可能是进行签名验证
这里是对运行环境的一个检测 前面补的环境就是Pass掉这些检测
往下看 这里就出现了加密的结果 根据偏移跳转过来
所以先把目光放在sub_4A9D8(v15, v17)
进入sub_4A9D8
->sub_4AA00
看sub_4AAB4
就是申请内存
由于样本被ollvm混淆过 所以变得罗里吧嗦
整段下来其实就只是一个内存拷贝
回到上层 现在关注的就是v17
了
在0x4A9D8
处下个console断点
根据代码可知 这里的a2
需要再偏移
这里已经存在有结果 所以还得继续找
忽视那些检测代码 剩下的代码并不多
而sub_4B458
就是做一个拼接操作
byte_1C91B0
对应的就是v2-
最后猜测具体的算法就在sub_655DC
中
进入sub_655DC
只有俩方法 先看sub_65540
这熟悉的数组 实锤MD5
但是 仔细观察 这个md5的魔数和我们普通的貌似不一样
确实不一样 实锤了魔改 先尝试拿MD5修改看看能不能对应得上
和主动调用的对比 确定了仅仅是改了魔数
在后在拼接上v2-
就完成这一个sign的加密
encodeAES
先主动调用 方便后面调试
public byte[] encodeAES() { ArrayList<Object> args = new ArrayList<>(3); args.add(vm.getJNIEnv()); args.add(0); String str = "123456"; args.add(vm.addLocalObject(new ByteArray(vm, str.getBytes(StandardCharsets.UTF_8)))); Number number = module.callFunction(emulator, 0x4a0b9, args.toArray()); Inspector.inspect((byte[]) vm.getObject(number.intValue()).getValue(), "AESencode"); return (byte[]) vm.getObject(number.intValue()).getValue(); }
接着根据偏移跳转0x4a0b9
sub_67178
作用为GetByteArrayElements
进入sub_5E0E0
->sub_5E0F8
看到这是一个标准的AES\CBC\128
加密 那么接下来的目标就是找出KEY和IV
IV生成位置在sub_5D090
中
进入sub_5D090
发现就是一个随机方法
拿到随机IV后 根据传入明文的长度进行计算出一个校验位 放在第2位中
根据hook结果可得到验证
并且根据输出结果可知 随机生成的iv拼接在了密文的前面传回服务器
接下来就是KEY 回到上层看到a2
存在dword_1CC3F0
中
拿到地址traceWrite
然后跳转偏移地址 跳转过来是一个内存拷贝操作
还得继续回到上层找
sub_5D090
所以KEY和IV一样是随机生成的 那么问题来了 随机生成的KEY怎么能让服务器知道呢 不像IV能拼接在密文前面
其实在native_init
的时候就生成了随机的KEY 并且每次打开App都只生成一次后不变动
那猜测一定是在打开App的时候将KEY上传了
打开抓包 并重新打开App 通过观察 可以看到频繁出现了X-Xc-Proto-Req
和X-Xc-Proto-Res
X-Xc-Proto-Req
X-Xc-Proto-Res
最终还得回到so分析
主动调用getProtocolKey
public void getProtocolKey() { ArrayList<Object> args = new ArrayList<>(2); args.add(vm.getJNIEnv()); args.add(0); Number number = module.callFunction(emulator, 0x4a419, args.toArray()); System.out.println(vm.getObject(number.intValue()).getValue().toString()); }
发现在native_init
后就生成了X-Xc-Proto-Req
所以应该是在初始化的时候就生成了KEY顺便也生成了X-Xc-Proto-Req
进入sub_4A068
->sub_5CD9C
进入sub_5D4D8
可以发现这是一个SHA256withRSA
asc_1C9E70
为公钥
将随机的KEY进行加密后作为X-Xc-Proto-Req
上传后即为成功上传KEY 然后会返回X-Xc-Proto-Res
在下次请求时作为标识带上即可
decodeAES
如名字所示 就是AES解密 KEY不变 IV为返回内容的前16位
自己实现AES解密后发现 返回的内容还是乱码 转为HEX时发现1F 8B 08
字眼
这是GZIP的头部标识
此时 只需要再将解密出来的内容再进行GZIP解密即可
在Python中实现
感谢各位大佬观看
感谢大佬们的文章分享
如有错误 还请海涵
共同进步 带带弟弟
点赞 在看 分享是你对我最大的支持
逆向lin狗