#01 NDK开发
-
进一步提升设备性能,以降低延迟或运行游戏或物理模拟等计算密集型应用。 -
重复使用自己或其他开发者的 C 或 C++ 库。
#02 NDK项目
(1) New Project选择Native C++
可以看到除了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层函数的关键。
#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是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数据类型加了个 j 、数据就是加了个 Array -
Java_com_hillstone_ndk_MainActivity_stringFromJNI 这里是静态注册的函数所用的函数命令规则,后面会学习动态注册。 就是前缀Java + 包名 + 类名 + 方法名,用 _ 拼接。 -
参数JNIEnv和jobject JNIEnv:指代了Java本地接口环境(Java Native Interface Environment),是一个JNI接口指针,指向了本地方法的一个函数表,该函数表中的每一个成员指向了一个JNI函数,本地方法通过JNI函数来访问JVM中的数据结构,详情如下图: jobject:要在Native层访问Java中的类和对象,就要用到jobject和jclass。当所声明Native方法是静态方法时,对应参数jclass,因为静态方法不依赖对象实例,而依赖于类,所以参数中传递的是一个jclass类型。相反,如果声明的Native方法时非静态方法时,那么对应参数是jobject。
阅读完这两段代码之后,大概可以知道,MainActivity调用了cpp文件的Java_com_hillstone_ndk_MainActivity_stringFromJNI函数,会显示“Hello from C++”字符,我们运行程序看看结果。
这里可以看到逆向的时候,so函数的第三个参数开始才是用户写的参数。
(4) 运行程序
到这里,第一个demo就成功了。
#03 native层实现加密算法
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;
}
}
String EncrytedText = EncryptUtils.encryRC4String(text,"654321","UTF-8");
Toast.makeText(MainActivity.this,"orginal text:"+text + "n" + "EncryptedText:"+ EncrytedText,Toast.LENGTH_LONG).show();
(二) native层
首先在clion里写个demo,然后移植过去比较好
#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;
}
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);
}
运行结果:
可以看到和java函数实现的效果一样,我们的目的就达成了,其他算法的移植也可以同样操作,但是还没有完,因为静态注册的函数很容易被反编译后找到,我们还需要学习动态注册。
#04 静态注册与动态注册
-
静态注册
-
动态注册
实现原理:在调用System.loadLibrary()时会在so层调用一个名为JNI_OnLoad()的函数,我们提供一个函数映射表,再在JNI_Onload()函数中通过JNI中提供的RegisterNatives()方法来注册函数。这样Java就可以通过函数映射表来调用函数,而不必通过函数名来查找对应函数。
用ida反编译生成的so文件,记录一下结果
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;
在导出表看不到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看一下,结果如下:
#05 总结
通过上面的学习我们了解了如何在java层调用c/c++函数,学会了移植算法到native层和动态注册native层的函数,之后我们学习的移动安全技术如加固、混淆、反调试手段都要用到ndk开发。
源码链接:
https://pan.baidu.com/s/1mN41vbfg-06mr1r6VRzovg?pwd=uz6t
原文始发于微信公众号(山石网科安全技术研究院):移动安全之NDK开发学习一