在Radare2静态分析apk(1) 对Radare静态分析apk进行了简单的介绍。 补充一下: 通过r2 apk://URI可以直接对apk中的dex进行分析。
在前面的文章中,对so文件进行了基本的介绍。Android的so有点不一样。
Android平台pic(位置无关代码)编译的原因,所有全局变量的引用都是通过got(全局偏移表)完成的,加载器会根据加载基址来修正,并向got填入正确的全局变量的地址。如某重定位数据a=S,app运行时的基址是A,pBuf的地址是B,则重定位a的值为S-A+B,这样便相当于从pBuf处加载so。
通过readelf -d来获取数据重定位的信息。后面会对android的so文件进行专门的分析。
开机的时候。
参考链接: http://androidxref.com/9.0.0_r3/xref/libnativehelper/include_jni/jni.h和https://www.androidos.net.cn/android/10.0.0_r6/xref/libnativehelper/include_jni/jni.h
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
•如果用C++编译器,那么JNIEnv就是_JNIEnv•如果用C编译器,那么JNIEnv就是JNINativeInterface
那_JNIEnv又是什么? 通过下面的代码可知*最终还是还是JNINativeInterface**。
struct _JNIEnv {
/* do not rename this; it does not seem to be entirely opaque */
const struct JNINativeInterface* functions;
#if defined(__cplusplus)
jint GetVersion()
{ return functions->GetVersion(this); }
jclass DefineClass(const char *name, jobject loader, const jbyte* buf,
jsize bufLen)
{ return functions->DefineClass(this, name, loader, buf, bufLen); }
. . .
#endif /*__cplusplus*/
};
JNINativeInterface又是什么?通过下面的代码可知定义了一系列的函数指针。
struct JNINativeInterface {
void* reserved0;
void* reserved1;
void* reserved2;
void* reserved3;
jint (*GetVersion)(JNIEnv *);
jclass (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*,
jsize);
jclass (*FindClass)(JNIEnv*, const char*);
jmethodID (*FromReflectedMethod)(JNIEnv*, jobject);
jfieldID (*FromReflectedField)(JNIEnv*, jobject);
/* spec doesn't show jboolean parameter */
jobject (*ToReflectedMethod)(JNIEnv*, jclass, jmethodID, jboolean);
jclass (*GetSuperclass)(JNIEnv*, jclass);
. . .
};
参考链接: https://www.androidos.net.cn/android/10.0.0_r6/xref/frameworks/base/cmds/app_process/app_main.cpp
Zygote进程是在Init进程启动的时候创建的。至于Init进程是怎么来的,可参考Android启动流程。
int main(int argc, char* const argv[])
{
if (!LOG_NDEBUG) {
String8 argv_String;
for (int i = 0; i < argc; ++i) {
argv_String.append("\"");
argv_String.append(argv[i]);
argv_String.append("\" ");
}
ALOGV("app_process main with argv: %s", argv_String.string());
}
AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
. . .
// Parse runtime arguments. Stop at first unrecognized option.
bool zygote = false;
bool startSystemServer = false;
bool application = false;
String8 niceName;
String8 className;
++i; // Skip unused "parent dir" argument.
while (i < argc) {
const char* arg = argv[i++];
if (strcmp(arg, "--zygote") == 0) {
zygote = true;
niceName = ZYGOTE_NICE_NAME;
. . .
}
. . .
if (zygote) {
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
} else if (className) {
runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
} else {
fprintf(stderr, "Error: no class name or --zygote supplied.\n");
app_usage();
LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
}
}
如果zygote是true,则说明当前运行在Zygote进程。走runtime.start("com.android.internal.os.ZygoteInit", args, zygote),start方法的实现如下:
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
ALOGD(">>>>>> START %s uid %d <<<<<<\n",
className != NULL ? className : "(unknown)", getuid());
. . .
/* start the virtual machine */
JniInvocation jni_invocation;
jni_invocation.Init(NULL);
JNIEnv* env;
//启动Java虚拟机 001
if (startVm(&mJavaVM, &env, zygote) != 0) {
return;
}
onVmCreated(env);
/*
* Register android functions.
*/
//为Java虚拟机注册JNI方法
if (startReg(env) < 0) {
ALOGE("Unable to register all android natives\n");
return;
}
/*
* We want to call main() with a String array with arguments in it.
* At present we have two arguments, the class name and an option string.
* Create an array to hold them.
*/
jclass stringClass;
jobjectArray strArray;
jstring classNameStr;
stringClass = env->FindClass("java/lang/String");
assert(stringClass != NULL);
strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL);
assert(strArray != NULL);
classNameStr = env->NewStringUTF(className);
assert(classNameStr != NULL);
env->SetObjectArrayElement(strArray, 0, classNameStr);
for (size_t i = 0; i < options.size(); ++i) {
jstring optionsStr = env->NewStringUTF(options.itemAt(i).string());
assert(optionsStr != NULL);
env->SetObjectArrayElement(strArray, i + 1, optionsStr);
}
/*
* Start VM. This thread becomes the main thread of the VM, and will
* not return until the VM exits.
*/
char* slashClassName = toSlashClassName(className != NULL ? className : "");
jclass startClass = env->FindClass(slashClassName);
if (startClass == NULL) {
ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
/* keep going */
} else {
//调ZygoteInit的main方法
jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
"([Ljava/lang/String;)V");//002
if (startMeth == NULL) {
ALOGE("JavaVM unable to find main() in '%s'\n", className);
/* keep going */
} else {
env->CallStaticVoidMethod(startClass, startMeth, strArray);
#if 0
if (env->ExceptionCheck())
threadExitUncaughtException(env);
#endif
}
}
free(slashClassName);
ALOGD("Shutting down VM\n");
if (mJavaVM->DetachCurrentThread() != JNI_OK)
ALOGW("Warning: unable to detach main thread\n");
if (mJavaVM->DestroyJavaVM() != 0)
ALOGW("Warning: VM did not shut down cleanly\n");
}
在上文的注释002处,进入ygoteInit的main方法。Zygote便进入了Java框架层,也就是说Zygote开创了Java框架层。
在注释001处,调用startVM启动Java虚拟机。
int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote)
{
. . .
/*
* Initialize the VM.
*
* The JavaVM* is essentially per-process, and the JNIEnv* is per-thread.
* If this call succeeds, the VM is ready, and we can start issuing
* JNI calls.
*/
if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) {
ALOGE("JNI_CreateJavaVM failed\n");
return -1;
}
return 0;
}
参考链接: https://www.androidos.net.cn/android/10.0.0_r6/xref/libnativehelper/JniInvocation.cpp
jint JniInvocationImpl::JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {
//调用前面从libart.so中找到的创建虚拟机的函数
return JNI_CreateJavaVM_(p_vm, p_env, vm_args);
}
NIEnv是在Zygote初始化的时候调用libart.so中的"JNI_CreateJavaVM"方法创建的。JNIEnv来的竟然如此复杂!
1.线程相关 : JNIEnv 是线程相关的, 即 在 每个线程中 都有一个 JNIEnv 指针, 每个JNIEnv 都是线程专有的, 其它线程不能使用本线程中的 JNIEnv, 线程 A 不能调用 线程 B 的 JNIEnv。JNIEnv 不能跨线程 :
•当前线程有效 : JNIEnv 只在当前线程有效, JNIEnv 不能在 线程之间进行传递, 在同一个线程中, 多次调用 JNI层方法, 传入的 JNIEnv 是相同的;•本地方法匹配多JNIEnv : 在 Java 层定义的本地方法, 可以在不同的线程调用, 因此 可以接受不同的 JNIEnv;
1. 所有的JNI调用都使用了JNIEnv*类型的指针,习惯上在CPP文件中将这个变量定义为evn,它是任意一个本地方法的第一个参数。env指针指向一个函数指针表,在VC中可以直接用"->"操作符访问其中的函数。2.jobject 指向在此 Java 代码中实例化的 Java 对象 LocalFunction的一个句柄,相当于this指针。后续的参数就是本地调用中有Java程序传进的参数。以下是我们经常会用到的字符串类型处理的函数:
•const char* GetStringUTFChars (jstring string,jboolean* isCopy) 返回指向字符串UTF编码的指针,如果不能创建这个字符数组,返回null。这个指针在调用ReleaseStringUTFChar()函数之前一直有效。 参数: string Java字符串对象 isCopy 如果进行拷贝,指向以JNI_TRUE填充的jboolean,否则指向以JNI_FALSE填充的jboolean。•void ReleaseStringUTFChars(jstring str, const char* chars) 通知虚拟机本地代码不再需要通过chars访问Java字符串。 参数: string Java字符串对象 chars 由GetStringChars返回的指针
•jstring NewStringUTF(const char *utf) 返回一个新的Java字符串并将utf内容拷贝入新串,如果不能创建字符串对象,返回null。通常在反值类型为string型时用到。 参数: utf UTF编码的字符串指针,对于数值型参数,在C/C++中可直接使用
通过ANdroid studio 新建c++项目,需要下载ndk和CMakelist。 核心代码:
MainActivity:
static {
System.loadLibrary("native-lib");
}
. . .
tv.setText(stringFromJNI());
native-lib.cpp
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
CMakeList.txt
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
native-lib.cpp )
使用pdf看一下,只能看出有字符串"Hello from c++"。至于怎么处理的看不出来。
decompiler看一下效果:
还是不能直接看出来怎么处理的“hello from c++”。 如果能把int64_t转换为JNIEnv *,同时相关的函数也可以带出来。我查询了一些资料,通过ESIL能够模拟出JNIEnv的地址,不过我没有成功,然后通过tl命令进行链接。类型处理是Radare2做得不太好的地方。不过问题总有解决办法,既然静态分析看不出来什么,那就使用动态分析。动态分析的内容会在后续文章中介绍。
这个使用Frida走一波,看看最终的返回值是什么。
function frida_Java() {
Java.perform(function () {
var targetClass = Java.use("com.example.myapplication.MainActivity");
targetClass.stringFromJNI.implementation = function(){
var result = this.stringFromJNI();
console.log("burning result "+result);
return result;
}
var targetFunc = Module.getExportByName("libnative-lib.so", 'Java_com_example_myapplication_MainActivity_stringFromJNI');
console.log("burning"+targetFunc);
Interceptor.attach(targetFunc, {
onEnter: function (args) {
console.log(args[0]);
console.log("burning Env"+JSON.stringify(Java.vm.getEnv()));
},
onLeave: function (retval) {
console.log("retval "+retval.toString());
}
});
});
}
setImmediate(frida_Java,0);
通过如下的命令运行,这样才能hook onCreate方法。
frida -U -f com.example.myapplication -l answner.js --no-pause
运行结果如下:
如果我想改返回值,
targetClass.stringFromJNI.implementation = function(){
var result = this.stringFromJNI();
console.log("burning result "+result);
result = "欢迎关注我的微信公众号:无情剑客";
return result;
}
通过Radare2静态分析so文件,不能看出直观的逻辑,即使使用decompiler也不是非常友好,使用ESIL模拟定位JNIEnv没能成功定位。希望Radare2在类型处理方面加强一些。动态调式在后续文章中会更新。
更多内容,欢迎关注我的微信公众号:无情剑客。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。