移动安全之NDK开发学习一

移动安全 11个月前 admin
76 0 0

#01 NDK开发

原生开发套件 (NDK) 是一套工具,开发者能够在 Android 应用中使用 C 或C++ 代码来开发android应用,至少有以下优势:
  • 进一步提升设备性能,以降低延迟或运行游戏或物理模拟等计算密集型应用。
  • 重复使用自己或其他开发者的 C 或 C++ 库。
对于移动安全来说,我们要将一些想保护的代码使用c或c++实现或者在so层实现app运行环境检测、反调试等功能就必须对NDK开发进行学习。
长话短说,下面的代码都是在Android Studio中进行开发的,所以先要安装好Android Studio并配置好android开发环境。

#02 NDK项目

安装好android studio后,新建一个Native C++项目,以此来了解下NDK项目和一般的纯java项目的不同之处。

(1) New Project选择Native C++

移动安全之NDK开发学习一

移动安全之NDK开发学习一

移动安全之NDK开发学习一

(2) 项目结构

移动安全之NDK开发学习一

可以看到除了java文件夹还有一个cpp文件夹,cpp文件夹下的.cpp文件就是我们编写c/c++代码的地方。

(3) 项目默认代码阅读

我们先来学习下,Native C++模板创建的项目生成的默认代码,首先看MainActivity.java:

package com.hillstone.ndk;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import com.hillstone.ndk.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {
    // Used to load the 'ndk' library on application startup.
    static {
        System.loadLibrary("ndk");
    }

    private ActivityMainBinding binding;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // Example of a call to a native method
        TextView tv = binding.sampleText;
        tv.setText(stringFromJNI());
    }
    /**
     * A native method that is implemented by the 'ndk' native library,
     * which is packaged with this application.
     */

    public native String stringFromJNI();
}

非常明显地可以看到System.loadLibrary和native关键字,它们就是在java层调用so层函数的关键。

System.loadLibrary找到lib文件夹下的so文件后,加载so文件;
native关键字说明其修饰的方法是一个用c/c++实现的原生态方法。
接下来看native-lib.cpp的代码:
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_hillstone_ndk_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */)
 
