安卓加固之so文件加固

Posted ciyze

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了安卓加固之so文件加固相关的知识,希望对你有一定的参考价值。

一、前言

  最近在学习安卓加固方面的知识,看到了jiangwei212的博客,其中有对so文件加固的两篇文章通过节加密函数和通过hash段找到函数地址直接加密函数,感觉写的特别好,然后自己动手实践探索so加密,这里记录一下学习遇到的困难和所得吧,收获还是非常大的。

 

二、通过加密节的方式加密函数

 1、加解密思路

  加密:我们自己写一个Demo根据ELF文件格式,找到我们要加密的节,加密保存在ELF文件中
  解密:这里有一个属性__attribute__((constructor)),这个属性使用的节优于main先执行,使我们解密有了可能。

 

 2、实现流程

  ①编写我们的native代码,在native中将要加密的函数置于一个节中,并将解密函数赋予__attribute__((constructor))属性

    a.在函数申明后面加上 __attribute__((section(".mytext"))) ,将函数定义在我们自己的section中

    b.我们需要编写一个解密函数,属性用__attribute((constructor))申明,这样就可以在在so被加载的时候,在main之前将我们的节解密。
     然后使用ndk-build将native代码编译成so文件

  ②编写加密程序(我这里使用VS2010)
    a.解析so文件,找到.mytext段的起始地址和大小,这里是遍历所有节,根据其在字符串节中的名称,确定.mytext节
    b.找到.mytext之后,进行加密,我们这里只是简单的异或,可以使用其他加密手段,最后写入文件

    ③将加密之后的so文件作为第三方库加载,注意这里不能直接编译后打包,要进行加密操作,android studio的加载方式可以参考我之前写的

         Android Studio使用JNI中的使用第三方库加载,这里就不在多余的说明了。

 

  3.代码实现

  ①Native代码,我们将要加密的函数置于一个新节中,利用__attribute__((section(".mytext")))属性

jint JNICALL native_Add(JNIEnv* env, jobject obj, jdouble num1, jdouble num2)  __attribute__((section (".mytext")));
jint JNICALL native_Sub(JNIEnv *env, jobject obj, jdouble num1, jdouble num2)  __attribute__((section (".mytext")));
jint JNICALL native_Mul(JNIEnv *env, jobject obj, jdouble num1, jdouble num2)  __attribute__((section (".mytext")));
jint JNICALL native_Div(JNIEnv *env, jobject obj, jdouble num1, jdouble num2)  __attribute__((section (".mytext")));

  ②Native代码,我们编写解密函数,我们给我们的解密函数__attribute__((constructor))属性,在so加载的时候优先执行

//此属性在so被加载时,优于main执行,开始解密
void init_native_Add() __attribute__((constructor));
unsigned long getLibAddr();



