動態so注入


動態so注入

https://jmpews.github.io/2016/12/27/pwn/linux%E8%BF%9B%E7%A8%8B%E5%8A%A8%E6%80%81so%E6%B3%A8%E5%85%A5/

在學習 hook 過程中, 有一個種方法是 PLT 注入, PLT 注入前的必要工作是需惡意的 so 注入, 找了很多關於注入的資料發現絕大部分實現都已經不適用, 幾個方面因素, 一部分是因為 ELF 文件結構變化, 一部分是因為 glibc 調用的函數改變, 另外有很多是由於注入位置不對導致不夠通用, 下面會詳細介紹幾種情況.

so 注入是對學習 ELF 結構極好的實踐.

本文中大部分 refs 和 一起文檔參考都在倉庫 pwn2exploit.

參考鏈接

ELF基礎

#包含新的.gnu.hash, 新的hash算法以及新的符號地址計算方法
ELF文件知識
#大部分ELF相關參考文檔都在 `refs/elf` 有包含, 比較多所以這里不重述, 只提幾個極為重要的參考文檔
#ELF標准文檔, 不包含 `.gnu.hash` 相關知識
https://refspecs.linuxbase.org/elf/elf.pdf
#詳細解釋.gnu.hash 相關知識
https://blogs.oracle.com/ali/entry/gnu_hash_elf_sections
#__kernel_vsyscall 介紹(這個在后面的代碼注入覆蓋了系統調用, 會有一個坑)
http://www.trilithium.com/johan/2005/08/linux-gate/
#內核 hash 和 bucket 相關概念
http://www.nowamagic.net/academy/detail/3008086

GDB/*技巧

#查看哪里觸發的 __kernel_vsyscall 系統函數調用(其中的2個地址為 __kernel_vsyscall 區間)
watch ($eip > 0xb770c418) && ($eip < 0xb770c42b)
#必備插件
peda
#查看系統arch相關常量
echo | gcc -E -dM - | grep 64
#查看ld詳細信息
ld --verbose

注入實例

#舊注入實例, 但是具有參考價值.
http://phrack.org/issues/59/8.html#article(國內很多文章都是參考這篇)
http://www.cnblogs.com/LittleHann/p/4594641.html(針對phrack的翻譯)
http://grip2.blogspot.jp/2006/12/blog-post.html(針對phrack的翻譯)
 
#可用注入實例, 但注入方法有限制, 並且注入位置不對只能在特定情況進行注入
https://github.com/gaffe23/linux-inject
 
#沒有采用代碼注入, 采用的是修改寄存器的eip到指定函數地址, 動態加載函數不再適用
http://www.xfocus.net/articles/200208/438.html

注入理論

其實本質就一句話, “調用dlopen加載外部so文件”, 或者說就一句代碼 dlopen("evil.so",RTLD_LAZY);. 但是顯然不可能對正在執行的程序執行 dlopen 操作, 所以:

問題1:如何能夠接觸到正在執行進程的內存空間.

通過 ptraceptrace 可以讓目標 pid 進程成為當前進程的子進程, 進而可以訪問目標進程的內存空間, 寄存器, 並且可以向目標內存空間寫內容. 需要了解 ptrace 函數的幾個關鍵宏.

參考鏈接 https://linux.die.net/man/2/ptrace
PTRACE_ATTACH 掛載目標pid
PTRACE_CONT 讓子程序繼續運行
PTRACE_PEEKTEXT 讀取內容
PTRACE_POKETEXT 寫入內容

ok, 現在既然已經可以讀寫目標進程的內存空間了, 下一步已經就是在目標內存空間調用 dlopen.既然要調用 dlopen, 肯定需要 dlopen 函數符號的地址. 但是默認 libc-2.19.so 是不包含 dlopen, 只有 __libc_dlopen_mode.

➜ elf readelf --dyn-syms /lib/i386-linux-gnu/libdl.so.2 | grep dlopen
29: 00000d30 101 FUNC GLOBAL DEFAULT 13 dlopen@@GLIBC_2.1
30: 00001900 108 FUNC GLOBAL DEFAULT 13 dlopen@GLIBC_2.0
➜ elf readelf --dyn-syms /lib/i386-linux-gnu/libc-2.19.so | grep dlopen
2294: 00123ae0 91 FUNC GLOBAL DEFAULT 12 __libc_dlopen_mode@@GLIBC_PRIVATE

這兩個函數實現的是一樣的效果, 其最終都是調用的 _dl_open, 可以通過 glic 源碼查看相關調用過程, 因此可以通過 void * __libc_dlopen_mode (const char *name, int mode) 加載外部so. 所以現在現在的問題是:

問題2: __libc_dlopen_mode 的內存地址是多少(如何查找)

有一種方法是, 通過查看 cat /proc/1234/maps 的加載地址, 加上函數符號在文件中的偏移來得到, 這里並不打算采用這種方法, 而是通過解析 ELF 文件結構得到 __libc_dlopen_mode 函數符號的地址. (這里需要比較多的 ELF 的文件結構的知識, 可以參考前面的\<elf文件知識\>)

ok, 先介紹幾個關於 ELF 的結構體, 這些結構體都在 eglibc-2.19/elf/elf.h 有相應的定義, 實在是不想貼所有結構體的定義, 但是如果不貼又對整個不太好理解.

//eglibc-2.19/elf/link.h
 
/* Structure describing a loaded shared object. The `l_next' and `l_prev'
members form a chain of all the shared objects loaded at startup.
 
These data structures exist in space used by the run-time dynamic linker;
modifying them may have disastrous results. */
 
