开发
Android studio新建项目
Android studio新建一个Native C++项目。
默认代码就是调用Native 方法stringFromJNI 返回一个字符串。
public native String stringFromJNI();
C++ 代码
stringFromJNI 函数的代码,默认使用的是静态注册的方式,静态注册是函数格式就是Java_函数路径。
代码很简单,定义一个字符串hello,调用NewStringUTF转成java字符串返回。
注意看参数,java方法是不带参数的,native函数里面有2个参数,一个是JNIEnv ,一个是jobject 。
如何java层是静态方法,那这里第二个参数就不是jobject 而是 jclass了。
参数这个点是逆向的时候需要注意的。
JNI的基础知识感兴趣的自行学习,这里不展开讲了。
extern "C" JNIEXPORT jstring JNICALL
Java_com_mycode_nativehello_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
动态注册
相对静态注册,动态注册的方式函数名字就可以是自定义的。
逆向的话就需要找动态注册的地方,比静态注册稍微安全一点。
jstring xxaa(JNIEnv *env, jobject instance) {
std::string hello = "Hello from C++ , 这是动态注册";
return env->NewStringUTF(hello.c_str());
}
jint RegisterNatives(JNIEnv *env) {
jclass clazz = env->FindClass("com/mycode/nativehello/MainActivity");
if (clazz == NULL) {
return JNI_ERR;
}
JNINativeMethod methods_MainActivity[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void *) xxaa}
};
return env->RegisterNatives(clazz, methods_MainActivity,
sizeof(methods_MainActivity) / sizeof(methods_MainActivity[0]));
}
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jint result = RegisterNatives(env);
return JNI_VERSION_1_6;
}
逆向
ida反编译so
将上面的demo编译好之后apk的lib目录下会生成so文件,分别是arm 和 x86的 32位和64位。
这里主要分析arm64
arm64 汇编
主要代码就下面三行
ADR指令将内存中的值取到寄存器,这个字符串 ida 已经自动识别出来。
然后是调用std::string::basic_string 和 NewStringUTF ,和源码都能一一对上。
ADR X1, aHelloFromC ; "Hello from C++"
...
BL ._ZNSt6__ndk112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEC2B7v170000IDnEEPKc ; std::string::basic_string<decltype(nullptr)>(char const*)
...
BL ._ZN7_JNIEnv12NewStringUTFEPKc ; _JNIEnv::NewStringUTF(char const*)
F5伪代码
TAB切换到伪代码界面,伪代码里面没有"Hello from C++" 字符串。这是为什么呢?
__int64 __fastcall Java_com_mycode_nativehello_MainActivity_stringFromJNI(_JNIEnv *a1)
{
const char *v1; // x0
__int64 v3; // [xsp+18h] [xbp-48h]
char v5[24]; // [xsp+40h] [xbp-20h] BYREF
__int64 v6; // [xsp+58h] [xbp-8h]
v6 = *(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
std::string::basic_string[abi:v170000]<decltype(nullptr)>();
v1 = sub_248A4(v5);
v3 = _JNIEnv::NewStringUTF(a1, v1);
std::string::~string(v5);
_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
return v3;
}
对比下x86的so
x86的so 正常有"Hello from C++" 字符串,对比上面的arm伪代码区别就在于basic_string的参数。
std::string::basic_string[abi:v170000]<decltype(nullptr)>(v4, “Hello from C++”);
__int64 __fastcall Java_com_mycode_nativehello_MainActivity_stringFromJNI(_JNIEnv *a1)
{
const char *v1; // rsi
__int64 v3; // [rsp+18h] [rbp-48h]
char v4[24]; // [rsp+40h] [rbp-20h] BYREF
unsigned __int64 v5; // [rsp+58h] [rbp-8h]
v5 = __readfsqword(0x28u);
std::string::basic_string[abi:v170000]<decltype(nullptr)>(v4, "Hello from C++");
v1 = sub_23AF0(v4);
v3 = _JNIEnv::NewStringUTF(a1, v1);
std::string::~string(v4);
return v3;
}
回到arm 伪代码界面,按Y确定。就ok了。
伪代码几乎是和源码差不多的,非常方便逆向分析,主要是有些解析的问题。
__int64 __fastcall Java_com_mycode_nativehello_MainActivity_stringFromJNI(_JNIEnv *a1)
{
const char *v1; // x0
__int64 v3; // [xsp+18h] [xbp-48h]
char v5[24]; // [xsp+40h] [xbp-20h] BYREF
__int64 v6; // [xsp+58h] [xbp-8h]
v6 = *(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
std::string::basic_string[abi:v170000]<decltype(nullptr)>(v5, "Hello from C++");
v1 = sub_248A4(v5);
v3 = _JNIEnv::NewStringUTF(a1, v1);
std::string::~string(v5);
_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
return v3;
}