查壳与修复
使用MT管理器对目标应用进行查壳,结果显示其使用了360加固。我们可以通过在线脱壳网站(如 https://56.al/)直接进行脱壳,也可以手动寻找脱壳点进行dump。由于本文侧重于算法还原,因此选择使用在线网站完成脱壳。
脱壳后可以得到原始的dex文件。

将得到的dex文件放入MT管理器中进行修复(若无MT会员,也可使用NP管理器)。修复完成后,将这些dex文件替换到原始APK中(删除原dex文件)。至此,我们已经可以将其放入jadx等反编译工具中进行分析。需要注意的是,由于应用存在onCreate方法被抽取以及残留大量stub特征的情况,若需进行深度分析,建议使用Android Studio动态调试,根据log报错定位问题点。对于大量相似的stub代码,可使用MT管理器的正则匹配功能批量替换为null(部分位置可能无法替换,需根据具体情况判断),此处不展开详述。
抓包分析
抓包配置
本次分析采用Reqable转发至BurpSuite的方式进行抓包,具体配置如下:


随后在BurpSuite中开启对应的端口转发监听。

通过此配置即可成功抓取到应用网络请求。
抓包检测绕过
该应用本身没有强力的抓包检测,但最初未能抓取到请求,重启手机后解决。此前在分析字节系应用时也遇到过类似问题,其通常会魔改libsscronet.so库以实现检测。由于目标应用的lib目录下未发现此文件,大概率未进行魔改,但此处仍将相关绕过方法进行分享。
通常需要从内存中dump libsscronet.so进行分析。

dump so的Frida脚本如下:
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即可绕过检测。
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层请求参数分析 (一)
首先对抓包中获取到的两个参数进行加密还原分析。
Java层定位
在jadx中搜索参数关键词,可定位到相关代码。



代码逻辑主要是对请求类型进行判断。

Java层核心是调用了jmd方法,该方法进一步调用了Native层的函数。


该Native函数被隐藏,需通过分析汇编代码查找其注册函数表。


继续分析,未发现明显的加密逻辑,但此处是重点。代码中加载了一些文件,具体逻辑未完全理清。我们采用主动调用并测试的方法。
let JNISecurity = Java.use("com.kanman.JNISecurity");
var str1 = "1"
var str2 = "2"
var ret1 = JNISecurity.jmd(str1, str2)
console.log("jmd加密:", ret1)

得到的结果看起来像是MD5,但由于次数限制,我们使用算法自吐脚本来辅助分析。
function hook_tess() {
Java.perform(function () {
// 算法自吐脚本(节选,包含MD5、SHA等摘要算法hook)
var messageDigest = Java.use('java.security.MessageDigest');
// ... 详细的hook实现,用于打印算法调用栈和输入输出
});
}
运行脚本后,可以发现关键调用。


输出显示,输入字符串与一个固定字符串进行了拼接,该字符串同样存在于so层,通过搜索或交叉引用可以定位到MD5函数内部。

至此逻辑对上,可以使用Python代码实现该算法。
Java层请求参数分析 (二)
分析m-request-did参数,同样通过jadx搜索定位。

该参数为加密后的设备码。

定位到具体加密方法进行分析。


逻辑清晰,还原代码如下:
Hook代码:
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;
};
Python还原算法:
from Cryptodome.Cipher import AES
import base64
from Cryptodome.Util.Padding import pad, unpad
def encryaes(data,key):
cipher=AES.new(key.encode('utf-8'),AES.MODE_ECB)
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层请求参数分析 (三)
Java层定位

Hook及主动调用代码:
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]);
let i = 4;
// 主动调用
let result = Orange.getClock(context, bArr, i);
console.log(" 参数:", "bArr", bArr, "i:", i)
console.log(' 返回值: ' + result);
console.log("=====")
})
}
hook_sig1()

继续搜索相关方法。

找到核心Native函数。

深入分析,最终定位到so库及其代码。
So层代码分析
该so使用静态注册,但包含大量花指令。


花指令主要由无用的死代码构成,影响静态分析,需要进行NOP修复。为此编写了IDAPython脚本进行自动化处理。
import ida_bytes
import idc
import idaapi
# ... 具体的花指令定位与修复代码
运行脚本后,代码可读性大大增强。