struct link_map
{
/* These first few members are part of the protocol with the debugger.
This is the same format used in SVR4. */
//共享庫加載地址
ElfW(Addr) l_addr; /* Difference between the address in the ELF
file and the addresses in memory. */
//共享庫名稱, 絕對路徑
char *l_name; /* Absolute file name object was found in. */
//動態鏈接section的地址
ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */
struct link_map *l_next, *l_prev; /* Chain of loaded objects. */
}

link_map 的作用就是記錄程序加載的所有共享庫的鏈表, 當需要查找符號時就需要遍歷該鏈表找到對應的共享庫.

//http://www.eglibc.org/cgi-bin/viewvc.cgi/branches/eglibc-2_19/libc/elf/elf.h?view=markup
//eglibc-2.19/elf/elf.h
 
ignore...

Elf32_Ehdr 是 ELF 頭, 注入需要使用 Elf32_Ehdr->e_phoff 取得正在執行進程的 Program header table.

這里需要注意的ELF 文件是按照 Segment 加載到內存中, 只會加載 $ readelf -l test 中的對應 section, 而比如 section header table 是不會被加載的, 通過查看 section header table 並不在 $ readelf -l test 內存映射的區間之內驗證, 所以是不能通過 section header table 對執行中的進程進行解析.

Elf32_Phdr 是 ELF 的 Segment 對應結構, Segment 是相似屬性的 section 集合, 僅是概念性的划分. 關於 segment 的內存頁對齊等細節, 這里不進行詳細介紹, 如有興趣請參考 \<程序員自我修養>. 注入需要使用 Elf32_Phdr->p_type 和 Elf32_Phdr->p_vaddr, 判斷並取得 Dynamic Segment.

Elf32_Dyn 是有關動態鏈接的 section 結構, 屬於 Dynamic Segmentso 注入需要根據它取得所需要的 section.

Elf32_Sym 是符號表的結構體, 需要根據 Elf32_Sym->st_name 拿到該符號在 .dynstr 對應的位置.

ok, 到目前為止幾個我們需要使用的結構體都大致簡單介紹了下. 下面開始具體的符號的地址查找過程.

因為 __libc_dlopen_mode 是在 libc.so.6 動態庫中, 所以需要先找到 libc.so.6, 在介紹 link_map 時說過, 它記錄目標進程加載的所有的動態庫, 所以只要遍歷 link_map 就可以找到 libc.so.6. 所以現在的問題是:

link_map 位於 .got.plt 表的第 2 位置(請查看關於 ELF 文件結構的知識), 而 .got.plt 表的地址位於 Elf32_Dyn->d_tag == DT_PLTGOT 的 Elf32_Dyn 中, ·而 Dynamic Segment 的地址位於 Elf32_Phdr->p_type == PT_DYNAMIC 的 Elf32_Phdr 中, 而 Program header table 的地址位於 Elf32_Ehdr->e_phoff, 這樣就可以逆轉整個過程, 以取得 link_map 的地址.

 
struct link_map *
locate_linkmap(int pid)
{
ElfW(Ehdr) ehdr;
ElfW(Phdr) phdr;
ElfW(Dyn) dyn;
struct link_map *l = malloc(sizeof(struct link_map));
ElfW(Addr) phdr_addr , dyn_addr , map_addr, gotplt_addr, text_addr;
 
ptrace_read(pid, PROGRAM_LOAD_ADDRESS, &ehdr , sizeof(ElfW(Ehdr)));
 
phdr_addr = PROGRAM_LOAD_ADDRESS + ehdr.e_phoff;
 
ptrace_read(pid , phdr_addr, &phdr , sizeof(ElfW(Phdr)));
 
while ( phdr.p_type != PT_DYNAMIC ) {
ptrace_read(pid, phdr_addr += sizeof(ElfW(Phdr)), &phdr, sizeof(ElfW(Phdr)));
}
 
/* now go through dynamic section until we find address of GOT.PLT */
ptrace_read(pid, phdr.p_vaddr, &dyn, sizeof(ElfW(Dyn)));
 
dyn_addr = phdr.p_vaddr;
 
while ( dyn.d_tag != DT_PLTGOT ) {
ptrace_read(pid, dyn_addr += sizeof(ElfW(Dyn)), &dyn, sizeof(ElfW(Dyn)));
}
 
/* link_map address, .got.plt address */
gotplt_addr = dyn.d_un.d_ptr;
 
/* now just read first link_map item and return it */
ptrace_read(pid, gotplt_addr + sizeof(ElfW(Addr)), &map_addr , sizeof(ElfW(Addr)));
ptrace_read(pid , map_addr, l , sizeof(struct link_map));
 
return l;
}

ok, 現在我們已經找到對應的動態庫的 link_map, 現在我們就需要從 link_map, 找到 __libc_dlopen_mode 函數符號的地址, 所以現在的問題是:

