本文意在解决进行安卓逆向时初学者(已掌握c语言)容易遇到的部分困难,仅以逆向者的角度分析问题,因此不讨论开发技术细节。
如果解决不了,也请不要打我
JNI(Java Native Interface)
JNI规范,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植。从而实现C/C++代码在JVM虚拟机中运行。而在JAVA代码中声明native方法,即可调用.so文件中的函数方法。
JNIEnv和jobject
话不多说,现看开发者要生成一个.so文件,通常要写的代码:
JNIEXPORT void JNICALL Java_com_jni_demo_Test(JNIEnv * env,jobject obj)
{
//代码部分
}
看到这里你小小的脑袋可能和我一样充满大大的疑惑
- JNIEXPORT和JNICALL的用途在于标识当前的函数是可以在JAVA层中被调用。
- 不同于JAVA层中用“.”进行标识包,这里使用“_”来标识。
- 本次函数中的两个参数为编写时的固定参数,通过传入JNIEnv*可以对JAVA层的代码进行操作,例如调用JAVA层的方法,获取对象等。
- 对于jobject参数,我们则需要到JAVA层中观察代码
public class TestJava{
//..........
public static native int Test(String arg0) {}
//..........
}
又或者是
public class TestJava{
//..........
public native int Test(String arg0) {}
//..........
}
当native方法是static时,obj就代表native方法的类的class对象实例,不是static时,obj则代表native方法的类实例。
**此时需要注意的是,*作为逆向者,当使用IDA时,在JAVA层声明时的参数个数会在IDA中莫名奇妙的多两个,其实也就是本文中提到的JNIEnv,jobject。然而IDA并不吃这一套 然而此处IDA会默认为int a1和int a2,导致部分代码中的方法非常难识别,比如:
因此,需要手动修改参数的类型,选中参数,修改a1和a2的类型分别为JNIEnv*和jobject。
函数也自动变得容易理解了。
但由于JAVA与C++仍是两种类型,二者数据类型存在如下的对应关系:
当IDA进行逆向后,会将数据类型还原成本地类型。
关于函数GetStringUTFChars,由于JAVA使用的是Unicode编码,因此进行一种转换 ,此处作为逆向者仅理解为将该函数的第二个参数转换为char *类型即可。ReleaseStringUTFChars即为将当前的字符串的内存释放。
关于函数memset:在一段内存块中填充某个给定的值
memset(void *buffer, int c, int count)
其中的buffer为指针,c为填充的内容,count为填充的前几个字符
说人话就是,将buffer对应的字符串的前count的字符填充为c
类似memset(buffer, 0, count)的用法,是将buffer对应的内存清空
关于函数memcpy
memcpy(void *s1,void *s2,size_t length)
向s1中复制s2的前length个字符
补一个函数__OFSUB__表示x-y是否溢出,溢出返回1,没有溢出返回0。
jclass类,以及C++获取Java类
获取jclass的几种方法:
- 获取Java中的类
jclass FindClass(const char* clsName)
//通过传入类的完整名称获得jclass,
//如:
jclass Java_class = env->FindClass("com/demo/test")
- 通过jobject获取Java中的类
jclass GetObjectClass(jobject obj)
//通过对象实例来获取类,类似Java中的getClass方法
//例:
jclass ax_list_jclass = env->GetObjectClass(String);
- 获取父类对象
jclass GetSuperClass(jclass obj)
//获取obj的父类对象,不再举例
获取JAVA属性和方法
- 获取属性/方法,前获取jfieldID/jmethodID
env->GetFiedID(jclass clazz,const char *name,const char *sign)
//其中clazz为属性对应的jcalss对象,name为获取属性名称,sign为字段的签名
//sign可由命令javap -s -p Demo.class命令获取,签名和数据类型相对应,(传入的参数)
env->GetMethodID(jclass clazz,const char *name,const char *sign)
//GetStaticMethodID和GetStaticFiedID与上面两种方法区别不大,不再赘述。
作为逆向者,此处还是以IDA反汇编结果为例,下图中的GetMethodID方法参数中,v3为JNIEnv* a1,v4为findClass获得的结果,即对应上述jclass,
C++中调用Java层的函数
调用方法:
env->Call<Type>Method(jobject,jFieldID,传入参数)
此外还有静态函数的调用方法:
env->CallStatic<Type>Method()
获取Java对象
jobject NewObject(jclass clazz,jmethodID ID,构造函数的其他参数)
//获取构造函数的jmethodID时,所用的签名为()V,方法名称为<init>
//另一种方法:
env->AllocObject(jclass clazz)
//作为非开发人员的逆向者,得知这两种方法可以获得java对象即可
对Java中字符串的操作
//获取字符串长度
jsize GetStringLength(jstring jstr)
//jstring 转换为 char*
env->GetStringRegion(jstring jstr,jsize start,jsize length,char* result)
//const jchar*转为jstring对象
jobject NewString(const jchar* jstr,int size)
//jstring 转换为 const jchar*
GetStringChars(jstring jstr,jboolean* copied)
//jboolean类型的指针用于标识是否对字符串进行了复制,已复制则设置为JNI_TRUE,否则为JNI_FALSE。若传入的指针为NULL,则不关心是否复制。
ReleaseStringChars(jstring jstr,const jcahr* jstr)
//用于对字符串释放内存
GetStringUTFChars()
用法与上方法类似,释放方法:
ReleaseStringUTFChars()
获取Java中数组的方法
//获取数组对象,其中数组类型,如jintArray
j<Type>Array array=(j<Type>Array)env->GetObjectField(jobject obj,jfieldID jarray_ID)
//获取数组对象指针
j<Type>* array=env->Get<type>ArrayElements(jfieldID jarray_ID,NULL)
//使用C++数组给Java数组进行赋值
Set<Type>ArrayRegion(<Type>Array arr,jsize start, jsize len,const <Type>* buffer)
//将Java数组中的值赋值给buffer
Get<Type>ArrayRegion(<Type>Array array,jsize start,jsize len,const <Type>*buffer)