緩衝區溢位攻擊之二(Buffer Overflow)

berming
berming
Apr 3, 2017 · 16 min read

在上一章我們成功控制了程式流程,但執行的僅限於現有的code,而且離開gdb環境後還會遭遇其他的安全防護導致失敗,在這一章我們就要來談談如何實際執行任意代碼(Shellcode),以及介紹一些電腦的安全防護。

什麼是Shellcode?引述wiki:

In hacking, a shellcode is a small piece of code used as the payload in the exploitation of a software vulnerability. It is called “shellcode” because it typically starts a command shell from which the attacker can control the compromised machine, but any piece of code that performs a similar task can be called shellcode

其實簡單來說就是一段byte code,用來注入目標並且達到你要的目的就可稱為Shellcode。

好像還是一頭霧水對吧?沒關係,馬上來實作一次就知道了!


p.s. 雖然想在介紹shellcode時一次講解Linux、Windows的實作方法,但無奈這兩者撰寫shellcode的方式實在是相差巨大,整體來說Windows比Linux複雜許多,所以在這章只介紹Linux的作法,Windows的做法也許到下一章會再做介紹。

以C為例,shellcode的形式大概是這樣:

char shellcode[] = "...byte code here...";
int main()
{
void(*func)() = (void(*)())shellcode;
(*func)();
}

void(*func)()宣告了一個叫做func的void型別function pointer,並將shellcode[]轉為其型別後assign給func,最後在呼叫這個func。其意義就是把shellcode[]當成一個函式來呼叫,shellcode[]裡面的內容也因如此會被電腦視為可執行的byte code。

那實際上shellcode[]的內容要怎麼寫呢?通常是寫組語、再用組譯器組譯(ex: nasm),最後看他產生的machine code(ex: objdump)就完成了。

為了簡化過程,我們利用線上工具來幫助我們寫shellcode-Online x86 / x64 Assembler and Disassembler,不但有x86、x64,也有組譯、反組譯,而且完成之後還有精美的排版,好工具不用嗎?


第一個Shellcode-Say HELLO

首先來撰寫Shellcode讓程式可以顯示”HELLO”並且正常關閉,Linux kernel提供了一組system call讓我們能夠與其互動,許多功能都必須藉由他們才可以辦到(ex: exit、execve、open/read/write)。

在x64環境下我們只要透過指令”syscall”即可呼叫system call,這裡提供了呼叫各種不同的system call所需要做的register設定資訊,在以下就不再多做說明,讀者可自行查閱。

第一部分我們需要system call “write”來顯示字串,以下是這部分的組語:

mov rax, 0x094f4c4c4548
mov rbx, 0x010000000000
add rax, rbx
push rax
mov rdi, 1
mov rsi, rsp
mov rdx, 6
mov rax, 1
syscall

前四行是為了準備字串"HELLO\n”(換行比較好看呀!),有兩點要注意:

1. 因為Little Endian的緣故字串要反過來。

2. 由於gets()讀到0x0a(\n)會終止,必須避免這個字元出現,所以我們用間接的方式產生。

這裡提供ASCII表供讀者參閱。

第6行”mov rsi, rsp”,由於rsp指向stack top,也就是指向”H”-這個字串的第一個字,也就代表目前rsp也等同於字串”HELLO\n”的指標。

第二部分是要讓程式正常的終止,所以需要system call ”exit”,以下是這部分的組語:

mov rax, 60
mov rdi, 0
syscall

把這兩部分合起來,拿到剛剛提供的線上工具組譯:

我們的”Say HELLO” Shellcode完成!

“\x48\xB8\x48\x45\x4C\x4C\x4F\x09\x00\x00\x48\xBB\x00\x00\x00\x00\x00\x01\x00\x00\x48\x01\xD8\x50\x48\xC7\xC7\x01\x00\x00\x00\x48\x89\xE6\x48\xC7\xC2\x06\x00\x00\x00\x48\xC7\xC0\x01\x00\x00\x00\x0F\x05\x48\xC7\xC0\x3C\x00\x00\x00\x48\xC7\xC7\x00\x00\x00\x00\x0F\x05”

將其複製到之前上面所說的shellcode[]中,compile後看看結果如何。

gcc -z execstack -o shell_hello shell_hello.c

