寫在前面
linux下的動態鏈接相關結構,重新回顧_dl_runtime_resolve的流程以及利用方法
動態鏈接相關結構
為了高效率的利用內存,多個進程可以共享代碼段、程序模塊化方便更新維護等,動態鏈接技術自然就出現了。不詳細介紹位置無關代碼和位置無關可執行程序這些基本知識,這里着重記錄一下ELF實現運行時重定位為了提高效率做的各種工作和用到的結構。動態鏈接的可執行文件裝載過程和靜態鏈接基本一樣,OS讀取可執行文件的頭部信息,檢查文件合法性后從Program Header中讀取每一個Segment的虛擬地址、文件地址和屬性,然后把他們映射到進程虛擬空間的相對位置。但是OS接下來不能把控制權交給可執行文件,因為動態鏈接中還有很多依賴於共享對象的無效地址,需要進一步處理。映射完成后,OS會啟動一個動態鏈接器(Dynamic Linker)——linux下就是ld.so。這個動態鏈接器實際上也是一個共享對象,OS也會通過映射的方式把它載入進程的地址空間中。然后OS就會把控制權交給DL的入口地址,它會進行一系列初始化操作,根據當前環境參數對可執行文件進行動態鏈接工作。完成之后再把控制權轉交給可執行文件。
pwn常見的so hell
動態鏈接器並非由系統配置或者環境參數決定,而是由ELF可執行文件自己決定!動態鏈接的ELF可執行文件中,有一個專門的段叫做.interp,這個段里就保存了一個字符串(可執行文件所需要的動態鏈接器的路徑)一般就是這個路徑
$ readelf -x .interp RNote3
Hex dump of section '.interp':
0x00000238 2f6c6962 36342f6c 642d6c69 6e75782d /lib64/ld-linux-
0x00000248 7838362d 36342e73 6f2e3200 x86-64.so.2.
比賽中遇到一個和系統ld不匹配的libc.so時,由於ELF中的動態鏈接器路徑指向系統默認的ld,然后就會出現修改LD_PRELOAD仍然無法加載指定libc的情況。一個做法是找到題目給的libc版本然后找一個匹配的ld,通過change_ld來加載指定libc。
def change_ld(binary, ld):
"""
Force to use assigned new ld.so by changing the binary
"""
if not os.access(ld, os.R_OK):
log.failure("Invalid path {} to ld".format(ld))
return None
if not isinstance(binary, ELF):
if not os.access(binary, os.R_OK):
log.failure("Invalid path {} to binary".format(binary))
return None
binary = ELF(binary)
for segment in binary.segments:
if segment.header['p_type'] == 'PT_INTERP':
size = segment.header['p_memsz']
addr = segment.header['p_paddr']
data = segment.data()
if size <= len(ld):
log.failure("Failed to change PT_INTERP from {} to {}".format(data, ld))
return None
binary.write(addr, ld.ljust(size, '\0'))
if not os.access('/tmp/pwn', os.F_OK): os.mkdir('/tmp/pwn')
path = '/tmp/pwn/{}_debug'.format(os.path.basename(binary.path))
if os.access(path, os.F_OK):
os.remove(path)
info("Removing exist file {}".format(path))
binary.save(path)
os.chmod(path, 0b111000000) #rwx------
success("PT_INTERP has changed from {} to {}. Using temp file {}".format(data, ld, path))
return ELF(path)
#example
elf = change_ld('./pwn', './ld.so')
p = elf.process(env={'LD_PRELOAD':'./libc.so.6'})
.dynamic段
ELF里專門用於動態鏈接的段還有幾個,首先是.dynamic。這個段里保存了動態鏈接器所需的基本信息,比如依賴於哪些共享對象,動態鏈接符號表的位置,動態鏈接重定位表的位置,共享對象初始化代碼的地址。.dynamic段的結構由一個類型變量加上一個附加的數值或者指針組成。
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
這里的d_tag表示這個表項的類型,可供取值范圍(參考程序員的自我修養p205)
這個section給動態鏈接器提供動態鏈接的各種信息入口,用readelf -d bin
可以看到詳細的關於此模塊的詳細動態鏈接信息。
$ readelf -d test
Dynamic section at offset 0xe18 contains 24 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x400468
0x000000000000000d (FINI) 0x4006a4
0x0000000000000019 (INIT_ARRAY) 0x600e08
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x600e10
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x400298
0x0000000000000005 (STRTAB) 0x400350
0x0000000000000006 (SYMTAB) 0x4002c0
0x000000000000000a (STRSZ) 95 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x601000
0x0000000000000002 (PLTRELSZ) 48 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x400438
0x0000000000000007 (RELA) 0x4003f0
0x0000000000000008 (RELASZ) 72 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x4003c0
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x4003b0
0x0000000000000000 (NULL) 0x0
重定位表(.rel.plt和.rel.dyn)
使用readelf -r bin
可以查看elf文件的重定位section.
$ readelf -r test
Relocation section '.rela.dyn' at offset 0x3f0 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000600fe8 000500000006 R_X86_64_GLOB_DAT 0000000000000000 printf@GLIBC_2.2.5 + 0
000000600ff0 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000600ff8 000400000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
Relocation section '.rela.plt' at offset 0x438 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 __stack_chk_fail@GLIBC_2.4 + 0
000000601020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 read@GLIBC_2.2.5 + 0
.rel.dyn 包含了需要重定位的變量的信息, 叫做變量重定位表
.rel.plt 包含了需要重定位的函數的信息, 叫做函數重定位表
32位和64位使用的重定位表有一點區別,都是結構體數組但是一般32位使用Rel,64位使用Rela.比如上面readelf解析出來的就是一個64位程序的重定位表。結構體長成下面這個樣子。
#define ELF32_R_SYM(i) ((i)>>8) // 獲得高24位,表示在符號表中的偏移 R_SYMBOL
#define ELF32_R_TYPE(i) ((unsigned char)(i)) //獲得低8位,表示重定位類型
#define ELF32_R_INFO(s,t) (((s)<<8)+(unsigned char)(t)) //通過R_SYM和Type重組info
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;
typedef struct elf32_rela{
Elf32_Addr r_offset;
Elf32_Word r_info;
Elf32_Sword r_addend;
} Elf32_Rela
typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
Elf64_Sxword r_addend; /* Addend */
} Elf64_Rela;
我們拿32位的舉例子,重定位表項結構體是Elf32_Rel類型,包含r_offset和r_info兩個信息,都是4個byte。r_info的高24位表示這個動態符號在動態鏈接符號表.dynsym中的位置。而r_info的低8位,表示這個待重定位對象的重定位類型。動態鏈接的重定向類型寫在下面了
/* i386 relocs. */
#define R_386_NONE 0 /* No reloc */
#define R_386_32 1 /* Direct 32 bit */
#define R_386_PC32 2 /* PC relative 32 bit */
#define R_386_GOT32 3 /* 32 bit GOT entry */
#define R_386_PLT32 4 /* 32 bit PLT address */
#define R_386_COPY 5 /* Copy symbol at runtime */
#define R_386_GLOB_DAT 6 /* Create GOT entry */
#define R_386_JMP_SLOT 7 /* Create PLT entry */
#define R_386_RELATIVE 8 /* Adjust by program base */
/* AMD x86-64 relocations. */
#define R_X86_64_NONE 0 /* No reloc */
#define R_X86_64_64 1 /* Direct 64 bit */
#define R_X86_64_PC32 2 /* PC relative 32 bit signed */
#define R_X86_64_GOT32 3 /* 32 bit GOT entry */
#define R_X86_64_PLT32 4 /* 32 bit PLT address */
#define R_X86_64_COPY 5 /* Copy symbol at runtime */
#define R_X86_64_GLOB_DAT 6 /* Create GOT entry */
#define R_X86_64_JUMP_SLOT 7 /* Create PLT entry */
#define R_X86_64_RELATIVE 8 /* Adjust by program base */
#define R_X86_64_GOTPCREL 9 /* 32 bit signed PC relative offset to GOT */
好像32位一般用來函數重定位就是R_386_JMP_SLOT,64位函數重定位R_X86_64_JUMP_SLOT類型,是看源碼的注釋也是Create PLT entry。轉而也能理解重定位表項里的r_offset的含義,r_offset為重定位對象的入口,用readelf做實驗可以發現對於函數重定位其實就是指向了.got.plt的對應項。后面會說.got.plt是全局偏移表中存儲重定位函數地址的地方。那么我們大概知道了,dl_runtime_resolve就是通過這個offset得知把解析出來的地址寫到哪里。
全局偏移表(.got和.got.plt)
GOT 表在 ELF 文件中分為兩個部分
- .got,存儲全局變量的引用。
- .got.plt,存儲函數的引用
在 Linux 的實現中,.got.plt 的前三項的具體的含義如下 - GOT[0],.dynamic 的地址。
- GOT[1],指向內部類型為 link_map 的指針,只會在動態裝載器中使用,包含了進行符號解析需要的當前 ELF 對象的信息。每個 link_map 都是一條雙向鏈表的一個節點,而這個鏈表保存了所有加載的 ELF 對象的信息。
- GOT[2],指向動態裝載器中 _dl_runtime_resolve 函數
之后的got表項存的是函數的真實地址(解析過后),解析前存的是對應plt表項中那段膠水代碼的第二條指令地址。整個解析過程就是在各自的plt傳遞reloc_arg, 在plt0傳遞link_map_obj.接着調用_dl_runtime_resolve.要注意的是,32位的reloc_arg和64位的有區別:32位使用reloc_offset, 64位使用reloc_index
#ifndef reloc_offset
#define reloc_offset reloc_arg
#define reloc_index reloc_arg / sizeof (PLTREL)
#endif
動態鏈接符號表(.dynsym)
是一個結構體數組,結構體為Elf32_Sym:
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility under glibc>=2.2 */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
#define ELF32_R_SYM(val) ((val) >> 8)
#define ELF32_R_TYPE(val) ((val) & 0xff)
#define ELF32_R_INFO(sym, type) (((sym) << 8) + ((type) & 0xff))
#define ELF64_R_SYM(i) ((i) >> 32)
#define ELF64_R_TYPE(i) ((i) & 0xffffffff)
#define ELF64_R_INFO(sym,type) ((((Elf64_Xword) (sym)) << 32) + (type))
我們主要關注動態符號中的兩個成員(注意32位和64位中這兩個值在結構體里的位置不一樣!)
- st_name, 該成員保存着動態符號在 .dynstr 表(動態字符串表)中的偏移。
- st_value,如果這個符號被導出,這個符號保存着對應的虛擬地址。
利用原理
動態鏈接下第一次調用glibc的函數需要通過plt表中的一段代碼解析函數的真實地址,這也是linux的lazy bind的特點。具體的解析方式就是_dl_runtime_resolve(link_map_obj, reloc_arg) ,如果我們可以控制整個解析過程中的參數,那么就能解析我們想要的函數地址。回顧一下整個流程:
- call printf@plt
- jmp *(printf@got) -> (第一次會jmp回來,之后就直接jmp到解析出來的地址了) -> push n -> jmp &plt[0] (跳到公共表項)
- push got[1] (link_map 可以理解為模塊ID) -> jmp *got[2] (跳轉到dl_runtime_resolve函數)
- 以上步驟相當於調用了dl_runtime_resolve(link_map_obj, reloc_arg)
- 解析完畢后會把解析出來的地址寫回reloc_arg定位到的.rel.plt表項中r_offset指向的位置(其實就是.got.plt的對應項)
- 弄懂dl_runtime_resolve的解析過程后,就可以通過偽造reloc_arg來解析出我們想要的libc函數地址並且寫回可控區域了
dl_runtime_resolve
- 通過link_map_obj訪問.dynamic section,分別取出.dynstr, .dynsym, .rel.plt的地址
- .rel.plt + reloc_index 求出當前函數重定位表項 Elf32_Rel的指針,記為rel
- rel->r_info的高24位作為.dynsym的下標,求出Elf32_Sym的指針,記作sym
- .dynstr + sym->st_name得到符號名字符串
- 在動態鏈接庫查找這個函數的地址,並且把找到的地址賦值給rel->r_offset,即.got.plt
- 最后調用這個函數
32位情況下構造payload
構造payload
- 思路:偽造reloc_arg,使得函數重定位表項落在可控內存段,就可以偽造r_offset和r_info讓動態鏈接符號表表項落在可控區域,接着偽造st_name讓動態鏈接字符串表項值為目標函數名稱。這些參數的構造需要拿到.dynstr,.dynsym,.rel.plt的地址。pwntools的ELF函數提供了這個接口:
elf= ELF(name)
rel_plt_addr = elf.get_section_by_name('.rel.plt').header.sh_addr #0x8048330
dynsym_addr = elf.get_section_by_name('.dynsym').header.sh_addr #0x80481d8
dynstr_addr = elf.get_section_by_name('.dynstr').header.sh_addr #0x8048278
偽造reloc_arg指向fake—rel,fake-rel里偽造好r_offset指到可控區域,構造r_info指向fake-sym,同時r_info要的低8位必須是7.fake-sym里偽造好st_name,讓.dynstr+st_name指向偽造好的system字符串,就完成了整個構造過程。例子和exp很容易找到,之后有空再補64位情況下的一些注意事項。