寻找so中符号的地址
总述
我们在使用so中的函数的时候可以使用dlopen和dlsym配合来寻找该函数的起始地址,但是在安卓高版本中android不允许打开白名单之外的so,这就让我们很头疼,比如我们hook libart.so中的函数都没有办法来找到函数的具体位置,所以有了此文,这里介绍3种方法来获得符号的地址,网上方案挺多的我这里主要介绍原理
通过程序头获得符号地址
首先是如何找到so的首地址,这个android系统中提供了maps文件来记录so的内存分步,所以我们可以遍历maps文件来寻找so的首地址,如下
char line[1024];
int *start;
int *end;
int n=1;
FILE *fp=fopen("/proc/self/maps","r");
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, "libart.so") ) {
__android_log_print(6,"r0ysue","");
if(n==1){
start = reinterpret_cast<int *>(strtoul(strtok(line, "-"), NULL, 16));
end = reinterpret_cast<int *>(strtoul(strtok(NULL, " "), NULL, 16));
}
else{
strtok(line, "-");
end = reinterpret_cast<int *>(strtoul(strtok(NULL, " "), NULL, 16));
}
n++;
}
}
通过elf头结构我们可以找到程序头的地址,ndk中自带了elf.h就很友好,就是e_phoff是相对于我们上面扫到的so首地址的偏移,e_phnum是我们的程序头表中结构体的总个数,程序头中存着elf装载信息,如下图
这里有一个问题就是上面的地址是so的起始地址,不是load_bias,所以我们在计算物理偏移的时候要减去一个首段的物理偏移,这里需要遍历程序头,得到第一个e_type为1的段记录下它的p_vaddr。其中对我们索引符号地址有用的就是Dynamic Segment,也就是type为2的段,这部分可以写一个循环来找到,去记录下其中的字符串表和符号表就可以了
Elf64_Ehdr header;
memcpy(&header, startr, sizeof(Elf64_Ehdr));
memcpy(&cc, ((char *) (startr) + header.e_phoff), sizeof(Elf64_Phdr));
for (int y = 0; y < header.e_phnum; y++) {//寻找首段偏移
memcpy(&cc, (char *) (startr) + header.e_phoff + sizeof(Elf64_Phdr) * y,
sizeof(Elf64_Phdr));
if (cc.p_type == 1) {
phof =cc.p_paddr
break;
}
}
for (int y = 0; y < header.e_phnum; y++) {
memcpy(&cc, (char *) (startr) + header.e_phoff + sizeof(Elf64_Phdr) * y,
sizeof(Elf64_Phdr));
if (cc.p_type == 2) {
Elf64_Dyn dd;
for (y = 0; y == 0 || dd.d_tag != 0; y++) {
memcpy(&dd, (char *) (startr) + cc.p_offset + y * sizeof(Elf64_Dyn) + 0x1000,
sizeof(Elf64_Dyn));
if (dd.d_tag == 5) {//符号表
strtab_ = reinterpret_cast< char *>((char *) startr + dd.d_un.d_ptr - phof);
}
if (dd.d_tag == 6) {//字符串表
symtab_ = reinterpret_cast<Elf64_Sym *>((
(char *) startr + dd.d_un.d_ptr - phof));
}
if (dd.d_tag == 10) {//字符串表大小
strsz = dd.d_un.d_val;
}
}
}
}
接下来遍历符号表就可以了,这里有一个问题就是如何确定符号表的大小,这里观察一下ida反编译的结果,发现符号表后面接的就是字符串表,那么用字符串表的首地址减去符号表的首地址就是符号表的大小,之后再用Elf64_Sym结构体解析,st_value就是该函数相对于load_bias的物理偏移,所以我们最后.再减去之前记录的首段偏移即可
char strtab[strsz];
memcpy(&strtab, strtab_, strsz);
Elf64_Sym mytmpsym;
for (n = 0; n < (long) strtab_ - (long) symtab_; n = n + sizeof(Elf64_Sym)) {//遍历符号表
memcpy(&mytmpsym,(char*)symtab_+n,sizeof(Elf64_Sym));
if(strstr(strtab_+mytmpsym.st_name,"artFindNativeMethod"))
{ __android_log_print(6,"r0ysue","%p %s",mytmpsym.st_value,strtab_+mytmpsym.st_name);
break;
}
}
return (char*)start+mytmpsym.st_value-phof;
通过节头获得符号地址
通过elf头结构我们也可以找到节头的地址,也就是e_shoff,节头表相对于程序头表就友好许多,它的项非常多,唯一不好的一点就是它不会加载到内存中,所以Execution View中就没有这个东西,所以我们只能通过绝对路径找到它,手动解析文件
int fd;
void *start;
struct stat sb;
fd = open(lib, O_RDONLY);
fstat(fd, &sb);
start = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
在这种解析方式中我们在elf头中需要的值是e_shoff、e_shentsize、e_shnum、e_shstrndx,就是节头表偏移,节大小,节个数,节头表字符串,不过我们最终的目标仍然是拿到符号表和字符串表,也就是下面的symtab和strtab中的sh_offset
Elf64_Ehdr header;
memcpy(&header, start, sizeof(Elf64_Ehdr));
int secoff = header.e_shoff;
int secsize = header.e_shentsize;
int secnum = header.e_shnum;
int secstr = header.e_shstrndx;
Elf64_Shdr strtab;
memcpy(&strtab, (char *) start + secoff + secstr * secsize, sizeof(Elf64_Shdr));
int strtaboff = strtab.sh_offset;
char strtabchar[strtab.sh_size];
memcpy(&strtabchar, (char *) start + strtaboff, strtab.sh_size);
Elf64_Shdr enumsec;
int symoff = 0;
int symsize = 0;
int strtabsize = 0;
int stroff = 0;
for (int n = 0; n < secnum; n++) {
memcpy(&enumsec, (char *) start + secoff + n * secsize, sizeof(Elf64_Shdr));
if (strcmp(&strtabchar[enumsec.sh_name], ".symtab") == 0) {
symoff = enumsec.sh_offset;
symsize = enumsec.sh_size;
}
if (strcmp(&strtabchar[enumsec.sh_name], ".strtab") == 0) {
stroff = enumsec.sh_offset;
strtabsize = enumsec.sh_size;
}
}
最后和上面一样遍历符号表即可可得到物理偏移
int realoff=0;
char relstr[strtabsize];
Elf64_Sym tmp;
memcpy(&relstr, (char *) start + stroff, strtabsize);
for (int n = 0; n < symsize; n = n + sizeof(Elf64_Sym)) {
memcpy(&tmp, (char *)start + symoff+n, sizeof(Elf64_Sym));
if(tmp.st_name!=0&&strstr(relstr+tmp.st_name,sym)){
realoff=tmp.st_value;
break;
}
}
return realoff;
这种方式能够找到非导出符号的地址,还是有一定作用的,比如我在寻找soinfo地址的时候就用到了寻找soinfo_map在linker中的相对地址
模仿安卓通过hash寻找符号
这种方式就是dlsym的官方写法,由于libart.so这种so自动就会加载到内存种所以就不需要dlopen了,我们只需要在map里面找到它的首地址就可以了,代码和上面一样就不贴了,这里我们主要看看官方如何实现的,一路追踪do_dlopen最终找到了函数soinfo::gnu_lookup,这里面是他的主要实现逻辑,我们只需要实现它即可,这里多了4个项我们之前没有提到,就是它的导出表4项,所以这种方法只能找到导出表当中的函数或者变量
size_t gnu_nbucket_ = 0;
// skip symndx
uint32_t gnu_maskwords_ = 0;
uint32_t gnu_shift2_ = 0;
ElfW(Addr) *gnu_bloom_filter_ = nullptr;
uint32_t *gnu_bucket_ = nullptr;
uint32_t *gnu_chain_ = nullptr;
int phof = 0;
Elf64_Ehdr header;
memcpy(&header, startr, sizeof(Elf64_Ehdr));
uint64 rel = 0;
size_t size = 0;
long *plt = nullptr;
char *strtab_ = nullptr;
Elf64_Sym *symtab_ = nullptr;
Elf64_Phdr cc;
memcpy(&cc, ((char *) (startr) + header.e_phoff), sizeof(Elf64_Phdr));
for (int y = 0; y < header.e_phnum; y++) {
memcpy(&cc, (char *) (startr) + header.e_phoff + sizeof(Elf64_Phdr) * y,
sizeof(Elf64_Phdr));
if (cc.p_type == 6) {
phof = cc.p_paddr - cc.p_offset;//改用程序头的偏移获得首段偏移用之前的方法也行
}
}
for (int y = 0; y < header.e_phnum; y++) {
memcpy(&cc, (char *) (startr) + header.e_phoff + sizeof(Elf64_Phdr) * y,
sizeof(Elf64_Phdr));
if (cc.p_type == 2) {
Elf64_Dyn dd;
for (y = 0; y == 0 || dd.d_tag != 0; y++) {
memcpy(&dd, (char *) (startr) + cc.p_offset + y * sizeof(Elf64_Dyn) + 0x1000,
sizeof(Elf64_Dyn));
if (dd.d_tag == 0x6ffffef5) {//0x6ffffef5为导出表项
gnu_nbucket_ = reinterpret_cast<uint32_t *>((char *) startr + dd.d_un.d_ptr -
phof)[0];
// skip symndx
gnu_maskwords_ = reinterpret_cast<uint32_t *>((char *) startr + dd.d_un.d_ptr -
phof)[2];
gnu_shift2_ = reinterpret_cast<uint32_t *>((char *) startr + dd.d_un.d_ptr -
phof)[3];
gnu_bloom_filter_ = reinterpret_cast<ElfW(Addr) *>((char *) startr +
dd.d_un.d_ptr + 16 - phof);
gnu_bucket_ = reinterpret_cast<uint32_t *>(gnu_bloom_filter_ + gnu_maskwords_);
// amend chain for symndx = header[1]
gnu_chain_ = reinterpret_cast<uint32_t *>( gnu_bucket_ +
gnu_nbucket_ -
reinterpret_cast<uint32_t *>(
(char *) startr +
dd.d_un.d_ptr - phof)[1]);
}
if (dd.d_tag == 5) {
strtab_ = reinterpret_cast< char *>((char *) startr + dd.d_un.d_ptr - phof);
}
if (dd.d_tag == 6) {
symtab_ = reinterpret_cast<Elf64_Sym *>((
(char *) startr + dd.d_un.d_ptr - phof));
}
}
}
}
之后模仿gnu_lookup函数即可,hashmap的查询方法
char* name_=symname;//直接抄的安卓源码
uint32_t h = 5381;
const uint8_t* name = reinterpret_cast<const uint8_t*>(name_);
while (*name != 0) {
h += (h << 5) + *name++; // h*33 + c = h + h * 32 + c = h + h << 5 + c
}
int index=0;
uint32_t h2 = h >> gnu_shift2_;
uint32_t bloom_mask_bits = sizeof(ElfW(Addr))*8;
uint32_t word_num = (h / bloom_mask_bits) & gnu_maskwords_;
ElfW(Addr) bloom_word = gnu_bloom_filter_[word_num];
n = gnu_bucket_[h % gnu_nbucket_];
do {
Elf64_Sym * s = symtab_ + n;
char * sb=strtab_+ s->st_name;
if (strcmp(sb ,reinterpret_cast<const char *>(name_)) == 0 )
<!-- @import "[TOC]" {cmd="toc" depthFrom=1 depthTo=6 orderedList=false} -->
<!-- code_chunk_output -->
- [寻找so中符号的地址](#寻找so中符号的地址)
- [总述](#总述)
- [通过程序头获得符号地址](#通过程序头获得符号地址)
- [通过节头获得符号地址](#通过节头获得符号地址)
- [模仿安卓通过hash寻找符号](#模仿安卓通过hash寻找符号)
- [总结](#总结)
<!-- /code_chunk_output -->
## 寻找so中符号的地址
### 总述
我们在使用so中的函数的时候可以使用dlopen和dlsym配合来寻找该函数的起始地址,但是在安卓高版本中android不允许打开白名单之外的so,这就让我们很头疼,比如我们hook libart.so中的函数都没有办法来找到函数的具体位置,所以有了此文,这里介绍3种方法来获得符号的地址,网上方案挺多的我这里主要介绍原理
### 通过程序头获得符号地址
首先是如何找到so的首地址,这个android系统中提供了maps文件来记录so的内存分步,所以我们可以遍历maps文件来寻找so的首地址,如下
```c
char line[1024];
int *start;
int *end;
int n=1;
FILE *fp=fopen("/proc/self/maps","r");
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, "libart.so") ) {
__android_log_print(6,"r0ysue","");
if(n==1){
start = reinterpret_cast<int *>(strtoul(strtok(line, "-"), NULL, 16));
end = reinterpret_cast<int *>(strtoul(strtok(NULL, " "), NULL, 16));
}
else{
strtok(line, "-");
end = reinterpret_cast<int *>(strtoul(strtok(NULL, " "), NULL, 16));
}
n++;
}
}
通过elf头结构我们可以找到程序头的地址,ndk中自带了elf.h就很友好,就是e_phoff是相对于我们上面扫到的so首地址的偏移,e_phnum是我们的程序头表中结构体的总个数,程序头中存着elf装载信息,如下图
这里有一个问题就是上面的地址是so的起始地址,不是load_bias,所以我们在计算物理偏移的时候要减去一个首段的物理偏移,这里需要遍历程序头,得到第一个e_type为1的段记录下它的p_vaddr。其中对我们索引符号地址有用的就是Dynamic Segment,也就是type为2的段,这部分可以写一个循环来找到,去记录下其中的字符串表和符号表就可以了
Elf64_Ehdr header;
memcpy(&header, startr, sizeof(Elf64_Ehdr));
memcpy(&cc, ((char *) (startr) + header.e_phoff), sizeof(Elf64_Phdr));
for (int y = 0; y < header.e_phnum; y++) {//寻找首段偏移
memcpy(&cc, (char *) (startr) + header.e_phoff + sizeof(Elf64_Phdr) * y,
sizeof(Elf64_Phdr));
if (cc.p_type == 1) {
phof =cc.p_paddr
break;
}
}
for (int y = 0; y < header.e_phnum; y++) {
memcpy(&cc, (char *) (startr) + header.e_phoff + sizeof(Elf64_Phdr) * y,
sizeof(Elf64_Phdr));
if (cc.p_type == 2) {
Elf64_Dyn dd;
for (y = 0; y == 0 || dd.d_tag != 0; y++) {
memcpy(&dd, (char *) (startr) + cc.p_offset + y * sizeof(Elf64_Dyn) + 0x1000,
sizeof(Elf64_Dyn));
if (dd.d_tag == 5) {//符号表
strtab_ = reinterpret_cast< char *>((char *) startr + dd.d_un.d_ptr - phof);
}
if (dd.d_tag == 6) {//字符串表
symtab_ = reinterpret_cast<Elf64_Sym *>((
(char *) startr + dd.d_un.d_ptr - phof));
}
if (dd.d_tag == 10) {//字符串表大小
strsz = dd.d_un.d_val;
}
}
}
}
接下来遍历符号表就可以了,这里有一个问题就是如何确定符号表的大小,这里观察一下ida反编译的结果,发现符号表后面接的就是字符串表,那么用字符串表的首地址减去符号表的首地址就是符号表的大小,之后再用Elf64_Sym结构体解析,st_value就是该函数相对于load_bias的物理偏移,所以我们最后.再减去之前记录的首段偏移即可
char strtab[strsz];
memcpy(&strtab, strtab_, strsz);
Elf64_Sym mytmpsym;
for (n = 0; n < (long) strtab_ - (long) symtab_; n = n + sizeof(Elf64_Sym)) {//遍历符号表
memcpy(&mytmpsym,(char*)symtab_+n,sizeof(Elf64_Sym));
if(strstr(strtab_+mytmpsym.st_name,"artFindNativeMethod"))
{ __android_log_print(6,"r0ysue","%p %s",mytmpsym.st_value,strtab_+mytmpsym.st_name);
break;
}
}
return (char*)start+mytmpsym.st_value-phof;
通过节头获得符号地址
通过elf头结构我们也可以找到节头的地址,也就是e_shoff,节头表相对于程序头表就友好许多,它的项非常多,唯一不好的一点就是它不会加载到内存中,所以Execution View中就没有这个东西,所以我们只能通过绝对路径找到它,手动解析文件
int fd;
void *start;
struct stat sb;
fd = open(lib, O_RDONLY);
fstat(fd, &sb);
start = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
在这种解析方式中我们在elf头中需要的值是e_shoff、e_shentsize、e_shnum、e_shstrndx,就是节头表偏移,节大小,节个数,节头表字符串,不过我们最终的目标仍然是拿到符号表和字符串表,也就是下面的symtab和strtab中的sh_offset
Elf64_Ehdr header;
memcpy(&header, start, sizeof(Elf64_Ehdr));
int secoff = header.e_shoff;
int secsize = header.e_shentsize;
int secnum = header.e_shnum;
int secstr = header.e_shstrndx;
Elf64_Shdr strtab;
memcpy(&strtab, (char *) start + secoff + secstr * secsize, sizeof(Elf64_Shdr));
int strtaboff = strtab.sh_offset;
char strtabchar[strtab.sh_size];
memcpy(&strtabchar, (char *) start + strtaboff, strtab.sh_size);
Elf64_Shdr enumsec;
int symoff = 0;
int symsize = 0;
int strtabsize = 0;
int stroff = 0;
for (int n = 0; n < secnum; n++) {
memcpy(&enumsec, (char *) start + secoff + n * secsize, sizeof(Elf64_Shdr));
if (strcmp(&strtabchar[enumsec.sh_name], ".symtab") == 0) {
symoff = enumsec.sh_offset;
symsize = enumsec.sh_size;
}
if (strcmp(&strtabchar[enumsec.sh_name], ".strtab") == 0) {
stroff = enumsec.sh_offset;
strtabsize = enumsec.sh_size;
}
}
最后和上面一样遍历符号表即可可得到物理偏移
int realoff=0;
char relstr[strtabsize];
Elf64_Sym tmp;
memcpy(&relstr, (char *) start + stroff, strtabsize);
for (int n = 0; n < symsize; n = n + sizeof(Elf64_Sym)) {
memcpy(&tmp, (char *)start + symoff+n, sizeof(Elf64_Sym));
if(tmp.st_name!=0&&strstr(relstr+tmp.st_name,sym)){
realoff=tmp.st_value;
break;
}
}
return realoff;
这种方式能够找到非导出符号的地址,还是有一定作用的,比如我在寻找soinfo地址的时候就用到了寻找soinfo_map在linker中的相对地址
模仿安卓通过hash寻找符号
这种方式就是dlsym的官方写法,由于libart.so这种so自动就会加载到内存种所以就不需要dlopen了,我们只需要在map里面找到它的首地址就可以了,代码和上面一样就不贴了,这里我们主要看看官方如何实现的,一路追踪do_dlopen最终找到了函数soinfo::gnu_lookup,这里面是他的主要实现逻辑,我们只需要实现它即可,这里多了4个项我们之前没有提到,就是它的导出表4项,所以这种方法只能找到导出表当中的函数或者变量
size_t gnu_nbucket_ = 0;
// skip symndx
uint32_t gnu_maskwords_ = 0;
uint32_t gnu_shift2_ = 0;
ElfW(Addr) *gnu_bloom_filter_ = nullptr;
uint32_t *gnu_bucket_ = nullptr;
uint32_t *gnu_chain_ = nullptr;
int phof = 0;
Elf64_Ehdr header;
memcpy(&header, startr, sizeof(Elf64_Ehdr));
uint64 rel = 0;
size_t size = 0;
long *plt = nullptr;
char *strtab_ = nullptr;
Elf64_Sym *symtab_ = nullptr;
Elf64_Phdr cc;
memcpy(&cc, ((char *) (startr) + header.e_phoff), sizeof(Elf64_Phdr));
for (int y = 0; y < header.e_phnum; y++) {
memcpy(&cc, (char *) (startr) + header.e_phoff + sizeof(Elf64_Phdr) * y,
sizeof(Elf64_Phdr));
if (cc.p_type == 6) {
phof = cc.p_paddr - cc.p_offset;//改用程序头的偏移获得首段偏移用之前的方法也行
}
}
for (int y = 0; y < header.e_phnum; y++) {
memcpy(&cc, (char *) (startr) + header.e_phoff + sizeof(Elf64_Phdr) * y,
sizeof(Elf64_Phdr));
if (cc.p_type == 2) {
Elf64_Dyn dd;
for (y = 0; y == 0 || dd.d_tag != 0; y++) {
memcpy(&dd, (char *) (startr) + cc.p_offset + y * sizeof(Elf64_Dyn) + 0x1000,
sizeof(Elf64_Dyn));
if (dd.d_tag == 0x6ffffef5) {//0x6ffffef5为导出表项
gnu_nbucket_ = reinterpret_cast<uint32_t *>((char *) startr + dd.d_un.d_ptr -
phof)[0];
// skip symndx
gnu_maskwords_ = reinterpret_cast<uint32_t *>((char *) startr + dd.d_un.d_ptr -
phof)[2];
gnu_shift2_ = reinterpret_cast<uint32_t *>((char *) startr + dd.d_un.d_ptr -
phof)[3];
gnu_bloom_filter_ = reinterpret_cast<ElfW(Addr) *>((char *) startr +
dd.d_un.d_ptr + 16 - phof);
gnu_bucket_ = reinterpret_cast<uint32_t *>(gnu_bloom_filter_ + gnu_maskwords_);
// amend chain for symndx = header[1]
gnu_chain_ = reinterpret_cast<uint32_t *>( gnu_bucket_ +
gnu_nbucket_ -
reinterpret_cast<uint32_t *>(
(char *) startr +
dd.d_un.d_ptr - phof)[1]);
}
if (dd.d_tag == 5) {
strtab_ = reinterpret_cast< char *>((char *) startr + dd.d_un.d_ptr - phof);
}
if (dd.d_tag == 6) {
symtab_ = reinterpret_cast<Elf64_Sym *>((
(char *) startr + dd.d_un.d_ptr - phof));
}
}
}
}
之后模仿gnu_lookup函数即可,hashmap的查询方法
char* name_=symname;//直接抄的安卓源码
uint32_t h = 5381;
const uint8_t* name = reinterpret_cast<const uint8_t*>(name_);
while (*name != 0) {
h += (h << 5) + *name++; // h*33 + c = h + h * 32 + c = h + h << 5 + c
}
int index=0;
uint32_t h2 = h >> gnu_shift2_;
uint32_t bloom_mask_bits = sizeof(ElfW(Addr))*8;
uint32_t word_num = (h / bloom_mask_bits) & gnu_maskwords_;
ElfW(Addr) bloom_word = gnu_bloom_filter_[word_num];
n = gnu_bucket_[h % gnu_nbucket_];
do {
Elf64_Sym * s = symtab_ + n;
char * sb=strtab_+ s->st_name;
if (strcmp(sb ,reinterpret_cast<const char *>(name_)) == 0 ) {
break;
}
} while ((gnu_chain_[n++] & 1) == 0);
Elf64_Sym * mysymf=symtab_+n;
long* finaladdr= reinterpret_cast<long*>(sb->st_value + (char *) start-phof);
return finaladdr;
总结
这里介绍了三种得到符号地址的方法,都比较简单,只是我们写hook或者主动调用框架的一个基础,只有深刻的了解了elf格式才能完成我们的目标
有兴趣可以加微信:roysu3一起学习呀{
break;
}
} while ((gnu_chain_[n++] & 1) == 0);
Elf64_Sym * mysymf=symtab_+n;
long* finaladdr= reinterpret_cast<long*>(sb->st_value + (char *) start-phof);
return finaladdr;
## 总结
这里介绍了三种得到符号地址的方法,都比较简单,只是我们写hook或者主动调用框架的一个基础,只有深刻的了解了elf格式才能完成我们的目标
有兴趣可以加微信:roysu3一起学习呀