AndroidLinkerとSOシェルテクノロジー上編
12387 ワード
1.背景Androidシステムのセキュリティがますます重要になり、従来のpcセキュリティの実行可能ファイルの強化のように、アプリケーションの強化はAndroidシステムのセキュリティにおいて非常に重要な一環である.現在、Androidアプリケーションの強化はdex強化とNative強化に分けられ、Native強化の保護対象はNative層のSOファイルであり、シェル、デバッグ、混同、VMなどの手段を用いてSOファイルの逆コンパイルの難しさを増している.現在最も主流のSOファイル保護案はやはりシェル技術であり、SOファイルのシェル化とシェル解除の攻防技術分野では、Linkerすなわちロードリンクメカニズムの理解が最も重要な基礎である.非安全方向開発者にとって,システムのマウントとリンクメカニズムを深く理解することもステップアップの必要条件である.本論文では,LinkerによるSOファイルのマウントとリンクプロセスを詳細に解析し,最後にSOシェルのキーテクノロジーについて簡単に紹介した.Linkerの学習には、Linkerの自己提案、実行可能ファイルのロードなどの技術も含まれるべきであるが、本人の技術レベルに限られ、本論文の議論範囲は、dlopen(「libxx.SO」)を呼び出した後のLinkerの処理過程に限定される.本明細書では、Android 5.0 AOSPソースに基づいて、ARMプラットフォームのみについて、可読性を向上させるために、ここで列挙したソースコードはすべて削除され、他のCPUアーキテクチャに関するソースコードおよびエラー処理が除去される.また、本文を読む読者はELFファイルの構造について一定の理解が必要です.
2.SOのマウントとリンク2.1全体の流れ説明2.1.1 do_dlopen呼び出しdl_Open後、真ん中をdlopen_経由ext,最初の主要関数do_に到達dlopen:
do_dlopenは2つの重要な関数を呼び出し、1つ目はfind_です.library、2番目はsoinfoのメンバー関数CallConstructors,find_library関数はSOマウントリンクの後続関数であり、SOのマウントリンクが完了した後、CallConstructorsを介してSOの初期化関数を呼び出す.
2.1.2 find_library_internalfind_libraryはfind_を直接呼び出しましたlibrary_internal、次はfind_を直接見ます.library_internal関数:
find_library_internalはまずfind_を通りますloaded_library_by_name関数は、ターゲットSOがロードされているか否かを判断し、ロードされている場合は対応するsoinfoポインタを直接返し、ロードされていない場合はload_を呼び出すlibraryはロードプロセスを継続し、load_を参照してください.library関数.
2.13 load_library
load_library関数はSOマウントリンクの全体の流れを示しており、主に3つのステップがある:1マウント:ElfReaderオブジェクトを作成し、ElfReaderオブジェクトのLoadメソッドによってSOファイルをメモリ2にマウントしてsoinfoを割り当てる:soinfoを呼び出すalloc関数はSOに新しいsoinfo構造を割り当て、ロード結果に従って対応するメンバー変数3リンクを更新します.soinfo_を呼び出します.link_イメージ完了SOのリンク先の解析で、load_library関数にはSOマウントリンクの主なプロセスが含まれており、後述するElfReaderクラスとsoinfo_を分析することによってlink_image関数では、SOのマウントとリンクの手順をそれぞれ説明します.
2.2 load_にマウントlibraryでは、まずelf_を初期化します.readerオブジェクト、最初のパラメータはSOの名前、2番目のパラメータはファイル記述子fd:ElfReader elf_reader(name,fd)の後にElfReaderのloadメソッドを呼び出してSOをロードする.
ElfReader::Loadメソッドは次のとおりです.
ElfReader::LoadメソッドはまずSOのelfヘッダを読み出し、elfヘッダを検証した後、programヘッダを読み出し、programヘッダに基づいてSOに必要なメモリサイズを計算して対応する空間を割り当て、次にSOをsegment単位でメモリにロードし、最後にメモリにロードされたSOの中でprogramヘッダを見つけ、その後のリンクプロセスで使用しやすい.以下、ElfReaderのメンバー関数について詳しく説明します.
2.2.1 read&verify elfheader
ReadElfHeader readを使用して直接SOファイルからelfheaderをheaderに読み込み、header_ElfReaderのメンバー変数、タイプはElf 32_Ehdrは、headerによってelf headerの各フィールドに容易にアクセスでき、elf headerにはprogram header table、section header tableなどの重要な情報が含まれています.elf headerの検証には、magicバイト32/64 bitが現在のプラットフォームと一致するかどうかのサイズエンドタイプ:実行可能ファイル、SO...バージョン:一般的に1であり、現在のバージョンプラットフォーム:ARM、x 86、amd 64...エラーが発生した場合、ロードに失敗することを示します.
2.2.2 Read ProgramHeader
プログラムヘッダをメモリに1部ずつマッピングし、プログラムヘッダを解析する際に一時的に使用します.SOをメモリにマウントすると、このメモリが解放され、マウントされたSOのプログラムヘッダが使用されます.
2.2.3 reserve space&計算load size
まずphdr_を呼び出すtable_get_load_size関数はSOのメモリに必要な空間load_を取得するsizeは、mmap匿名マッピングを使用して、対応する空間を予約します.
loadbiasについて:SOはロード・ベース・アドレスを指定できますが、SOが指定したロード・ベース・アドレスはページ整列ではない可能性があります.この場合、実際のマッピング・アドレスと指定したロード・アドレスにずれが発生します.この偏差はload_です.bias_,その後、仮想アドレスの計算にload_を使用する必要があります.bias_修正.通常のSOではロードベースアドレスが指定されていませんが、min_vaddr=0の場合load_bias_ = load_start_,すなわちload_bias_ベースアドレスのロードと等しく、load_bias_直接ベースアドレスと呼ぶ.次はphdr_に進みますtable_get_load_size分析してload_sizeの計算:メンバー変数phdrを使用するtableはすべてのprogram headerを遍歴し、すべてのタイプがPTであることを発見した.LOADのsegmentのp_vaddrの最小値、p_vaddr + p_memszの最大値は、それぞれmin_としてvaddrとmax_vaddrは、2つの値をそれぞれヘッダーと末尾に揃え、最終的には揃えたmax_を使用します.vaddr - min_vaddrはload_を得るsize.
2.2.4 Load Segmentsはプログラムheader tableを遍歴し、見つけたタイプはPT_である.LOADのsegment:メモリ空間におけるsegmentの開始アドレスsegstartと終了アドレスseg_を計算するend,seg_startは仮想オフセットとベースアドレスloadに等しいbias、同時にmmapの要求のため、すべてページの境界に位置合わせしてseg_を得るpage_startとseg_page_end.ファイル内のsegmentのページ整列後の開始アドレスfile_を計算するpage_startと長さfile_length.mmapを使用してsegmentをメモリにマッピングし、マッピングアドレスをseg_として指定します.page_start、長さfile_length、ファイルオフセットfile_page_start.
2.3 soinfoload_の割り当てlibraryはload_を呼び出していますsegmentsがマウントを完了したら、soinfo_を呼び出します.alloc関数はターゲットSOにsoinfo,soinfo_を割り当てるalloc関数は次のように実現されます.
LinkerはSOごとにsoinfo構造を維持し、dlopenを呼び出すと、返されるハンドルはSOを指すsoinfoポインタである.soinfoは、SOロードリンクおよび実行中に必要な各種情報を保存しています.簡単に列挙します.ロードリンク中に主に使用されるメンバー:マウント情報 const ElfW(Phdr)* phdr;size_t phnum;ElfW(Addr) base;size_t size; シンボル情報 const char* strtab;ElfW(Sym)* symtab; 再配置情報 ElfW(Rel)* plt_rel;size_t plt_rel_count;ElfW(Rel)* rel;size_t rel_count; init関数とfinit関数 Linker_function_t* init_array;size_t init_array_count;Linker_function_t* fini_array;size_t fini_array_count;Linker_function_t init_func;Linker_function_t fini_func;実行中に主に使用されるメンバー:書き出しシンボル検索(dlsym): const char* strtab;ElfW(Sym)* symtab;size_t nbucket;size_t nchain;unsigned* bucket;unsigned* chain;ElfW(Addr) load_bias; 異常処理: unsigned* ARM_exidx;size_t ARM_exidx_count;load_libraryはSOにsoinfoを割り当てると、マウント結果をsoinfoに更新し、後続のリンクプロセスはsoinfoの関連フィールドを直接使用してSOの情報にアクセスできます.
...si->base = elf_reader.load_start();si->size = elf_reader.load_size();si->load_bias = elf_reader.load_bias();si->phnum = elf_reader.phdr_count();si->phdr = elf_reader.loaded_phdr();...
(次編は引き続き更新します)
2.SOのマウントとリンク2.1全体の流れ説明2.1.1 do_dlopen呼び出しdl_Open後、真ん中をdlopen_経由ext,最初の主要関数do_に到達dlopen:
soinfo* do_dlopen(const char* name, int flags, const Android_dlextinfo* extinfo) {
protect_data(PROT_READ | PROT_WRITE);
soinfo* si = find_library(name, flags, extinfo); // SO
if (si != NULL) {
si->CallConstructors(); // SO init
}
protect_data(PROT_READ);
return si;
}
do_dlopenは2つの重要な関数を呼び出し、1つ目はfind_です.library、2番目はsoinfoのメンバー関数CallConstructors,find_library関数はSOマウントリンクの後続関数であり、SOのマウントリンクが完了した後、CallConstructorsを介してSOの初期化関数を呼び出す.
2.1.2 find_library_internalfind_libraryはfind_を直接呼び出しましたlibrary_internal、次はfind_を直接見ます.library_internal関数:
static soinfo* find_library_internal(const char* name, int dlflags, const Android_dlextinfo* extinfo) {
if (name == NULL) {
return somain;
}
soinfo* si = find_loaded_library_by_name(name); // SO
if (si == NULL) {
TRACE("[ '%s' has not been found by name. Trying harder...]", name);
si = load_library(name, dlflags, extinfo); // SO
}
if (si != NULL && (si->flags & FLAG_LINKED) == 0) {
DL_ERR("recursive link to \"%s\"", si->name);
return NULL;
}
return si;
}
find_library_internalはまずfind_を通りますloaded_library_by_name関数は、ターゲットSOがロードされているか否かを判断し、ロードされている場合は対応するsoinfoポインタを直接返し、ロードされていない場合はload_を呼び出すlibraryはロードプロセスを継続し、load_を参照してください.library関数.
2.13 load_library
static soinfo* load_library(const char* name, int dlflags, const Android_dlextinfo* extinfo) {
int fd = -1;
...
// Open the file.
fd = open_library(name); // SO , fd
ElfReader elf_reader(name, fd); // ElfReader
...
// Read the ELF header and load the segments.
if (!elf_reader.Load(extinfo)) { // ElfReader Load , SO
return NULL;
}
soinfo* si = soinfo_alloc(SEARCH_NAME(name), &file_stat); // SO soinfo
if (si == NULL) {
return NULL;
}
si->base = elf_reader.load_start(); // , soinfo
si->size = elf_reader.load_size();
si->load_bias = elf_reader.load_bias();
si->phnum = elf_reader.phdr_count();
si->phdr = elf_reader.loaded_phdr();
...
if (!soinfo_link_image(si, extinfo)) { // soinfo_link_image SO
soinfo_free(si);
return NULL;
}
return si;
}
load_library関数はSOマウントリンクの全体の流れを示しており、主に3つのステップがある:1マウント:ElfReaderオブジェクトを作成し、ElfReaderオブジェクトのLoadメソッドによってSOファイルをメモリ2にマウントしてsoinfoを割り当てる:soinfoを呼び出すalloc関数はSOに新しいsoinfo構造を割り当て、ロード結果に従って対応するメンバー変数3リンクを更新します.soinfo_を呼び出します.link_イメージ完了SOのリンク先の解析で、load_library関数にはSOマウントリンクの主なプロセスが含まれており、後述するElfReaderクラスとsoinfo_を分析することによってlink_image関数では、SOのマウントとリンクの手順をそれぞれ説明します.
2.2 load_にマウントlibraryでは、まずelf_を初期化します.readerオブジェクト、最初のパラメータはSOの名前、2番目のパラメータはファイル記述子fd:ElfReader elf_reader(name,fd)の後にElfReaderのloadメソッドを呼び出してSOをロードする.
...
// Read the ELF header and load the segments.
if (!elf_reader.Load(extinfo)) {
return NULL;
}
...
ElfReader::Loadメソッドは次のとおりです.
bool ElfReader::Load(const Android_dlextinfo* extinfo) {
return ReadElfHeader() && // elf header
VerifyElfHeader() && // elf header
ReadProgramHeader() && // program header
ReserveAddressSpace(extinfo) &&//
LoadSegments() && // program header segments
FindPhdr(); // phdr
}
ElfReader::LoadメソッドはまずSOのelfヘッダを読み出し、elfヘッダを検証した後、programヘッダを読み出し、programヘッダに基づいてSOに必要なメモリサイズを計算して対応する空間を割り当て、次にSOをsegment単位でメモリにロードし、最後にメモリにロードされたSOの中でprogramヘッダを見つけ、その後のリンクプロセスで使用しやすい.以下、ElfReaderのメンバー関数について詳しく説明します.
2.2.1 read&verify elfheader
bool ElfReader::ReadElfHeader() {
ssize_t rc = read(fd_, &header_, sizeof(header_));
if (rc != sizeof(header_)) {
return false;
}
return true;
}
ReadElfHeader readを使用して直接SOファイルからelfheaderをheaderに読み込み、header_ElfReaderのメンバー変数、タイプはElf 32_Ehdrは、headerによってelf headerの各フィールドに容易にアクセスでき、elf headerにはprogram header table、section header tableなどの重要な情報が含まれています.elf headerの検証には、magicバイト32/64 bitが現在のプラットフォームと一致するかどうかのサイズエンドタイプ:実行可能ファイル、SO...バージョン:一般的に1であり、現在のバージョンプラットフォーム:ARM、x 86、amd 64...エラーが発生した場合、ロードに失敗することを示します.
2.2.2 Read ProgramHeader
bool ElfReader::ReadProgramHeader() {
phdr_num_ = header_.e_phnum; // program header
// mmap
ElfW(Addr) page_min = PAGE_START(header_.e_phoff);
ElfW(Addr) page_max = PAGE_END(header_.e_phoff + (phdr_num_ * sizeof(ElfW(Phdr))));
ElfW(Addr) page_offset = PAGE_OFFSET(header_.e_phoff);
phdr_size_ = page_max - page_min;
// mmap program header
void* mmap_result = mmap(NULL, phdr_size_, PROT_READ, MAP_PRIVATE, fd_, page_min);
phdr_mmap_ = mmap_result;
// ElfReader phdr_table_ program header table
phdr_table_ = reinterpret_cast(reinterpret_cast(mmap_result) + page_offset);
return true;
}
プログラムヘッダをメモリに1部ずつマッピングし、プログラムヘッダを解析する際に一時的に使用します.SOをメモリにマウントすると、このメモリが解放され、マウントされたSOのプログラムヘッダが使用されます.
2.2.3 reserve space&計算load size
bool ElfReader::ReserveAddressSpace(const Android_dlextinfo* extinfo) {
ElfW(Addr) min_vaddr;
// SO
load_size_ = phdr_table_get_load_size(phdr_table_, phdr_num_, &min_vaddr);
// min_vaddr , SO
uint8_t* addr = reinterpret_cast(min_vaddr);
void* start;
int mmap_flags = MAP_PRIVATE | MAP_ANONYMOUS;
start = mmap(addr, load_size_, PROT_NONE, mmap_flags, -1, 0);
load_start_ = start;
load_bias_ = reinterpret_cast(start) - addr;
return true;
}
まずphdr_を呼び出すtable_get_load_size関数はSOのメモリに必要な空間load_を取得するsizeは、mmap匿名マッピングを使用して、対応する空間を予約します.
loadbiasについて:SOはロード・ベース・アドレスを指定できますが、SOが指定したロード・ベース・アドレスはページ整列ではない可能性があります.この場合、実際のマッピング・アドレスと指定したロード・アドレスにずれが発生します.この偏差はload_です.bias_,その後、仮想アドレスの計算にload_を使用する必要があります.bias_修正.通常のSOではロードベースアドレスが指定されていませんが、min_vaddr=0の場合load_bias_ = load_start_,すなわちload_bias_ベースアドレスのロードと等しく、load_bias_直接ベースアドレスと呼ぶ.次はphdr_に進みますtable_get_load_size分析してload_sizeの計算:メンバー変数phdrを使用するtableはすべてのprogram headerを遍歴し、すべてのタイプがPTであることを発見した.LOADのsegmentのp_vaddrの最小値、p_vaddr + p_memszの最大値は、それぞれmin_としてvaddrとmax_vaddrは、2つの値をそれぞれヘッダーと末尾に揃え、最終的には揃えたmax_を使用します.vaddr - min_vaddrはload_を得るsize.
size_t phdr_table_get_load_size(const ElfW(Phdr)* phdr_table, size_t phdr_count,
ElfW(Addr)* out_min_vaddr,
ElfW(Addr)* out_max_vaddr) {
ElfW(Addr) min_vaddr = UINTPTR_MAX;
ElfW(Addr) max_vaddr = 0;
bool found_pt_load = false;
for (size_t i = 0; i < phdr_count; ++i) {
const ElfW(Phdr)* phdr = &phdr_table[i];
if (phdr->p_type != PT_LOAD) {
continue;
}
found_pt_load = true;
if (phdr->p_vaddr < min_vaddr) {
min_vaddr = phdr->p_vaddr; //
}
if (phdr->p_vaddr + phdr->p_memsz > max_vaddr) {
max_vaddr = phdr->p_vaddr + phdr->p_memsz; //
}
}
if (!found_pt_load) {
min_vaddr = 0;
}
min_vaddr = PAGE_START(min_vaddr); //
max_vaddr = PAGE_END(max_vaddr); //
if (out_min_vaddr != NULL) {
*out_min_vaddr = min_vaddr;
}
if (out_max_vaddr != NULL) {
*out_max_vaddr = max_vaddr;
}
return max_vaddr - min_vaddr; // load_size = max_vaddr - min_vaddr
}
2.2.4 Load Segmentsはプログラムheader tableを遍歴し、見つけたタイプはPT_である.LOADのsegment:メモリ空間におけるsegmentの開始アドレスsegstartと終了アドレスseg_を計算するend,seg_startは仮想オフセットとベースアドレスloadに等しいbias、同時にmmapの要求のため、すべてページの境界に位置合わせしてseg_を得るpage_startとseg_page_end.ファイル内のsegmentのページ整列後の開始アドレスfile_を計算するpage_startと長さfile_length.mmapを使用してsegmentをメモリにマッピングし、マッピングアドレスをseg_として指定します.page_start、長さfile_length、ファイルオフセットfile_page_start.
bool ElfReader::LoadSegments() {
for (size_t i = 0; i < phdr_num_; ++i) {
const ElfW(Phdr)* phdr = &phdr_table_[i];
if (phdr->p_type != PT_LOAD) {
continue;
}
// Segment .
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;
//
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_length != 0) {
// segment
void* seg_addr = mmap(reinterpret_cast(seg_page_start),
file_length,
PFLAGS_TO_PROT(phdr->p_flags),
MAP_FIXED|MAP_PRIVATE,
fd_,
file_page_start);
}
// segment , , segemnt end 。
if ((phdr->p_flags & PF_W) != 0 && PAGE_OFFSET(seg_file_end) > 0) {
memset(reinterpret_cast(seg_file_end), 0, PAGE_SIZE - PAGE_OFFSET(seg_file_end));
}
seg_file_end = PAGE_END(seg_file_end);
// ( - )
if (seg_page_end > seg_file_end) {
void* zeromap = mmap(reinterpret_cast(seg_file_end),
seg_page_end - seg_file_end,
PFLAGS_TO_PROT(phdr->p_flags),
MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE,
-1,
0);
}
}
return true;
}
2.3 soinfoload_の割り当てlibraryはload_を呼び出していますsegmentsがマウントを完了したら、soinfo_を呼び出します.alloc関数はターゲットSOにsoinfo,soinfo_を割り当てるalloc関数は次のように実現されます.
static soinfo* soinfo_alloc(const char* name, struct stat* file_stat) {
soinfo* si = g_soinfo_allocator.alloc(); // , malloc
// Initialize the new element.
memset(si, 0, sizeof(soinfo));
strlcpy(si->name, name, sizeof(si->name));
si->flags = FLAG_NEW_SOINFO;
sonext->next = si; // soinfo
sonext = si;
return si;
}
LinkerはSOごとにsoinfo構造を維持し、dlopenを呼び出すと、返されるハンドルはSOを指すsoinfoポインタである.soinfoは、SOロードリンクおよび実行中に必要な各種情報を保存しています.簡単に列挙します.ロードリンク中に主に使用されるメンバー:
...si->base = elf_reader.load_start();si->size = elf_reader.load_size();si->load_bias = elf_reader.load_bias();si->phnum = elf_reader.phdr_count();si->phdr = elf_reader.loaded_phdr();...
(次編は引き続き更新します)
( )