TADA~成功顯示了"HELLO”並且程式正常終止,第一支Shellcode成功!


運用Shellcode在Buffer Overflow中

我們寫Shellcode的目的就是為了在Buffer Overflow中不只是執行現有代碼,而是執行任意代碼(Shellcode),但現在寫完了、之後呢?原理很簡單,我們不是能控制程式流程、讓他跳到任意的地方了嗎?那只要把Shellcode輸入進buffer中,之後再控制程式(rip)跳到buffer上面不就完成啦!但實際上如何做呢?我們先來看最簡單的例子。

#include <stdio.h>

這是上一章的程式,一樣compile完後執行、一樣進gdb環境跑、一樣輸入一堆A讓程式崩潰。

gcc overflow.c -o overflow -z exec-stack -fno-stack-protector

看一下register,看看rsp的值:

在上一章已經知道nonSecure的ret距離name[]應該是24個bytes(hex : 0x18),所以name[]的位址應該是0x7fffffffdab8–0x18=0x7fffffffdaa0 (注意,你的電腦看到的數值可能不會和我一樣)。

展示stack檢查一下:

可以看到開始出現"0x41"的地方(ASCII : A)的確是0x7fffffffdaa0

p.s. 較嚴謹的方式是看assembly code算位移,但在此想先用較簡易明瞭的方式。

所以把這個位址覆蓋到ret上面,並且輸入shellcode進去name[]裡面不就完成了嗎?還沒有,還有一個小細節要修正,如同上頭所說name[]距離ret只有24個bytes,這個空間並不足以放我們的shellcode(p.s. “HELLO”的shellcode長度為66bytes)。

那怎麼辦呢?很簡單,放到ret後面的地方就可以了。但現在覆蓋ret的值就要再加上32(0x20)bytes(原本是name[]的位址,現在放到ret「後面」,ret本身長度為8bytes,所以name[]+24+8=name[]+32)

0x7fffffffdaa0+0x20=0x7fffffffdac0。

有了這個數字後就可以來建構我們的payload,並在gdb下執行:

OK,直接拿去執行應該也會成功吧......咦?怎麼失敗了?

首先第一個可能原因是ASLR,這裡先做極短的簡介-ASLR會讓記憶體位址隨機化,也就是說我們填入ret的值是沒有用的(我們填入的是一個固定值)。

輸入以下指令:

cat /proc/sys/kernel/randomize_va_space

若顯示的不是0,代表ASLR作用中,只要把他設定為0就可以關閉了。

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

那麼這次該成功了吧......咦?怎麼還是失敗了?

再來可能的原因是縱然執行同一個程式,用什麼方法執行他也會有差異,例如用gdb執行的stack和用bash執行的stack長的並不一樣,環境變數的差異、執行檔路徑的差異(由於這並不是我們要討論的重點,若想更詳細了解可參考這篇),以上提到的兩個其實就是main函式的參數,還記得上一章我們所說的吧?參數的確會影響stack的長相-stack不一樣,buffer的位址就不一樣,shellcode的擺放位址也就不一樣。

當然不只有gdb、bash的差別,即使執行的方法都用bash,但兩個不同的bash下執行其結果也仍可能不一樣,以下是筆者小小修改了overflow.c-讓他printf出buffer位址,所測到的兩個不同結果:

而除了執行方法的差異外,可能的原因還有-同一個程式被載入到記憶體中,他們的虛擬記憶體位址(Virtual Memory)一樣,但虛擬記憶體所映射(map)到的實體記憶體位址(Physical Memory)可能不一樣,這取決於你的OS!

OK......ASLR都關了還不行,那現在要怎麼辦呢?以下提供兩個解法:

1. NOP Sleds

在不知道buffer位址的情況下,一般會想到怎麼做?當然最直覺的方式就是用「猜」的,但如何提升猜中的成功率呢?先假設整個shellcode範圍就像一個平台,你要想辦法跳進去,那麼是不是你有個大平台之後就會很容易的成功呢?但隨便跳會有個顯而易見的問題,假設有個shellcode是這樣:

a=1;

b=2;

如果順利跳到”a=1”那當然是皆大歡喜,但萬一跳到”b=2”這行,那麼”a=1”就沒有執行到了,這當然不行!

