热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【技术分享】如何从命令行调用AndroidJNI函数并传递Java对象参数

翻译:興趣使然的小胃预估稿费:200RMB投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿一、前言当我们对某个使用原生库(native library)的恶意软件或者应用进行分析或

http://p9.qhimg.com/t015ec9331e64246128.jpg

翻译:興趣使然的小胃

预估稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


一、前言

当我们对某个使用原生库(native library)的恶意软件或者应用进行分析或渗透测试时,如果能够对库函数进行隔离和执行是再好不过的事情,这样做我们就可以使用其自身的代码来调试对抗恶意软件。举个例子,如果恶意软件包含加密字符串,并使用原生函数完成解密过程,你可以选择花大量时间逆向分析算法来编写自己的解密函数,也可以选择直接利用这个函数来处理任意输入数据。如果使用后一种方法,即使恶意软件作者完全改变了软件的加密算法,你也可能不需要做任何修改即可完成任务。在这篇文章中,我将向读者介绍如何利用并执行原生库函数,即使调用这些函数时需要传入JVM实例作为参数也没问题。

在之前的一篇文章中,我介绍了如何从Android原生代码中创建一个Java虚拟机,但我没有给出一个具体的例子。因此,我会在本文中给出一个具体的例子来说明这一点。

我们至少可以使用两种方法来调用原生函数。第一种方法是对应用进行修改,使应用接受你的输入数据并传递给原生函数。例如,你可以写一个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”的应用。

