当分析恶意软件或渗透测试使用原生库(native library)的应用程序时,对库函数进行隔离和执行是一个很好的方案。这可以帮我们在一个隔离的环境中测试和调试恶意软件的代码。例如,如果恶意软件具有加密字符串,并且解密是由原生函数完成的,那么你就可以逆向算法来编写自己的解密函数,或者你可以利用该函数测试任意输入的数据。如果恶意软件作者完全改变了恶意软件的加密算法,你也可能不需要更改任何内容就可以解密。在这篇文章中,我将解释如何利用执行原生库函数,即使调用它们需要传入JVM实例作为参数也可以执行。
在上一篇文章中,我解释了如何从Android Native代码创建Java VM,但是我没有给出任何如何使用它的例子。在这篇文章中,我将给出一个具体的例子来说明如何使用它。
这里我们至少有两种方法来调用原生函数。第一种方法是修改应用程序,使应用程序接受来自你的输入并将其传递给原生函数。例如,你可以编写意图过滤器(intent filter),并将其转换为Smali语言,将代码添加到目标应用程序,修改manifest文件,运行应用程序,并使用adb命令将参数发送到Intent。还有的就是,你可以添加一个小型socket或Web服务器,而不用通过意图过滤器发送curl
请求,这不需要修改manifest
文件。
第二种方法是创建一个使用命令行运行的小型原生可执行工具,用来调用目标函数、加载库文件、传递输入任何参数。我们就可以只单独运行一个可执行文件而不是运行整个应用程序,这会使得调试更容易。
我创建了一个示例应用程序,让你可以按着示例来学习。它被称为native-harness-target。你可以自己复制和构建(记得自己修改$ANDROID_*
变量):
git clone https://github.com/CalebFenton/native-harness-target.git cd native-harness-target echo 'ndk.dir=$ANDROID_NDK' > local.properties echo 'sdk.dir=$ANDROID_SDK' >> local.properties ./gradlew build
APK在app/build/outputs/apk/
中生成。在这篇文章中,我将使用x86模拟器镜像和app-universal-debug.apk
应用程序。
改应用程序具有加密字符串,并在运行时使用原生库解密字符串。以下是Smali中字符串解密的方式:
const/16 v3, 0x57 new-array v1, v3, [B fill-array-data v1, :array_2a .local v1, "encryptedStringBytes":[B invoke-static {}, Lorg/cf/nativeharness/Cryptor;->getInstance()Lorg/cf/nativeharness/Cryptor; move-result-object v0 .line 21 .local v0, "c":Lorg/cf/nativeharness/Cryptor; # v3 contains a String made from encrypted bytes new-instance v3, Ljava/lang/String; invoke-direct {v3, v1}, Ljava/lang/String;-><init>([B)V # Call the decryption method, move result back to v3 invoke-virtual {v0, v3}, Lorg/cf/nativeharness/Cryptor;->decryptString(Ljava/lang/String;)Ljava/lang/String; move-result-object v3
我使用Tim‘diff’Strazzere(RedNaga成员!)作为native-shim工具的基础。shim像中间垫片一样加载库并调用”JNI_OnLoad
”方法。它使调试更加简单容易,我们可以使用调试器启动shim,并将具体路径以参数形式传递给目标库。设置调试器的断点,在库加载时触发断点,来破坏库的加载,这样你就可以进入“JNI_OnLoad
”函数的处理流程逐步浏览。另外,native-shim
还有其他功能,如:加载库文件(.so文件),获取对函数的引用,并调用函数。
首先,我添加了部分代码来初始化Java VM实例并将该实例传递给JNI_OnLoad
函数。这样的话可以更准确的JNI初始化。如果没有真正的虚拟机实例,JNI库的内部状态可能会有点怪异。这取决于JNI_OnLoad
特定库实现的方式,不同库文件之间都可能不相同。但这不重要,重点是从这些实现都会检查JNI版本,为此,我们需要一个VM的实例。
printf(" [+] Initializing JavaVM Instance\n"); JavaVM *vm = NULL; JNIEnv *env = NULL; int status = init_jvm(&vm, &env); if (status == 0) { printf(" [+] Initialization success (vm=%p, env=%p)\n", vm, env); } else { printf(" [!] Initialization failure (%i)\n", status); return -1; } printf(" [+] Calling JNI_OnLoad\n"); onLoadFunc(vm, NULL);
最终,目标是通过harness打开socket服务器,通过读取socket传输参数,并使用这些参数调用该函数。这样一来,解密函数就成了一个服务,Python脚本可以很容易地与它进行通信。
要调用函数,你需要函数签名和返回类型。为了得到这个,我们来看看一个反编译, org.cf.nativeharness.Cryptor
类它声明了decryptString
原生方法,如下:
public class Cryptor { private static Cryptor instance = null; public static Cryptor getInstance() { if (instance == null) { instance = new Cryptor(); } return instance; } public native String decryptString(String encryptedString); }
从这段代码中,你可以看到该方法使用了String
对象,
并返回了一个String
对象。看起来很简单,现在我们将它转换为原生函数形式签名。
Java_org_cf_nativeharness_Cryptor_decryptString(JNIEnv *env, jstring encryptedString)
每个JNI本机方法都需要JNIEnv
对象作为第一个参数。这意味着我们的函数的typedef语句应该是这样:
typedef jstring(*decryptString_t)(JNIEnv *, jstring);
不幸的是,如果你尝试使用上面的typedef语句执行这个函数,你会得到一个错误信息:
E/dalvikvm: JNI ERROR (app bug): attempt to use stale local reference 0x1 E/dalvikvm: VM aborting A/libc: Fatal signal 6 (SIGABRT) at 0x00000a9a (code=-6), thread 2714 (harness)
这让我困惑了一阵子。我以为是我在某个地方使用了空指针引用,所以我添加了很多printf
s语句来显示所有相关指针的内存位置。这个错误好像是有一个参数有问题,但是所有的指针看起来都没有问题,没有一个是null空引用。
我确定我传递的参数没有问题。也许是我忘记的JNI出问题了?为了证明这点,我使用javah
它来生成实现原生方法所需的C语言头文件和源文件。
为此,我们需要安装dex2jar
和类路径,将更改的platforms/android-19
指向要安装的任何平台。
$ d2j-dex2jar.sh app-universal-debug.apk dex2jar app-universal-debug.apk -> ./app-universal-debug-dex2jar.jar $ javah -cp app-universal-debug-dex2jar.jar:$ANDROID_SDK/platforms/android-19/android.jar org.cf.nativeharness.Cryptor
这将创建creates _org_cf_nativeharness_Cryptor.h_
文件,其中包含:
$ d2j-dex2jar.sh app-universal-debug.apk dex2jar app-universal-debug.apk -> ./app-universal-debug-dex2jar.jar $ javah -cp app-universal-debug-dex2jar.jar:$ANDROID_SDK/platforms/android-19/android.jar org.cf.nativeharness.Cryptor
这里可以看到jobject
是作为第二个参数。这是为什么?是什么赋予了?如果已经知道了这个答案,我敢打赌你花了很多时间来看Smali,特别是invoke-virtual
方法。无论何时调用虚拟方法(通常都是非静态方法),第一个参数都是实现该方法的对象实例。在这种情况下,第一个参数应该是org.cf.nativeharness.Cryptor
类的一个实例。
当然,你可以试试其他方法,如审查str-crypt.c来找到函数的具体调用形式,但是如果你真的是逆向工程师或渗透测试人员,你就不会有可能拿到源代码。
因为正确的typedef语句中应该包含Cryptor实例的一个jobject对象,如下所示:
typedef jstring(*decryptString_t)(JNIEnv *, jobject, jstring);
你可能想知道为什么我们不以静态方法的开始。其实也没有什么特别的理由,主要是我在写这篇文章时,我遇到的这个问题中的原始应用程序的目标方法不是静态,就这样罢了。
这里的收获是,如果你不确定函数的具体调用形式是什么时,可以尝试javah
命令,并记住虚拟方法与Java中的Method#invoke()类似,使用某个实例对象作为第一个参数。
这是harness中最不重要的部分。如果你不介意,我会跳过这个部分。你可以查看具体的代码。如果你认为代码写的像狗屎,我相信你,因为我只是一个C语言的游客。但是,如果你想告诉我这是狗屎,你可以提出请求联系我。
以下是测试harness中所需步骤的概述:
将应用程序和应用原生库安装到设备:
$ adb push app/build/output/apk/app-universal-debug.apk /data/local/tmp/target-app.apk $ unzip app/build/outputs/apk/app-universal-debug.apk lib/x86/libstr-crypt.so Archive: app/build/outputs/apk/app-universal-debug.apk inflating: lib/x86/libstr-crypt.so $ adb push lib/x86/libstr-crypt.so /data/local/tmp lib/x86/libstr-crypt.so: 1 file pushed. 1.5 MB/s (5476 bytes in 0.004s)
将harness安装到设备中
cd harness make && make install
注意:以下命令会将x86库安装到设备。如果你真的想用其他模拟器镜像,你可以用adb push libs/<your emulator flavor>/harness /data/local/tmp
命令替换
命令。make install
现在,运行harness
并将目标库的路径作为第一个参数传入:
$ adb shell /data/local/tmp/harness /data/local/tmp/libstr-crypt.so [*] Native Harness [+] Loading target: [ /data/local/tmp/libstr-crypt.so ] [+] Library Loaded! [+] Found JNI_OnLoad, good [+] Initializing JavaVM Instance WARNING: linker: libdvm.so has text relocations. This is wasting memory and is a security risk. Please fix. [+] Initialization success (vm=0xb8e420a0, env=0xb8e420e0) [+] Calling JNI_OnLoad [+] Found decryptString function, good (0xb761f4f0) [+] Finding Cryptor class [+] Found Cryptor class: 0x1d2001d9 [+] Found Cryptor.getInstance(): 0xb27bc270 [+] Instantiated Cryptor class: 0x1d2001dd [+] Starting socket server on port 5001
想要测试是否正常运行,可以在另一个终端运行如下命令:
$ ./decrypt_string.py Sending encrypted string Decrypted string: "Seek freedom and become captive of your desires. Seek discipline and find your liberty."
最后,如果你在输出结果看到解密后的字符串,恭喜你,成功了。
现在你应该能够使用harness工具修改目标函数来运行所需的任何函数了。但是这并不总是100%可以运行的,因为程序是很复杂的,可能回有各种各样怪异的狗屎。
*作者:Caleb Fenton,