當然可以通過 $ readelf --dyn-syms /lib/i386-linux-gnu/libc-2.19.so | grep __libc_dlopen_mode 找到函數符號在文件中的偏移加上動態庫的加載地址, 就可以得到該符號的在內存中的地址. 這種方式並不通用, 比如: so 文件丟失, 不存在 readelf 命令.

ok, 那么現在的方法就是遍歷 .dynsym 表, 查找符號名稱為 __libc_dlopen_mode 的 Elf32_Sym(其實是查找Elf32_Sym->st_name 在 .dynstr 中的索引對應的字符串). ** 然而 .dynsym 的長度是多少, 或者說遍歷停止的條件? **.如果要得到 .dynsym 的長度, 也只能讀 so 文件, 根據一些沒有加載到內存中的但是存在於文件中的內容解析, 比如: 根據 section header table 找到 .dynsym 的 Elf32_Shdr 結構, Elf32_Shdr->sh_size /Elf32_Shdr->sh_entsize 即為符號表的長度. 所以這種方法也不可行.

ok, 另一種方法就是根據 glibc 中的方法進行查找符號地址, 這里需要用 .gnu.hash 表, 同時在 glibc 中 利用 _dl_lookup_symbol_x -> do_lookup_x 進行函數符號的地址查找, 關於 .gnu.hash 的詳細介紹, 請參考 https://blogs.oracle.com/ali/entry/gnu_hash_elf_sections, 這里簡單提一下, ELF 在加載動態庫時並不是直接全部解析出所有符號的地址, 而是通過 PLT 延遲加載的方式, 在執行的過程中通過查找符號的地址, 這需要利用 .gnu.hash 進行 hash 算法快速查找. 建議先閱讀參考文檔中關於 .gnu.hash 的介紹, 以及了解關於如何利用 hash桶 的方法解決 hash 沖突.

下面是 glibc 在進行符號查找一段核心代碼, 位於 eglibc-2.19/elf/dl-lookup.c 中的 do_lookup_x 函數中.

//http://www.eglibc.org/cgi-bin/viewvc.cgi/branches/eglibc-2_19/libc/elf/dl-lookup.c?view=markup
//eglibc-2.19/elf/dl-lookup.c
 
const ElfW(Sym) *sym;
const ElfW(Addr) *bitmask = map->l_gnu_bitmask;
if (__builtin_expect (bitmask != NULL, 1))
{
ElfW(Addr) bitmask_word
= bitmask[(new_hash / __ELF_NATIVE_CLASS)
& map->l_gnu_bitmask_idxbits];
 
unsigned int hashbit1 = new_hash & (__ELF_NATIVE_CLASS - 1);
unsigned int hashbit2 = ((new_hash >> map->l_gnu_shift)
& (__ELF_NATIVE_CLASS - 1));
 
if (__builtin_expect ((bitmask_word >> hashbit1)
& (bitmask_word >> hashbit2) & 1, 0))
{
Elf32_Word bucket = map->l_gnu_buckets[new_hash
% map->l_nbuckets];
if (bucket != 0)
{
const Elf32_Word *hasharr = &map->l_gnu_chain_zero[bucket];
 
do
if (((*hasharr ^ new_hash) >> 1) == 0)
{
symidx = hasharr - map->l_gnu_chain_zero;
sym = check_match (&symtab[symidx]);
if (sym != NULL)
goto found_it;
}
while ((*hasharr++ & 1u) == 0);
}
}
/* No symbol found. */
symidx = SHN_UNDEF;
}

這里有點坑, 就是此時 link_map 結構並非 #include <link.h> 中的結構, 在 eglibc-2.19/include/link.h 中有這么一段代碼:

#define link_map link_map_public
#define la_objopen la_objopen_wrongproto
#include <elf/link.h>
#undef link_map
#undef la_objope

所以需要我們自己去構造這些結構體變量的成員, 構造的方法可以參考 eglibc-2.19/elf/dl-lookup.c 中 _dl_setup_hash 函數. 這里附上一段代碼作為參考

 
void
internal_function
_dl_setup_hash (struct link_map *map)
{
Elf_Symndx *hash;
 
if (__builtin_expect (map->l_info[DT_ADDRTAGIDX (DT_GNU_HASH) + DT_NUM
+ DT_THISPROCNUM + DT_VERSIONTAGNUM
+ DT_EXTRANUM + DT_VALNUM] != NULL, 1))
{
Elf32_Word *hash32
= (void *) D_PTR (map, l_info[DT_ADDRTAGIDX (DT_GNU_HASH) + DT_NUM
+ DT_THISPROCNUM + DT_VERSIONTAGNUM
+ DT_EXTRANUM + DT_VALNUM]);
map->l_nbuckets = *hash32++;
Elf32_Word symbias = *hash32++;
Elf32_Word bitmask_nwords = *hash32++;
/* Must be a power of two. */
/* Important!!! */
assert ((bitmask_nwords & (bitmask_nwords - 1)) == 0);
map->l_gnu_bitmask_idxbits = bitmask_nwords - 1;
map->l_gnu_shift = *hash32++;
 
map->l_gnu_bitmask = (ElfW(Addr) *) hash32;
hash32 += __ELF_NATIVE_CLASS / 32 * bitmask_nwords;
 
map->l_gnu_buckets = hash32;
hash32 += map->l_nbuckets;
map->l_gnu_chain_zero = hash32 - symbias;
return;
}
 
if (!map->l_info[DT_HASH])
return;
hash = (void *) D_PTR (map, l_info[DT_HASH]);
 
map->l_nbuckets = *hash++;
/* Skip nchain. */
hash++;
map->l_buckets = hash;
hash += map->l_nbuckets;
map->l_chain = hash;
}

