长亭百川云 - 文章详情

自实现Linker加载SO

看雪学苑

102

2024-07-13

前言

前一陣子在研究so加固,發現其中涉及自實現的Linker加載so的技術,而我對此知之什少,因此只好先來學習下Linker的加載流程。

本文參考AOSP源碼和r0ysue大佬的文章(https://bbs.kanxue.com/thread-269484.htm,不知為何文中給出的那個demo我一直跑不起來 )來實現一個簡單的自實現Linker Demo。

環境:Pixel1XLAOSP - Oreo - 8.1.0_r81

Demo實現

Linker在加載so時大致可以分成五步:

1.讀取so文件:讀取ehdr( Elf header )、phdr( Program header )等信息。

2.載入so:預留一片內存空間,隨後將相關信息加載進去,最後修正so。

3.預鏈接:主要處理.dynamic節的內容。

4.正式鏈接:處理重定位的信息。

5.調用.init.init_array

Read

利用open+mmap來將待加載的so文件映射到內存空間,存放在start_addr_中。然後調用Read函數來獲取ehdr、phdr等信息。

int fd;
struct stat sb;
fd = open(path, O_RDONLY);
fstat(fd, &sb);
start_addr_ = static_cast<void **>(mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0));

// 1. 讀取so文件
if(!Read(path, fd, 0, sb.st_size)){
LOGD("Read so failed");
munmap(start_addr_, sb.st_size);
close(fd);
}

Read函數實現如下,調用ReadElfHeaderReadProgramHeaders來讀取ehdr和phdr。

AOSP源碼的Read中還會讀取Section Headers和Dynamic節,一開始我也有實現這部份的邏輯,但後來發現讀取後的信息根本沒有被用到,因此就把這部份給刪了。

bool MyLoader::Read(const char* name, int fd, off64_t file_offset, off64_t file_size) {
bool res = false;

name\_ = name;  
fd\_ = fd;  
file\_offset\_ = file\_offset;  
file\_size\_ = file\_size;  

if (ReadElfHeader() &&  
    ReadProgramHeaders()) {  
    res = true;  
}  

return res;  

}

ReadElfHeader的實現如下,直接通過memcpy來賦值。

bool MyLoader::ReadElfHeader() {
return memcpy(&(header_),start_addr_,sizeof(header_));
}

ReadProgramHeaders的實現直接copy源碼就可以,本質上還是內存映射的過程。

bool MyLoader::ReadProgramHeaders() {

phdr\_num\_ = header\_.e\_phnum;  

size\_t size = phdr\_num\_ \* sizeof(ElfW(Phdr));  

void\* data = Utils::getMapData(fd\_, file\_offset\_, header\_.e\_phoff, size);  
if(data == nullptr) {  
    LOGE("ProgramHeader mmap failed");  
    return false;  
}  
phdr\_table\_ = static\_cast<ElfW(Phdr)\*>(data);  

return true;  

}

void* Utils::getMapData(int fd, off64_t base_offset, size_t elf_offset, size_t size) {
off64_t offset;
safe_add(&offset, base_offset, elf_offset);

off64\_t page\_min = page\_start(offset);  
off64\_t end\_offset;  

safe\_add(&end\_offset, offset, size);  
safe\_add(&end\_offset, end\_offset, page\_offset(offset));  

size\_t map\_size = static\_cast<size\_t>(end\_offset - page\_min);  

uint8\_t\* map\_start = static\_cast<uint8\_t\*>(  
        mmap64(nullptr, map\_size, PROT\_READ, MAP\_PRIVATE, fd, page\_min));  

if (map\_start == MAP\_FAILED) {  
    return nullptr;  
}  

return map\_start + page\_offset(offset);  

}

Load

載入so基本信息

調用Load來載入so。

// 2. 載入so
if(!Load()) {
LOGD("Load so failed");
munmap(start_addr_, sb.st_size);
close(fd);
}

Load的實現如下:

ReserveAddressSpace用於生成一片新的內存空間,之後的操作基本上都是在這片內存空間進行。LoadSegmentsFindPhdr用於將待加載so的對應信息填充到此內存空間。

最後要修正so,將當前so修正為待加載的so,這部份放到後面來解析。