{
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

extern “C”是编译成so文件的时候按c语言风格来编译。

重点关注返回类型jstring函数名Java_com_hillstone_ndk_MainActivity_stringFromJNI参数JNIEnv和jobject
  • jstring是JNI的数据类型,下面是Java数据类型与JNI数据类型的映射关系的总结:
    Java 数据类型 JNI 数据类型 含义 长度(字节)
    boolean jboolean unsigned char 1
    byte jbyte signed char 1
    char jchar unsigned short 2
    short jshort signed short 2
    int jint、jsize signed int 4
    long jlong signed long 8
    float jfloat signed float 4
    double jdouble signed double 8
    Class jclass Class 类对象 1
    String jstring 字符串对象 /
    Object jobject 对象 /
    Throwable jthrowable 异常对象 /
    boolean[] jbooleanArray 布尔数组 /
    byte[] jbyteArray byte 数组 /
    char[] jcharArray char 数组 /
    short[] jshortArray short 数组 /
    int[] jinitArray int 数组 /
    long[] jlongArray long 数组 /
    float[] jfloatArray float 数组 /
    double[] jdoubleArray double 数组 /
    可以看到JNI数据类型加了个 、数据就是加了个 Array 
  • Java_com_hillstone_ndk_MainActivity_stringFromJNI
    这里是静态注册的函数所用的函数命令规则,后面会学习动态注册。
    就是前缀Java + 包名 + 类名 + 方法名,用 _ 拼接。
  • 参数JNIEnv和jobject
    JNIEnv:指代了Java本地接口环境(Java Native Interface Environment),是一个JNI接口指针,指向了本地方法的一个函数表,该函数表中的每一个成员指向了一个JNI函数,本地方法通过JNI函数来访问JVM中的数据结构,详情如下图:

    移动安全之NDK开发学习一

    jobject:要在Native层访问Java中的类和对象,就要用到jobject和jclass。当所声明Native方法是静态方法时,对应参数jclass,因为静态方法不依赖对象实例,而依赖于类,所以参数中传递的是一个jclass类型。相反,如果声明的Native方法时非静态方法时,那么对应参数是jobject。

阅读完这两段代码之后,大概可以知道,MainActivity调用了cpp文件的Java_com_hillstone_ndk_MainActivity_stringFromJNI函数,会显示“Hello from C++”字符,我们运行程序看看结果。

这里可以看到逆向的时候,so函数的第三个参数开始才是用户写的参数。

(4) 运行程序

移动安全之NDK开发学习一

到这里,第一个demo就成功了。


#03 native层实现加密算法

我们来实现一个用c写的rc4加密算法,然后放到ndk项目里供java层调用。
(一) java层
我们先在java层实现一下
新建EncryptUtils.java如下:

移动安全之NDK开发学习一

EncryptUtils.java代码如下:
package com.hillstone.ndk;

import java.io.UnsupportedEncodingException;

public class EncryptUtils {

    public static String encryRC4String(String data, String key, String chartSet) throws UnsupportedEncodingException {
        if (data == null || key == null) {
            return null;
        }
        return bytesToHex(encryRC4Byte(data, key, chartSet));
    }

    public static byte[] encryRC4Byte(String data, String key, String chartSet) throws UnsupportedEncodingException, UnsupportedEncodingException {
        if (data == null || key == null) {
            return null;
        }
        if (chartSet == null || chartSet.isEmpty()) {
            byte bData[] = data.getBytes();
            return RC4Base(bData, key);
        } else {
            byte bData[] = data.getBytes(chartSet);
            return RC4Base(bData, key);
        }
    }


    public static String decryRC4(String data, String key,String chartSet) throws UnsupportedEncodingException {
        if (data == null || key == null) {
            return null;
        }
        return new String(RC4Base(hexToByte(data), key),chartSet);
    }

    private static byte[] initKey(String aKey) {
        byte[] bkey = aKey.getBytes();
        byte state[] = new byte[256];

        for (int i = 0; i < 256; i++) {
            state[i] = (byte) i;
        }
        int index1 = 0;
        int index2 = 0;
        if (bkey.length == 0) {
            return null;
        }
        for (int i = 0; i < 256; i++) {
            index2 = ((bkey[index1] & 0xff) + (state[i] & 0xff) + index2) & 0xff;
            byte tmp = state[i];
            state[i] = state[index2];
            state[index2] = tmp;
            index1 = (index1 + 1) % bkey.length;
        }
        return state;
    }


    public static String bytesToHex(byte[] bytes) {
        StringBuffer sb = new StringBuffer();
        for(int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(bytes[i] & 0xFF);
            if(hex.length() < 2){
                sb.append(0);
            }
            sb.append(hex);
        }
        return sb.toString();
    }


    public static byte[] hexToByte(String inHex){
        int hexlen = inHex.length();
        byte[] result;
        if (hexlen % 2 == 1){
            hexlen++;
            result = new byte[(hexlen/2)];
            inHex="0"+inHex;
        }else {
            result = new byte[(hexlen/2)];
        }
        int j=0;
        for (int i = 0; i < hexlen; i+=2){
            result[j]=(byte)Integer.parseInt(inHex.substring(i,i+2),16);
            j++;
        }
        return result;
    }

    private static byte[] RC4Base(byte[] input, String mKkey) {
        int x = 0;
        int y = 0;
        byte key[] = initKey(mKkey);
        int xorIndex;
        byte[] result = new byte[input.length];
        for (int i = 0; i < input.length; i++) {
            x = (x + 1) & 0xff;
            y = ((key[x] & 0xff) + y) & 0xff;
            byte tmp = key[x];
            key[x] = key[y];
            key[y] = tmp;
            xorIndex = ((key[x] & 0xff) + (key[y] & 0xff)) & 0xff;
            result[i] = (byte) (input[i] ^ key[xorIndex]);
        }
        return result;
    }

}
然后在MainActivity.java里调用一下。
String EncrytedText = EncryptUtils.encryRC4String(text,"654321","UTF-8");
Toast.makeText(MainActivity.this,"orginal text:"+text + "n" + "EncryptedText:"+ EncrytedText,Toast.LENGTH_LONG).show();
运行结果:

移动安全之NDK开发学习一

(二) native层

首先在clion里写个demo,然后移植过去比较好

移动安全之NDK开发学习一

这里给一下c的代码:
#include <stdio.h>
#include <cstring>

static void rc4_init(unsigned char* s_box, unsigned char* key, unsigned int key_len)
{
    unsigned char Temp[256];
    int i;
    for (i = 0; i < 256; i++)
    {
        s_box[i] = i;//顺序填充S盒
        Temp[i] = key[i%key_len];//生成临时变量T
    }
    int j = 0;
    for (i = 0; i < 256; i++)//打乱S盒
    {
        j = (j + s_box[i] + Temp[i]) % 256;
        unsigned char tmp = s_box[i];
        s_box[i] = s_box[j];
        s_box[j] = tmp;
    }
}

void rc4_crypt(unsigned char* data, unsigned int data_len, unsigned char* key, unsigned int key_len)
{
    unsigned char s_box[256];
    rc4_init(s_box, key, key_len);
    unsigned int i = 0, j = 0, t;
    unsigned int Temp;
    for (Temp = 0; Temp < data_len; Temp++)
    {
        i = (i + 1) % 256;
        j = (j + s_box[i]) % 256;
        unsigned char tmp = s_box[i];
        s_box[i] = s_box[j];
        s_box[j] = tmp;
        t = (s_box[i] + s_box[j]) % 256;
        data[Temp] ^= s_box[t];
    }
}

int main()
{
    unsigned char text[] = "aaabbbccc";
    unsigned char key[] = "654321";
    unsigned int i;
    printf("plaintext:");
    for (i = 0; i < strlen((const char*)text); i++)
        printf("%c", text[i]);
    printf("n");
    rc4_crypt(text, strlen((const char*)text), key, strlen((const char*)key));
    char hex_str[sizeof(text) * 2 + 1];
    for (i = 0; i < sizeof(text); i++) {
        sprintf(&hex_str[i * 2], "%02x", text[i]); // convert byte to hex and write it to the string
    }
    printf("hex_str:%s",hex_str);
    return 0;
}
回顾我们之前说的数据类型问题,需要改造一下main函数才能正确加密,代码如下:
extern "C"
JNIEXPORT jstring JNICALL
Java_com_hillstone_ndk_MainActivity_rc4encrypt(JNIEnv *env, jobject thiz, jstring text) 
{

    unsigned char key[] = "654321";
    unsigned int i;
    unsigned char* text_ = (unsigned char *) env->GetStringUTFChars(text, 0);
    rc4_crypt(text_, sizeof text_, key, strlen((const char*)key));
    char hex_str[sizeof(text) * 2 + 1];
    for (i = 0; i < sizeof(text); i++) {
        sprintf(&hex_str[i * 2], "%02x", text_[i]); // convert byte to hex and write it to the string
    }
    return env->NewStringUTF(hex_str);
}

然后修改MainActivity的代码:

public class MainActivity extends AppCompatActivity {
    // Used to load the 'ndk' library on application startup.
    static {
        System.loadLibrary("ndk");
    }

    private ActivityMainBinding binding;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // Example of a call to a native method
        TextView tv = binding.sampleText;
        tv.setText(stringFromJNI());

        Button btn1 = (Button) findViewById(R.id.btn1);
        btn1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                EditText editText= (EditText) findViewById(R.id.editTextText);
                String text = editText.getText().toString();
                // String EncrytedText = EncryptUtils.encryRC4String(text,"654321","UTF-8");
                String EncryptedText = rc4encrypt(text);
                Toast.makeText(MainActivity.this,"orginal text:"+text + "n" + "EncryptedText:"+ EncryptedText,Toast.LENGTH_LONG).show();

            }
        });
    }
    /**
     * A native method that is implemented by the 'ndk' native library,
     * which is packaged with this application.
     */

    public native String stringFromJNI();

    public native String rc4encrypt(String text);
}

