【移动安全】脑洞型so加载过程实验

作者:penguin_wwy
i春秋社区
文章难易度【★★★★★】
阅读基础:熟悉Android虚拟机源码、so加载过程、Native编程
【预备~~~起】
前几天有人问我,ELF的可执行文件能不能调用so文件的JNI_OnLoad函数。这倒是一个有脑洞的想法,我尝试了一夜,就把尝试的过程记录下来。
【一二三四】
先从理论上分析一下可能性,对于so文件我们在代码里是可以dlopen函数打开,然后dlsym函数定位so文件中的函数地址执行调用的。也就是说只要我们可以解决参数问题,调用so文件中的任意函数都是可以的。
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM vm, void reserved);
这是jni.h中JNI_OnLoad函数的声明,对于第二个参数我们可以不管。第一个参数是一个虚拟机实例。在正常的APK调用so的过程中,Java层会将自己的JavaVM传递到JNI_OnLoad中,通过JavaVM对象中的函数表中的GetEnv,可以获得JNIEnv对象,JNIEnv对象的函数表中保存了我们在编写so代码中经常用到的函数如NewStringUTF,该函数接受一个char 字符串转化为jstring字符串。
也就是说,如果我们要正确转化Java层和Native层互相传递的参数,或者正确使用他们之间的相互调用,JNIEnv对象不能少,而JNIEnv对象依托于JavaVM,JavaVM同样需要正确的存在(传递到JNI_OnLoad当中)。搞清楚了这一点,我们就知道目前的问题了,因为要正确调用JNIEnv中的函数表,所以JavaVM必须正确传递。
我们必须要有一个正确的JavaVM对象,而这是一个虚拟机实例,难道我要创建一个虚拟机?
那能不能借用其他进程的JavaVM呢,应该是可以的,这样就相当于被借用进程的Java层加载了这个so文件。只要有足够的权限应该可以办到。然而我并没有采用这种办法。
既然不借用,那就只能自己创建一个虚拟机了。但是创建之前必须搞清楚一点,我们需要这个JavaVM对象干嘛?从前面的分析知道,这个JavaVM对象之所以不可或缺是因为我们需要JNIEnv的对象,而需要JNIEnv对象的目的是为了正确调用JNIEnv对象中的那些函数。本质上我们是在使用那些函数,JavaVM和JNIEnv只是调用那些函数的桥梁而已。
我们似乎可以有这么一个猜想:既然Native层需要JavaVM只是为了通过它获得函数表中的函数,那如果我们创建一个空的JavaVM(以及JNIEnv),然后将需要调用的函数注册了,也就是将JNIEnv函数表中的函数指针指向我们自己的函数,这样在调用函数的时候也就能正确执行。
如何佐证这点呢?事实上,不管是加载过程还是加载成功后我们在Native层写的代码,从来没有检查过JNIEnv对象的完整性,也就是Android系统根本不关心这个JNIEnv是不是一个真正的JNIEnv对象,理论上做一个空对象是可行的。
我们通过Android源码看一下JNI_OnLoad中JavaVM对象是怎么来的
gDvmJni是一个全局变量,保存了与虚拟机相关的设置信息
可以看到gDvmJni.jniVm保存的是pVM
上面是pVM的创建过程。仿照这个过程,便可以创建一个空的JavaVM对象
【二二三四】
根据上面的分析,我们准备进行实验,验证之前的猜想。由上面得知,需要测试的是我们自己创建的空JavaVM对象能否顺利传入JNI_OnLoad并且通过本地的函数顺利执行。所以这里并没有直接用ELF加载so文件调用JNI_OnLoad,而是在APK中组建两个不同的so文件,取名为native-lib和main-lib,Java层通过API调用native-lib文件,然后在native-lib中创建空JavaVM对象,继而加载main-lib并调用。这样做一来可以利用AndroidStudio强大的调试能力,便于调试;二来可以调用Apk的JavaVM帮助我们实现部分功能。
public class MainActivity extends Activity {
static {

“会场枪击”打出多少制度漏洞

System.loadLibrary(“native-lib”);

}

@Override
public void onCreate(Bundle saveInstanceState) {
super.onCreate(saveInstanceState);
setContentView(R.layout.main_activity);

安全生产、职业健康、环境保护
Button lib = (Button)findViewById(R.id.libnative);
Button main = (Button)findViewById(R.id.main);
final TextView mTextView = (TextView)findViewById(R.id.textView);
mTextView.setText(mTextView.getText(), TextView.BufferType.EDITABLE);

lib.setOnClickListener(new View.OnClickListener() {
@Override

戴尔EMC补丁在VMAX存储系统中出现漏洞

public void onClick(View view) {
mTextView.append(strFromLib() + “\n”);
}
});

main.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//mTextView.append(strFromMain() + “\n”);
}
});

}