這里算法不進行具體的解釋, 可以參考下文的對應的實現, 這題提一下本來是參照 https://blogs.oracle.com/ali/entry/gnu_hash_elf_sections 實現的符號查找算法, 但總感覺不太標准, 就又按照 glibc 實現了一遍, 本質大同小異, 會在代碼中做一些對比說明.

這里另外提兩點關於 glibc 中代碼實現風格的, 1. 在 glibc 實現關於符號的符號查找的 hash 算法的過程中大量利用了 移位代替取模, 比如上面的 bitmask_nwords Must be a power of two. 這點在下文的具體算法的實現中會有體現. 2. 在 glibc 中大量使用宏來處理不同處理器的兼容性問題, 比如 ElfW(Addr)ElfW 的定義是:

// #include <bits/elfclass.h>
#define __ELF_NATIVE_CLASS 32
 
// #include <link.h>
#include <bits/elfclass.h>
#define ElfW(type) _ElfW (Elf, __ELF_NATIVE_CLASS, type)
#define _ElfW(e,w,t) _ElfW_1 (e, w, _##t)
#define _ElfW_1(e,w,t) e##w##t

__ELF_NATIVE_CLASS 定義當前機子的字長, 這樣 ElfW(Addr) 就被解析為 Elf32_Addr 或者 Elf64_Addr, 最后根據 #include <elf.h> 定義的類型來做處理.

ok, 先放出來關於 setup_hash 的實現, 作用就是在解析 Dynamic Segment 的過程中順便完善 link_map 結構, 方便下文的 hash 算法的使用.

void
setup_hash(int pid, struct link_map *map, struct link_map_more *map_more) {
Elf32_Word *gnu_hash_header = (Elf32_Word *)malloc(sizeof(Elf32_Word) * 4);
ptrace_read(pid, map_more->gnuhash_addr, gnu_hash_header, sizeof(Elf32_Word) * 4);
 
// .gnu.hash
map_more->nbuckets =gnu_hash_header[0];
map_more->symndx = gnu_hash_header[1];
map_more->nmaskwords = gnu_hash_header[2];
map_more->shift2 = gnu_hash_header[3];
map_more->bitmask_addr = map_more->gnuhash_addr + 4 * sizeof(Elf32_Word);
map_more->hash_buckets_addr = map_more->bitmask_addr + map_more->nmaskwords * sizeof(ElfW(Addr));
map_more->hash_values_addr = map_more->hash_buckets_addr + map_more->nbuckets * sizeof(Elf32_Word);
}

這里根據 .gnu.hash 的結構進行解析, 應該沒有什么問題.

ok, 再放出關於符號查找 hash 算法的具體, 這里整個的核心.

 
//eglibc-2.19/elf/dl-lookup.c
unsigned long
dl_new_hash (const char *s)
{
unsigned long h = 5381;
unsigned char c;
for (c = *s; c != '\0'; c = *++s)
h = h * 33 + c;
return h & 0xffffffff;
}
 
