固定参数-以京东sign逆向为例
前言
在逆向过程中,需要结合frida或unidbg,对整个算法进行一步步的分析,有时候我们分析完某一部分,想要继续往下分析时,需要重新发起请求,这时候的参数往往都已经改变了,这样会打断我们的节奏,影响效率。此外,有些算法除了我们外部传进去的参数外,还有一些其他的参数参与了加密,比如时间戳,随机数,一旦这些参与了算法,那么即使每次的传入参数不变,加密的结果还是会变。
外部输入参数的固定
frida
在京东app的hook中,我们选择编写一个函数,能够固定的调用Java层的native函数。
var bptr = Module.findBaseAddress('libjdbitmapkit.so')
console.log(bptr);
function hook_12ECC() {
Interceptor.attach(bptr.add(0x12ECC+1), {
onEnter: function(args) {
this.arg0 = args[0];
this.arg3 = args[3];
console.log(args[0], args[1], args[2], args[3], args[4]);
dump("arg0-ptr0", args[0].readPointer());
dump("arg0-ptr1", args[0]);
dump("arg0-ptr1", args[0].add(4).readPointer(), 64);
console.log("arg1", args[1].readCString());
// dump("arg2", args[2]);
console.log("arg3", args[3].readCString(parseInt(args[4])));
},
onLeave: function(){
console.log("arg0-ret", this.arg0);
dump("ret-arg0-ptr1", this.arg0);
// dump("ret-arg0-ptr2", this.arg0.add(8).readPointer());
dump("12ecc-ret-arg2", this.arg3)
}
})
}
function callBitmapkitUtils() {
var BitmapkitUtils = Java.use('com.jingdong.common.utils.BitmapkitUtils');
var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
var context = currentApplication.getApplicationContext();
var b = 'clientImage';
var c = '{"moduleParams":{"18":"1565611060638","19":"1565229712150","25":"1567478504636","27":"1602488415048","28":"1631069159956","30":"1567404005627","32":"1567997588476","34":"1593508185597","35":"1568708316462","37":"1630293538664","42":"1623741761542","44":"1569247647090","46":"1588839806224","47":"1571295610042","61":"1582091758495","70":"1585279774645","74":"1586781606615"}}';
var d = 'd5a585639f505b18';
var e = 'android';
var f = '10.2.0';
var res = BitmapkitUtils.getSignFromJni(context, b, c, d, e, f);
console.log('res: ', res);
}
Java.perform(function() {
hook_12ECC();
})
然后启动frida之后,可以在shell中输入callBitmapkitUtils()
来调用函数。
这样一来不像在手机上滑动页面、点击页面那样,有时会有多个请求发出,会多次调用加密的方法。这样的好处是,再也不用在手机上操作了,而且请求的内容和个数是可控的。不过我们注意到京东的参数里有st
和sv
这两个参数,这可不是由我们传入的,属于内部输入参数,接下来我们要固定它们。
unidbg
对于unidbg而言,在我们编写代码的时候,一般都是固定输入的
内部输入参数的固定
frida
对于内部输入参数而言,可能有时间戳,随机数,常量(数字或字符),其中前2个是会改变的,这会影响逆向分析,所以需要固定这两个参数。时间戳一般是调用libc.so
的gettimeofday
函数,随机数则是调用libc.so
的lrand48
或srand48
function hook_libc(){
var ptr_t = Module.findExportByName("libc.so", "gettimeofday");
Interceptor.attach(ptr_t, {
onEnter: function(args){
this.arg0 = args[0];
},
onLeave: function() {
this.arg0.writeU32(1639393559);
this.arg0.add(4).writeU32(0);
}
});
Interceptor.attach(Module.findExportByName("libc.so" , "lrand48"), {
onEnter: function(args) {
},
onLeave:function(retval){
retval.replace(1);
}
});
}
Java.perform(function() {
hook_libc();
hook_12ECC();
})
这样一来,即使多次运行,st
和sv
也不会改变,有利于我们的分析。
不过,固定时间戳有个问题,其他函数在获取时间戳的时候发现不对,可能会导致frida环境崩溃,所以我们希望只在调用的时候固定时间戳。所以我们更新如下代码
var logvar = 0;
function callBitmapkitUtils() {
var BitmapkitUtils = Java.use('com.jingdong.common.utils.BitmapkitUtils');
var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
var context = currentApplication.getApplicationContext();
var b = 'clientImage';
var c = '{"moduleParams":{"18":"1565611060638","19":"1565229712150","25":"1567478504636","27":"1602488415048","28":"1631069159956","30":"1567404005627","32":"1567997588476","34":"1593508185597","35":"1568708316462","37":"1630293538664","42":"1623741761542","44":"1569247647090","46":"1588839806224","47":"1571295610042","61":"1582091758495","70":"1585279774645","74":"1586781606615"}}';
var d = 'd5a585639f505b18';
var e = 'android';
var f = '10.2.0';
logvar = 1;
var res = BitmapkitUtils.getSignFromJni(context, b, c, d, e, f);
logvar = 0;
console.log('res: ', res);
}
function hook_libc(){
var ptr_t = Module.findExportByName("libc.so", "gettimeofday");
Interceptor.attach(ptr_t, {
onEnter: function(args){
this.arg0 = args[0];
},
onLeave: function() {
if (logvar) {
this.arg0.writeU32(1639393559);
this.arg0.add(4).writeU32(0);
}
}
});
Interceptor.attach(Module.findExportByName("libc.so" , "lrand48"), {
onEnter: function(args) {
},
onLeave:function(retval){
if (logvar){
retval.replace(1);
}
}
});
}
这样一来,只有在logvar
值为1时才会固定参数。而logvar
的默认值为0,只有在调用callBitmapkitUtils
方法的时候才会改为1,调用完成后又会改为0。
unidbg
对于unidbg,我们运行多次后会发现,时间戳st
会变,sv
一直是111,好像和frida上的表现不一样,难道出了什么问题?
其实如果研究了京东libjdbitmapkit.so
就会发现sv的后两位都是随机数余3。
而在unidbg对libc.so
的随机数生成的实现中,种子是固定的(我猜的,没深究源码),导致生成的随机数的顺序是固定的,继而导致余数是固定的。
public void hook_libc() {
IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.wrap(module.findSymbolByName("lrand48"), new WrapCallback<HookZzArm32RegisterContext>() {
@Override
public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
}
@Override
public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
int old = ctx.getIntArg(0);
System.out.println("Origin rand:" + old);
ctx.setR0(1);
}
});
hookZz.wrap(module.findSymbolByName("gettimeofday"), new WrapCallback<HookZzArm32RegisterContext>() {
@Override
public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
Pointer pointer = ctx.getR0Pointer();
ctx.push(pointer);
}
@Override
public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
Pointer pointer = ctx.pop();
pointer.setLong(0, 1639388888);
pointer.setLong(4, 0);
}
});
}
public static void main(String[] args) {
JingDong test = new JingDong();
test.hook_libc();
test.callSign();
}
可以看到,时间戳已经被固定下来了,而打印出来的两个随机数,他们的除以3的余数都为1,这也说明了为什么固定参数之前sv
的值一直是111。
unidbg完整代码
package com.jingdong;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.hook.hookzz.*;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.linux.android.dvm.wrapper.DvmInteger;
import com.github.unidbg.memory.Memory;
import com.sun.jna.Pointer;
import sun.security.pkcs.PKCS7;
import sun.security.pkcs.ParsingException;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.security.cert.X509Certificate;
public class JingDong extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
public static String pkgName = "com.jingdong.app.mall";
public static String apkPath = "unidbg-android/src/test/java/com/jingdong/jingdong9.2.2.apk";
public static String soPath = "unidbg-android/src/test/java/com/jingdong/libjdbitmapkit.so";
private static final String APK_PATH = "/data/app/com.jingdong.app.mall.apk";
JingDong() {
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName(pkgName).build();
final Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File(apkPath));
DalvikModule dm = vm.loadLibrary(new File(soPath), false);
vm.setJni(this);
vm.setVerbose(true);
dm.callJNI_OnLoad(emulator);
module = dm.getModule();
}
@Override
public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
switch (signature) {
case "com/jingdong/common/utils/BitmapkitUtils->a:Landroid/app/Application;": {
return vm.resolveClass("android/app/Activity", vm.resolveClass("android/content/ContextWrapper", vm.resolveClass("android/content/Context"))).newObject(null);
}
}
return super.getStaticObjectField(vm, dvmClass, signature);
}
@Override
public DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature) {
switch (signature) {
case "android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;": {
return new StringObject(vm, APK_PATH);
}
}
return super.getObjectField(vm, dvmObject, signature);
}
@Override
public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
switch (signature) {
case "com/jingdong/common/utils/BitmapkitZip->unZip(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)[B": {
StringObject apkPath = varArg.getObjectArg(0);
StringObject directory = varArg.getObjectArg(1);
StringObject filename = varArg.getObjectArg(2);
if (APK_PATH.equals(apkPath.getValue()) &&
"META-INF/".equals(directory.getValue()) &&
".RSA".equals(filename.getValue())) {
byte[] data = vm.unzip("META-INF/JINGDONG.RSA");
return new ByteArray(vm, data);
}
}
case "com/jingdong/common/utils/BitmapkitZip->objectToBytes(Ljava/lang/Object;)[B": {
DvmObject<?> obj = varArg.getObjectArg(0);
byte[] bytes = objectToBytes(obj.getValue());
return new ByteArray(vm, bytes);
}
}
return super.callStaticObjectMethod(vm ,dvmClass, signature, varArg);
}
@Override
public DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
switch (signature) {
case "sun/security/pkcs/PKCS7-><init>([B)V": {
ByteArray array = varArg.getObjectArg(0);
try {
return vm.resolveClass("sun/security/pkcs/PKCS7").newObject(new PKCS7(array.getValue()));
} catch (ParsingException e) {
throw new IllegalStateException(e);
}
}
}
return super.newObject(vm, dvmClass, signature, varArg);
}
@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature) {
case "sun/security/pkcs/PKCS7->getCertificates()[Ljava/security/cert/X509Certificate;": {
PKCS7 pkcs7 = (PKCS7) dvmObject.getValue();
X509Certificate[] certificates = pkcs7.getCertificates();
return ProxyDvmObject.createObject(vm, certificates);
}
}
return super.callObjectMethod(vm, dvmObject, signature, varArg);
}
@Override
public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature) {
case "java/lang/StringBuffer-><init>()V": {
return vm.resolveClass("java/lang/StringBuffer").newObject(new StringBuffer());
}
case "java/lang/Integer-><init>(I)V": {
return DvmInteger.valueOf(vm, vaList.getIntArg(0));
}
}
return super.newObjectV(vm, dvmClass, signature, vaList);
}
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "java/lang/StringBuffer->append(Ljava/lang/String;)Ljava/lang/StringBuffer;": {
StringBuffer buffer = (StringBuffer) dvmObject.getValue();
StringObject str = vaList.getObjectArg(0);
buffer.append(str.getValue());
return dvmObject;
}
case "java/lang/Integer->toString()Ljava/lang/String;": {
return new StringObject(vm, ((Integer)dvmObject.getValue()).toString());
}
case "java/lang/StringBuffer->toString()Ljava/lang/String;": {
return new StringObject(vm, ((StringBuffer)dvmObject.getValue()).toString());
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
private static byte[] objectToBytes(Object obj) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.flush();
byte[] array = baos.toByteArray();
oos.close();
baos.close();
return array;
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
public void hook_libc() {
IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.wrap(module.findSymbolByName("lrand48"), new WrapCallback<HookZzArm32RegisterContext>() {
@Override
public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
}
@Override
public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
int old = ctx.getIntArg(0);
System.out.println("Origin rand:" + old);
ctx.setR0(1);
}
});
hookZz.wrap(module.findSymbolByName("gettimeofday"), new WrapCallback<HookZzArm32RegisterContext>() {
@Override
public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
Pointer pointer = ctx.getR0Pointer();
ctx.push(pointer);
}
@Override
public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
Pointer pointer = ctx.pop();
pointer.setLong(0, 1639388888);
pointer.setLong(4, 0);
}
});
}
public void callSign() {
DvmClass cBitmapkitUtils = vm.resolveClass("com/jingdong/common/utils/BitmapkitUtils");
StringObject ret = cBitmapkitUtils.callStaticJniMethodObject(emulator, "getSignFromJni()(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
vm.resolveClass("android/content/Context").newObject(null),
"clientImage",
"{\"moduleParams\":{\"18\":\"1565611060638\",\"19\":\"1565229712150\",\"25\":\"1567478504636\",\"27\":\"1602488415048\",\"28\":\"1631069159956\",\"30\":\"1567404005627\",\"32\":\"1567997588476\",\"34\":\"1593508185597\",\"35\":\"1568708316462\",\"37\":\"1630293538664\",\"42\":\"1623741761542\",\"44\":\"1569247647090\",\"46\":\"1588839806224\",\"47\":\"1571295610042\",\"61\":\"1582091758495\",\"70\":\"1585279774645\",\"74\":\"1586781606615\"}}",
"d5a585639f505b18",
"android",
"10.2.0");
System.out.println(ret.getValue());
}
public static void main(String[] args) {
JingDong test = new JingDong();
test.hook_libc();
test.callSign();
}
}
关于京东加密
其实京东app有3套加密方案,会根据随机数不同来选择不同的方案,而unidbg生成的随机数和京东cv
的生成方案导致sv
一直是固定的,从而一直调用其中一套方案。我们在逆向的时候,其实只需要逆向出其中一套方案即可,那个简单选哪个,如果刚好是调用的那套方案的话那还好。如果不是,一时之间又不知道怎么处理的话就有点倒霉了。