-
-
[原创]unidbg补环境实战(某文学网站)
-
发表于: 2025-1-6 22:06 2015
-
一、概述
在当前的网络安全领域中,逆向工程和接口分析已成为重要的技术手段,而模拟和复现运行环境则是实现这些技术的关键一步。在实际工作中,我们经常需要面对复杂的加密逻辑或接口签名校验,这些校验通常依赖于特定的运行环境。Unidbg 作为一款强大的 Android 动态调试工具,为我们提供了在本地模拟原生库运行的能力,大大降低了环境复现的难度。本文以某文学网站为例,结合实际案例,详细讲解如何通过 Unidbg 补齐运行环境,并成功模拟目标应用的关键加密逻辑。
二、案例样本逻辑分析
jadx反编译apk找到找到关键函数的签名入口。getSigValue(Context context, Map<String, String> map, int i)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import android.content.Context; import com.umeng.analytics.pro.UContent; import java.util. Map ; import p746h.p755d0.p757c.C9767h; / * compiled from : Confusion.kt * / / * loaded from : classes2.dex * / public final class Confusion { / * renamed from : a * / public static final Confusion f21521a = new Confusion(); static { System.loadLibrary( "conxx-lib" ); } private Confusion() { } / * renamed from : a * / public final String m26215a(Context context, Map <String, String> map , int i) { C9767h.m1060e(context, UContent.f16415R); C9767h.m1060e( map , "map" ); String sigValue = getSigValue(context, map , i); return sigValue = = null ? "" : sigValue; } public final native String getSigValue(Context context, Map <String, String> map , int i); } |
然后在IDA中分析conxx-lib.so,导出表中搜索getSigValue进入目标函数简单分析下代码
主要逻辑分解
- 参数检查
if (!a3 || (a2 && !getSha1(a1)))
return 0;
检查 a3 是否为空。如果为空,直接返回 0(表示失败)。
如果 a2 不为零,还检查 getSha1(a1) 的结果。如果失败,同样返回 0。 - 获取 a3 对象的类信息并调用方法
v7 = (*a1)->GetObjectClass(a1, a3);
v8 = (*a1)->GetMethodID(a1, v7, "entrySet", "()Ljava/util/Set;");
v9 = _JNIEnv::CallObjectMethod(a1, a3, v8);
获取 a3 对象的类(v7)。
获取 entrySet 方法的 ID(v8),该方法返回一个集合(Set)。
调用 entrySet 方法,获取结果对象(v9)。 - 获取集合的迭代器
v13 = (*a1)->GetMethodID(a1, v12, "iterator", "()Ljava/util/Iterator;");
v14 = _JNIEnv::CallObjectMethod(a1, v9, v13);
获取集合的迭代器方法 iterator 的 ID。
调用该方法获取迭代器对象(v14)。 - 遍历集合,处理键值对
while (_JNIEnv::CallBooleanMethod(a1, v14, v16))
{
v62 = _JNIEnv::CallObjectMethod(a1, v14, v59); // 获取当前项
v63 = _JNIEnv::CallObjectMethod(a1, v62, v58); // 获取键
v54 = _JNIEnv::CallObjectMethod(a1, v63, v64); // 获取键的字节数组
jstringTostring(&v65, a1); // 转为 C++ 字符串
}
使用 hasNext 方法检查是否有下一项。
调用 next 获取集合中的一个元素(键值对)。
获取键对象并转换为字节数组形式,存储并处理。 - 签名的生成
v33 = (*a1)->FindClass(a1, "java/security/MessageDigest");
v34 = (*a1)->GetStaticMethodID(a1, v33, "getInstance", "(Ljava/lang/String;)Ljava/security/MessageDigest;");
v35 = (*a1)->NewStringUTF(a1, "SHA224");
v36 = _JNIEnv::CallStaticObjectMethod(a1, v33, v34, v35);
v38 = _JNIEnv::CallObjectMethod(a1, v36, v37);
使用 MessageDigest 类获取 SHA224 的实例。
对数据进行摘要计算(digest 方法)。 - 将摘要结果转换为字符串
for (j = 0; j < v39; ++j)
{
v42[2 * j] = a0123456789abcd_0[v40[j] >> 4];
v44[1] = a0123456789abcd_0[v45 & 0xF];
}
v42[2 * v39] = 0;
将字节数组转换为十六进制字符串。 - 返回结果
v28 = (*a1)->NewStringUTF(a1, v42);
return v28;
最终,将生成的十六进制字符串创建为 jstring,并返回。
三、unidbg初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | package com.xx.reader; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Module; 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.memory.Memory; import java.io. File ; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util. * ; public class MainActivity2 extends AbstractJni { private final AndroidEmulator emulator; private final Module module; private final Memory memory; private final VM vm; MainActivity2() { emulator = AndroidEmulatorBuilder.for32Bit().build(); memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver( 23 )); vm = emulator.createDalvikVM(new File ( "unidbg-android/src/test/java/com/xx/reader/zz.apk" )); vm.setVerbose(true); vm.setJni(this); DalvikModule dalvikModule = vm.loadLibrary(new File ( "unidbg-android/src/test/java/com/xx/reader/libconxx-lib.so" ), false); module = dalvikModule.getModule(); vm.callJNI_OnLoad(emulator, module); } public static void main(String[] args) { MainActivity2 mainActivity = new MainActivity2(); } } |
四、补环境基本要素
● 基本类型直接传递。 byte、short、int、long、double、float、boolean、char,只需要直接传递即可。
● 字符串、字节数组等基本对象直接传递,其内部会做封装,也可以自己调用new StringObject(vm, str)、new ByteArray(vm, value)等。
● JDK 标准库对象,如 HashMap、JSONObject 等,使用ProxyDvmObject.createObject(vm, value)处理。
● 非 JDK 标准库对象,如 Android Context、SharedPreference 等,使用vm.resolveClass(vm,className).newObject(value)处理。
五、发起调用
模拟getSigValue函数的调用,参数有三个 Context context, Map<String, String> map, int i,getSigValue的地址:0xfD92
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | private void sign() { ArrayList< Object > args = new ArrayList<>(); / / 第一个参数 args.add(vm.getJNIEnv()); / / 第二个参数 args.add( 0 ); / / 第三个参数 实际第一个 DvmObject<?> context = vm.resolveClass( "android/content/Context" ).newObject(null); args.add(vm.addLocalObject(context)); / / map 第四个参数 实际第二个 TreeMap<String, String> keyMap = new TreeMap<String, String>(); keyMap.put( "device" , "5555" ); DvmClass map = vm.resolveClass( "java/util/Map" ); DvmClass abstractMap = vm.resolveClass( "java/util/AbstractMap" , map ); DvmObject<?> argsMpa = vm.resolveClass( "java/util/TreeMap" , abstractMap).newObject(keyMap); args.add(vm.addLocalObject(argsMpa)); / / 实际第三个 args.add( 602 ); Number number = module.callFunction(emulator, 0xfD92 + 1 , args.toArray()); System.out.println( "[addr] number is ==> " + number.intValue()); DvmObject<?> object = vm.getObject(number.intValue()); System.out.println( "[addr] Call the so md5 function result is ==> " + object .getValue()); } |
执行结果:
抛出异常,提示缺少的环境,java/util/TreeMap->entrySet()Ljava/util/Set; 表示模拟的代码中调用了 TreeMap 的 entrySet() 方法,但在 Unidbg 中未正确处理。
java.lang.UnsupportedOperationException: java/util/TreeMap->entrySet()Ljava/util/Set;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:416)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
dvmObject.getValue()获取对象的值,java/util/Set 是JDK 标准库对象,使用ProxyDvmObject.createObject(vm, value)处理,所以我们根据知识点三的要求,就很好补了。
1 2 3 4 5 6 7 8 9 10 | @Override public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { if (signature.equals( "java/util/TreeMap->entrySet()Ljava/util/Set;" )) { TreeMap<String, String> treeMap = (TreeMap<String, String>) dvmObject.getValue(); Set < Map .Entry<String, String>> setE = treeMap.entrySet(); DvmObject<?> set = ProxyDvmObject.createObject(vm,setE); return set ; } return super .callObjectMethodV(vm, dvmObject, signature, vaList); } |
再次运行后,得到一个新的异常,说之前的环境补充是正确的。新的异常:
java.lang.UnsupportedOperationException: java/util/TreeMap$EntrySet->iterator()Ljava/util/Iterator;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:416)
at com.zongheng.reader.MainActivity2.callObjectMethodV(MainActivity2.java:114)
java/util/TreeMap$EntrySet->iterator()Ljava/util/Iterator; 也是JDK 标准库的,返回值用ProxyDvmObject.createObject
1 2 3 4 5 6 7 8 9 10 | @Override public int callIntMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { if (signature.equals( "java/util/TreeMap$EntrySet->iterator()Ljava/util/Iterator;" )) { Set < Map .Entry< Object , Object >> set = ( Set < Map .Entry< Object , Object >>) dvmObject.getValue(); Iterator< Map .Entry< Object , Object >> iterator = set .iterator(); DvmObject<?> iteratorO = ProxyDvmObject.createObject(vm,iterator); return iteratorO; } return super .callIntMethodV(vm, dvmObject, signature, vaList); } |
运行后,得到一个新的异常:
java.lang.UnsupportedOperationException: java/util/TreeMap$EntryIterator->hasNext()Z
at com.github.unidbg.linux.android.dvm.AbstractJni.callBooleanMethodV(AbstractJni.java:624)
at com.github.unidbg.linux.android.dvm.AbstractJni.callBooleanMethodV(AbstractJni.java:602)
at com.github.unidbg.linux.android.dvm.DvmMethod.callBooleanMethodV(DvmMethod.java:119)
1.方法签名检查:if (signature.equals("java/util/TreeMap$EntryIterator->hasNext()Z")): 检查传入的方法签名是否匹配 TreeMap.EntryIterator 的 hasNext() 方法。Z 表示返回值是一个布尔类型。
2.提取迭代器对象:Iterator<Object> iterator = (Iterator<Object>) dvmObject.getValue();: 从 dvmObject 中提取实际的 Iterator 对象。dvmObject 是 unidbg 对 Java 对象的抽象表示。
3.检查是否有下一个元素:boolean b = iterator.hasNext();: 调用 Iterator 的 hasNext() 方法,返回一个布尔值,指示迭代器是否有更多的元素。
4.调用父类方法:return super.callBooleanMethodV(vm, dvmObject, signature, vaList);: 如果传入的方法签名不匹配 TreeMap.EntryIterator.hasNext(),则调用父类的 callBooleanMethodV 方法处理其他情况。
1 2 3 4 5 6 7 8 9 10 | @Override public boolean callBooleanMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { if (signature.equals( "java/util/TreeMap$EntryIterator->hasNext()Z" )) { Iterator< Object > iterator = (Iterator< Object >)dvmObject.getValue(); boolean b = iterator.hasNext(); return b; } return super .callBooleanMethodV(vm, dvmObject, signature, vaList); } |
运行后,得到一个新的异常:
java.lang.UnsupportedOperationException: java/util/TreeMap$EntryIterator->next()Ljava/lang/Object;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:416)
at com.zongheng.reader.MainActivity2.callObjectMethodV(MainActivity2.java:121)
1.方法签名检查:if (signature.equals("java/util/TreeMap$EntryIterator->next()Ljava/lang/Object;")): 检查传入的方法签名是否匹配 TreeMap.EntryIterator 的 next() 方法。该方法的签名表示它返回一个 Object。
2.提取迭代器对象:Iterator<?> iterator = (Iterator<?>) dvmObject.getValue();: 从 dvmObject 中提取实际的 Iterator 对象。dvmObject 是 unidbg 中对 Java 对象的封装。
3.获取下一个元素:Object next = iterator.next();: 调用 Iterator 的 next() 方法获取迭代器的下一个元素。
1 2 3 4 5 6 7 8 9 | @Override public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { if (signature.equals( "java/util/TreeMap$EntryIterator->next()Ljava/lang/Object;" )) { Iterator<?> iterator = (Iterator<?>) dvmObject.getValue(); Object next = iterator. next (); DvmObject<?> nextO = ProxyDvmObject.createObject(vm, next ); return nextO; } } |
运行后,得到一个新的异常:
java.lang.UnsupportedOperationException: java/util/Map$Entry->getKey()Ljava/lang/Object;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:416)
at com.zongheng.reader.MainActivity2.callObjectMethodV(MainActivity2.java:126)
1.方法签名检查:if (signature.equals("java/util/TreeMap$EntryIterator->next()Ljava/lang/Object;")): 这一行检查传入的签名是否匹配 TreeMap.EntryIterator 的 next() 方法。这个签名表示方法返回一个 Object。
2.获取迭代器:Iterator<?> iterator = (Iterator<?>) dvmObject.getValue();: 这里从 dvmObject 中提取实际的 Java Iterator 对象。dvmObject 是 unidbg 中对 Java 对象的抽象封装。
3.调用 next() 方法,Object next = iterator.next();: 调用 Iterator 的 next() 方法获取下一个元素。
4.包装返回值:DvmObject<?> nextO = ProxyDvmObject.createObject(vm, next);: 使用 unidbg 的 ProxyDvmObject.createObject 方法将原生 Java 对象 next 转换为 DvmObject,以便 unidbg 能正确地处理和返回这个对象。
1 2 3 4 5 6 7 8 | @Override public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { if (signature.equals( "java/util/Map$Entry->getKey()Ljava/lang/Object;" )) { Map .Entry entry = ( Map .Entry) dvmObject.getValue(); String key = (String) entry.getKey(); return new StringObject(vm, key); } } |
运行后,得到一个新的异常:
java.lang.UnsupportedOperationException: java/util/Map$Entry->getValue()Ljava/lang/Object;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:416)
at com.zongheng.reader.MainActivity2.callObjectMethodV(MainActivity2.java:126)
1.方法签名检查:if (signature.equals("java/util/Map$Entry->getValue()Ljava/lang/Object;")): 这行代码检查传入的签名是否匹配 Map.Entry 接口的 getValue() 方法,该方法返回一个 Object 类型的值。
2.提取 Map.Entry 对象:Map.Entry entry = (Map.Entry) dvmObject.getValue();: 通过 dvmObject.getValue() 提取实际的 Map.Entry 对象。这意味着 dvmObject 包装了一个 Map.Entry 实例。
3.获取值:String value = (String) entry.getValue();: 调用 Map.Entry 的 getValue() 方法来获取存储的值,并将其强制转换为 String 类型。这里假设 getValue() 返回的是一个 String。
4.封装返回值:return new StringObject(vm, value);: 使用 unidbg 提供的 StringObject 类将 String 值封装起来,并返回一个 DvmObject。StringObject 是 unidbg 中对 Java 字符串对象的封装,使得框架可以处理和返回这个字符串。
1 2 3 4 5 6 7 8 | @Override public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { if (signature.equals( "java/util/Map$Entry->getValue()Ljava/lang/Object;" )) { Map .Entry entry = ( Map .Entry) dvmObject.getValue(); String value = (String) entry.getValue(); return new StringObject(vm, value); } } |
运行后,得到一个新的异常:
java.lang.UnsupportedOperationException: java/util/TreeMap->size()I
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethodV(AbstractJni.java:562)
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethodV(AbstractJni.java:528)
1.方法签名检查:if (signature.equals("java/util/TreeMap->size()I")): 这行代码检查传入的方法签名是否匹配 TreeMap 的 size() 方法。I 表示返回值是一个整数。
2.获取 TreeMap 对象:TreeMap<String, String> treeMap = (TreeMap<String, String>) dvmObject.getValue();: 从 dvmObject 中提取实际的 TreeMap 对象。dvmObject 是 unidbg 中对 Java 对象的抽象表示,这里将其强制转换为 TreeMap 类型。
3.获取 TreeMap 大小:int size = treeMap.size();: 调用 TreeMap 的 size() 方法获取映射的大小,即键值对的数量。
1 2 3 4 5 6 7 8 9 | @Override public int callIntMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { if (signature.equals( "java/util/TreeMap->size()I" )) { TreeMap<String, String> treeMap = (TreeMap<String, String>) dvmObject.getValue(); int size = treeMap.size(); return size; } return super .callIntMethodV(vm, dvmObject, signature, vaList); } |
运行后,得到一个新的异常:
java.lang.IllegalStateException: java.security.NoSuchAlgorithmException: SHA224 MessageDigest not available
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:488)
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:437)
1.方法签名检查:if (signature.equals("java/security/MessageDigest->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;")): 这行代码检查传入的方法签名是否匹配 MessageDigest 类的 getInstance 方法,该方法接受一个 String 参数并返回一个 MessageDigest 实例。
2.获取算法名称:StringObject type = vaList.getObjectArg(0);: 从 vaList 参数列表中获取第一个参数,这是一个表示算法名称的 StringObject。
3.判断算法类型:if (type.getValue().equals("SHA224")): 检查传入的算法名称是否是 SHA224。
4.模拟返回 SHA-224 实例:使用 vm.resolveClass 和 newObject 方法来创建一个 MessageDigest 的模拟对象,模拟返回一个 SHA-224 的实例。
5.实际获取 SHA-224 实例:eturn messageDigestClass.newObject(MessageDigest.getInstance("SHA-224"));: 使用 Java 标准库的 MessageDigest.getInstance("SHA-224") 方法实际获取一个 SHA-224 实例,并将其封装为 DvmObject 返回。
6.处理其他签名:return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);: 如果传入的方法签名不是 SHA-224 的 getInstance,则调用父类的 callStaticObjectMethodV 方法处理其他情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | @Override public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) { if (signature.equals( "java/security/MessageDigest->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;" )) { StringObject type = vaList.getObjectArg( 0 ); if ( type .getValue().equals( "SHA224" )) { / / 模拟返回一个 SHA - 224 的实例 DvmClass messageDigestClass = vm.resolveClass( "java/security/MessageDigest" ); DvmObject<?> messageDigestInstance = messageDigestClass.newObject( type .getValue()); DvmObject<?> algorithmObj = vaList.getObjectArg( 0 ); String algorithm = (String) algorithmObj.getValue(); DvmObject<?> messageDigestInstance1 = messageDigestClass.newObject(algorithm); System.out.println( "Returning SHA-224 MessageDigest instance" ); try { return messageDigestClass.newObject(MessageDigest.getInstance( "SHA-224" )); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(e); } } } return super .callStaticObjectMethodV(vm, dvmClass, signature, vaList); } |
运行结果:
赞赏
- [原创]unidbg补环境实战(某文学网站) 2016
- [原创]CTF自毁程序密码:逆向分析 10649
- [原创]unidbg入门笔记 29663
- [原创]CTF 丛林的秘密:算法分析 13011
- [原创]MobileCTF easy系列合集:Flag算法还原 9467