protected native String strFromLib();
MainActivity类,设置一个按钮,按下后调用native-lib中的strFromLib函数,该函数返回一个jstring,如果正确执行,则返回“jni_onload success”。
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).
# Associated headers in the same location as their source
# file are automatically included.
src/main/jni/native.cpp )

add_library( # Sets the name of the library.
main-lib

# Sets the library as a shared library.
SHARED

# Provides a relative path to your source file(s).
# Associated headers in the same location as their source
# file are automatically included.
src/main/jni/main.cpp )
CMakeLists.txt中,准备两个so库文件
先看main-lib中的代码
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM vm, void reserved) {
void env;
if (vm->GetEnv((void )&env, JNI_VERSION_1_4) == JNI_OK) { //获取JNI_Env
jstring tmp= ((JNIEnv )env)->NewStringUTF(“jni_onload success”); //调用NewStringUTF

480.8万次网站高危漏洞被扫描发现 同比增长80%

const char ptr = ((JNIEnv )env)->GetStringUTFChars(tmp, 0); //调用GetStringUTFChars
strcpy(gString, ptr);
((JNIEnv )env)->ReleaseStringUTFChars(tmp, ptr); //调用ReleaseStringUTFChars
}

return JNI_VERSION_1_4;
}
gString是一个全局的字符串。如果执行成功,先调用NewStringUTF生成一个jstring,内容为jni_onload success,之后调用GetStringUTFChars转换成一个char 字符串,再拷贝到gString,然后通过ReleaseStringUTFChars将ptr收回。JNI_OnLoad函数中调用了GetEnv,NewStringUTF,GetStringUTFChars,ReleaseStringUTFChars。这四个函数。再看native-lib
typedef struct NewStruct {
JavaVM javaVM;
JNIEnv jniEnv;
JNIEnv really;
}newStruct;

newStruct ptr;
为了方便之后的调用,准备一个新的结构,包含之后自己创建的JavaVM和JNIEnv,最后一个really变量是Apk中的真正的JNIEnv。ptr是newStruct对象的一个全局指针。
newStruct newJavaVM() {
newStruct ptr = (newStruct )malloc(sizeof(newStruct));
memset(ptr, 0, sizeof(ptr));

JavaVM javaVM = (JavaVM )malloc(sizeof(javaVM));
memset(javaVM, 0, sizeof(javaVM));

JNIEnv jniEnv = (JNIEnv )malloc(sizeof(JNIEnv));
memset(jniEnv, 0, sizeof(jniEnv));

JNIInvokeInterface jniInvokeInterface = (JNIInvokeInterface )malloc(sizeof(JNIInvokeInterface));
memset(jniInvokeInterface, 0, sizeof(JNIInvokeInterface));
jniInvokeInterface->GetEnv = getEnv;

javaVM->functions = jniInvokeInterface;

JNINativeInterface jniNativeInterface = (JNINativeInterface )malloc(sizeof(JNINativeInterface));
memset(jniNativeInterface, 0, sizeof(JNINativeInterface));
jniNativeInterface->NewStringUTF = newStringUTF;
jniNativeInterface->GetStringUTFChars = getStringUTFChars;
jniNativeInterface->ReleaseStringUTFChars = releaseStringUTFChars;
jniNativeInterface->GetJavaVM = getJavaVM;

jniEnv->functions = jniNativeInterface;

ptr->javaVM = javaVM;
ptr->jniEnv = jniEnv;

return ptr;
}
该函数用来创建我们之前分析提到的结构jniInvokeInterface和jniNativeInterface是JavaVM和JNIEnv中的函数表结构。这当中的函数指针设置为本地的函数,除了之前说到的四个函数,还有一个GetJavaVM
jint getEnv(JavaVM javaVM, void env, jint version) {
env = ptr->jniEnv;
return JNI_OK;
}