/* seach symbol name in elf(so) */
ElfW(Sym) *
symhash(int pid, struct link_map_more *map_more, const char *symname)
{
unsigned long c;
Elf32_Word new_hash, h2;
unsigned int hb1, hb2;
unsigned long n;
Elf_Symndx symndx;
ElfW(Addr) bitmask_word;
ElfW(Addr) addr;
ElfW(Addr) sym_addr;
ElfW(Addr) hash_addr;
char symstr[256];
ElfW(Sym) * sym = malloc(sizeof(ElfW(Sym)));
 
new_hash = dl_new_hash(symname);
 
/* new-hash % __ELF_NATIVE_CLASS */
hb1 = new_hash & (__ELF_NATIVE_CLASS - 1);
hb2 = (new_hash >> map_more->shift2) & (__ELF_NATIVE_CLASS - 1);
 
printf("[*] start gnu hash search:\n\tnew_hash: 0x%x(%u)\n", symname, new_hash, new_hash);
 
/* ELFCLASS size */
//__ELF_NATIVE_CLASS
 
/* nmaskwords must be power of 2, so that allows the modulo operation */
/* ((new_hash / __ELF_NATIVE_CLASS) % maskwords) */
n = (new_hash / __ELF_NATIVE_CLASS) & (map_more->nmaskwords - 1);
printf("\tn: %lu\n", n);
 
/* Use hash to quickly determine whether there is the symbol we need */
addr = map_more->bitmask_addr + n * sizeof(ElfW(Addr));
ptrace_read(pid, addr, &bitmask_word, sizeof(ElfW(Addr)));
/* eglibc-2.19/elf/dl-loopup.c:236 */
/* https://blogs.oracle.com/ali/entry/gnu_hash_elf_sections */
/* different method same result */
if(((bitmask_word >> hb1) & (bitmask_word >> hb2) & 1) == 0)
return NULL;
 
/* The first index of `.dynsym` to the bucket .dynsym */
addr = map_more->hash_buckets_addr + (new_hash % map_more->nbuckets) * sizeof(Elf_Symndx);
ptrace_read(pid, addr, &symndx, sizeof(Elf_Symndx));
printf("\thash buckets index: 0x%x(%u), first dynsym index: 0x%x(%u)\n", (new_hash % map_more->nbuckets), (new_hash % map_more->nbuckets), symndx, symndx);
 
if(symndx == 0)
return NULL;
 
sym_addr = map_more->dynsym_addr + symndx * sizeof(ElfW(Sym));
hash_addr = map_more->hash_values_addr + (symndx - map_more->symndx) * sizeof(Elf32_Word);
 
printf("[*] start bucket search:\n");
do
{
ptrace_read(pid, hash_addr, &h2, sizeof(Elf32_Word));
printf("\th2: 0x%x(%u)\n", h2, h2);
/* 1. hash value same */
if(((h2 ^ new_hash) >> 1) == 0) {
 
sym_addr = map_more->dynsym_addr + ((map_more->symndx + (hash_addr - map_more->hash_values_addr) / sizeof(Elf32_Word)) * sizeof(ElfW(Sym)));
/* read ElfW(Sym) */
ptrace_read(pid, sym_addr, sym, sizeof(ElfW(Sym)));
addr = map_more->dynstr_addr + sym->st_name;
/* read string */
ptrace_read(pid, addr, symstr, sizeof(symstr));
 
/* 2. name same */
if(!strcmp(symname, symstr))
return sym;
}
hash_addr += sizeof(sizeof(Elf32_Word));
} while((h2 & 1u) == 0); // search in same bucket
return NULL;
}

這里介紹下上面算法的流程

首先需要利用 bitmask 根據 Bloom Filter(布隆過濾器) 判斷是否存在於符號表, 這里先放出 wiki 的參考鏈接 Bloom Filter, 這里簡單介紹下 Bloom Filter, 它可以在常量時間內判斷 hash_value 是否存在於 hash_values_buckets, 但是它是有誤差的, 也就是說如果 Bloom Filter 判斷出不存在就是一定不在, 但是如果判斷存在則可能存在, 僅僅是可能存在, 原因就是因為 hash 沖突的存在, 具體參考下 Bloom Filter 的原理.

接下來就是根據該符號的 hash value, 確定該符號在哪一個 bucket, 找到該 bucket(桶) 內第一個符號結構, 之后便開始在桶內進行符號查找.

接下來就是桶內查找, 需要滿足兩個條件, 1. hash value 相同 2. 字符串相同.

剩下的大家可以通過閱讀來具體理解下, 大部分我都加了注釋.

ok, 到這一步, 就可以拿到 __libc_dlopen_mode 的地址了, 然后下一步的問題就是:

問題5: 如何調用 __libc_dlopen_mode?

大概有兩種思路, 單純依靠寄存器 eip 修改執行位置, 其他寄存器傳參數, 這個方法在之前 _dl_open 是被定義為 internal_function, 也就是通過寄存器傳參, 但是對於 __libc_dlopen_mode 已經不是寄存器傳遞參數. 另一個就是注入一段代碼, 執行這段代碼, 通過正常的函數調用方法調用 __libc_dlopen_mode, 執行完畢后恢復這段內存原始代碼, 這里主要分析下第二種方法.

既然采用注入代碼方法, 那么問題就來了, 代碼應該注入到哪里? 可能首先想到的就是注入到當前 %eip 的位置, 因為畢竟運行后會恢復為原始內存, 但是有一個問題就是, 假如被覆蓋的這塊內存在執行的過程中需要被二次使用怎么辦, 此時內存代碼已經不是原來的代碼, 並且還有沒有被恢復? 這就是上面說的這種注入方法存在局限性.

這里先給大家放一段目前普遍采用的注入方式, 也就是注入到當前 %eip 的位置, 並為大家復現這個情況.

這里采用手動注入一段代碼的方式, 用 gdb attach 該進程.

#include <stdio.h>
int main()
{
char *evilso = "/vagrant/inject/evil.so";
while(1)
{
printf("Going to sleep...\n");
sleep(3);
printf("Wake up\n");
}
return 0;
}

下面就是相關的分析過程,

# 當前gdb掛載點
gdb-peda$ disassemble
Dump of assembler code for function __kernel_vsyscall:
0xb7700418 <+0>: push ecx
0xb7700419 <+1>: push edx
0xb770041a <+2>: push ebp
0xb770041b <+3>: mov ebp,esp
0xb770041d <+5>: sysenter
0xb770041f <+7>: nop
0xb7700420 <+8>: nop
0xb7700421 <+9>: nop
0xb7700422 <+10>: nop
0xb7700423 <+11>: nop
0xb7700424 <+12>: nop
0xb7700425 <+13>: nop
0xb7700426 <+14>: int 0x80
=> 0xb7700428 <+16>: pop ebp
0xb7700429 <+17>: pop edx
0xb770042a <+18>: pop ecx
0xb770042b <+19>: ret
End of assembler dump.
 