void init_native_Add(){
    char name[15];
    unsigned int nblock;
    unsigned int nsize;
    unsigned long base;
    unsigned long text_addr;
    unsigned int i;
    Elf32_Ehdr *ehdr;
    Elf32_Shdr *shdr;
    base=getLibAddr(); //在/proc/id/maps文件中找到我们的so文件,活动so文件地址
    ehdr=(Elf32_Ehdr *)base;
    text_addr=ehdr->e_shoff+base;//加密节的地址
    nblock=ehdr->e_entry >>16;//加密节的大小
    nsize=ehdr->e_entry&0xffff;//加密节的大小
    LOGD("nblock =  0x%d,nsize:%d", nblock,nsize);
    LOGD("base =  0x%x", text_addr);
    printf("nblock = %d\\n", nblock);
    //修改内存权限
    if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){
        puts("mem privilege change failed");
        LOGD("mem privilege change failed");
    }
    //进行解密,是针对加密算法的
    for(i=0;i<nblock;i++){
        char *addr=(char*)(text_addr+i);
        *addr=~(*addr);
    }
    if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC) != 0){
        puts("mem privilege change failed");
    }
    puts("Decrypt success");
}
//获取到SO文件加载到内存中的起始地址,只有找到起始地址才能够进行解密;
unsigned long getLibAddr(){
    unsigned long ret=0;
    char name[]="libJniTest.so";
    char buf[4096];
    char *temp;
    int pid;
    FILE *fp;
    pid=getpid();
    sprintf(buf,"/proc/%d/maps",pid);  //这个文件中保存了进程映射的模块信息  cap /proc/id/maps  查看
    fp=fopen(buf,"r");
    if(fp==NULL){
        LOGD("Error open maps file in progress %d",pid);
        puts("open failed");
        goto _error;
    }
    while (fgets(buf,sizeof(buf),fp)){
        if(strstr(buf,name)){
            temp = strtok(buf, "-");  //分割字符串,返回 - 之前的字符
            LOGD("Target so is %s\\r\\n",temp);
            ret = strtoul(temp, NULL, 16);  //获取地址
            LOGD("Target so address is %x",ret);
            break;
        }
    }
    _error:
    fclose(fp);
    return ret;
}

  解密函数的实现很简单,这里我们首先在getLibAddr函数中通过/proc/<pid>/maps文件获得加载的so文件路径,其中<pid>是该程序的id,maps文件中存放了加载的所有so文件的路径和基址,可通过shell命令 cat /proc/id/maps获得所有模块信息,也可以通过cat /proc/id/maps | grep libJniTest.so获得libJniTest.so模块的信息

  然后我们通过ehdr->e_entry这个变量获取到被加密节的大小,ehdr->e_shoff获得加密节的地址偏移(加密的时候将加密节的信息写入这两个变量中,所以这里可以直接读取解密)。

  然后在实现native中的注册代码,这里也不多说明了,可以看之前的Android Studio使用JNI,也可以看上传的代码

 

  ③使用VS2010编写加密程序

  这里需要熟悉ELF格式文件,找到我们自己定义的节.mytext,将节使用加密算法加密,将基地址和大小存入e_shoff和e_entry中

int _tmain(int argc, _TCHAR* argv[])
{

    char szSoPath[MAX_PATH] = "libJniTest.so";
    char szSection[] = ".mytext";
    
    char *shstr = NULL;
    char *content = NULL;
    

    int i;
    unsigned int base, length;
    unsigned short nblock;
    unsigned short nsize;
    unsigned char block_size = 16;

    char* szFileData = NULL;
    unsigned int ulLow;
    HANDLE hFile;
    ULONG ulHigh = 0;
    ULONG ulReturn = 0;

    //读取文件到内存
    hFile = CreateFileA(szSoPath,GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
    if (hFile==INVALID_HANDLE_VALUE)
    {
        printf("打开的文件不存在!");
        return -1;
    }
    ulLow = GetFileSize(hFile,&ulHigh); 
    szFileData = new char[ulLow + 20];
    printf("Read File at 0x%x\\r\\n",szFileData);
    if (ReadFile(hFile,szFileData,ulLow,&ulReturn,NULL)==0)
    {
        CloseHandle(hFile);
        delete szFileData;
        return FALSE;
    }

    Elf32_Ehdr* ehdr = (Elf32_Ehdr*)(szFileData);
    Elf32_Shdr* shdrstr =  (Elf32_Shdr*)(szFileData + ehdr->e_shoff + sizeof(Elf32_Shdr) * ehdr->e_shstrndx); //字符串表的索引,偏移到字符串表
    shstr = (char*)(szFileData + shdrstr->sh_offset);//偏移到字符串表
    Elf32_Shdr* Shdr = (Elf32_Shdr*)(szFileData + ehdr->e_shoff);


    for(i = 0; i < ehdr->e_shnum; i++){
        //根据字符串表的名称比较
        if(strcmp(shstr + Shdr->sh_name, szSection) == 0){
            base = Shdr->sh_offset;
            length = Shdr->sh_size;
            printf("Find section %s at 0x%x the size is 0x%x\\n", szSection,base,length);
            break;
        }
        Shdr++;
    }

    content= (char*)(szFileData + base);
    nblock = length / block_size;
    nsize = base / 4096 + (base % 4096 == 0 ? 0 : 1);
    printf("base = 0x%x, length = 0x%x\\n", base, length);
    printf("nblock = %d, nsize = %d\\n", nblock, nsize);

    //将节的地址和大小写入
    ehdr->e_entry = (length << 16) + nsize;
    ehdr->e_shoff = base; //节的地址
    
    printf("content is %x",content);
    //加密
    for(i=0;i<length;i++){
        content[i] = ~content[i];
    }

    strcat(szSoPath,"_");
    HANDLE hFile1 = CreateFileA(szSoPath,GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);
    if (hFile1==INVALID_HANDLE_VALUE)
    {
        printf("创建文件失败!");
        return -1;
    }
    BOOL bRet = WriteFile(hFile1,szFileData,ulLow,&ulReturn,NULL);
    if(bRet)
    {
        printf("写入成功!\\r\\n");
    }
    else
    {
        int a = GetLastError();
        printf("写入失败:%d\\r\\n",a);
    }

_error:
    delete(szFileData);
    CloseHandle(hFile);

    return 0;
}

  1)作为动态链接库,e_entry入口地址是无意义的,因为程序被加载时,设定的跳转地址是动态连接器的地址,这个字段是可以被作为数据填充的。

  2)so装载时,与链接视图没有关系,即e_shoff、e_shentsize、e_shnum和e_shstrndx这些字段是可以任意修改的。被修改之后,使用readelf和ida等工具打开,会报各种错误,相信读者已经见识过了。

 

  ④运行结果

  1)加密前

  

 

  2)加密后

  

 

  3)运行结果

  (注:这里结果在原结果上加2)

 

  4)使用grep命令查看加载模块

  

 

  4.相关知识点

  ①将要加密的函数置于一个节中,解密函数使用__attribute__((constructor))属性优先执行,对于so动态链接库e_entry和e_shoff是可以修改存放我们加密节的大小和地址的,便于解密。

  ②掌握ELF文件格式知识,遍历节表,对应于字符串表中的节名,找到要加密的节,进行加密操作。

 