所以我們希望跳到的地方不影響shellcode其本身目的,解決方法就是放一堆NOP指令(NOP sleds),也就是"不做任何事"的指令,只要把一堆NOP放在shellcode前面,當我們跳到NOP的範圍內,程式流程就會不斷進行下去,並順利的進入shellcode範圍。

當buffer位址差異只是因為如上面所提到-環境變數等原因,通常其差異都很小,再加上有NOP Sleds後會非常好猜,這裡提供exploit程式:

p.s.1 NOP machine code : 0x90

p.s.2 當然由於位址差距很小,也可以用手動更改要猜的位址,但這裡還是提供較嚴謹的作法

#!/usr/bin/python

程式不斷根據正負offset(也就是NOP Sleds的長度)產生對應的payload,並且不斷嘗試直到成功為止。

以下是成果:

2. CALL RSP

想像一下,今天如果shellcode是個函式,那麼要如何執行他呢?只要執行這行指令就行:

shellcode();

好像是廢話對吧?那如果從assembly的角度來看會是怎麼樣呢?

call shellcode

恩......好像也沒什麼問題。

如果上面的例子都沒問題的話,就應該知道函式其實就是個指標,執行函式說白話點就只是改變RIP的成為該函式的位址而已。那如果函式的位址剛好就是RSP的值,是不是只要"call rsp"就行了?

事實上我們的shellcode在nonSecure()返回後,RSP會剛好指著他!因為nonSecure()返回時RSP指向返回位址,又因為我們設計shellcode緊接在返回位址之後,所以當他返回後(也就是執行"ret"後),RSP就指向下一個-也就是shellcode的起頭。

利用"CALL RSP"這種方法,我們就不用寫死位址在返回位址上面了(而且也充滿不確定性),而且RSP的值是什麼也根本不需要知道呀,這也是我們上面一直在煩惱的問題-找不到確切的位址。

但說了那麼多,但實際上要怎麼做呢......把CALL RSP這條指令放在shellcode裡面嗎?當然不行,現在問題就是不知道shellcode的實際位址,所以裡面不管放什麼都是一樣意思。

CALL RSP也是一條指令,byte code是 “\xFF\xD4”。在記憶體中找到 “\xFF\xD4”是有可能的,只要他不在stack區段(section)-例如.data區段、.text區段等等......(如同我上面一直強調的,stack所得到的位址是不穩定的),就可藉由找到某個記憶體位址,裡面存放著指 “\xFF\xD4”,然後把這個位址填到返回位址中(就如同上一章節的hacker函式),當函式ret時就會到這個位址執行”CALL RSP”,之後程式流程就跳轉到shellcode上。

但我們的程式實在是太小了,很有可能會找不到“\xFF\xD4”,為了方便我們直接修改overflow.c,讓他裡面真的有“\xFF\xD4”

#include <stdio.h>

重新編譯後,先用readelf看看.data section在哪(這個區段存放著我們的指令字串)。

可以看到.data section是從0x601048開始,大小為0x1048。

接著我們用gdb指令find來找這個字串:

p.s. 由於筆者使用gdb套件的關係,指令用法跟原本gdb略有差異

找到了,在0x601058,之後只要把這個位址放在ret處就可以了,以下是新的exploit程式:

執行結果:

到這裡為止我們已經學習了如何在buffer overflow中利用shellcode,包含兩種讓程式流程可以跳到shellcode上面的方法,到此先到一段落,礙於篇幅問題,電腦的安全防護(包含各位在這章一直看到的ASLR)留到之後再談。

在下一章將會介紹windows上的shellcode,如同本章開頭所提到的,windows的shellcode和linux的有著巨大的不同,在windows下如何撰寫shellcode、並利用在buffer overflow中,且在下一章將會有行為較有趣的shellcode出現(例如新增一個管理員帳號、下載東西等等......),那麼我們下次再見!

p.s. ......那為什麼這章的shellcode那麼單調、只是顯示一個hello呢?其實若讀者們想練習其他種shellcode可以自行嘗試(例如生一個shell並切換為root權限等等),因為在linux下shellcode撰寫相對容易,在本章不再贅述。

berming

Written by

berming

Security Consultant & CTF lover :)

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade