qimao小说sign字段逆向及unidbg实现
Java层
image-20220123212829789apk是加固的,这次脱壳使用的是frida_dump
frida -U --no-pause -f com.kmxs.reader -l dump_dex.js
然后再重新打包
import pathlib
import zipfile
def get_files(dex_dir):
fdir = pathlib.Path(dex_dir)
infos = {}
for item in fdir.glob('*.dex'):
size = item.stat().st_size
infos[size] = item
fdict = {}
for idx, key in enumerate(sorted(infos, reverse=True)):
name = 'classes{}.dex'.format(str(idx) if idx else '')
fdict[name] = infos[key]
return fdict
def pack(apk_path, dex_dir):
dst = apk_path + '.pack.apk'
with zipfile.ZipFile(apk_path) as zf, zipfile.ZipFile(dst, 'w') as zout:
for item in zf.infolist():
if item.filename.startswith('classes') and item.filename.endswith('.dex'):
print('Ignore:', item.filename)
else:
buffer = zf.read(item.filename)
zout.writestr(item, buffer)
for filename, fpath in get_files(dex_dir).items():
print('Add:', fpath)
zinfo = zipfile.ZipInfo(filename)
with open(fpath, 'rb') as fin:
zout.writestr(zinfo, fin.read())
if __name__ == '__main__':
pack(r"E:\workspace\qimao\qimao613.apk", r"E:\workspace\qimao\dump_dex_com.kmxs.reader")
然后jadx打开搜索"sign"
可以看到url和header里面的sign都是调用同一个加密函数。
com.km.repository.net.config.interceptor.HeaderInterceptor.b
com.qimao.qmsdk.tools.encryption.Encryption.sign
com.km.encryption.api.Security.sign
先hook看看
android hooking watch class_method com.km.encryption.api.Security.sign --dump-args --dump-return
image-20220123140659715
image-20220123140752489
虽然找到了native函数,但是看不出是在哪个so里面注册的。
对a
函数查找用例
com.qimao.qmsdk.tools.encryption.Encryption.init
对init
函数查找用例
defpackage.qf.run
看来就是在libcommon-encryption.so
注册的。
so层
由于手机和app都支持64位指令,所以分析的是64位so
函数窗口搜索java
有点奇怪,其他几个函数都有了,唯独少了sign函数。每个都点进去看看
image-20220123003710978在Java_com_km_encryption_api_Security_token
这个函数里看到了Java_com_km_encryption_api_Security_sign
,难道它们是同一个函数?看看这个函数
从它的实现来看,就是在Java层的输入后面加了个keyData
,然后做个MD5,这很大概率就是sign函数,因为签名就是32位长度的。
看看MessageDigestAlgorithm::MessageDigestAlgorithm
函数
看看MessageDigestAlgorithm::init
hook一下MessageDigestAlgorithm::init
函数
function dump(name, addr, length) {
console.log("======================== " + name + " ============");
console.log(hexdump(addr, {length:length||32}));
}
function hook_key(){
var bptr = Module.findBaseAddress("libcommon-encryption.so");
Interceptor.attach(bptr.add(0x19394), {
onEnter: function(args) {
console.log(args[0], args[1], args[2]);
dump("input", args[1], parseInt(args[2]));
},
onLeave: function(retval) {
}
})
}
image-20220123142052258
只有第一次的是输入,其他的是算法的填充。cyberchef上验证一下是不是标准MD5
image-20220123142115586没有问题,是对的。
header的sign调用的也是这个函数,只是输入不一样。
unidbg实现
习惯性选择调用32位的so,按照惯例,搭个框架
public class Qimao extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
public static String pkgName = "com.kmxs.reader";
public static String apkPath = "unidbg-android/src/test/java/com/qimao/qimao613.apk";
public static String soPath = "";
public Qimao() {
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName(pkgName).build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File(apkPath));
vm.setJni(this);
vm.setVerbose(true);
DalvikModule dm = vm.loadLibrary("common-encryption", true);
module = dm.getModule();
dm.callJNI_OnLoad(emulator);
}
public static void main(String[] args) {
Qimao test = new Qimao();
}
}
然后就是报错和补环境
image-20220123194410678@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "java/lang/Class->getClassLoader()Ljava/lang/ClassLoader;": {
return new ClassLoader(vm, signature);
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
image-20220123195042845
开始正式调用
public void call_sign() {
DvmClass clz = vm.resolveClass("com/km/encryption/api/Security");
String methodSign = "sign([B)Ljava/lang/String;";
StringObject ret = clz.callStaticJniMethodObject(emulator, methodSign, new ByteArray(vm, "book_privacy=1cache_ver=1642759975gender=2read_preference=2tab_type=2".getBytes(StandardCharsets.UTF_8)));
}
public static void main(String[] args) {
Qimao test = new Qimao();
test.call_sign();
}
image-20220123214354010
日志里可以看到sign
函数的地址,跳转过去也是Java_com_km_encryption_api_Security_token
这个函数
继续补环境
@Override
public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
switch (signature) {
case "com/km/encryption/generator/KeyGenerator->assetManager:Landroid/content/res/AssetManager;": {
return new AssetManager(vm, signature);
}
}
return super.getStaticObjectField(vm, dvmClass, signature);
}
image-20220123195727498
报错了,但看不出什么。不过如果对AssetManager在native层的实现有所了解的话,就知道它是通过libandroid.so
实现的。可惜的是unidbg并没有实现这个so,不过它提供了一个Android VirtualModule
,实现了libandroid.so
中的几个函数。
从打印的日志也可以看到,libcommon-encryption.so
依赖了libandroid.so
。
public Qimao() {
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName(pkgName).build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File(apkPath));
vm.setJni(this);
vm.setVerbose(true);
new AndroidModule(emulator, vm).register(memory); // Load AndroidModule
DalvikModule dm = vm.loadLibrary("common-encryption", true);
module = dm.getModule();
dm.callJNI_OnLoad(emulator);
}
再次运行
image-20220123203430887还是报错了,这次跳转到0xfd29
看看。
可以看到调用了几个AAsset_*
函数,可惜的是目前unidbg并没有实现其中的AAsset_seek
函数
所以需要自己实现一下,在unidbg-android/src/main/java/com/github/unidbg/virtualmodule/android/AndroidModule.java
添加代码
@Override
protected void onInitialize(Emulator<?> emulator, final VM vm, Map<String, UnidbgPointer> symbols) {
// ..
symbols.put("AAsset_seek", svcMemory.registerSvc(is64Bit ? new Arm64Svc() {
@Override
public long handle(Emulator<?> emulator) {
return seek(emulator, vm);
}
} : new ArmSvc() {
@Override
public long handle(Emulator<?> emulator) {
return seek(emulator, vm);
}
}));
}
private static int seek(Emulator<?> emulator, VM vm) {
RegisterContext context = emulator.getContext();
UnidbgPointer pointer = context.getPointerArg(0);
int offset = context.getIntArg(1);
int whence = context.getIntArg(2);
if (log.isDebugEnabled()) {
log.debug("AAset_seek pointer=" + pointer + ", offset=" + offset + ", whence=" + whence + ", LR=" + context.getLRPointer());
}
final int SEEK_SET = 0;
final int SEEK_CUR = 1;
final int SEEK_END = 2;
if ((whence == SEEK_SET && offset >= 0) || whence == SEEK_CUR || whence == SEEK_END) {
Asset asset = vm.getObject(pointer.toIntPeer());
return asset.seek(offset, whence);
}
throw new BackendException("offset=" + offset + ", whence=" + whence + ", LR=" + context.getLRPointer());
}
在unidbg-android/src/main/java/com/github/unidbg/linux/android/dvm/api/Asset.java
添加代码
public int seek(int offset, int whence) {
Pointer pointer = memoryBlock.getPointer();
int index = pointer.getInt(0);
int length = pointer.getInt(4);
final int SEEK_SET = 0;
final int SEEK_CUR = 1;
final int SEEK_END = 2;
if (whence == SEEK_SET) {
index = offset;
}
else if (whence == SEEK_CUR) {
index = index + offset;
}
else if (whence == SEEK_END) {
index = length + offset;
}
pointer.setInt(0, index);
return index;
}
重新运行
image-20220123205804729恢复正常的报错了,getKey()
需要返回一个字符串,jadx看看这个类。
可以看到是返回成员变量key
,可以使用objection + Wallbreaker
查看
plugin wallbreaker classdump com.km.encryption.generator.KeyGenerator
image-20220123210219431
所以返回"8w1"
@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature) {
case "com/km/encryption/generator/KeyGenerator->getKey()Ljava/lang/String;": {
return new StringObject(vm, "8w1");
}
}
return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}
image-20220123210350433
和抓包结果一样。
完整代码
public class Qimao extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
public static String pkgName = "com.kmxs.reader";
public static String apkPath = "unidbg-android/src/test/java/com/qimao/qimao613.apk";
public static String soPath = "";
public Qimao() {
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName(pkgName).build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File(apkPath));
vm.setJni(this);
vm.setVerbose(true);
new AndroidModule(emulator, vm).register(memory);
DalvikModule dm = vm.loadLibrary("common-encryption", true);
module = dm.getModule();
dm.callJNI_OnLoad(emulator);
}
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "java/lang/Class->getClassLoader()Ljava/lang/ClassLoader;": {
return new ClassLoader(vm, signature);
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
@Override
public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
switch (signature) {
case "com/km/encryption/generator/KeyGenerator->assetManager:Landroid/content/res/AssetManager;": {
return new AssetManager(vm, signature);
}
}
return super.getStaticObjectField(vm, dvmClass, signature);
}
@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature) {
case "com/km/encryption/generator/KeyGenerator->getKey()Ljava/lang/String;": {
return new StringObject(vm, "8w1");
}
}
return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}
public void call_sign() {
DvmClass clz = vm.resolveClass("com/km/encryption/api/Security");
String methodSign = "sign([B)Ljava/lang/String;";
StringObject ret = clz.callStaticJniMethodObject(emulator, methodSign, new ByteArray(vm, "book_privacy=1cache_ver=1642759975gender=2read_preference=2tab_type=2".getBytes(StandardCharsets.UTF_8)));
// System.out.println("sign:" + ret.getValue());
}
public static void main(String[] args) {
Qimao test = new Qimao();
test.call_sign();
}
}
网友评论