尋找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一起學習呀