bool MyLoader::Load() {
bool res = false;
if (ReserveAddressSpace() &&
LoadSegments() &&
FindPhdr()) {

    LOGD("Load Done.........");  
    res = true;  
}  

// 獲取當前so (加載器的so)  
si\_ = Utils::get\_soinfo("libnglinker.so");  

if(!si\_) {  
    LOGE("si\_ return nullptr");  
    return false;  
}  
LOGD("si\_ -> base: %lx", si\_->base);  

// 使si\_可以被修改  
mprotect((void\*) PAGE\_START(reinterpret\_cast<ElfW(Addr)>(si\_)), 0x1000, PROT\_READ | PROT\_WRITE);  

// 修正so  
si\_->base = load\_start();  
si\_->size = load\_size();  

// si_->set_mapped_by_caller(elf_reader.is_mapped_by_caller());
si_->load_bias = load_bias();
si_->phnum = phdr_count();
si_->phdr = loaded_phdr();

return res;  

}

ReserveAddressSpace的具體實現如下,先計算出load_size_mmap一片內存,在我這個demo中min_vaddr0,因此load_start_ == load_bias_load_bias_代表的就是這片內存,而這片內存是用來存放待加載的so。

bool MyLoader::ReserveAddressSpace() {
ElfW(Addr) min_vaddr;
load_size_ = phdr_table_get_load_size(phdr_table_, phdr_num_, &min_vaddr);
LOGD("load_size_: %x", load_size_);
if (load_size_ == 0) {
LOGE("\"%s\" has no loadable segments", name_.c_str());
return false;
}

uint8\_t\* addr = reinterpret\_cast<uint8\_t\*>(min\_vaddr);  

void\* start;  

// Assume position independent executable by default.  
void\* mmap\_hint = nullptr;  

start = mmap(mmap\_hint, load\_size\_, PROT\_NONE, MAP\_PRIVATE | MAP\_ANONYMOUS, -1, 0);  

load\_start\_ = start;  
load\_bias\_ = reinterpret\_cast<uint8\_t\*>(start) - addr;  

return true;  

}

LoadSegments的具體實現如下,遍歷Program Header Table將所有type為PT_LOAD的段加載進內存,源碼中是采用mmap來映射,但我嘗試後發現會有權限問題,因而采用memcpy的方案。

bool MyLoader::LoadSegments() {
// 在這個函數中會往 ReserveAddressSpace
// 裡mmap的那片內存填充數據

for (size\_t i = 0; i < phdr\_num\_; ++i) {  
    const ElfW(Phdr)\* phdr = &phdr\_table\_\[i\];  

    if (phdr->p\_type != PT\_LOAD) {  
        continue;  
    }  

    // Segment addresses in memory.  
    ElfW(Addr) seg\_start = phdr->p\_vaddr + load\_bias\_;  
    ElfW(Addr) seg\_end   = seg\_start + phdr->p\_memsz;  

    ElfW(Addr) seg\_page\_start = PAGE\_START(seg\_start);  
    ElfW(Addr) seg\_page\_end   = PAGE\_END(seg\_end);  

    ElfW(Addr) seg\_file\_end   = seg\_start + phdr->p\_filesz;  

    // File offsets.  
    ElfW(Addr) file\_start = phdr->p\_offset;  
    ElfW(Addr) file\_end   = file\_start + phdr->p\_filesz;  

    ElfW(Addr) file\_page\_start = PAGE\_START(file\_start);  
    ElfW(Addr) file\_length = file\_end - file\_page\_start;  

    if (file\_size\_ <= 0) {  
        LOGE("\\"%s\\" invalid file size: %", name\_.c\_str(), file\_size\_);  
        return false;  
    }  

    if (file\_end > static\_cast<size\_t>(file\_size\_)) {  
        LOGE("invalid ELF file");  
        return false;  
    }  

    if (file\_length != 0) {  
        // 按AOSP裡那樣用mmap會有問題, 因此改為直接 memcpy  
        mprotect(reinterpret\_cast<void \*>(seg\_page\_start), seg\_page\_end - seg\_page\_start, PROT\_WRITE);  
        void\* c = (char\*)start\_addr\_ + file\_page\_start;  
        void\* res = memcpy(reinterpret\_cast<void \*>(seg\_page\_start), c, file\_length);  

        LOGD("\[LoadSeg\] %s  seg\_page\_start: %lx   c : %lx", strerror(errno), seg\_page\_start, c);  

    }  

    // if the segment is writable, and does not end on a page boundary,  
    // zero-fill it until the page limit.  
    if ((phdr->p\_flags & PF\_W) != 0 && PAGE\_OFFSET(seg\_file\_end) > 0) {  
        memset(reinterpret\_cast<void\*>(seg\_file\_end), 0, PAGE\_SIZE - PAGE\_OFFSET(seg\_file\_end));  
    }  

    seg\_file\_end = PAGE\_END(seg\_file\_end);  

    // seg\_file\_end is now the first page address after the file  
    // content. If seg\_end is larger, we need to zero anything  
    // between them. This is done by using a private anonymous  
    // map for all extra pages.  

    if (seg\_page\_end > seg\_file\_end) {  
        size\_t zeromap\_size = seg\_page\_end - seg\_file\_end;  
        void\* zeromap = mmap(reinterpret\_cast<void\*>(seg\_file\_end),  
                             zeromap\_size,  
                             PFLAGS\_TO\_PROT(phdr->p\_flags),  
                             MAP\_FIXED|MAP\_ANONYMOUS|MAP\_PRIVATE,  
                             -1,  
                             0);  
        if (zeromap == MAP\_FAILED) {  
            LOGE("couldn't zero fill \\"%s\\" gap: %s", name\_.c\_str(), strerror(errno));  
            return false;  
        }  

        // 分配.bss節  
        prctl(PR\_SET\_VMA, PR\_SET\_VMA\_ANON\_NAME, zeromap, zeromap\_size, ".bss");  
    }  
}  

return true;  

}

