qimao小說sign字段逆向及unidbg實現(xiàn)
Java層
apk是加固的,這次脫殼使用的是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都是調(diào)用同一個加密函數(shù)。
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
雖然找到了native函數(shù)甫煞,但是看不出是在哪個so里面注冊的鱼的。
對a
函數(shù)查找用例
com.qimao.qmsdk.tools.encryption.Encryption.init
對init
函數(shù)查找用例
defpackage.qf.run
看來就是在libcommon-encryption.so
注冊的慈鸠。
so層
由于手機(jī)和app都支持64位指令逝段,所以分析的是64位so
函數(shù)窗口搜索java
有點奇怪私股,其他幾個函數(shù)都有了刷允,唯獨少了sign函數(shù)冤留。每個都點進(jìn)去看看
在Java_com_km_encryption_api_Security_token
這個函數(shù)里看到了Java_com_km_encryption_api_Security_sign
,難道它們是同一個函數(shù)恃锉?看看這個函數(shù)
從它的實現(xiàn)來看搀菩,就是在Java層的輸入后面加了個keyData
,然后做個MD5破托,這很大概率就是sign函數(shù)肪跋,因為簽名就是32位長度的。
看看MessageDigestAlgorithm::MessageDigestAlgorithm
函數(shù)
看看MessageDigestAlgorithm::init
hook一下MessageDigestAlgorithm::init
函數(shù)
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) {
}
})
}
只有第一次的是輸入土砂,其他的是算法的填充州既。cyberchef上驗證一下是不是標(biāo)準(zhǔn)MD5
沒有問題,是對的萝映。
header的sign調(diào)用的也是這個函數(shù)吴叶,只是輸入不一樣。
unidbg實現(xiàn)
習(xí)慣性選擇調(diào)用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();
}
}
然后就是報錯和補環(huán)境
@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);
}
開始正式調(diào)用
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();
}
日志里可以看到sign
函數(shù)的地址,跳轉(zhuǎn)過去也是Java_com_km_encryption_api_Security_token
這個函數(shù)
繼續(xù)補環(huán)境
@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);
}
報錯了奥秆,但看不出什么逊彭。不過如果對AssetManager在native層的實現(xiàn)有所了解的話,就知道它是通過libandroid.so
實現(xiàn)的构订∥甓#可惜的是unidbg并沒有實現(xiàn)這個so,不過它提供了一個Android VirtualModule
悼瘾,實現(xiàn)了libandroid.so
中的幾個函數(shù)囊榜。
從打印的日志也可以看到审胸,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);
}
再次運行
還是報錯了卸勺,這次跳轉(zhuǎn)到0xfd29
看看砂沛。
可以看到調(diào)用了幾個AAsset_*
函數(shù),可惜的是目前unidbg并沒有實現(xiàn)其中的AAsset_seek
函數(shù)
所以需要自己實現(xiàn)一下孔庭,在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;
}
重新運行
恢復(fù)正常的報錯了尺上,getKey()
需要返回一個字符串,jadx看看這個類圆到。
可以看到是返回成員變量key
怎抛,可以使用objection + Wallbreaker
查看
plugin wallbreaker classdump com.km.encryption.generator.KeyGenerator
所以返回"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);
}
和抓包結(jié)果一樣。
完整代碼
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();
}
}