运行结果:

移动安全之NDK开发学习一

可以看到和java函数实现的效果一样,我们的目的就达成了,其他算法的移植也可以同样操作,但是还没有完,因为静态注册的函数很容易被反编译后找到,我们还需要学习动态注册。


#04 静态注册与动态注册

  • 静态注册
我们前面使用的方式就是静态注册。函数名的特点就是以字符串Java为前缀,并且用_下划线将包名、类名以及native方法名连接起来。
优点是实现简单,易于理解;缺点是必须遵循某些规则,JNI方法名过长,同时安全性不高,后面我们会分别反编译静态注册和动态注册下生成的so文件来观察同样功能的函数。
  • 动态注册

实现原理:在调用System.loadLibrary()时会在so层调用一个名为JNI_OnLoad()的函数,我们提供一个函数映射表,再在JNI_Onload()函数中通过JNI中提供的RegisterNatives()方法来注册函数。这样Java就可以通过函数映射表来调用函数,而不必通过函数名来查找对应函数。

用ida反编译生成的so文件,记录一下结果

移动安全之NDK开发学习一

移动安全之NDK开发学习一

先将stringFromJNI改为动态注册来看看效果,代码如下:
JNIEXPORT jstring
JNICALL stringFromJNI(JNIEnv *env,jobject /* this */) 
{
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv * env;
    vm->GetEnv((void**)&env,JNI_VERSION_1_6);
    JNINativeMethod methods[] = {
            {"stringFromJNI","()Ljava/lang/String;",(void*)stringFromJNI},
    };
    env->RegisterNatives(env->FindClass("com/hillstone/ndk/MainActivity"),methods,1);
    return JNI_VERSION_1_6;
}
看这行代码
env->RegisterNatives(env->FindClass("com/hillstone/ndk/MainActivity"),methods,1);

