Android ELF系列:ELF文件格式简析和linker的链接so文件原理分析
Android ELF系列:实现一个so文件加载器
3. Android ELF系列:手写一个so文件(包含两个导出函数)
Android ELF系列:实现一些小功能
Android ELF系列:实现一个so的加密壳
Android ELF系列:待续............
手写so文件
准备工作
我们对so文件的定位是拥有两个导出函数,可以正常被dlopen加载,可以通过dlsym获取我们的函数.
而且根据前面的分析so文件的dynmic段必须具有 DT_HASH, DT_STRTAB , DT_SYMTAB.
这个so文件我们不需要section但必须有program 头.
所以我打算是只定义两个必须的程序头 PT_LOAD 和一个 PT_DYNMIC.
大致安排结构如下.
我们先一步一步来:
elf头typedef struct {
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
e_ident[EI_NIDENT]:7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 00
- 0:"\177ELF" ELFMAG
- 4:1 ELFCLASS32
- 5:1 ELFDATA2LSB
- 6:1 E_CURRENT
- 7-15:0
e_type:03 00 ET_DYN
e_machine:28 00 EM_ARM
e_version:01 00 00 00 EV_CURRENT
e_entry:00 00 00 00
e_phoff:34 00 00 00 因为Elf header的大小事34h
e_shoff:00 00 00 00 不需要
e_flags:00 02 00 05 额这个随便写
e_ehsize:34 00 ELF 头大小
e_phentsize:20 00 每个程序头表的大小
e_phnum:02 00 2各程序头表
e_shentsize:00 00
e_shnum:00 00
e_shstrndx:00 00
所以ELF头的十六进制为:
7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 00
03 00 28 00 01 00 00 00 00 00 00 00 34 00 00 00
00 00 00 00 00 02 00 05 34 00 20 00 02 00 00 00
00 00 00 00
程序头
ELF头之后紧接着就是程序头
PT_LOAD
typedef struct {
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;
p_type :01 00 00 00 PT_LOAD
p_offset:00 00 00 00
p_vaddr :00 00 00 00
p_paddr :00 00 00 00
p_filesz:77 77 77 01 文件大小后期需要修改,前面都是77 77 77 后面是需要改的序号这个是第一处
p_memsz :00 10 00 00 0x1000大小的内存应该足够了
p_flags :07 00 00 00 R_W_X 可读_可写_可执行
p_align :00 10 00 00 一页对齐PT_DYNAMIC
p_type :02 00 00 00 PT_DYNMIC
p_offset:74 00 00 00 34H+2*20H =74H
p_vaddr :74 00 00 00
p_paddr :00 00 00 00
p_filesz:20 00 00 00 一个DT_HASH,DT_STRTAB,DT_SYMTAB,DT_NULL 一共4*8 = 32 = 20h
p_memsz :20 00 00 00
p_flags :06 00 00 00 R_W 可读_可写
p_align :04 00 00 00 4字节对齐
现在so文件的内容为
7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 00
03 00 28 00 01 00 00 00 00 00 00 00 34 00 00 00
00 00 00 00 00 02 00 05 34 00 20 00 02 00 00 00
00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 30 01 00 00 30 01 00 00 07 00 00 00
00 10 00 00 02 00 00 00 74 00 00 00 74 00 00 00
00 00 00 00 20 00 00 00 20 00 00 00 06 00 00 00
04 00 00 00
DYNMIC
现在我们先不填充DYNMIC段,先全部填充 77.DT_NULL就填充00.为了补齐我们后面也全部填满一行00
现在so文件的内容为
7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 00
03 00 28 00 01 00 00 00 00 00 00 00 34 00 00 00
00 00 00 00 00 02 00 05 34 00 20 00 02 00 00 00
00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 77 77 77 01 30 01 00 00 07 00 00 00
00 10 00 00 02 00 00 00 74 00 00 00 74 00 00 00
00 00 00 00 20 00 00 00 20 00 00 00 06 00 00 00
04 00 00 00 77 77 77 77 77 77 77 77 77 77 77 77
77 77 77 77 77 77 77 77 77 77 77 02 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
插入函数代码
我们打算写两个函数AddNumber,SubNumber.
extern "C" JNIEXPORT jint JNICALL AddNumber(jint a,jint b)//A0 记录函数位置
{
return a+b;
}
extern "C" JNIEXPORT jint JNICALL SubNumber(jint a,jint b)//B8
{
return a-b;
}
然后在Android Studio中获取函数的二进制数据即可.
方法1:
调试,在AddNumber,SubNumber,下断,触发这两个函数,当触发时切换到lldb界面
方法2:自己把编译的so文件拖出来定位到指定函数抽取二进制代码吧.
现在so文件的内容为
7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 00
03 00 28 00 01 00 00 00 00 00 00 00 34 00 00 00
00 00 00 00 00 02 00 05 34 00 20 00 02 00 00 00
00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 77 77 77 01 30 01 00 00 07 00 00 00
00 10 00 00 02 00 00 00 74 00 00 00 74 00 00 00
00 00 00 00 20 00 00 00 20 00 00 00 06 00 00 00
04 00 00 00 77 77 77 77 77 77 77 77 77 77 77 77
77 77 77 77 77 77 77 77 77 77 77 02 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
84 B0 0A 46 03 46 03 90 02 91 03 98 02 99 08 44
01 92 00 93 04 B0 70 47 84 B0 0A 46 03 46 03 90
02 91 03 98 02 99 40 1A 01 92 00 93 04 B0 70 47
str表
记录位置:0xD0
我们导入了两个函数所以str表只需要两个字符串
\0AddNumber\0SubNumber\0\0
现在so文件的内容为(后面补齐一行0):
7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 00
03 00 28 00 01 00 00 00 00 00 00 00 34 00 00 00
00 00 00 00 00 02 00 05 34 00 20 00 02 00 00 00
00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 77 77 77 01 30 01 00 00 07 00 00 00
00 10 00 00 02 00 00 00 74 00 00 00 74 00 00 00
00 00 00 00 20 00 00 00 20 00 00 00 06 00 00 00
04 00 00 00 77 77 77 77 77 77 77 77 77 77 77 77
77 77 77 77 77 77 77 77 77 77 77 02 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
84 B0 0A 46 03 46 03 90 02 91 03 98 02 99 08 44
01 92 00 93 04 B0 70 47 84 B0 0A 46 03 46 03 90
02 91 03 98 02 99 40 1A 01 92 00 93 04 B0 70 47
00 41 64 64 4E 75 6D 62 65 72 00 53 75 62 4E 75
6D 62 65 72 00 00 00 00 00 00 00 00 00 00 00 00
sym表
记录位置: 0xf0
我们有两个导出函数所以需要2个sym表.但第一个sym表必须是SH_UNDEF.所以有3个.
AddNumber
typedef struct {
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Section st_shndx;
} Elf32_Sym;
st_name :01 00 00 00 AddNumber在字符串表的偏移是1
st_value:A0 00 00 00 AddNumber的地址
st_size :16 00 00 00 函数又24个字节
st_info :12 STB_GLOBAL|STT_FUNC
st_other:0
st_shndx:01 00SubNumberst_name :0B 00 00 00 SubNumber 在字符串表的偏移是1
st_value:B8 00 00 00 SubNumber 的地址
st_size :16 00 00 00 函数又24个字节
st_info :12 STB_GLOBAL|STT_FUNC
st_other:0
st_shndx:01 00
现在so文件的内容为(后面补齐一行0):
7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 00
03 00 28 00 01 00 00 00 00 00 00 00 34 00 00 00
00 00 00 00 00 02 00 05 34 00 20 00 02 00 00 00
00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 77 77 77 01 30 01 00 00 07 00 00 00
00 10 00 00 02 00 00 00 74 00 00 00 74 00 00 00
00 00 00 00 20 00 00 00 20 00 00 00 06 00 00 00
04 00 00 00 77 77 77 77 77 77 77 77 77 77 77 77
77 77 77 77 77 77 77 77 77 77 77 02 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
84 B0 0A 46 03 46 03 90 02 91 03 98 02 99 08 44
01 92 00 93 04 B0 70 47 84 B0 0A 46 03 46 03 90
02 91 03 98 02 99 40 1A 01 92 00 93 04 B0 70 47
00 41 64 64 4E 75 6D 62 65 72 00 53 75 62 4E 75
6D 62 65 72 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
01 00 00 00 A0 00 00 00 16 00 00 00 12 00 01 00
0B 00 00 00 B8 00 00 00 16 00 00 00 12 00 01 00
hash表
记录位置:0x120
hash表结构
struct
{
unsigned nbucket;
unsigned nchain;
unsigned bucket[nbucket];
unsigned chain[nchain];
};
在说hash表之前,我们得先说两个函数.
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 Elf32_Sym* soinfo_elf_lookup(soinfo* si, unsigned hash, const char* name) {
Elf32_Sym* symtab = si->symtab;
const char* strtab = si->strtab;
TRACE_TYPE(LOOKUP, "SEARCH %s in %s@0x%08x %08x %d",
name, si->name, si->base, hash, hash % si->nbucket);
for (unsigned n = si->bucket[hash % si->nbucket]; n != 0; n = si->chain[n]) {
Elf32_Sym* s = symtab + n;
if (strcmp(strtab + s->st_name, name)) continue;
/* only concern ourselves with global and weak symbol definitions */
switch(ELF32_ST_BIND(s->st_info)){
case STB_GLOBAL:
case STB_WEAK:
if (s->st_shndx == SHN_UNDEF) {
continue;
}
TRACE_TYPE(LOOKUP, "FOUND %s in %s (%08x) %d",
name, si->name, s->st_value, s->st_size);
return s;
}
}
return NULL;
}
假如我们调用dlsym(so,"AddNumber");//查找函数地址的时候那么最终会有这样的调用
soinfo_elf_lookup(so,elfhash("AddNumber"),"AddNumber");
我们先算一下hash值:
AddNumber:157056866
SubNumber:123495026
因为我们只有2个符号,所以定义
nbucket = 2
那么
157056866 % 2 = 0
157056866 % 2 = 0
那么我们定义如下:
nbucket :02 00 00 00
nchain :02 00 00 00
bucket :01 00 00 00 00 00 00 00
chain :00 00 00 00 02 00 00 00
最终定义如下:
7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 00
03 00 28 00 01 00 00 00 00 00 00 00 34 00 00 00
00 00 00 00 00 02 00 05 34 00 20 00 02 00 00 00
00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 77 77 77 01 30 01 00 00 07 00 00 00
00 10 00 00 02 00 00 00 74 00 00 00 74 00 00 00
00 00 00 00 20 00 00 00 20 00 00 00 06 00 00 00
04 00 00 00 77 77 77 77 77 77 77 77 77 77 77 77
77 77 77 77 77 77 77 77 77 77 77 02 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
84 B0 0A 46 03 46 03 90 02 91 03 98 02 99 08 44
01 92 00 93 04 B0 70 47 84 B0 0A 46 03 46 03 90
02 91 03 98 02 99 40 1A 01 92 00 93 04 B0 70 47
00 41 64 64 4E 75 6D 62 65 72 00 53 75 62 4E 75
6D 62 65 72 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
01 00 00 00 A0 00 00 00 16 00 00 00 12 00 01 00
0B 00 00 00 B8 00 00 00 16 00 00 00 12 00 01 00
02 00 00 00 02 00 00 00 01 00 00 00 00 00 00 00
00 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00
修正文件
我们有两处需要修改的地方
一个是文件大小,一个是dynmic段.
现在我们先把信息汇总一下:
文件大小:0x140
str表:0xD0
sym表:0xF0
hash表:0x120
修改文件大小,也就是77 77 77 01处为 40 01 00 00.
填充dynmic段04 00 00 00 DT_HASH
20 01 00 00
05 00 00 00 DT_STRTAB
d0 00 00 00
06 00 00 00 DT_SYMTAB
f0 00 00 00
也就是把77 . . . 77 02改为以上内容:
最终so文件的内容为
7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 00
03 00 28 00 01 00 00 00 00 00 00 00 34 00 00 00
00 00 00 00 00 02 00 05 34 00 20 00 02 00 00 00
00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 40 01 00 00 30 01 00 00 07 00 00 00
00 10 00 00 02 00 00 00 74 00 00 00 74 00 00 00
00 00 00 00 20 00 00 00 20 00 00 00 06 00 00 00
04 00 00 00 04 00 00 00 20 01 00 00 05 00 00 00
D0 00 00 00 06 00 00 00 F0 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
84 B0 0A 46 03 46 03 90 02 91 03 98 02 99 08 44
01 92 00 93 04 B0 70 47 84 B0 0A 46 03 46 03 90
02 91 03 98 02 99 40 1A 01 92 00 93 04 B0 70 47
00 41 64 64 4E 75 6D 62 65 72 00 53 75 62 4E 75
6D 62 65 72 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
01 00 00 00 A0 00 00 00 16 00 00 00 12 00 01 00
0B 00 00 00 B8 00 00 00 16 00 00 00 12 00 01 00
02 00 00 00 02 00 00 00 01 00 00 00 00 00 00 00
00 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00
验证新建一个ndk项目
写如下代码
native-lib.cpp
#include
#include
#include
#include
#define TAG "chpmesotest"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
typedef jint (*SubNumber)(jint a,jint b);
typedef jint (*AddNumber)(jint a,jint b);
SubNumber Sub;
AddNumber Add;
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_mi_testmesotes_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
void *meso = dlopen("/data/data/com.example.mi.testmesotes/lib/meso.so",0);
if(meso)
{
Add = (AddNumber) dlsym(meso,"AddNumber");
Sub = (SubNumber) dlsym(meso,"SubNumber");
LOGD("meso:%x Add:%x Sub:%x",meso,Add,Sub);
jint a = Add(3,4);
jint b = Sub(8,1);
LOGD("a:%d b:%d",a,b);
}
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
把我们手写的meso.so文件放入/data/data/com.example.mi.testmesotes/lib
adb push meso.so /data/local/tmp
adb shell
su
cd /data/data/com.example.mi.testmesotes/lib
cp /data/local/tmp/meso.so ./
chmod 777 meso.so
结束再次运行发现缺奔溃了.
但我们发现打印了一行log.证明加载so文件是成功了,但是缺发生了错误
02-24 04:57:10.849 25107-25107/? D/chpmesotest: meso:71cdcdb4 Add:753930a0 Sub:753930b8
说明是在调用这两个函数的时候发生了错误.请看到前面我们抽取的函数代码.
我们抽取的函数代码是Thumb.要切换的Thumb一般都是通过把最后一位置为1:但函数Add:753930a0 Sub:753930b8.的地址最后一位都不是1.所以我们需要再次修改so文件.就是把两个sym符号的st_value的最后一位都置为1.
修改如下:
AddNumber
st_name :01 00 00 00 AddNumber在字符串表的偏移是1
st_value:A1 00 00 00 最后一位置1,变为Thumb执行代码
st_size :16 00 00 00 函数又24个字节
st_info :12 STB_GLOBAL|STT_FUNC
st_other:0
st_shndx:01 00SubNumberst_name :0B 00 00 00 SubNumber 在字符串表的偏移是1
st_value:B9 00 00 00 最后一位置1,变为Thumb执行代码
st_size :16 00 00 00 函数又24个字节
st_info :12 STB_GLOBAL|STT_FUNC
st_other:0
st_shndx:01 00
再次把修改好后的meso.so文件放进 /data/data/com.example.mi.testmesotes/lib.
然后运行程序:
输出log:
02-24 05:08:42.939 25898-25898/? D/chpmesotest: meso:71cdcdb4 Add:753930a1 Sub:753930b9
02-24 05:08:42.939 25898-25898/? D/chpmesotest: a:7 b:7
证明我们手写的so文件成功了.此处应该有掌声