接下来进行静态分析,并辅以Frida脚本进行动态验证。
// 用于辅助静态分析的Frida Hook脚本(节选)
function hook_so_sig1() {
var offest = 0x3F48;
var soName = 'libsgcore.so';
const module = Process.findModuleByName(soName)
Interceptor.attach(module.base.add(offest), {
onEnter: function (args) {
console.log("开始调用参数")
console.log(args[0])
console.log(args[1])
console.log(Memory.readByteArray(this.context.x1, 0x40))
},
onLeave: function (retval) {
console.log("返回值", retval)
}
})
}
以下是核心Native函数的伪代码分析摘要:
jstring __fastcall Java_com_kwai_sgcore_SGCore_getClock(JNIEnv *JNIEnv, __int64 a2, __int64 a3, void *a4)
{
// ... 变量声明、初始化及错误检查
// 关键步骤:
// 1. 将输入参数与固定字符串"ca8e86efb32e"拼接。
inp += "ca8e86efb32e";
// 2. 对拼接后的字符串进行MD5运算。
MD5_state(state);
MD5_update(state, inp, inp_len);
// 3. 对MD5结果的前15字节求和,并根据和值对前15字节进行异或扰动。
for(i=0; i<15; i++) sum += md5_bytes[i];
for(i=0; i<15; i++) tmp[i] = md5_bytes[i] ^ i ^ (sum & 0xff);
tmp[15] = sum & 0xff;
// 4. 进行第二轮异或扰动(与一个固定key相关)。
// 5. 调用 encryfinal_xor 函数进行最终加密。
result = encryfinal_xor(tmp, 16, fixed_key);
// 6. 将结果转换为十六进制字符串返回。
return hex_string;
}
encryfinal_xor函数的伪代码如下:
void __fastcall encryfinal_xor(const void *src, size_t num_16, int key)
{
// 分配内存,构建一个特定结构体,包含固定头部和输入的16字节数据。
// 对结构体的前23字节求和。
// 根据和值,对结构体数据进行异或扰动。
for(i=1; i<23; i++) {
output[i] = input[i] ^ i ^ (sum & 0xff);
}
// 返回处理后的数据。
}
为了验证算法,使用Unidbg进行了模拟执行。
public class kanmanenc {
// ... Unidbg环境初始化
// 调用so中的关键函数 (0x22D0)
module.callFunction(emulator, 0x22D0, srcPtr, num_16, key);
}
综合动态与静态分析,最终还原出sig1参数的生成Python代码:
import hashlib
def sig1(inp:str):
inp += "ca8e86efb32e" # MD5盐值
md5_str = hashlib.md5(inp.encode('utf-8')).hexdigest()
s = list(bytes.fromhex(md5_str))
# 第一轮异或扰动
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)
else:
tmp1.append(sums1 & 0xff)
# 第二轮异或扰动 (与固定key: 0xb0da86aa 相关)
key1 = [0x1, 0x1, 1, 0xaa, 0x86, 0xda, 0xb0]
tmp2 = key1 + tmp1
sums2 = sum(tmp2[:23]) & 0xff if (sum(tmp2[:23]) < 0xff) else (-sum(tmp2[:23])) & 0xffff
tmp2[0] = (sums2 ^ 1) & 0xff
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)
final_str = "".join(f'{byte:02x}' for byte in final)
return final_str
登录请求分析还原
Java层定位
搜索x_data参数,定位到加密函数。

So层算法分析
跟踪发现,so层又调用了Java层的加密方法,本质是标准的AES-CBC加密,但使用了特定参数。



通过Frida Hook获取关键的Key和IV。
function hook_enc() {
Java.perform(function () {
// Hook AES加密相关类,打印密钥和IV
var SecretKeySpec = Java.use("javax.crypto.spec.SecretKeySpec");
SecretKeySpec.$init.overload('[B', 'java.lang.String').implementation = function (keyBytes, algorithm) {
console.log("Algorithm: " + algorithm + ", Key: " + bytesToHex(keyBytes));
return this.$init(keyBytes, algorithm);
};
var IvParameterSpec = Java.use("javax.crypto.spec.IvParameterSpec");
IvParameterSpec.$init.overload('[B').implementation = function (ivBytes) {
console.log("IV: " + bytesToHex(ivBytes));
return this.$init(ivBytes);
};
});
}
Hook结果显示,Key为4548ded8c9e02690,IV为1992360ee9bc4f8f。
登录请求的加解密Python实现如下:
import base64
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad, unpad
def 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'))
dec = cipher.decrypt(enc_bytes)
# 去除PKCS7填充
return unpad(dec, AES.block_size).decode('utf-8')
def login_encry(inp:str):
key="4548ded8c9e02690"
iv="1992360ee9bc4f8f"
cipher=AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))
# 添加PKCS7填充
padded_inp = pad(inp.encode('utf-8'), AES.block_size)
enc = cipher.encrypt(padded_inp)
return base64.b64encode(enc).decode('utf-8')
# 示例
encrypted_data = '0OFm2t/mYjPIhqlECGE+Hs+...' # 截断的密文
print(login_decry(encrypted_data))
plain_json = '{"client-channel":"qihoo","service":"qmmh","mobile":"xxxxxxxxxxx"...}'
print(login_encry(plain_json))
服务器返回的数据是一个PNG图片的base64编码,使用CyberChef等工具即可解码查看。
VIP功能初步分析
此部分未进行完整分析,仅定位到关键代码位置供参考。


总结
通过本次对目标应用的逆向分析,实践了360加固脱壳、Frida动态调试与脚本编写、Unidbg模拟执行、IDAPython去除花指令等多种技术。后续可进一步加强对加固应用脱壳后修复运行的能力。
免责声明: 本文所有技术内容仅用于安全研究与学习目的,严禁用于任何非法用途。如涉及侵权,请联系删除。