RegisterNatives的三个参数分别是类名,method数组和数组长度。

JNINativeMethod是一个结构体,定义如下:

typedef struct {
    const char* name;         // native方法名
    const char* signature;    // 方法签名,例如()Ljava/lang/String;
    void*       fnPtr;        // 函数指针
} JNINativeMethod;
fnPtr是函数地址,signature是函数签名包括入参和返回值的。
现在我们再用ida反编译修改后的So文件

移动安全之NDK开发学习一

移动安全之NDK开发学习一

在导出表看不到stringFromJNI了,需要在JNI_ONLoad里去找。

现在我们可以试着将加密算法动态注册了

jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv * env;
    vm->GetEnv((void**)&env,JNI_VERSION_1_6);

    JNINativeMethod methods[] = {
            {"stringFromJNI","()Ljava/lang/String;",(void*)stringFromJNI},
            {"rc4encrypt","(Ljava/lang/String;)Ljava/lang/String;",(void*)rc4encrypt}
    };
    env->RegisterNatives(env->FindClass("com/hillstone/ndk/MainActivity"),methods,2);
    return JNI_VERSION_1_6;
}

我们再把符号给隐藏下

__attribute__((visibility("hidden"))) jstring rc4encrypt(JNIEnv *env, jobject thiz, jstring text) {

    unsigned char key[] = "654321";
    unsigned int i;
    unsigned char* text_ = (unsigned char *) env->GetStringUTFChars(text, 0);
    rc4_crypt(text_, sizeof text_, key, strlen((const char*)key));
    char hex_str[sizeof(text) * 2 + 1];
    for (i = 0; i < sizeof(text); i++) {
        sprintf(&hex_str[i * 2], "%02x", text_[i]); // convert byte to hex and write it to the string
    }
    return env->NewStringUTF(hex_str);
}

修改好后,生成app,用ida看一下,结果如下:

移动安全之NDK开发学习一

移动安全之NDK开发学习一

#05 总结

通过上面的学习我们了解了如何在java层调用c/c++函数,学会了移植算法到native层和动态注册native层的函数,之后我们学习的移动安全技术如加固、混淆、反调试手段都要用到ndk开发。

源码链接:

https://pan.baidu.com/s/1mN41vbfg-06mr1r6VRzovg?pwd=uz6t

原文始发于微信公众号(山石网科安全技术研究院):移动安全之NDK开发学习一

版权声明:admin 发表于 2023年12月28日 上午11:03。
转载请注明:移动安全之NDK开发学习一 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...