//jstring (NewStringUTF)(JNIEnv , const char );
jstring newStringUTF(JNIEnv jniEnv, const char bytes) {
return ptr->really->NewStringUTF(bytes);
}

const char getStringUTFChars(JNIEnv jniEnv, jstring string, jboolean isCopy) {
return ptr->really->GetStringUTFChars(string, isCopy);
}

void releaseStringUTFChars(JNIEnv jniEnv, jstring string, const char utf) {

ptr->really->ReleaseStringUTFChars(string, utf);
}

jint getJavaVM(JNIEnv jniEnv, JavaVM vm) {
vm = ptr->javaVM;
return 0;
}
这五个函数完全按照jni.h中的声明实现,为了方便并没有完全自己实现,几个复杂的函数直接调用了really中对应的函数,毕竟我们的目的是看能否顺利执行,如何实现先不管。
/
其实所有函数都位于libdvm.so中(Debug时通过地址可以看到)
但是每个函数都会调用一个JNIEnv,并不能保证函数内部没有调用这个JNIEnv对象
所以意味着要么自己实现,要么调用一个真正的JNIEnv

/

ptr = newJavaVM();
ptr->really = really_env; //对于没有办法自己写的函数,调用真正的JNIEnv的函数表中的函数填充

JNIEnv env = ptr->jniEnv; //使用自己构造的JNIEnv
void hand = (void )dlopen(“libmain-lib.so”, RTLD_LAZY);
jint (jni_onload)(JavaVM , void );
jni_onload = (jint ()(JavaVM , void ))dlsym(hand, “JNI_OnLoad”);
if (jni_onload != NULL) {
void javaVM;
env->GetJavaVM((JavaVM )&javaVM);
jni_onload((JavaVM )javaVM, (void )0);
char gString = (char )dlsym(hand, “gString”);
if (gString != NULL) {
return env->NewStringUTF(gString);
}
}
近年来,针对政府、金融、交通、电力、教育、科研等领域系统的攻击数量明显上升,攻击者会选择和利用我们的最薄弱环节,所以要想在未来的攻防博弈游戏中取得胜利,我们得加强安全体系建设,提升整体的信息安全防范水平。
return env->NewStringUTF(“jni_onload fail”);
然后调用JNI_OnLoad,并且获取全局变量gString,如果JNI_OnLoad执行成功,则返回正确的字符串“jni_onload success”
【三二三四】
之后看执行情况
从调试结果看,执行了我们自己定义的本地函数
最终结果成功。
【四二三四】
总结一下。最开始收到问题ELF文件能否调用so文件中的JNI_OnLoad函数。逐步分析:(1)JavaVM参数必须传入(2)传入的目的是为了JNIEnv中的functions,也就是函数表中的函数(3)由(1)和(2)猜想是否只是需要函数表中的函数指针指向正确的地址,其余内容可以为空(4)猜想(3)的佐证,加载过程和执行过程中没有对JNIEnv做完整性检查(5)对猜想(3)进行验证,执行过程中成功调用了本地的函数此外,在正常程序的调试过程中可以看到所有的函数指针指向的位置都在libdvm.so中,第一个参数都是JNIEnv。在Dalvik虚拟机的源码解读中一直提到一个Java环境和JNI环境,其实所谓的环境最直观的便是能否直接使用相对应的函数指针。对Apk加固的意义,既然空的JavaVM在完善部分函数后就可以当作参数传入,那么意味着对于多个so文件的Apk,完全可以通过一个so加载其余so,并且通过自定义的JavaVM。在本地的函数中添加各种加密、hook等等手段达到加固的目的。
试起来吧少年!
本文属i春秋原创奖励计划,未经许可禁止转载。
参与活动在i春秋社区发帖并注明参与奖励计划即可!
该文章作者已设置需关注才可以留言
微信扫一扫关注该公众号

企业信息安全面临更猛烈冲击,移动化和个人化带来更多威胁,让企业网络的安全问题更为复杂。

猜您喜欢

ORICO推出PCI-E M.2 SSD磁盘阵列卡
CyberSecurity网络安全宣传——勿忘在外时的资产保护
移动支付中间人攻击防范
全国检察已提起公益诉讼15件
UEHTML PRODUKTE24
互联网金融您不知道的肮胀交易