0x01 IOS分析 拿到的是IOS版本的报文随机先直接解压IPA包
解压提取出来的二进制文件放入IDA里面发现会反编译失败 拖进去直接会显示
说明ios是有壳的随机使用 frida-ios-dump开始砸壳 砸壳后开始反编译完成后搜索字符串
发现是在此函数中调用
在此函数中定位关键位置 两个接口 并发现setHttpbody位置 可以看出v58就是setHttpbody的内容 继续往上跟 v58来自v55 v55是v54编码后 v54来自v53 v53来自v52 v52来自v51来自InnoSecureMain类的innoSecureEncode方法返回值 v48就是传进去的参数(应该是明文数据)
继续分析InnoSecureMain类的innoSecureEncode方法直接定位关键函数
IDA反编译失败
汇编代码如下
除了Encode函数其实还有Decode函数
其实可以看这里有很多常量 可以google 或者github搜一下这些常量 但是本次搜索无结果
此函数的汇编更加长
可以发现现在需要一步步看汇编 因为不想看那么多汇编 太浪费时间 转而分析Android端
0x02 分析Android 下载X键盘Android端
直接拖进GDA发现dex有360的壳 一开始使用FRAT的frida版 发现脱不出来 随即使用 脱壳工具整理:https://blog.csdn.net/u010671061/article/details/122085944 https://github.com/lasting-yang/frida_dump 最后使用frida_dump成功脱壳 脱出来结果拷贝出来如下
塞回apk中使用apktool反编译 导入android studio搜索关键词 cloudpredict继续跟踪 同时配合GDA或者JADX来看代码
在这里继续跟踪当然也可以直接打断点调试 发现最后发送的请求会在这里
IMCoreService.c方法发现和IMCoreService.a方法对应native接口如下
定位so
把so拖进ida反编译
(一)首先看函数名 发现都是去符号的sub函数
(二)关键词encrypt decrypt等等 继续往上跟一无所获
(三)看initarray 把这里所有的sub函数都hook一遍发现没有一个会走的
(四)Hook registernative函数 看注册了哪些函数 但是一hook就崩溃 (可能是反hook不一定)
var RevealNativeMethods = function() {
var pSize = Process.pointerSize;
var env = Java.vm.getEnv();
var RegisterNatives = 215, FindClassIndex = 6;
var jclassAddress2NameMap = {};
function getNativeAddress(idx) {
return env.handle.readPointer().add(idx * pSize).readPointer();
}
Interceptor.attach(getNativeAddress(FindClassIndex), {
onEnter: function(args) {
jclassAddress2NameMap[args[0]] = args[1].readCString();
}
});
Interceptor.attach(getNativeAddress(RegisterNatives), {
onEnter: function(args) {
for (var i = 0, nMethods = parseInt(args[3]); i < nMethods; i++) {
var structSize = pSize * 3;
var methodsPtr = ptr(args[2]);
var signature = methodsPtr.add(i * structSize + pSize).readPointer();
var fnPtr = methodsPtr.add(i * structSize + (pSize * 2)).readPointer();
var jClass = jclassAddress2NameMap[args[0]].split('/');
var methodName = methodsPtr.add(i * structSize).readPointer().readCString();
var str_name_so = "libPinyinCore.so";
var n_addr_so = Module.findBaseAddress(str_name_so);
console.log('\x1b[3' + '6;01' + 'm', JSON.stringify({
module: DebugSymbol.fromAddress(fnPtr)['moduleName'],
package: jClass.slice(0, -1).join('.'),
class: jClass[jClass.length - 1],
method: methodName,
signature: signature.readCString(),
address: (fnPtr-n_addr_so).toString(16)
}), '\x1b[39;49;00m');
}
}
});
}
function hook_java() {
Java.perform(RevealNativeMethods);
}
function main() {
hook_java();
}
setImmediate(main);
(五)JNIOnload分析 到关键函数发现被处理过了 不得不说很大可能是有意而为之
继续点进去看 Coreencryptda
Coredecryptdat
都是反编译失败的十六进制数据现在就是难办了 找不到注册的函数
0x03 无其他方法下定位关键位置
native输入输出参数分析 这里有点猫腻 进去是222字节 出来也是222字节 首先排除块加密 很大概率就是字节处理 说到字节处理那经常用到的不就是异或吗 直接搜异或的相关指令
边搜边使用frida进行hook 打调用栈 看看会不会调用 前面反编译失败的十六进制位置 这个时候就是看指令对不对头 这个看起来不对劲就hook一下 但是我第一次hook就成功了(瞎猫碰上死耗子)
hook sub_24D6C 调用栈如下:(这里其实也可以直接看函数参数 也能发现有报文内容)
RegisterNatives called from:\n0x55a48d8b libPinyinCore.so!0x40d8b\n0x55ae9c09 libPinyinCore.so!0xe1c09\n0xeb6fc2df libart.so!art_quick_generic_jni_trampoline+0x2e\n0xeb6f77d7 libart.so!art_quick_invoke_stub_internal+0x46\n0xebb1450d libart.so!art_quick_invoke_static_stub+0x118\n0xeb750897 libart.so!_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc+0xaa\n0xeb8649c3 libart.so!_ZN3art11interpreter34ArtInterpreterToCompiledCodeBridgeEPNS_6ThreadEPNS_9ArtMethodEPNS_11ShadowFrameEtPNS_6JValueE+0x102\n0xeb85c6e3 libart.so!_ZN3art11interpreter6DoCallILb0ELb0EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE+0x2ee\n0xeb704fd5 libart.so!_ZN3art11interpreter20ExecuteSwitchImplCppILb0ELb0EEEvPNS0_17SwitchImplContextE+0x761c\n0xeb6fce77 libart.so!ExecuteSwitchImplAsm+0x6\n0xeb8556b3 libart.so!_ZN3art11interpreterL7ExecuteEPNS_6ThreadERKNS_20CodeItemDataAccessorERNS_11ShadowFrameENS_6JValueEbb.llvm.94699277460487538+0x186\n0xeb85bf0d libart.so!_ZN3art11interpreter33ArtInterpreterToInterpreterBridgeEPNS_6ThreadERKNS_20CodeItemDataAccessorEPNS_11ShadowFrameEPNS_6JValueE+0x94\n0xeb85c6cb libart.so!_ZN3art11interpreter6DoCallILb0ELb0EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE+0x2d6\n0xeb704fd5 libart.so!_ZN3art11interpreter20ExecuteSwitchImplCppILb0ELb0EEEvPNS0_17SwitchImplContextE+0x761c\n0xeb6fce77 libart.so!ExecuteSwitchImplAsm+0x6\n0xeb8556b3 libart.so!_ZN3art11interpreterL7ExecuteEPNS_6ThreadERKNS_20CodeItemDataAccessorERNS_11ShadowFrameENS_6JValueEbb.llvm.94699277460487538+0x186\n
按G跳转 0xe1c09
往上拉 这不刚好就是刚刚反汇编失败的
(关于定位解密response body位置 也是直接搜异或因为定位到了 加密 解密指令应该也差不多 也可以分析一下加密流程也能得出结果) 或者直接搜加密的 ORR.W指令 刚好紧跟着就是response body解密
0x04 汇编分析 定位到位置 现在就是分析解密逻辑了
我们发现v13只有定义没有赋值 结合汇编来看可以看出 R1是v13 指令 LDRB.W R0,[R8,R1] 表示从R8 偏移 R1加载到R0 那么 R1从哪里来的 最近一次使用R1 是在 mov R1,R9 也就是函数sub_1D1C34的第二个参数 那也有可能R1是在这里面被改变了
hook sub1D1C34这个函数发现返回值就是传进去的参数 因此可以说明这个函数只是改变R1值作用 其他并没有实际作用 只是为了混淆我们观察得对象 接下来我们直接hook内存地址 不hook函数 直接hook 0x24E10这个地址 第一个参数就是R0 第二个参数就是我们想要得
var R1_addr = libPinyinCore.add(0x24E10+1);
console.log("R1_addr:",R1_addr);
Interceptor.attach(R1_addr,{
onEnter: function(args){
console.log(args[0],"---------------------hook2 on enter",args[1]," r4:",args[4]);
},onLeave: function(retval){
console.log(args[0],"---------------------hook2 on leave",args[1]);
}
})
hook出来的结果为 可以发现第二个参数R1 就是1-12循环往复 到此解密逻辑分析完毕(关于为什么第一个参数是author:v-max的第一位 因为00024E10这个内存地址的指令还没有执行完 所以值暂时没有改变 EORS R0, R4 指令执行完后会把结果放到R0 而我们hook的是在onEnter阶段 这也进一步验证了frida 内存劫持以及插桩的原理)
0x61 ---------------------hook2 on enter 0x0 r4: 0xc
0x75 ---------------------hook2 on enter 0x1 r4: 0xc
0x74 ---------------------hook2 on enter 0x2 r4: 0xc
0x68 ---------------------hook2 on enter 0x3 r4: 0xc
0x6f ---------------------hook2 on enter 0x4 r4: 0xc
0x72 ---------------------hook2 on enter 0x5 r4: 0xc
0x3a ---------------------hook2 on enter 0x6 r4: 0xc
0x76 ---------------------hook2 on enter 0x7 r4: 0xc
0x2d ---------------------hook2 on enter 0x8 r4: 0xc
0x6d ---------------------hook2 on enter 0x9 r4: 0xc
0x61 ---------------------hook2 on enter 0xa r4: 0xc
0x78 ---------------------hook2 on enter 0xb r4: 0xc
0x61 ---------------------hook2 on enter 0x0 r4: 0xc
0x75 ---------------------hook2 on enter 0x1 r4: 0xc
0x74 ---------------------hook2 on enter 0x2 r4: 0xc
0x68 ---------------------hook2 on enter 0x3 r4: 0xc
0x6f ---------------------hook2 on enter 0x4 r4: 0xc
0x72 ---------------------hook2 on enter 0x5 r4: 0xc
0x3a ---------------------hook2 on enter 0x6 r4: 0xc
0x76 ---------------------hook2 on enter 0x7 r4: 0xc
0x2d ---------------------hook2 on enter 0x8 r4: 0xc
0x6d ---------------------hook2 on enter 0x9 r4: 0xc
0x61 ---------------------hook2 on enter 0xa r4: 0xc
0x78 ---------------------hook2 on enter 0xb r4: 0xc
0x61 ---------------------hook2 on enter 0x0 r4: 0xc
0x75 ---------------------hook2 on enter 0x1 r4: 0xc
0x74 ---------------------hook2 on enter 0x2 r4: 0xc
0x68 ---------------------hook2 on enter 0x3 r4: 0xc
0x6f ---------------------hook2 on enter 0x4 r4: 0xc
0x72 ---------------------hook2 on enter 0x5 r4: 0xc
0x3a ---------------------hook2 on enter 0x6 r4: 0xc
0x76 ---------------------hook2 on enter 0x7 r4: 0xc
解密代码编写完成后 发现也能解android端的另外一个接口和ios端的两个接口 至此分析完成
#include <iostream>
#include <string.h>
#include <stdlib.h>
using namespace std;
static const int B64_LINE_LENGTH = 76;
static const int LINE_GROUPS = 19;
static const char CRLF_CR = '\r';
static const char CRLF_LF = '\n';
static const char PADDING_CHAR = '=';
static const unsigned char data_bin2ascii[65] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static const unsigned char data_ascii2bin[128] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xE0, 0xF0, 0xFF, 0xFF, 0xF1, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0x3E, 0xFF, 0xF2, 0xFF, 0x3F,
0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B,
0x3C, 0x3D, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF,
0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20,
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30,
0x31, 0x32, 0x33, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
};
static unsigned char conv_ascii2bin(unsigned char a, const unsigned char* table) {
return table[a];
}
size_t decodeB64(const unsigned char* in, unsigned char* out, size_t len) {
size_t outLength = 0;
int groupIndex = 0;
unsigned long l;
const unsigned char* table = data_ascii2bin;
unsigned char group[4];
unsigned char temp;
for (int i = 0; i < len; i++) {
temp = *(in++);
if (temp == PADDING_CHAR) {
break;
}
if (temp == CRLF_CR || temp == CRLF_LF) {
continue;
}
group[groupIndex++] = conv_ascii2bin(temp, table);
if (groupIndex == 4) {
l = ((((unsigned long)group[0]) << 18L) |
(((unsigned long)group[1]) << 12L) |
(((unsigned long)group[2]) << 6L) | (((unsigned long)group[3])));
*(out++) = (unsigned char)(l >> 16L) & 0xff;
*(out++) = (unsigned char)(l >> 8L) & 0xff;
*(out++) = (unsigned char)(l) & 0xff;
outLength += 3;
groupIndex = 0;
}
}
if (groupIndex > 0) {
l = ((((unsigned long)group[0]) << 18L) |
(((unsigned long)group[1]) << 12L) |
(((unsigned long)group[2]) << 6L) | (((unsigned long)group[3])));
*(out++) = (unsigned char)(l >> 16L) & 0xff;
if (groupIndex == 3) {
*(out++) = (unsigned char)(l >> 8L) & 0xff;
}
outLength += groupIndex - 1;
}
*out = '\0';
return outLength;
}
int main()
{
const char *base = "AWOAo2PpgQgja8pJ6CChA0HpYArrAQKhgMAAZQDgikAJ44sp6mjIq4tJYcnDqypJ6MJjwGPiSEACaUDiYuAiYWNnisALI4vJ6ihISykiqoDrAUCBRsJjQ0HpYcmCaw==";
int outBufLen = 2000;
unsigned char *input = new unsigned char[strlen(base)];
memset(input,0,strlen(base));
int outLength = decodeB64((unsigned char *)base, input, strlen(base));
uint8_t str[] = {0x61,0x75,0x74,0x68,0x6F,0x72,0x3A,0x76,0x2D,0x6D,0x61,0x78};
uint8_t* result = new uint8_t[strlen((char *)input)];
for (int i = 0; i < outLength; i++)
{
result[i] = ((input[i] >> 5) | (input[i] << 3)) ;
result[i] = result[i] ^ str[i % 12];
}
for (int j = 0; j < outLength; j++) {
printf("%c", result[j]);
}
return 0;
}
|