FindPhdr的具體實現如下,簡單來說就是將Phdr信息填充進load_bias_那片內存。

bool MyLoader::FindPhdr() {

const ElfW(Phdr)\* phdr\_limit = phdr\_table\_ + phdr\_num\_;  

// If there is a PT\_PHDR, use it directly.  
for (const ElfW(Phdr)\* phdr = phdr\_table\_; phdr < phdr\_limit; ++phdr) {  
    if (phdr->p\_type == PT\_PHDR) {  
        return CheckPhdr(load\_bias\_ + phdr->p\_vaddr);  
    }  
}  

// Otherwise, check the first loadable segment. If its file offset  
// is 0, it starts with the ELF header, and we can trivially find the  
// loaded program header from it.  
for (const ElfW(Phdr)\* phdr = phdr\_table\_; phdr < phdr\_limit; ++phdr) {  
    if (phdr->p\_type == PT\_LOAD) {  
        if (phdr->p\_offset == 0) {  
            ElfW(Addr)  elf\_addr = load\_bias\_ + phdr->p\_vaddr;  
            const ElfW(Ehdr)\* ehdr = reinterpret\_cast<const ElfW(Ehdr)\*>(elf\_addr);  
            ElfW(Addr)  offset = ehdr->e\_phoff;  
            return CheckPhdr(reinterpret\_cast<ElfW(Addr)>(ehdr) + offset);  
        }  
        break;  
    }  
}  

LOGE("can't find loaded phdr for \\"%s\\"", name\_.c\_str());  
return false;  

}

bool MyLoader::CheckPhdr(ElfW(Addr) loaded) {
const ElfW(Phdr)* phdr_limit = phdr_table_ + phdr_num_;
ElfW(Addr) loaded_end = loaded + (phdr_num_ * sizeof(ElfW(Phdr)));
for (const ElfW(Phdr)* phdr = phdr_table_; phdr < phdr_limit; ++phdr) {
if (phdr->p_type != PT_LOAD) {
continue;
}
ElfW(Addr) seg_start = phdr->p_vaddr + load_bias_;
ElfW(Addr) seg_end = phdr->p_filesz + seg_start;
if (seg_start <= loaded && loaded_end <= seg_end) {
loaded_phdr_ = reinterpret_cast<const ElfW(Phdr)*>(loaded);
return true;
}
}
LOGE("\"%s\" loaded phdr %p not in loadable segment",
name_.c_str(), reinterpret_cast<void*>(loaded));
return false;
}

修正so