gdb-peda$ disassemble main
Dump of assembler code for function main:
0x0804844d <+0>: push ebp
0x0804844e <+1>: mov ebp,esp
0x08048450 <+3>: and esp,0xfffffff0
0x08048453 <+6>: sub esp,0x20
0x08048456 <+9>: mov DWORD PTR [esp+0x1c],0x8048520
0x0804845e <+17>: mov DWORD PTR [esp],0x8048538
0x08048465 <+24>: call 0x8048320 <puts@plt>
0x0804846a <+29>: mov DWORD PTR [esp],0x3
0x08048471 <+36>: call 0x8048310 <sleep@plt>
0x08048476 <+41>: mov DWORD PTR [esp],0x804854a
0x0804847d <+48>: call 0x8048320 <puts@plt>
0x08048482 <+53>: jmp 0x804845e <main+17>
End of assembler dump.
 
#找到需要加載的so的路徑參數
gdb-peda$ x/s 0x8048520
0x8048520: "/vagrant/inject/evil.so"
 
#找到__libc_dlopen_mode函數符號的地址
gdb-peda$ x/i __libc_dlopen_mode
0xb7675ae0 <__libc_dlopen_mode>: push esi
 
#手動代碼注入后是下面的peda顯示, 注意 `ebx`, 當前 `eip` 的內容, 棧的內容的變化.
[----------------------------------registers-----------------------------------]
EAX: 0xfffffdfc
EBX: 0xb7675ae0 (<__libc_dlopen_mode>: push esi)
ECX: 0xbf81f9bc --> 0x2
EDX: 0xb76fd000 --> 0x1aada8
ESI: 0x0
EDI: 0xbf81fa44 --> 0x0
EBP: 0xbf81f9c4 --> 0x10000
ESP: 0xbf81f97c --> 0x8048520 ("/vagrant/inject/evil.so")
EIP: 0xb770b428 (<__kernel_vsyscall+16>: call ebx)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0xb770b424 <__kernel_vsyscall+12>: nop
0xb770b425 <__kernel_vsyscall+13>: nop
0xb770b426 <__kernel_vsyscall+14>: int 0x80
=> 0xb770b428 <__kernel_vsyscall+16>: call ebx
0xb770b42a <__kernel_vsyscall+18>: add BYTE PTR [eax],al
0xb770b42c: add BYTE PTR [esi],ch
0xb770b42e: jae 0xb770b498
0xb770b430: jae 0xb770b4a6
Guessed arguments:
arg[0]: 0x8048520 ("/vagrant/inject/evil.so")
arg[1]: 0x1
[------------------------------------stack-------------------------------------]
0000| 0xbf81f97c --> 0x8048520 ("/vagrant/inject/evil.so")
0004| 0xbf81f980 --> 0x1
0008| 0xbf81f984 --> 0xbf81f9bc --> 0x2
0012| 0xbf81f988 --> 0xb7607b70 (<nanosleep+32>: mov ebx,edx)
0016| 0xbf81f98c --> 0xb760793d (<sleep+205>: test eax,eax)
0020| 0xbf81f990 --> 0xbf81f9bc --> 0x2
0024| 0xbf81f994 --> 0xbf81f9bc --> 0x2
0028| 0xbf81f998 --> 0x0
[------------------------------------------------------------------------------]
 
#這里我們再加一個watchpoint, 需要監視哪個函數再一次調用了 __kernel_vsyscall, 這段技巧在上面也提到過.
watch ($eip > 0xb7700418) && ($eip < 0xb770042b)
 
