0x01 前言

当分析恶意软件或渗透测试使用原生库(native library)的应用程序时,对库函数进行隔离和执行是一个很好的方案。这可以帮我们在一个隔离的环境中测试和调试恶意软件的代码。例如,如果恶意软件具有加密字符串,并且解密是由原生函数完成的,那么你就可以逆向算法来编写自己的解密函数,或者你可以利用该函数测试任意输入的数据。如果恶意软件作者完全改变了恶意软件的加密算法,你也可能不需要更改任何内容就可以解密。在这篇文章中,我将解释如何利用执行原生库函数,即使调用它们需要传入JVM实例作为参数也可以执行。

在上一篇文章中,我解释了如何从Android Native代码创建Java VM,但是我没有给出任何如何使用它的例子。在这篇文章中,我将给出一个具体的例子来说明如何使用它。

这里我们至少有两种方法来调用原生函数。第一种方法是修改应用程序,使应用程序接受来自你的输入并将其传递给原生函数。例如,你可以编写意图过滤器(intent filter),并将其转换为Smali语言,将代码添加到目标应用程序,修改manifest文件,运行应用程序,并使用adb命令将参数发送到Intent。还有的就是,你可以添加一个小型socket或Web服务器,而不用通过意图过滤器发送curl请求,这不需要修改manifest文件。

第二种方法是创建一个使用命令行运行的小型原生可执行工具,用来调用目标函数、加载库文件、传递输入任何参数。我们就可以只单独运行一个可执行文件而不是运行整个应用程序,这会使得调试更容易。

0x02 目标应用

我创建了一个示例应用程序,让你可以按着示例来学习。它被称为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

0x03 构建Harness

我使用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脚本可以很容易地与它进行通信。

0x04 了解目标函数

要调用函数,你需要函数签名和返回类型。为了得到这个,我们来看看一个反编译, 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)

这让我困惑了一阵子。我以为是我在某个地方使用了空指针引用,所以我添加了很多printfs语句来显示所有相关指针的内存位置。这个错误好像是有一个参数有问题,但是所有的指针看起来都没有问题,没有一个是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()类似,使用某个实例对象作为第一个参数。

0x05 构建Socket服务器

这是harness中最不重要的部分。如果你不介意,我会跳过这个部分。你可以查看具体的代码。如果你认为代码写的像狗屎,我相信你,因为我只是一个C语言的游客。但是,如果你想告诉我这是狗屎,你可以提出请求联系

0x06 Harness利用方法

以下是测试harness中所需步骤的概述:

  1. 启动一个模拟器
  2. 将harness安装到设备
  3. 将目标原生库和其他依赖关系库安装到设备(在本文示例中没有依赖关系库)
  4. 将harness原生的目标应用程序安装到设备
  5. 启动harness
  6. 将模拟器端口转发到主机上
  7. 运行decrypt_string.py

将应用程序和应用原生库安装到设备:

$ 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."

最后,如果你在输出结果看到解密后的字符串,恭喜你,成功了。

0x07 结论

现在你应该能够使用harness工具修改目标函数来运行所需的任何函数了。但是这并不总是100%可以运行的,因为程序是很复杂的,可能回有各种各样怪异的狗屎。

 

*作者:Caleb Fenton

源链接

Hacking more

...