Load函數最後是在對soinfo的修正,將當前so( 加載器 )修正為待加載的so。AOSP源碼中的si_是通過特定方法new出來的全新soinfo,而我看大多數文章都是獲取當前so作為si_,然後修正其中的信息。

本來是想嘗試按AOSP源碼那樣new一個soinfo看看結果有什麼不同,但最終被soinfo結構的複雜性勸退了。

修正so的第一步是要獲取當前so的soinfo對象,從這篇文章(https://nszdhd1.github.io/2020/07/03/%E7%BD%91%E6%98%93%E4%BF%9D%E6%8A%A4%E5%88%86%E6%9E%90/#/%E7%BD%91%E6%98%93%E4%BF%9D%E6%8A%A4%E7%A0%94%E7%A9%B6)發現`find_containing_library`這個函數,似乎可以一步到位直接獲取soinfo對象。該函數位於`linker64`中,將它拉入IDA,能直接搜尋到該函數,這意味著能夠「借用」這個函數。

想要「借用」linker64裡的find_containing_library,需要知道linker64在內存的基址和find_containing_library的函數偏移( 相對基址的偏移 ),前者可以通過遍歷/proc/self/maps來取得,而後者的獲取有以下兩種思路:

1.直接從IDA查看其偏移(0x9AB0)

2.解析linker64的文件,自動獲取,具體實現在Utils::get_export_func中。

成功獲取find_containing_library地址後,強轉成FunctionPtr函數指針後即可調用,參數為當前so的地址( 同樣是遍歷maps取得的 ),最終會返回當前so的soinfo對象。

soinfo* Utils::get_soinfo(const char* so_name) {
typedef soinfo* (*FunctionPtr)(ElfW(Addr));

char line\[1024\];  
ElfW(Addr) linker\_base = 0;  
ElfW(Addr) so\_addr = 0;  
FILE \*fp=fopen("/proc/self/maps","r");  
while (fgets(line, sizeof(line), fp)) {  
    if (strstr(line, "linker64") && !linker\_base) {  
        char\* addr = strtok(line, "-");  
        linker\_base = strtoull(addr, NULL, 16);  

    }else if(strstr(line, so\_name) && !so\_addr) {  
        char\* addr = strtok(line, "-");  
        so\_addr = strtoull(addr, NULL, 16);  

    }  

    if(linker\_base && so\_addr)break;  

}  

ElfW(Addr) func\_offset = Utils::get\_export\_func("/system/bin/linker64", "find\_containing\_library");  
if(!func\_offset) {  
    LOGE("func\_offset == 0? check it ---> get\_soinfo");  
    return nullptr;  
}  

// ElfW(Addr) find_containing_library_addr = static_cast<ElfW(Addr)>(linker_base + 0x9AB0);
ElfW(Addr) find_containing_library_addr = static_cast<ElfW(Addr)>(linker_base + func_offset);
FunctionPtr find_containing_library = reinterpret_cast(find_containing_library_addr);

return find\_containing\_library(so\_addr);  

}

get_export_func的實現如下,主要依賴於elf的文件結構,可以參考下我之前寫的文章(https://ngiokweng.github.io/2024/05/30/elf%E6%96%87%E4%BB%B6%E7%B5%90%E6%A7%8B/),大致原理如下:

1.elf header的e_shstrndx是一個索引,指向了.shstrtab節區,而.shstrtab節區存儲著所有節區的名字。

2.遍歷所有節區,找到名為.symtab.strtab的節區(.symtab節每項都有一個st_name屬性,是.strtab節區的一個索引值,指向某符號名 )。

3.遍歷.symtab節區,對比func_name,匹配則返回對應的函數偏移。

ElfW(Addr) Utils::get_export_func(char* path, char* func_name) {

struct stat sb;  
int fd = open(path, O\_RDONLY);  
fstat(fd, &sb);  
void\* base = mmap(NULL, sb.st\_size, PROT\_READ | PROT\_WRITE, MAP\_PRIVATE, fd, 0);  

// 讀取elf header  
ElfW(Ehdr) header;  
memcpy(&(header), base, sizeof(header));  

// 讀取Section header table  
size\_t size = header.e\_shnum \* sizeof(ElfW(Shdr));  
void\* tmp = mmap(nullptr, size, PROT\_READ | PROT\_WRITE, MAP\_PRIVATE | MAP\_ANONYMOUS, -1, 0); // 注: 必須要 MAP\_ANONYMOUS  
LOGD("error: %s", strerror(errno));  
ElfW(Shdr)\* shdr\_table;  
memcpy(tmp, (void\*)((ElfW(Off))base + header.e\_shoff), size);  
shdr\_table = static\_cast<ElfW(Shdr)\*>(tmp);  

char\* shstrtab = reinterpret\_cast<char\*>(shdr\_table\[header.e\_shstrndx\].sh\_offset + (ElfW(Off))base);  

void\* symtab = nullptr;  
char\* strtab = nullptr;  
uint32\_t symtab\_size = 0;  

// 遍歷獲取.symtab和.strtab節  
for (size\_t i = 0; i < header.e\_shnum; ++i) {  
    const ElfW(Shdr) \*shdr = &shdr\_table\[i\];  
    char\* section\_name = shstrtab + shdr->sh\_name;  
    if(!strcmp(section\_name, ".symtab")) {  

// LOGD("[test] %d: shdr->sh_name = %s", i, (shstrtab + shdr->sh_name));
symtab = reinterpret_cast<void*>(shdr->sh_offset + (ElfW(Off))base);
symtab_size = shdr->sh_size;
}
if(!strcmp(section_name, ".strtab")) {
// LOGD("[test] %d: shdr->sh_name = %s", i, (shstrtab + shdr->sh_name));
strtab = reinterpret_cast<char*>(shdr->sh_offset + (ElfW(Off))base);
}

    if(strtab && symtab)break;  
}  

// 讀取 Symbol table  
ElfW(Sym)\* sym\_table;  
tmp = mmap(nullptr, symtab\_size, PROT\_READ | PROT\_WRITE, MAP\_PRIVATE | MAP\_ANONYMOUS, -1, 0);  
memcpy(tmp, symtab, symtab\_size);  
sym\_table = static\_cast<ElfW(Sym)\*>(tmp);  

int sym\_num = symtab\_size / sizeof(ElfW(Sym));  

// 遍歷 Symbol table  
for(int i = 0; i < sym\_num; i++) {  
    const ElfW(Sym) \*sym = &sym\_table\[i\];  
    char\* sym\_name = strtab + sym->st\_name;  
    if(strstr(sym\_name, func\_name)) {  
        return sym->st\_value;  
    }  

}  

return 0;  

}

成功獲取si_後要修改其對應屬性。在這裡我遇到一個很玄學的問題,就是一開始不知為什麼死活修改不了si_的屬性,一改就會報內存讀寫的錯,即使mprotect賦予可讀可寫權限也無用,嘗試了各種方法都無用,在這卡了我好幾天,直到某次重啟手機後就突然好了?

// 使si_可以被修改
mprotect((void*) PAGE_START(reinterpret_cast<ElfW(Addr)>(si_)), 0x1000, PROT_READ | PROT_WRITE);

// 修正so
si_->base = load_start();
si_->size = load_size();
// si_->set_mapped_by_caller(elf_reader.is_mapped_by_caller());
si_->load_bias = load_bias();
si_->phnum = phdr_count();
si_->phdr = loaded_phdr();

補充:soinfo結構( 巨TM坑 )

soinfo結構體定義在bionic/linker/linker_soinfo.h中。

將它copy到本地後會有很多報錯,一開始我是將那些沒有用到又報紅的直接刪掉,但後來發現這樣做會間接導致最後發生「android linker java.lang.unsatisfiedlinkerror: no implementation found for XXX」的錯誤( 這個錯誤我排查了很久很久,最終才發現是soinfo結構的問題,果然細節決定成敗…… )。

正確的做法是必須要保留所有的成員變量( 即使該變量用不到也要留下來占位 ),函數由於不占空間可以隨便刪掉。

prelink_image

預鏈接,主要是在遍歷.dynamic節獲取各種動態信息並保存在修正後的soinfo中。

// 3. 預鏈接, 主要處理 .dynamic節
si_->prelink_image()

prelink_image的具體實現太長( 基本上是copy源碼的 )就不展示了,比較大的改動是在DT_NEEDED時手動保存對應的依賴庫,之後重定向時會用到。

bool soinfo::prelink_image() {
/* Extract dynamic section */
ElfW(Word) dynamic_flags = 0;
Utils::phdr_table_get_dynamic_section(phdr, phnum, load_bias, &dynamic, &dynamic_flags);

if (dynamic == nullptr) {  
    return false;  
} else {  
}  

for (ElfW(Dyn)\* d = dynamic; d->d\_tag != DT\_NULL; ++d) {  
    LOGD("d = %p, d\[0\](tag) = %p d\[1\](val) = %p",  
          d, reinterpret\_cast<void\*>(d->d\_tag), reinterpret\_cast<void\*>(d->d\_un.d\_val));  
    switch (d->d\_tag) {  
                // ...  
                  case DT\_NEEDED:  
            // 手動保留所有依賴庫, 用於之後的重定位  
            myneed\[needed\_count\] = d->d\_un.d\_val;  
            ++needed\_count;  
            break;  
           // ...  
    }  
}  

return true;  

}

link_image

link_image裡處理重定向信息。

// 4. 正式鏈接, 在這裡處理重定位的信息
si_->link_image();

link_image的實現如下,android_relocs_的重定向我沒有處理( 嘗試處理過,但有點問題就刪了 ),好像問題不大?

之後調用relocaterela_plt_rela_的內容進行重定向。

bool soinfo::link_image() {
local_group_root_ = this;

if (android\_relocs\_ != nullptr) {  
    LOGD("android\_relocs\_ 不用處理?");  

} else {  
    LOGE("bad android relocation header.");  

// return false;
}

///*
#if defined(USE_RELA)
if (rela_ != nullptr) {
LOGD("[ relocating %s ]", get_realpath());
if (!relocate(plain_reloc_iterator(rela_, rela_count_))) {
return false;
}
}
if (plt_rela_ != nullptr) {
LOGD("[ relocating %s plt ]", get_realpath());
if (!relocate(plain_reloc_iterator(plt_rela_, plt_rela_count_))) {
return false;
}
}
#else
LOGE("TODO: !defined(USE_RELA) ");
#endif

LOGD("\[ finished linking %s \]", get\_realpath());  

// We can also turn on GNU RELRO protection if we're not linking the dynamic linker  
// itself --- it can't make system calls yet, and will have to call protect\_relro later.  
if (!((flags\_ & FLAG\_LINKER) != 0) && !protect\_relro()) {  
    return false;  
}  

return true;  

}

relocate函數的實現如下,在重定位時最需要確定的就是目標函數的真實地址。

這裡采用一種偷懶的方式,直接遍歷所有依賴庫( 之前保存在myneed中 ),調用dlopen+dlsym查找對應函數地址,找到的結果會保存在sym_addr中,後續再根據type來決定重定位的方式;而如果遍歷完所有依賴庫都沒有找到,則嘗試從symtab_[sym].st_value裡獲取。

template
bool soinfo::relocate(ElfRelIteratorT&& rel_iterator) {
for (size_t idx = 0; rel_iterator.has_next(); ++idx) {
const auto rel = rel_iterator.next();
if (rel == nullptr) {
return false;
}

    ElfW(Word) type = ELFW(R\_TYPE)(rel->r\_info);  
    ElfW(Word) sym = ELFW(R\_SYM)(rel->r\_info);  

    // reloc 指向需要重定向的內容, 根據type來決定重定向成什麼  
    ElfW(Addr) reloc = static\_cast<ElfW(Addr)>(rel->r\_offset + load\_bias);  
    ElfW(Addr) sym\_addr = 0;  
    const char\* sym\_name = nullptr;  
    ElfW(Addr) addend = Utils::get\_addend(rel, reloc);  

// LOGD("Processing \"%s\" relocation at index %zd", get_realpath(), idx);
if (type == R_GENERIC_NONE) {
continue;
}

    const ElfW(Sym)\* s = nullptr;  
    soinfo\* lsi = nullptr;  

    if (sym != 0) {  

        sym\_name = get\_string(symtab\_\[sym\].st\_name);  
        LOGD("sym = %lx   sym\_name: %s   st\_value: %lx", sym, sym\_name, symtab\_\[sym\].st\_value);  

        for(int s = 0; s < needed\_count; s++) {  
            void\* handle = dlopen(get\_string(myneed\[s\]),RTLD\_NOW);  
            sym\_addr = reinterpret\_cast<Elf64\_Addr>(dlsym(handle, sym\_name));  
            if(sym\_addr) break;  

        }  

        if(!sym\_addr) {  
            if(symtab\_\[sym\].st\_value != 0) {  
                sym\_addr = load\_bias + symtab\_\[sym\].st\_value;  
            }else {  
                LOGE("%s find addr fail", sym\_name);  
            }  

        }else {  
            LOGD("%s find addr success : %lx", sym\_name, sym\_addr);  
        }  
    }  

    LOGD("reloc addr: %x", (reloc - base));  
    LOGD("type: %x", type);  
    switch (type) {  
        case R\_GENERIC\_JUMP\_SLOT:  
            \*reinterpret\_cast<ElfW(Addr)\*>(reloc) = (sym\_addr + addend);  
            break;  
        case R\_GENERIC\_GLOB\_DAT:  
            \*reinterpret\_cast<ElfW(Addr)\*>(reloc) = (sym\_addr + addend);  
            break;  
        case R\_GENERIC\_RELATIVE:  
            \*reinterpret\_cast<ElfW(Addr)\*>(reloc) = (load\_bias + addend);  
            break;  
        case R\_GENERIC\_IRELATIVE:  
            {  

                ElfW(Addr) ifunc\_addr = Utils::call\_ifunc\_resolver(load\_bias + addend);  
                \*reinterpret\_cast<ElfW(Addr)\*>(reloc) = ifunc\_addr;  
            }  
            break;  

#if defined(__aarch64__)
case R_AARCH64_ABS64:
LOGD("R_AARCH64_ABS64 %lx addend: %lx", sym_addr + addend, addend);
*reinterpret_cast<ElfW(Addr)*>(reloc) = sym_addr + addend;
break;
case R_AARCH64_ABS32:
{
const ElfW(Addr) min_value = static_cast<ElfW(Addr)>(INT32_MIN);
const ElfW(Addr) max_value = static_cast<ElfW(Addr)>(UINT32_MAX);
if ((min_value <= (sym_addr + addend)) &&
((sym_addr + addend) <= max_value)) {
*reinterpret_cast<ElfW(Addr)*>(reloc) = sym_addr + addend;
} else {
LOGE("0x%016llx out of range 0x%016llx to 0x%016llx",
sym_addr + addend, min_value, max_value);
return false;
}
}
break;
case R_AARCH64_ABS16:
{
const ElfW(Addr) min_value = static_cast<ElfW(Addr)>(INT16_MIN);
const ElfW(Addr) max_value = static_cast<ElfW(Addr)>(UINT16_MAX);
if ((min_value <= (sym_addr + addend)) &&
((sym_addr + addend) <= max_value)) {
*reinterpret_cast<ElfW(Addr)*>(reloc) = (sym_addr + addend);
} else {
LOGE("0x%016llx out of range 0x%016llx to 0x%016llx",
sym_addr + addend, min_value, max_value);
return false;
}
}
break;
case R_AARCH64_PREL64:
*reinterpret_cast<ElfW(Addr)*>(reloc) = sym_addr + addend - rel->r_offset;
break;
case R_AARCH64_PREL32:
{
const ElfW(Addr) min_value = static_cast<ElfW(Addr)>(INT32_MIN);
const ElfW(Addr) max_value = static_cast<ElfW(Addr)>(UINT32_MAX);
if ((min_value <= (sym_addr + addend - rel->r_offset)) &&
((sym_addr + addend - rel->r_offset) <= max_value)) {
*reinterpret_cast<ElfW(Addr)*>(reloc) = sym_addr + addend - rel->r_offset;
} else {
LOGE("0x%016llx out of range 0x%016llx to 0x%016llx",
sym_addr + addend - rel->r_offset, min_value, max_value);
return false;
}
}
break;
case R_AARCH64_PREL16:
{
const ElfW(Addr) min_value = static_cast<ElfW(Addr)>(INT16_MIN);
const ElfW(Addr) max_value = static_cast<ElfW(Addr)>(UINT16_MAX);
if ((min_value <= (sym_addr + addend - rel->r_offset)) &&
((sym_addr + addend - rel->r_offset) <= max_value)) {
*reinterpret_cast<ElfW(Addr)*>(reloc) = sym_addr + addend - rel->r_offset;
} else {
LOGE("0x%016llx out of range 0x%016llx to 0x%016llx",
sym_addr + addend - rel->r_offset, min_value, max_value);
return false;
}
}
break;

case R_AARCH64_COPY:
LOGE("%s R_AARCH64_COPY relocations are not supported", get_realpath());
return false;
case R_AARCH64_TLS_TPREL64:
LOGD("RELO TLS_TPREL64 *** %16llx <- %16llx - %16llx\n",
reloc, (sym_addr + addend), rel->r_offset);
break;
case R_AARCH64_TLS_DTPREL32:
LOGD("RELO TLS_DTPREL32 *** %16llx <- %16llx - %16llx\n",
reloc, (sym_addr + addend), rel->r_offset);
break;
#endif
default:
LOGE("unknown reloc type %d @ %p (%zu) sym_name: %s", type, rel, idx, sym_name);
return false;
}
// */
}
return true;
}

call_constructors

調用soinfo的構建函數:.init.init_array內所有函數。

.

// 使被加載的so有執行權限, 否則在調用.init_array時會報錯
mprotect(reinterpret_cast<void *>(load_bias_), sb.st_size, PROT_READ | PROT_WRITE | PROT_EXEC);
//...

// 5. 調用.init和.init_array
si_->call_constructors();

原版Linker在調用.init.init_array時傳入的是0, nullptr, nullptr,我這裡與其保持一致。

void soinfo::call_constructors() {
// 對於so文件來說, 由於沒有_start函數
// 因此init_func_和init_array_都無法傳參, 只能是默認值

if(init\_func\_) {  
    LOGD("init func: %p", init\_func\_);  
    init\_func\_(0, nullptr, nullptr);  
}  
if(init\_array\_) {  
    for(int i = 0; i < init\_array\_count\_; i++) {  
        if(!init\_array\_\[i\])continue;  
        init\_array\_\[i\](0, nullptr, nullptr);  
    }  
}  

}


完整代碼

項目地址:https://github.com/ngiokweng/ng1ok-linker

===

測試

隨便寫一個so作為待加載的so( 名為libdemo1.so),內容如下,將它push到/data/local/tmp

#include <jni.h>
#include
#include <android/log.h>

#define TAG "nglog"

#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG,__VA_ARGS__)

extern "C" JNIEXPORT jstring JNICALL
Java_ng1ok_demo1_NativeLib_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}

extern "C"
JNIEXPORT jstring JNICALL
Java_ng1ok_linker_MainActivity_demo1Func(JNIEnv *env, jobject thiz) {
LOGD("Java_ng1ok_linker_MainActivity_demo1Func calleeeeeeeddddddddd");
std::string str = "Java_ng1ok_linker_MainActivity_demo1Func";

return env->NewStringUTF(str.c\_str());  

}

__attribute__((constructor()))
void sayHello(){
LOGD("[from libdemo1.so .init_array] Hello~~~");
}

extern "C" {
void _init(void){
LOGD("[from libdemo1.so .init] _init~~~~");
}
}

Demo的用例如下,實例化MyLoader,調用run函數加載指定路徑的so。

Java層的onCreate如下,在test之後調用待加載so裡的demo1Func函數。

輸出如下,大功告成~

===

===

結語

前前後後弄了兩、三周的時間,最終總算是弄好了這一個小Demo。自知該Demo仍有很多不足之處( 如無法捕獲try…catch ),而且只經過簡單的測試,定然存在諸多的BUG,歡迎各位大佬的指正!有任何也問題歡迎評論,或者私聊我/找我聊聊天都可以!

看雪ID:ngiokweng

https://bbs.kanxue.com/user-home-946537.htm

*本文为看雪论坛优秀文章,由 ngiokweng 原创,转载请注明来自看雪社区

# 往期推荐

1、Win10和Win11内存区域划分及动态随机的本质

2、Windows主机入侵检测与防御内核技术深入解析

3、反沙箱钓鱼远控样本分析

4、安全浏览器历史记录数据库解密算法逆向

5、APP小说VIP功能分析

球分享

球点赞

球在看

点击阅读原文查看更多

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2