#繼續執行 等待觸發watchpoint, 查看調用棧, 發現
gdb-peda$ bt
#0 0xb7735418 in __kernel_vsyscall ()
#1 0xb765efde in brk () from /lib/i386-linux-gnu/libc.so.6
#2 0xb765f084 in sbrk () from /lib/i386-linux-gnu/libc.so.6
#3 0xb75f4ccf in __default_morecore () from /lib/i386-linux-gnu/libc.so.6
#4 0xb75f1352 in ?? () from /lib/i386-linux-gnu/libc.so.6
#5 0xb75f2888 in malloc () from /lib/i386-linux-gnu/libc.so.6
#6 0xb75f295f in malloc () from /lib/i386-linux-gnu/libc.so.6
#7 0xb773b026 in local_strdup (s=s@entry=0x8048520 "/vagrant/inject/evil.so")
at dl-load.c:162
#8 0xb773d4d0 in expand_dynamic_string_token (l=l@entry=0xb7757c28,
s=s@entry=0x8048520 "/vagrant/inject/evil.so", is_path=is_path@entry=0x0)
at dl-load.c:429
#9 0xb773e0dc in _dl_map_object (loader=loader@entry=0xb7757c28,
name=name@entry=0x8048520 "/vagrant/inject/evil.so", type=type@entry=0x2,
trace_mode=trace_mode@entry=0x0, mode=mode@entry=0x10000001, nsid=0x0)
at dl-load.c:2538
#10 0xb7748c14 in dl_open_worker (a=0xbfc39228) at dl-open.c:235
#11 0xb7744c06 in _dl_catch_error (objname=objname@entry=0xbfc39220,
errstring=errstring@entry=0xbfc39224, mallocedp=mallocedp@entry=0xbfc3921f,
operate=operate@entry=0xb7748b50 <dl_open_worker>, args=args@entry=0xbfc39228)
at dl-error.c:187
#12 0xb7748644 in _dl_open (file=0x8048520 "/vagrant/inject/evil.so", mode=0x1,
caller_dlopen=0xb773542a <__kernel_vsyscall+18>, nsid=<optimized out>,
argc=0x1, argv=0xbfc396a4, env=0xbfc396ac) at dl-open.c:661
#13 0xb769f9ab in ?? () from /lib/i386-linux-gnu/libc.so.6
#14 0xb7744c06 in _dl_catch_error (objname=0xbfc39394, errstring=0xbfc39398,
mallocedp=0xbfc39393, operate=0xb769f950, args=0xbfc393cc) at dl-error.c:187
#15 0xb769fa9b in ?? () from /lib/i386-linux-gnu/libc.so.6
#16 0xb769fb21 in __libc_dlopen_mode () from /lib/i386-linux-gnu/libc.so.6
#17 0xb773542a in __kernel_vsyscall ()
#18 0xbfc3942c in ?? ()
#19 0x00000000 in ?? ()
 
# 繼續執行失敗

ok, 這也就不難發現, 為什么說現在的絕大部分注入都是有限制的, 當我們在系統中斷函數進行代碼注入, 導致在 __libc_dlopen_mode 執行過程中需要調用 malloc (eglibc-2.19/elf/dl-lookup.c 中 local_strdup)進行堆空間分配, 但由於此時沒有cache, 因而需要觸發系統調用使用 brk 分配一塊堆內存, 這一塊具體查看linux內存分配相關的文章, <程序員的自我修養> 簡單提到過一些關於 malloc 堆內存分配.

ok, 現在問題復現了, 需要采用其他方法進行注入, 這里采用的是將代碼注入到程序入口點位置, 然后修改 %eip 為程序入口點位置, 這樣就不會存在影響, 或者查找一塊連續的 nop 內存塊進行注入. 這里直接注入到程序入口點位置.

 
void
inject_code(int pid, char *evilso, ElfW(Addr) dlopen_addr) {
struct user_regs_struct regz, regzbak;
unsigned long len;
unsigned char *backup = NULL;
unsigned char *loader = NULL;
ElfW(Addr) entry_addr;
 
setaddr(soloader + 12, dlopen_addr);
 
entry_addr = locate_start(pid);
printf("[+] entry point: 0x%x\n", entry_addr);
 
len = sizeof(soloader) + strlen(evilso);
loader = malloc(sizeof(char) * len);
memcpy(loader, soloader, sizeof(soloader));
memcpy(loader+sizeof(soloader) - 1 , evilso, strlen(evilso));
 
backup = malloc(len + sizeof(ElfW(Word)));
ptrace_read(pid, entry_addr, backup, len);
 
if(ptrace(PTRACE_GETREGS , pid , NULL , &regz) < 0) exit(-1);
if(ptrace(PTRACE_GETREGS , pid , NULL , &regzbak) < 0) exit(-1);
printf("[+] stopped %d at eip:%p, esp:%p\n", pid, regz.eip, regz.esp);
 
/* `eip` points to the next instruction, so current instruction is `entry_addr` */
regz.eip = entry_addr + 2;
 
/* code inject */
ptrace_write(pid, entry_addr, loader, len);
 
/* set eip as entry_point */
ptrace(PTRACE_SETREGS , pid , NULL , &regz);
ptrace_cont(pid);
 
if(ptrace(PTRACE_GETREGS , pid , NULL , &regz) < 0) exit(-1);
printf("[+] inject code done %d at eip:%p\n", pid, regz.eip);
 
/* restore backup data */
// ptrace_write(pid,entry_addr, backup, len);
ptrace(PTRACE_SETREGS , pid , NULL , &regzbak);
}

至此所有問題得到解答, so 注入完成, 程序已經上傳到 github.

