kuaidui作业unidbg逆向
nativeInitBaseUtil
在unidbg实现中,调用nativeGetSign
之前,需要先调用nativeSetToken
,这是因为需要先设置objSpamServer.random_number
而nativeSetToken
的入参又来自于nativeInitBaseUtil
先分析代码比较少的nativeInitBaseUtil
。
看看getChallenge
函数
很简单,生成随机10位字符串
剩下的函数就比较容易明白它的作用。CRYMd5
生成MD5,CRYStringCat
字符串拼接,DES_Encrypt
des加密,str2hex
将输入转为16进制字符串。现在需要分析的其实只有后面两个函数。
unidbg实现
先用unidbg调用nativeInitBaseUtil
,之后再结合ida来逆向
public class Kuaidui extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
public static String pkgName = "com.kuaiduizuoye.scan";
public static String apkPath = "unidbg-android/src/test/java/com/kuaidui/kuaidui540.apk";
public static String soPath = "";
public Kuaidui() {
emulator = AndroidEmulatorBuilder.for64Bit().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("baseutil", true);
module = dm.getModule();
dm.callJNI_OnLoad(emulator);
}
@Override
public int getStaticIntField(BaseVM vm, DvmClass dvmClass, String signature) {
switch (signature) {
case "android/content/pm/PackageManager->GET_SIGNATURES:I": {
return 64;
}
}
return super.getStaticIntField(vm, dvmClass, signature);
}
public void call_init() {
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv());
list.add(0);
list.add(vm.addLocalObject(vm.resolveClass("android/content/Context").newObject(null)));
list.add(vm.addLocalObject(new StringObject(vm, "F5D53AD5A66144B57783C7C67611F0F7|0")));
Number ret = module.callFunction(emulator, 0x1010, list.toArray());
}
public static void main(String[] args) {
Kuaidui test = new Kuaidui();
test.call_init();
}
报了个错,跳转过去看看
好像不支持,我们简单的把它加上去试试
出结果了,不过有一点不好的就是,每次运行生成的结果都不一样。这是因为getChallenge
函数里面的rand
函数导致的。将它hook住,改为固定值。
public void hook_libc() {
IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.enable_arm_arm64_b_branch();
hookZz.wrap(module.findSymbolByName("rand"), new WrapCallback<HookZzArm64RegisterContext>() {
@Override
public void preCall(Emulator<?> emulator, HookZzArm64RegisterContext ctx, HookEntryInfo info) {
}
@Override
public void postCall(Emulator<?> emulator, HookZzArm64RegisterContext ctx, HookEntryInfo info) {
ctx.setXLong(0, 1L);
}
});
}
public static void main(String[] args) {
Kuaidui test = new Kuaidui();
test.hook_libc();
test.call_init();
}
由于随机数的结果一直为1,所以getChallenge
的结果为BBBBBBBBBB
。
逆向
接下来开始逆向DES_Encrypt
和str2hex
hook看看参数
emulator.attach().addBreakPoint(module.base+0x61f4);
很容易构造出。
blr
在函数返回处下断点,c
运行到函数返回处,mx0
看看返回结果
有了输入,key和输出,在cyberchef验证下
emm,和标准结果差的不是一点半点,有可能是DES-CBC
,或者DES经过了魔改。
这里是实现块加密的代码,可以看出就是DES-ECB
,没有向量或其他东西。那说明DES很可能经过了魔改。
这部分是密钥生成部分,涉及到的常量有PC_1, MOVE_TIMES, PC_2
比较容易看到的修改就是PC_2
有个值被改了。
还有就是块加密部分的常量
实际操作上,直接把这些常量全部拷贝下来就行了,根本不用对比,免得自己看漏了。
另外一个就是byte转bit的时候高位低位是相反的,比如0x8d
在标准实现中是10001101
,这里则是11010001
。
在标准DES实现的基础上做出修改后,再调用试试
和unidbg hook到的结果一致。
接下来就是str2hex函数。
str2hex的输入就是DES_Encrypt
的结果,输出就是最后的结果。
其实对照着代码也挺容易实现的,就是把一个byte转成bit之后再逆序,然后再转成2个byte,最后变成hex形式。
def byte2hex(data):
"""
8d
|
10001101
|
10110001
/ \
1011 0001
| |
0b 01
"""
result = []
for v9 in data:
d = f'{v9:08b}'[::-1]
result.append(int(d[:4], 2))
result.append(int(d[4:], 2))
return bytes(result).hex()
把前面的拼接起来验证下
def calc_request_hex(challenge, devid, pkg_sign='2fb53de6d38eff7109f19d68e047123b'):
"""
Args:
challenge: length-of-10 random string, charset: a-zA-Z0-9
devid: cuid
pkg_sign: md5 of pkg signature, default: sign of v5.4.0
"""
data = '##'.join(('8&%d*', challenge, pkg_sign, devid))
data2 = encrypt_hex(data.encode(), b'@fG2SuLA')
return data2
def encrypt_hex(data, key):
cryptor = DES(key, padmode=PAD_PKCS5)
data2 = cryptor.encrypt(data)
data3 = byte2hex(data2)
return data3
def test_request_hex():
challenge = 'B' * 10
devid = 'F5D53AD5A66144B57783C7C67611F0F7|0'
req_str = calc_request_hex(challenge, devid)
print(req_str)
和unidbg结果一致。
nativeSetToken
nativeSetToken
和nativeInitBaseUtil
相比是负责解密数据。
其实主要也就这几个地方。
hex2Str
就是str2hex
的反向操作,重写一下即可。
def hex2byte(data):
"""
0b 01
| |
1011 0001
\ /
10110001
|
10001101
|
8d
"""
data2 = bytes.fromhex(data)
result = []
for idx in range(0, len(data2), 2):
d = f'{data2[idx]:04b}{data2[idx+1]:04b}'
result.append(int(d[::-1], 2))
return bytes(result)
接下来用hook到的真实数据试试。
先将请求数据解密看看
def decrypt_hex(data, key):
data2 = hex2byte(data)
cryptor = DES(key, padmode=PAD_PKCS5)
data3 = cryptor.decrypt(data2)
return data3
def test_decode2():
key = b'@fG2SuLA'
data = '03090b0106000807080e00080201090708060f00040700020d0503070c0606000c01000a080e07050808020d00060b0d070f0f080306060b00000f0d04070d0d02070b0f020a01010c0d030a09080e090b040f02080c0b040c0e0e060c0d0201020f0e02030d0107020d000e0e02090401070505030a0c0605080303040e0803020c020e0d020408010b030e0b090f060302000e0f0902010706040c00080d0e060d000f0805040b0e07000d020b0f07'
req_str = decrypt_hex(data, key)
print(req_str)
可以看到和nativeInitBaseUtil
中的形式是一样的。
然后是响应数据解密,它的key是原始请求数据的7-12位加上#G4
,也就是wobBX#G4
def test_decode():
key = b'wobBX#G4'
data = '0a040609080d020b0a03090e010306050d0d050c0c060207070105010b0b01090801000a020d0c0b03030209030e0d0a'
resp_str = decrypt_hex(data, key)
print(resp_str)
objSpamServer.random_number
映入眼帘。