该应用程序包含一个加密字符串,并会在运行时使用原生库对字符串进行解密。以下是在Smail中字符串的解密过程:

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;->([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

三、构建Harness工具

我使用的是Tim 'diff' Strazzere开发的一款名为“native-shim”的工具(Tim是RedNaga的一名成员)作为整套利用工具的基础,我将这个工具命名为“Harness”。在Android中,shim就像一个中间垫片,作用是加载一个库,并调用其“JNI_OnLoad”方法。它可以使调试工作更加简单,我们只需要使用调试器启动shim,并将具体路径以参数形式传递给目标库即可。我们可以设置调试器的断点,在库加载时触发断点,这样就能进入“JNI_OnLoad”函数的处理流程。此外,native-shim还可以加载库文件(.so文件)、获取函数的引用并调用函数,这一切对我们来说都非常实用。

首先,我添加了部分代码以初始化一个Java虚拟机实例,并将该实例传递给JNI_OnLoad函数,这样处理可以使JNI的初始化过程更为准确。如果没有真实的虚拟机实例,JNI库的内部状态看起来可能会有些奇怪。不同库文件的JNI_OnLoad的实现可能不尽相同,但这并不重要,重要的是这些实现都会检查JNI版本,如这段代码所示。因此我们需要创建一个虚拟机实例。

printf(" [+] Initializing JavaVM Instancen");
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_OnLoadn");
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语句,将内存中所有相关指针的位置全部打印出来。这个错误信息貌似在提示我某个参数出现了问题,但我排查后发现所有指针都是正常的,没有空引用情况。

我敢肯定我传递的参数没有问题,问题可能出在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

上述命令可以生成“_org_cf_nativeharness_Cryptor.h_”文件,其中包含如下信息:

JNIEXPORT jstring JNICALL Java_org_cf_nativeharness_Cryptor_decryptString(JNIEnv *, jobject, jstring);

我们可以看到多了一个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()类似,使用某个实例对象作为第一个参数。


五、构建Socket服务器

这是整个工作中最无趣的一个环节,如果你不介意的话,我会跳过这一部分。你可以自行查看具体的实现源码,如果愿意的话也可以提出修改意见。


六、Harness工具的使用方法

你可以通过如下几个步骤来使用Harness工具。

1、启动模拟器

2、将harness push到设备中

3、将目标原生库及其他依赖库push到设备中(本文示例中不涉及到依赖库)

4、将目标应用push到设备中

5、运行harness工具

6、将模拟器的端口转发到宿主机上

7、运行“decrypt_string.py”,祈祷一切顺利

你可以使用以下命令将应用及原生库push到设备中。

$ 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工具push到设备中。

cd harness
make && make install

注意:以上命令会将x86库push到设备中,如果你确实想要使用其他的模拟器镜像,你可以使用“adb push libs//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%有效。


推荐阅读
  • 如何将TS文件转换为M3U8直播流:HLS与M3U8格式详解
    在视频传输领域,MP4虽然常见,但在直播场景中直接使用MP4格式存在诸多问题。例如,MP4文件的头部信息(如ftyp、moov)较大,导致初始加载时间较长,影响用户体验。相比之下,HLS(HTTP Live Streaming)协议及其M3U8格式更具优势。HLS通过将视频切分成多个小片段,并生成一个M3U8播放列表文件,实现低延迟和高稳定性。本文详细介绍了如何将TS文件转换为M3U8直播流,包括技术原理和具体操作步骤,帮助读者更好地理解和应用这一技术。 ... [详细]
  • 在开发过程中,我最初也依赖于功能全面但操作繁琐的集成开发环境(IDE),如Borland Delphi 和 Microsoft Visual Studio。然而,随着对高效开发的追求,我逐渐转向了更加轻量级和灵活的工具组合。通过 CLIfe,我构建了一个高度定制化的开发环境,不仅提高了代码编写效率,还简化了项目管理流程。这一配置结合了多种强大的命令行工具和插件,使我在日常开发中能够更加得心应手。 ... [详细]
  • 解决Android应用在手机安装时出现安全风险提示的方法与对策
    解决Android应用在手机安装时出现安全风险提示的方法与对策 ... [详细]
  • 本文介绍如何在 Android 中自定义加载对话框 CustomProgressDialog,包括自定义 View 类和 XML 布局文件的详细步骤。 ... [详细]
  • 微信小程序详解:概念、功能与优势
    微信公众平台近期向200位开发者发送了小程序的内测邀请。许多人对微信小程序的概念还不是很清楚。本文将详细介绍微信小程序的定义、功能及其独特优势。 ... [详细]
  • 基于Net Core 3.0与Web API的前后端分离开发:Vue.js在前端的应用
    本文介绍了如何使用Net Core 3.0和Web API进行前后端分离开发,并重点探讨了Vue.js在前端的应用。后端采用MySQL数据库和EF Core框架进行数据操作,开发环境为Windows 10和Visual Studio 2019,MySQL服务器版本为8.0.16。文章详细描述了API项目的创建过程、启动步骤以及必要的插件安装,为开发者提供了一套完整的开发指南。 ... [详细]
  • Android 构建基础流程详解
    Android 构建基础流程详解 ... [详细]
  • 在探讨如何在Android的TextView中实现多彩文字与多样化字体效果时,本文提供了一种不依赖HTML技术的解决方案。通过使用SpannableString和相关的Span类,开发者可以轻松地为文本添加丰富的样式和颜色,从而提升用户体验。文章详细介绍了实现过程中的关键步骤和技术细节,帮助开发者快速掌握这一技巧。 ... [详细]
  • 深入探索HTTP协议的学习与实践
    在初次访问某个网站时,由于本地没有缓存,服务器会返回一个200状态码的响应,并在响应头中设置Etag和Last-Modified等缓存控制字段。这些字段用于后续请求时验证资源是否已更新,从而提高页面加载速度和减少带宽消耗。本文将深入探讨HTTP缓存机制及其在实际应用中的优化策略,帮助读者更好地理解和运用HTTP协议。 ... [详细]
  • 在 CentOS 6.5 系统上部署 VNC 服务器的详细步骤与配置指南
    在 CentOS 6.5 系统上部署 VNC 服务器时,首先需要确认 VNC 服务是否已安装。通常情况下,VNC 服务默认未安装。可以通过运行特定的查询命令来检查其安装状态。如果查询结果为空,则表明 VNC 服务尚未安装,需进行手动安装。此外,建议在安装前确保系统的软件包管理器已更新至最新版本,以避免兼容性问题。 ... [详细]
  • 2016-2017学年《网络安全实战》第三次作业
    2016-2017学年《网络安全实战》第三次作业总结了教材中关于网络信息收集技术的内容。本章主要探讨了网络踩点、网络扫描和网络查点三个关键步骤。其中,网络踩点旨在通过公开渠道收集目标信息,为后续的安全测试奠定基础,而不涉及实际的入侵行为。 ... [详细]
  • Android 网络请求中的下载断点续传技术解析与实现
    本文详细解析了 Android 平台下网络请求中下载断点续传的技术原理与实现方法。断点续传技术在下载过程中尤为重要,当下载因网络中断或其他原因暂停时,该技术允许从上次中断的位置继续下载,而无需重新开始。文章重点介绍了断点续传的逻辑思路和关键实现步骤,包括如何记录下载进度、处理 HTTP 请求头以及优化下载性能。通过具体示例代码,读者可以更好地理解和应用这一技术,提高应用程序的用户体验和可靠性。 ... [详细]
  • 在CentOS上部署和配置FreeSWITCH
    在CentOS系统上部署和配置FreeSWITCH的过程涉及多个步骤。本文详细介绍了从源代码安装FreeSWITCH的方法,包括必要的依赖项安装、编译和配置过程。此外,还提供了常见的配置选项和故障排除技巧,帮助用户顺利完成部署并确保系统的稳定运行。 ... [详细]
  • 如何将PHP文件上传至服务器及正确配置服务器地址 ... [详细]
  • 为了优化直播应用底部聊天框的弹出机制,确保在不同设备上的布局稳定性和兼容性,特别是在配备虚拟按键的设备上,我们对用户交互流程进行了调整。首次打开应用时,需先点击首个输入框以准确获取键盘高度,避免直接点击第二个输入框导致的整体布局挤压问题。此优化通过调整 `activity_main.xml` 布局文件实现,确保了更好的用户体验和界面适配。 ... [详细]
author-avatar
周郎某某某
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有