➜ inject gcc -w -o inject /vagrant/inject/inject.c /vagrant/inject/utils.c && sudo ./inject 24506 /vagrant/inject/evil.so
attached to pid 24506
[*] start search '__libc_dlopen_mode':
----------------------------------------------------------------
[+] libaray path: /lib/i386-linux-gnu/libc.so.6
[+] gnu.hash:
nbuckets: 0x3f3
symndx: 0xa
nmaskwords: 0x200
shift2: 0xe
bitmask_addr: 0xb75281c8
hash_buckets_addr: 0xb75289c8
bitmask_addr: 0xb75281c8 [0/1762]
hash_buckets_addr: 0xb75289c8
hash_values_addr: 0xb7529994
[+] dynstr: 0xb7535474
[+] dynysm: 0xb752bed4
[+] soname: libc.so.6
[*] start gnu hash search:
new_hash: 0x8049891(4073429154)
n: 197
hash buckets index: 0x3c6(966), first dynsym index: 0x8f5(2293)
[*] start bucket search:
h2: 0xd5e07632(3588257330)
h2: 0xf2cb98a2(4073429154)
----------------------------------------------------------------
[+] Found '__libc_dlopen_mode' at 0xb764bae0
[+] entry point: 0x8048350
[+] stopped 24506 at eip:0xb76e1428, esp:0xbfec2fec
[+] inject code done 24506 at eip:0x8048366
[*] start search 'evilfunc':
----------------------------------------------------------------
[+] libaray path: /vagrant/inject/evil.so
[+] gnu.hash:
nbuckets: 0x3
symndx: 0x7
nmaskwords: 0x2
shift2: 0x6
bitmask_addr: 0xb76db148
hash_buckets_addr: 0xb76db150
hash_values_addr: 0xb76db15c
[+] dynstr: 0xb76db244
[+] dynysm: 0xb76db174
[*] start gnu hash search:
new_hash: 0x80498ec(701380385)
n: 1
hash buckets index: 0x2(2), first dynsym index: 0xb(11)
[*] start bucket search:
h2: 0x29ce3720(701380384)
----------------------------------------------------------------
[+] Found 'evilfunc' at 0xb76db53b
[*] lib injection done!
 
#查看pid對應maps可以查看到已經加載了惡意的so
➜ inject cat /proc/24506/maps
08048000-08049000 r-xp 00000000 08:01 266876 /home/vagrant/pwn/elf/hello
08049000-0804a000 r--p 00000000 08:01 266876 /home/vagrant/pwn/elf/hello
0804a000-0804b000 rw-p 00001000 08:01 266876 /home/vagrant/pwn/elf/hello
084a2000-084c3000 rw-p 00000000 00:00 0 [heap]
b7527000-b7528000 rw-p 00000000 00:00 0
b7528000-b76d0000 r-xp 00000000 08:01 2134 /lib/i386-linux-gnu/libc-2.19.so
b76d0000-b76d1000 ---p 001a8000 08:01 2134 /lib/i386-linux-gnu/libc-2.19.so
b76d1000-b76d3000 r--p 001a8000 08:01 2134 /lib/i386-linux-gnu/libc-2.19.so
b76d3000-b76d4000 rw-p 001aa000 08:01 2134 /lib/i386-linux-gnu/libc-2.19.so
b76d4000-b76d7000 rw-p 00000000 00:00 0
b76db000-b76dc000 r-xp 00000000 00:1a 1974 /vagrant/inject/evil.so
b76dc000-b76dd000 r--p 00000000 00:1a 1974 /vagrant/inject/evil.so
b76dd000-b76de000 rw-p 00001000 00:1a 1974 /vagrant/inject/evil.so
b76de000-b76e1000 rw-p 00000000 00:00 0
b76e1000-b76e2000 r-xp 00000000 00:00 0 [vdso]
b76e2000-b7702000 r-xp 00000000 08:01 2153 /lib/i386-linux-gnu/ld-2.19.so
b7702000-b7703000 r--p 0001f000 08:01 2153 /lib/i386-linux-gnu/ld-2.19.so
b7703000-b7704000 rw-p 00020000 08:01 2153 /lib/i386-linux-gnu/ld-2.19.so
bfea3000-bfec4000 rw-p 00000000 00:00 0 [stack]
➜ inject

附錄

如何生成匯編對應16進制?

這里使用 nasm 工具.

先寫好匯編代碼

➜ cat __libc_dlopen_mode.asm
_start: jmp string
begin: pop eax ; char *file
mov edx, 0x1 ; int mode
push edx ;
push eax ;
mov ebx, 0x12345678 ; addr of __libc_dlopen_mode()
call ebx ; call __libc_dlopen_mode()
add esp, 0x8 ; resotre stack
int3 ; breakpoint
 
string: call begin
db "/tmp/ourlibby.so",0x00

之后使用 nasm -f elf32 -o __libc_dlopen_mode.o __libc_dlopen_mode.asm 即可生成目標文件, 之后使用 objdump -d __libc_dlopen_mode.o 即可查看匯編對應的16進制

➜ objdump -d __libc_dlopen_mode.o
 
__libc_dlopen_mode.o: file format elf32-i386
 
 
Disassembly of section .text:
 
00000000 <_start>:
0: eb 13 jmp 15 <string>
 
00000002 <begin>:
2: 58 pop %eax
3: ba 01 00 00 00 mov $0x1,%edx
8: 52 push %edx
9: 50 push %eax
a: bb 78 56 34 12 mov $0x12345678,%ebx
f: ff d3 call *%ebx
11: 83 c4 08 add $0x8,%esp
14: cc int3
 
00000015 <string>:
15: e8 e8 ff ff ff call 2 <begin>
1a: 2f das
1b: 74 6d je 8a <string+0x75>
1d: 70 2f jo 4e <string+0x39>
1f: 6f outsl %ds:(%esi),(%dx)
20: 75 72 jne 94 <string+0x7f>
22: 6c insb (%dx),%es:(%edi)
23: 69 62 62 79 2e 73 6f imul $0x6f732e79,0x62(%edx),%esp

 

========== End

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM