The last article 【Android NDK】 (three) using c++ parse so file structure tells about so library parsing, for this chapter laid a good foundation, the article mainly tells how to realize so library encryption and decryption, increase the security of dynamic library.
1. So encryption scheme
-
Dynamic loading of so System.load(String pathName) supports loading of SO in absolute path, that is to say, we can completely encrypt the whole SO and put it in APP Assets or server, and then decrypt it before loading. In this way, it is simple and crude. Moreover, if the APP is downloaded dynamically, SO does not need to be packaged with the APP to achieve the effect of slimming the APP. The disadvantage is that the process is complex and the loading time is long.
-
In the current mode, encrypt so and decrypt it when the APP runs. After encrypting the so library locally, put it in the lib directory again, package it, install and run, use System. LoadLibrary (fileName) when loading, and then decrypt it, so that the project directory does not need to change, and the loading and parsing time is fast.
The first kind is not discussed at present, interested can check on the Internet, this chapter focuses on the realization of the second point.
2. Principle and feasibility analysis of the scheme
Two questions need to be considered first:
- Will it load properly after encryption?
- When will it be decrypted?
Problem # 1: we could try to decrypt the so file in its entirety, but then the APP would crash on system.loadLibrary ().
Why did it crash?
This is because after so is loaded, so will be parsed and loaded. If we encrypt the entire file, the structure will break, the system will not be able to parse and will crash. So we’re only looking for a partial area to encrypt. Loading process can refer to Android Linker and SO shell technology Android
What areas need to be encrypted?
First we need to be clear: what do we need to encrypt? In fact, it is very simple, is the same as Java confusion, we write our own Java code confusion, here is the c++ code we wrote encryption processing, improve the difficulty of cracking. So the area we encrypt is the area where we store the source code.
How do I determine the location area of the source code?
The analysis process of so library explained in the previous chapter is as follows:
- Read the so file and parse out the ELF Header.
- Parse Program Headers information
- Find the Program Header of type PT_DYNAMIC.
- PT_DYNAMIC resolves the symbol table address, symbol hash address, string table address, and the total size of string bytes.
- Iterate over the index of all symbol tables in the hash table to find the corresponding symbol structure in the symbol table.
- Filter the system function based on the name of the symbol structure, and what is left is our own source code symbol structure, and then determine the area of the method body based on the offset and size of the symbol structure.
How to encrypt?
We found the offset and size of our source code in the file above. After we read it out, we take the reverse or character offset +1 (we can use a more reliable symmetric encryption method, which will not be discussed in this chapter), mainly cannot change the length and can be restored.
How do I verify encryption?
- Use the IDA Pro tool to look at so. There is a method called encryptOrDecrypt in SO.
Encrypted before:
After the encryption:
You can see to the right, obviously you can’t see the instructions, so it’s encrypted.
How do I verify that encryption is successful?
Just seeing the encryption is still not enough, we also need to load successfully to be encrypted. At this point, we will replace the encrypted so, run the APP, and use system.loadLibrary () to load the code. Note that we can not call our native method just after loading. After loading, the program does not crash, indicating that the previous encryption is indeed successful! Call native method at this time, the program will crash, because encryption, is a normal phenomenon.
So far, the first question above, after encryption can still load normally? The answer is yes!
Second question, when do we decrypt it? To do this, we need to find the location of the so library first.
After APP loads so library, where is so library?
In Linux, there is a saying, “Everything is a file”, and so are processes. /proc/APP/pid/maps.
// Check the phone process
adb shell ps
// -- find the pid corresponding to the current package name, such as 1001
// Go to the phone console
adb shell
cd /proc/1001 // Pid 1001 found above
su // If you look directly at cat maps, you may not have permission, so you need to upgrade the permission
cat maps
// Here are the results of Cat Maps. 8fb8c000-8fb93000 r-xp00000000 fd:20 22465 /data/app/com.kongge.solibencryption-ns70YbB0b5JyL9sKnYs4Q==/lib/x86/libDataEncryptionLib.so
8fb93000-8fb95000 r-xp 00007000 fd:20 22465 /data/app/com.kongge.solibencryption-ns70YbB0b5JyL9sKnYs4Q==/lib/x86/libDataEncryptionLib.so
...
// The first 8fb8c000 is the base address
Copy the code
How to decrypt it?
Once the base address is found, we can start parsing and decrypting as we did before parsing encryption.
How to verify decryption success?
The last step is very simple, call native method, can run correctly, represents the decryption success.
So, the second question above, when do you decrypt it? The answer is after system.loadLibrary () and before calling the native method
After a series of groping and solving the first two problems, we can confirm that the scheme is feasible!
3. Scheme implementation
3.1 File encryption implementation
// The prefix does not need to be decrypted
const string excludePreStr[] = {
"_Z"."__"
};
// The following function names do not require encryption or decryption
const string excludeNameStr[] = {
"JNI_OnLoad"."etext"."_etext"."edata"."_edata"."end"."_end"
};
bool isExcludeFunc(const string& funcName) {
for (int i = 0; i < sizeof(excludePreStr) / sizeof(excludePreStr[0]); ++i) {
if (funcName.find(excludePreStr[i]) == 0) {
return true; }}for (int i = 0; i < sizeof(excludeNameStr) / sizeof(excludeNameStr[0]); ++i) {
if (funcName.compare(excludeNameStr[i]) == 0) {
return true; }}return false;
}
typedef struct elf32_Hash {
Elf32_Word nbucket;
Elf32_Word nchain;
Elf32_Word* bucketArr;
Elf32_Word* chainArr;
} Elf32_Hash;
// Take a simple inverse
void encryptOrDecyptContent(char* content, long long int size, int isDecrypt) {
for (int i = 0; i < size; ++i) { content[i] = ~content[i]; }}// File encryption implementation
void ELF32Struct::encryptOrDecryptSo(fstream &ioFile, int isDecrypt) {
Elf32_Ehdr* elf32Ehdr = NULL;
// Read elf headers
elf32Ehdr = new Elf32_Ehdr[1];
ioFile.seekg(0, ios::beg);
ioFile.read((char*) elf32Ehdr, sizeof(Elf32_Ehdr));
// Parse the Program Header
Elf32_Phdr* elf32Phdr = new Elf32_Phdr[elf32Ehdr->e_phnum];
ioFile.seekg(elf32Ehdr->e_phoff, ios::beg);
ioFile.read((char*) elf32Phdr, sizeof(Elf32_Phdr) * elf32Ehdr->e_phnum);
Elf32_Phdr* elf32PhdrPTDynamic = NULL;
for (int i = 0; i < elf32Ehdr->e_phnum; ++i) {
if (elf32Phdr[i].p_type == PT_DYNAMIC) {
elf32PhdrPTDynamic = &elf32Phdr[i];
break; }}if (elf32PhdrPTDynamic == NULL) {
LOGD("cannot find PT_DYNAMIC in program headers\n");
return;
}
int dynNum = elf32PhdrPTDynamic->p_filesz / sizeof(Elf32_Dyn);
// Find the corresponding Section in the dynamic Section
Elf32_Dyn dyn;
Elf32_Word dyn_size, dyn_strsz;
Elf32_Addr dyn_symtab, dyn_strtab, dyn_hashtab;
int flag = 0;
ioFile.seekg(elf32PhdrPTDynamic->p_offset, ios::beg);
for (int i = 0; i < dynNum; ++i) {
ioFile.read((char*)&dyn, sizeof(Elf32_Dyn));
if (dyn.d_tag == DT_SYMTAB) {// Symbol table address
dyn_symtab = dyn.d_un.d_ptr;
flag++;
} else if (dyn.d_tag == DT_HASH) {// symbol hash address
dyn_hashtab = dyn.d_un.d_ptr;
flag++;
} else if (dyn.d_tag == DT_STRTAB) {// Address of the string table
dyn_strtab = dyn.d_un.d_ptr;
flag++;
} else if (dyn.d_tag == DT_STRSZ) {// DT_STRTAB total size of bytesdyn_strsz = dyn.d_un.d_val; flag++; }}if(flag ! =4) {
return;
}
char *dynstr = new char[dyn_strsz];// Dynamic string
ioFile.seekg(dyn_strtab, ios::beg);
ioFile.read(dynstr, dyn_strsz);
Elf32_Hash elf32Hash;
ioFile.seekg(dyn_hashtab, ios::beg);
ioFile.read((char*)&elf32Hash.nbucket, sizeof(Elf32_Word));
ioFile.read((char*)&elf32Hash.nchain, sizeof(Elf32_Word));
elf32Hash.bucketArr = new Elf32_Word[elf32Hash.nbucket];
elf32Hash.chainArr = new Elf32_Word[elf32Hash.nchain];
ioFile.read((char*) elf32Hash.bucketArr, sizeof(Elf32_Word) * elf32Hash.nbucket);
ioFile.read((char*) elf32Hash.chainArr, sizeof(Elf32_Word) * elf32Hash.nchain);
// Iterate over all symbol table indexes in hash table, find corresponding symbol structure in symbol table for encryption
for (int i = 0; i < elf32Hash.nbucket; ++i) {
for (intj = elf32Hash.bucketArr[i]; j ! =0; j = elf32Hash.chainArr[j]) {
Elf32_Sym funSym;
ioFile.seekg(dyn_symtab + j * sizeof(Elf32_Sym), ios::beg);
ioFile.read((char*)&funSym, sizeof(Elf32_Sym));
if(funSym.st_size ! =0 && ELF32_ST_TYPE(funSym.st_info) == 2) {
string targetFuncStr = dynstr + funSym.st_name;
bool isExclude = isExcludeFunc(targetFuncStr);
if (isExclude) {
continue;
}
Elf32_Off offset = funSym.st_value;
Elf32_Word size = funSym.st_size;
char* codeContent = new char[size];
// Distinguish between thumb and ARM instructions
if (funSym.st_value & 0x0000001) {
offset = offset - 1;
}
ioFile.seekg(offset, ios::beg);
ioFile.read(codeContent, size);
encryptOrDecyptContent(codeContent, size, isDecrypt);
ioFile.seekg(offset, ios::beg);
ioFile.write(codeContent, size);
if (isDecrypt) {
LOGD("func name=%s decrypt succeed! \n", targetFuncStr.c_str());
} else {
LOGD("func name=%s encrypt succeed! \n", targetFuncStr.c_str());
}
}
}
}
}
Copy the code
3.2 The base address is read after so loading
static unsigned long long getLibAddr(const char *name) {
unsigned long long ret = 0;
char buf[4096], *temp;
int pid;
FILE *fp;
pid = getpid(a);// Get the process PID
sprintf(buf, "/proc/%d/maps", pid); // Generate process maps path
LOGE("buf :%s", buf);
fp = fopen(buf, "r"); // Open maps
if (fp == NULL) {
LOGE("open failed");
} else {
// Read by line
while (fgets(buf, sizeof(buf), fp)) {
// Find the corresponding library information according to the target function name
if (strstr(buf, name)) {
LOGE("buf :%s", buf);
// String cutting returns the base address of the library function
temp = strtok(buf, "-");
// Convert a string to an unsigned integer
ret = strtoull(temp, NULL.16);
LOGE("ret :%lld", ret);
break; }}}fclose(fp);
return ret;
}
Copy the code
3.3 Decryption Implementation
void decrypt32FuncInfo(unsigned long long base, Elf32_Sym* elf32Sym) {
Elf32_Off offset = elf32Sym->st_value;
Elf32_Word size = elf32Sym->st_size;
// Distinguish between thumb and ARM instructions
if (elf32Sym->st_value & 0x0000001) {
offset = offset - 1;
}
long long start = (base + offset) / PAGE_SIZE * PAGE_SIZE;
long long baseAddress = base + offset;
unsigned int curPage = size / PAGE_SIZE + ((size % PAGE_SIZE == 0)?0 : 1);
if ((baseAddress + size) > (start + curPage * PAGE_SIZE)) {
curPage++;
}
// Modify read and write permissions
if (mprotect((void*) start, curPage * PAGE_SIZE, PROT_READ | PROT_EXEC | PROT_WRITE) ! =0) {
LOGE("mprotect failed\n");
return;
}
encryptOrDecyptContent((char*)baseAddress, size, 1);
// Restore read and write permissions
if (mprotect((void*) start, curPage * PAGE_SIZE, PROT_READ | PROT_EXEC) ! =0) {
LOGE("mprotect restore failed\n");
return; }}void ELF64Struct::decryptSo(unsigned long long base) {
Elf32_Ehdr *elf32Ehdr = (Elf32_Ehdr *) base;
int flag = 0;
Elf32_Phdr *elf32Phdr = (Elf32_Phdr *) (base + elf32Ehdr->e_phoff);
for (int i = 0; i < elf32Ehdr->e_phnum; ++i) {
if (elf32Phdr->p_type == PT_DYNAMIC) {
flag = 1;
break;
}
elf32Phdr++;
}
if (flag == 0) {
return;
}
Elf32_Off dynVaddr = elf32Phdr->p_vaddr + base;
Elf32_Word dynSize, dynStrsz;
Elf32_Addr dynSymtab, dynStrtab, dynHashtab;
dynSize = elf32Phdr->p_filesz;
flag = 0;
int dyn_num = dynSize / sizeof(Elf32_Dyn);
Elf32_Dyn *dyn;
for (int i = 0; i < dyn_num; ++i) {
dyn = (Elf32_Dyn *) (dynVaddr + i * sizeof(Elf32_Dyn));
if (dyn->d_tag == DT_HASH) {
dynHashtab = (dyn->d_un).d_ptr;
flag++;
}
if (dyn->d_tag == DT_SYMTAB) {
dynSymtab = (dyn->d_un).d_ptr;
flag++;
}
if (dyn->d_tag == DT_STRTAB) {
dynStrtab = (dyn->d_un).d_ptr;
flag++;
}
if(dyn->d_tag == DT_STRSZ) { dynStrsz = (dyn->d_un).d_val; flag++; }}if(flag ! =4) {
LOGD("can not find four need section! \n");
return;
}
dynSymtab += base;
dynHashtab += base;
dynStrtab += base;
Elf32_Sym *funSym = (Elf32_Sym *) dynSymtab;
char *dynstr = (char *) dynStrtab;
unsigned nBucket, nChain;
unsigned int *bucket, *chain;
nBucket = *((int *) dynHashtab);
nChain = *((int *) dynHashtab + sizeof(int*));
bucket = (unsigned int *) (dynHashtab + sizeof(int*) * 2);
chain = (unsigned int *) (dynHashtab + sizeof(int* (*)2 + nBucket));
for (int i = 0; i < nBucket; i++) {
for (intj = bucket[i]; j ! =0; j = chain[j]) {
Elf32_Sym* elf32SymTarget = funSym + j;
string targetFuncStr = dynstr + elf32SymTarget->st_name;
if (elf32SymTarget->st_value == 0 || elf32SymTarget->st_size == 0) {
continue;
}
bool isExclude = isExcludeFunc(targetFuncStr);
if (isExclude) {
continue;
}
decrypt32FuncInfo(base, elf32SymTarget);
LOGD("func name=%s decrypt succeed! \n", targetFuncStr.c_str()); }}}Copy the code
3.4 Java Decryption interface
package com.kongge.soencryptionlib;
import android.text.TextUtils;
import android.util.Log;
public class SoLibLoadUtil {
private static final String TAG = "SoLibLoadUtil";
static {
System.loadLibrary("SoDecryptionLib");
}
public static void loadLibrary(String soLibName) {
if (TextUtils.isEmpty(soLibName)) return;
System.loadLibrary(soLibName);
decryptLibrary(soLibName);
}
public static void decryptLibrary(String soLibName) {
if (TextUtils.isEmpty(soLibName)) return;
boolean isDecryption = decryptSoLib("lib" + soLibName + ".so");
if (isDecryption) {
Log.i(TAG, "soLibName : " + soLibName + " decrypt succeed");
} else {
Log.i(TAG, "soLibName : " + soLibName + " decrypt failed"); }}private static native boolean decryptSoLib(String name);
}
Copy the code
3.5 the jni decryption
#define ANDROID_PROGRAM 1
#include <string.h>
#include <jni.h>
#include <cstdio>
#include <unistd.h>
#include <stdlib.h>
#include <stdexcept>
#include "ElfParser.h"
#include "include/android_log.h"
extern "C" {
static unsigned long long getLibAddr(const char *name) {... See the above}JNIEXPORT jboolean JNICALL
Java_com_kongge_soencryptionlib_SoLibLoadUtil_decryptSoLib(JNIEnv *env, jclass clazz, jstring name) {
LOGD("load solib in");
jboolean b = false;
const char* nameCharArr = env->GetStringUTFChars(name, &b);
if(nameCharArr == NULL) {
return false; //OutOfMemoryError already thrown
}
LOGD("load solib name = %s" , nameCharArr);
unsigned long long base = getLibAddr(nameCharArr);
LOGD("base addr = 0x%llx;", base);
if(base ! =0) {
char fileName[strlen(nameCharArr)];
strcpy(fileName, nameCharArr);
LOGD("fileName = %s" , fileName);
try {
if (isElf64(base)) {
ELF64Struct elf64Struct;
elf64Struct.fileName = fileName;
elf64Struct.decryptSo(fileContent);
} else {
ELF32Struct elf32Struct;
elf32Struct.fileName = fileName;
elf32Struct.decryptSo(base); }}catch (runtime_error err) {
LOGE("err = %s", err.what());
} catch (exception err) {
LOGE("err = %s", err.what());
} catch(...). {LOGE("err..."); }}return true; }}Copy the code
4. Problems encountered
4.1 After encryption, system.loadLibrary () crashed immediately.
JNI_OnLoad methods, _Z, _ _ do not need to be encrypted because the system function is encrypted. As soon as system.loadLibrary () loads so, it calls the JNI_OnLoad method, which will crash if it is encrypted.
4.2 During decryption, the ELF header file read is different from that parsed, and the parsed address is also different.
Because so loads in three steps, loading, allocating soinfo and links, the structure is not continuous, as it is when parsing files.
4.3 LOG output problem, c++ encryption, printf is used, jni decryption is used LOG, if the same encryption and decryption header file, how compatible?
I have defined c_log.h and android_log.h for c++ printf and Android logs respectively. Package LOGD, LOGE and other methods, the unified use of LOGD, LOGE, c++ environment will execute printf output, APP form will LOG output.
c_log.h
#ifndef SOLIBENCRYPTION_C_LOG_H
#define SOLIBENCRYPTION_C_LOG_H
#include <stdarg.h>
void LOGV(const char* format, ...) {
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
}
void LOGD(const char* format, ...) {
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
}
void LOGI(const char* format, ...) {
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
}
void LOGW(const char* format, ...) {
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
}
void LOGE(const char* format, ...) {
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
}
void LOGF(const char* format, ...) {
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
}
#endif //SOLIBENCRYPTION_C_LOG_H
Copy the code
android_log.h
#ifndef NATIVE_AUDIO_ANDROID_DEBUG_H_H
#define NATIVE_AUDIO_ANDROID_DEBUG_H_H
#include <android/log.h>
#define MODULE_NAME "jniLog"
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, MODULE_NAME, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, MODULE_NAME, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, MODULE_NAME, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,MODULE_NAME, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,MODULE_NAME, __VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,MODULE_NAME, __VA_ARGS__)
#endif //NATIVE_AUDIO_ANDROID_DEBUG_H_H
Copy the code
Elfparser. h — Parses header files
#ifndef ANDROID_PROGRAM // ANDROID_PROGRAM is not defined
#define ANDROID_PROGRAM / / define ANDROID_PROGRAM
// if c++ encrypts CPP files without defining ANDROID_PROGRAM, these headers will be introduced
#define PAGE_SIZE 4096
#include "include/c_log.h"
#include "include/elf.h" Linux has this header file, but Windows does not
#else
// The decryption CPP in JNI defines ANDROID_PROGRAM, which introduces these header files
#include "include/android_log.h"
#include <elf.h> // The JNI environment has elf.h, so using the system will do
#endif
Copy the code
Sodecrypt. CPP — JNI decrypts CPP
#define ANDROID_PROGRAM 1
#include "ElfParser.h"
Copy the code
4.3 how to parse 64SO?
ELF Header magic number inside the fifth (EI_CLASS) judgment 01 – > 32, 64, 02 – > parsing and 32-bit process exactly the same, you can copy the 32 parsing and encryption, will Elf32_Ehdr Elf64_Ehdr instead, other structure is the same, Change the number 32 to 64.
4.4 Failed to Modify memory During Decryption?
Local modification of so file can be done anywhere, but after APP loads SO, it is not possible, because Linux system only has read and execute permissions on some code segments, but does not have write permissions. In this case, you need to modify read and write permissions, and then restore after modification.
5. Summary
It is necessary to have some understanding of so file structure, NDK development and C/C ++ syntax. In the middle, I also encountered a lot of problems. Thanks for the information of online god and the help of my friends, I want to achieve non-invasive SO reinforcement later. Think about it, I am a little excited about it
6. References:
Android SO(ELF) file parsing Android SO(ELF) file based on function name find function location Android So file function encryption Android Linker and so shell technology