三、直接加密指定函数

   1.原理

  在so文件中,每个函数的结构描述是存放在.dynsym段中,每个函数名称保存在.dynstr段中,在ELF格式中有一个.hash段,由Elf32_Word对象组成的哈希表支持符号表访问。

    

  bucket数组包含nbucket个项目,chain数组包含了nchain个项目,下标都是从0开始。

  bucket和chain中都保存符号表索引,chain和符号表存在对应关系,符号表项的数目应该和nchain相等,所以符号表的索引也可用来选取chain表项,哈希函数能够接受符号名并且返回一个可以用来计算bucket的索引。
  因此,如果哈希函数针对某个名字返回了数值X,则bucket[X%nbucket]给出了一个索引y,该索引可用于符号表,也可用于chain表。如果符号表不是所需要的,那么chain[y]则给出了具有相同哈希值的下一个符号表项。我们可以沿着chain链一直搜索,直到所选中的符号表项包含了所需要的符号,或者chain项中包含值STN_UNDEF。

  上面的话有些复杂,简单来说就是用函数名称在hash函数中得到一个hash值,通过这个hash在chain中的位置就可以找到这个函数对应在.dynsym中对应的条目了。

  hash函数如下

unsigned long elf_hash(const unsigned char* name){
  unsigned long h = 0,g;
  while(*name)
  {
      h = (h<<4)+*name++;
      if(g = h & 0xf0000000)
      {
          h^=g>>24;
          h&=-g;
      }
      return h;
  }
}

  那么我们只用得到.hash段即可,但是我们怎么得到这个section呢?

  

  由于so被加载到内存之后,就没有section了,对应的是segment了,而一个section包含多个section,相同的section可以被包含到不同的segment中。.dynamic段一般用于动态链接,所以.dynsym和.dynstr,.hash肯定包含在这里。我们可以解析了程序头信息之后,通过type获取到.dynamic程序头信息,然后获取到这个segment的偏移地址和大小,在进行解析成elf32_dyn结构。

 

  2.实现方案

  我们给函数加密,加密和解密都是基于装载视图实现,需要注意的是,被加密函数如果用static声明,那么函数是不会出现在.dynsym中,是无法在装载视图中通过函数名找到进行解密的。

  ①加密流程:
  1)读取文件头,获取e_phoff、e_phentsize和e_phnum信息
  2)通过Elf32_Phdr中的p_type字段,找到DYNAMIC。其实,DYNAMIC就是.dynamic section。从p_offset和p_filesz字段得到文件中的起始位置和长度。
  3)遍历.dynamic,找到.dynsym、.dynstr、.hash section文件中的偏移和.dynstr的大小,
  4)根据函数名称,计算hash值
  5)根据hash值,找到下标hash%nbuckets的bucket;根据bucket中的值,读取.dynsym中的对应索引的Elf32_Sym符号,从符号的st_name索引找到在.dynstr中对应的字符串与函数名进行比较。若不等,则根据chain[hash%nbuckets]找下一个Elf32_Sym符号,直到找到或者chain终止为止。

  6)找到函数对应的Elf32_Sym符号之后,即可以根据st_value和st_size字段找到函数的位置和大小

  7)加密,写入文件

 

   ②解密流程为加密逆过程,找到函数地址的方式和加密流程中的方法是一致的,都是通过chain在dynsym中找到对应的函数项,然后在函数地址处进行解密。

 

   3.代码实现

   ①使用VS2010编写加密代码

// Encrypting.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <stdio.h>
#include <iostream>
using namespace std;
#include <Windows.h>
#include "elf.h"

typedef struct _funcInfo{  
    Elf32_Addr st_value;  
    Elf32_Word st_size;  
}funcInfo;  


static Elf32_Off findTargetSectionAddr(char* szFileData, const char *szSection);
static char getTargetFuncInfo(char* szFileData, const char *funcName, funcInfo *info);
static unsigned elfhash(const char *_name); 



int _tmain(int argc, _TCHAR* argv[])
{
        char *shstr = NULL;
        funcInfo info;  
        int i;
        char* szFileData = NULL;
        unsigned int ulLow;
        HANDLE hFile;
        ULONG ulHigh = 0;
        ULONG ulReturn = 0;
 
        char funcNameAdd[] = "native_Add";
        char funcNameSub[] = "native_Sub";
        char funcNameMul[] = "native_Mul";
        char funcNameDiv[] = "native_Div";
        char szSoPath[MAX_PATH] = "libJniTest.so";
        char szSection[] =  ".text";
        Elf32_Off secOff;  

        //读入文件在内存中
        hFile = CreateFileA(szSoPath,GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
        if (hFile==INVALID_HANDLE_VALUE)
        {
            printf("打开的文件不存在!");
            return -1;
        }
        ulLow = GetFileSize(hFile,&ulHigh); 
        szFileData = new char[ulLow + 20];
        if (ReadFile(hFile,szFileData,ulLow,&ulReturn,NULL)==0)
        {
            CloseHandle(hFile);
            delete szFileData;
            return FALSE;
        }
        
        //通过hash段中chain链获得的索引,获取在dynsym对应的条目
        if(getTargetFuncInfo(szFileData, funcNameAdd, &info) == -1){  
            printf("Find function %s failed\\n", funcNameAdd);  
            goto _error;  
        }  

        //得到函数地址
        for(i=0;i<info.st_size-1;i++){
            char *content = (char*)(szFileData + info.st_value -1 + i);
            *content = ~(*content);
        }

        //通过hash段中chain链获得的索引,获取在dynsym对应的条目
        if(getTargetFuncInfo(szFileData, funcNameSub, &info) == -1){  
            printf("Find function %s failed\\n", funcNameSub);  
            goto _error;  
        }  
        //得到函数地址
        for(i=0;i<info.st_size-1;i++){
            char *content = (char*)(szFileData + info.st_value -1 + i);
            *content = ~(*content);
        }
        //通过hash段中chain链获得的索引,获取在dynsym对应的条目
        if(getTargetFuncInfo(szFileData, funcNameMul, &info) == -1){  
            printf("Find function %s failed\\n", funcNameMul);  
            goto _error;  
        }  
        //得到函数地址
        for(i=0;i<info.st_size-1;i++){
            char *content = (char*)(szFileData + info.st_value -1 + i);
            *content = ~(*content);
        }
        //通过hash段中chain链获得的索引,获取在dynsym对应的条目
        if(getTargetFuncInfo(szFileData, funcNameDiv, &info) == -1){  
            printf("Find function %s failed\\n", funcNameDiv);  
            goto _error;  
        }  
        //得到函数地址
        for(i=0;i<info.st_size-1;i++){
            char *content = (char*)(szFileData + info.st_value -1 + i);
            *content = ~(*content);
        }

        //写入文件保存
        strcat(szSoPath,"_");
        HANDLE hFile1 = CreateFileA(szSoPath,GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);
        if (hFile1==INVALID_HANDLE_VALUE)
        {
            printf("创建文件失败!");
            return -1;
        }
        BOOL bRet = WriteFile(hFile1,szFileData,ulLow,&ulReturn,NULL);
        if(bRet)
        {
            printf("写入成功!\\r\\n");
        }
        else
        {
            int a = GetLastError();
            printf("写入失败:%d\\r\\n",a);
        }
_error:
        delete(szFileData);
        CloseHandle(hFile);

    return 0;
}


static unsigned elfhash(const char *_name)  
{  
    const unsigned char *name = (const unsigned char *) _name;  
    unsigned h = 0, g;  

    while(*name) {  
        h = (h << 4) + *name++;  
        g = h & 0xf0000000;  
        h ^= g;  
        h ^= g >> 24;  
    }  
    return h;  
}  static char getTargetFuncInfo(char* szFileData, const char *funcName, funcInfo *info){  
    char flag = -1;
    char *dynstr = NULL;  
    int i;  
    Elf32_Sym* funSym;  
    Elf32_Phdr* phdr;  
    Elf32_Off dyn_off;  
    Elf32_Word dyn_size, dyn_strsz;  
    Elf32_Dyn* dyn;  
    Elf32_Addr dyn_symtab, dyn_strtab, dyn_hash;  
    unsigned funHash, nbucket, nchain, funIndex;  
    Elf32_Ehdr* ehdr = (Elf32_Ehdr*)szFileData;

    //视图模式
    phdr = (Elf32_Phdr*)(szFileData + ehdr->e_phoff);
    for(i=0;i < ehdr->e_phnum; i++){  
        //获得动态链接节
        if(phdr->p_type ==  PT_DYNAMIC){  
            dyn_size = phdr->p_filesz;  
            dyn_off = phdr->p_offset;  
            flag = 0;  
            printf("Find section %s, size = 0x%x, addr = 0x%x\\n", ".dynamic", dyn_size, dyn_off);  
            break;  
        }  
        phdr++;
    }  
    if(flag){  
        puts("Find .dynamic failed");  
        goto _error;  
    }  
    flag = 0;  

    printf("dyn_size:%d\\n",dyn_size);  
    printf("count:%d\\n",(dyn_size/sizeof(Elf32_Dyn)));  
    printf("off:%x\\n",dyn_off);  


    dyn = (Elf32_Dyn*)(szFileData + dyn_off);
    for(i=0;i < dyn_size / sizeof(Elf32_Dyn); i++){  
        
        //符号表位置
        if(dyn->d_tag == DT_SYMTAB){  
            dyn_symtab = dyn->d_un.d_ptr;  
            flag += 1;  
            printf("Find .dynsym, addr = 0x%x, val = 0x%x\\n", dyn_symtab, dyn->d_un.d_val);  
        }  
        //获得hash段
        if(dyn->d_tag == DT_HASH){  
            dyn_hash = dyn->d_un.d_ptr;  
            flag += 2;  
            printf("Find .hash, addr = 0x%x\\n", dyn_hash);  
        }  
        //保存函数字符串的位置
        if(dyn->d_tag == DT_STRTAB){  
            dyn_strtab = dyn->d_un.d_ptr;  
            flag += 4;  
            printf("Find .dynstr, addr = 0x%x\\n", dyn_strtab);  
        }  
        //字符串长度
        if(dyn->d_tag == DT_STRSZ){  
            dyn_strsz = dyn->d_un.d_val;  
            flag += 8;  
            printf("Find .dynstr size, size = 0x%x\\n", dyn_strsz);  
        }  
        dyn++;
    }  

    if((flag & 0x0f) != 0x0f){  
        puts("Find needed .section failed\\n");  
        goto _error;  
    }  

    dynstr = (char*) malloc(dyn_strsz);  
    if(dynstr == NULL){  
        printf("Malloc .dynstr space failed");  
        goto _error;  
    }  
    memcpy(dynstr,szFileData + dyn_strtab,dyn_strsz);

/*     nbucket                                                                 
 *-----------------
 *       nchain
 *------------------
 *      bucket[0]
 *       ...
 *   bucket[nbucket-1]
 * ------------------
 *     chain[0]
 *       ...
 *   chain[nchain-1]
 */
    funHash = elfhash(funcName);  //获得函数名称经过hash运行后的值
    printf("Function %s hashVal = 0x%x\\n", funcName, funHash);  

    nbucket = *(int*)(szFileData + dyn_hash); //获得nbucket的值
    printf("nbucket = %d\\n", nbucket);  

    nchain = *(int*)(szFileData + dyn_hash + 4);//获得nchain的值
    printf("nchain = %d\\n", nchain);  

    funHash = funHash % nbucket;        //bucket[X%nbucket]给出了一个索引y,该索引可用于符号表,也可用于chain表
    printf("funHash mod nbucket = %d \\n", funHash);  

    funIndex = *(int*)(szFileData + dyn_hash + 8 + funHash * 4);//y = bucket[X%nbucket]返回的索引y
    printf("funcIndex:%d\\n", funIndex);  
    funSym = (Elf32_Sym*)(szFileData + dyn_symtab + funIndex*sizeof(Elf32_Sym));//该索引对应的符号表
    

    if(strcmp(dynstr + funSym->st_name, funcName) != 0){  //如果索引y对应的符号表不是所需要的,那么chain[y]则给出了具有相同哈希值的下一个符号表项
        while(1){  
            //我们可以沿着chain链一直搜索,直到所选中的符号表项包含了所需要的符号
            printf("hash:%x,nbucket:%d,funIndex:%d\\n",dyn_hash,nbucket,funIndex);  
            funIndex = *(int*)(szFileData + dyn_hash + 4*(2+nbucket+funIndex));  //搜索chain链
            printf("funcIndex:%d\\n", funIndex);  

            if(funIndex == 0){  
                puts("Cannot find funtion!\\n");  
                goto _error;  
            }  
            funSym = (Elf32_Sym*)(szFileData + dyn_symtab + funIndex*sizeof(Elf32_Sym)); //chain[]中对应的符号表
            if(strcmp(dynstr + funSym->st_name, funcName) == 0){  
                break;  
            }  
        }  
    }  

    printf("Find: %s, offset = 0x%x, size = 0x%x\\n", funcName, funSym->st_value, funSym->st_size);  
    info->st_value = funSym->st_value;  
    info->st_size = funSym->st_size;  
    free(dynstr);  
    return 0;  

_error:  
    free(dynstr);  
    return -1;  
}  

  

  ②Native代码

#include <jni.h>
#include <stdio.h>
//#include <assert.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <android/log.h>
#include <elf.h>
#include <sys/mman.h>

#define LOG_TAG "Jiami"
#define LOGD(fmt,args...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,fmt,##args)

typedef struct _funcInfo{
  Elf32_Addr st_value;
  Elf32_Word st_size;
}funcInfo;




JNIEXPORT jint JNICALL native_Add
(JNIEnv *env, jobject obj, jdouble num1, jdouble num2)
{
    return (jint)(num1 + num2 +3);
}


JNIEXPORT jint JNICALL native_Sub
        (JNIEnv *env, jobject obj, jdouble num1, jdouble num2)
{
    return (jint)(num1 - num2 +3);
}


JNIEXPORT jint JNICALL native_Mul
        (JNIEnv *env, jobject obj, jdouble num1, jdouble num2)
{
    return (jint)(num1 * num2 +3);
}

JNIEXPORT jint JNICALL native_Div
        (JNIEnv *env, jobject obj, jdouble num1, jdouble num2)
{
    if以上是关于安卓加固之so文件加固的主要内容,如果未能解决你的问题,请参考以下文章

AndroidLinker与SO加壳技术之上篇

AndroidLinker与SO加壳技术之上篇

【移动安全】腾讯乐固对apk文件加固失败

安卓应用加固之静态反反汇编技术总结

经过360加固的apk怎么破解

安卓逆向之基于Xposed-ZjDroid脱壳