有問題可以提醒我修正
Discord:台女#4240
PWN1
PWN2
Pwn
Angelboy
gdb -n(最原始的gdb) -q(不要顯示一大串的資訊) [binary]
python寫的gdb插件
Useful Feature
-Lab1
根據OS不同有所不同
根據讀寫執行權限和特性分類
在Linux執行一支Binary的過程
static Linked
Dynamic Linked
User Mode:
一開始會先呼叫fork()去複製原本的程序(Process)產生一個子程序(child-process),child-process再去執行execve()
接下來就進到
Kernel Mode:
裡面主要會先呼叫sys_execve()
去檢查參數的格式是不是正確的。
例如:argv要是一個指標陣列,你傳字串進去他就會回傳-1然後跳出來,成功通過的話就執行下一個函式。
do_execve()
去搜尋執行檔的位置是否存在這個執行檔,沒有的話就回傳-1,讀取的同時也會順便讀前128byte取得執行檔的格式。
search_binary_handler()
根據前面讀到的檔案格式選擇執行那個程式的function。
例如:前面讀到他是一個python的script,那就用load_script()去處理。
前面讀到是一個elf就用load_elf_binary()去處理。
load_elf_binary()
檢查程式header,如果是Dynamic linking會利用.interp這個section確定loader的路徑,接下來把program header紀錄的位置mapping到memory中,最後把sys_execve()的return address改成loader(ld.so)的entry point;static linking則是改成elf的entry point。
程式是怎麼maps到虛擬記憶體的?
在程式的header中記錄著哪些segment應該mapping到甚麼位置,以及讀寫執行權限,還有哪些section屬於哪些segment。
一個segment可能有0個或是多個section。
一些指令:
查看program header
查看section header
查看dynamic section內容
有AT&T和Intel兩種
除了前面的"%"符號以外,最大的差異就是AT&T是把前面的值給後面的值,Intel則相反。
mov
add/sub/xor/and
push/pop
lea
jump/call/ret
leave
nop
System call
Calling convention
這邊我有自己做一份ppt介紹calling convention的過程。
這邊建議多看幾次確定要很熟,後面很多攻擊都會需要很清楚知道程式怎麼call funtion的。
開始寫第一個Assembly code
用Vim把這些寫進叫做hello.s的檔案裡,再用下面這兩行指令編成執行檔
這時候應該會有這三個檔案
跑hello
逐行解釋這段code
這是指寫在.text這個Segmant的意思,這邊
第二行
有點類似C語言中的int main(),不過這邊可以放任意的function,不一定是_start。
ref
接下來就是_start,write,msg這三個funtion
_start裡面只有一行程式碼
就是直接跳到msg這個funtion,這邊有用到jmp這個指令,補充一下這個instruction是無條件跳過去,還有很多跳的instruction,之後應該會有介紹,這邊
目前已經跳到msg這個funtion了
這裡有兩行
分別是呼叫write和
定義Hello world這個字串到0xa,db就是define bytes的縮寫
還記得前一個msg有用到call這個instruction,這會把他的下一行("db 'Hello world',0xa")的位址push到stack裡面(頂端),然後再pop ecx,把stack頂端("db 'Hello world',0xa"的位址)存到ecx,方便到時候funtion epilogue的時候能復原成call之前的stack狀態。
接下來
可以在Syscall的Document這裡,找到write的eax要放什麼?這邊的意思是系統會看你的eax去決定要做哪個system call,中文好像是呼叫慣例(?
這邊放4,系統就知道你要做write這個funtion。
接下來
ebx要放的的是叫做fd(file descriptor)的東西,這邊放1表示std_out,想看更詳細的這邊
還有這邊
總之就是讓字串可以輸出在螢幕上~
stdin:標準輸入裝置(standard in),預設是鍵盤。
stdout:標準輸出裝置(standard out),預設是螢幕。
stderr:標準錯誤記錄裝置(standard error),預設是螢幕。
接下來
這是指'Hello world'這個字串的長度為12,你自己去數會發現包括空白字元只有11個字元,為什麼是12呢?因為字串最後面還有一個空字元告訴程式這個字串到這而已,想看更多看這邊
接下來
這是System call的用法,在使用 int 0x80 之前,必須要將我們剛查出來的號碼填到 eax 內;填好後,只要一使用 int 0x80,相對應的 system call 就會被呼叫。這邊
接下來
這邊就是把原本eax的4改成1,也就是原本call write後來call exit結束掉程式,最後再用一次int 0x80去完成exit的動作。
完結灑花><
以上就是第一個Assembly code
攻擊者為了拿到shell注入的程式碼叫做shellcode,由machine code組成。
產生shellcode有很多方法,例如:
或是pwntools
這樣就能印出hello world的shellcode了
在本地跑的話就是這樣:
記得用
$ gcc -m32 -z execstack test.c -o test
去編譯,這邊test.c就看你上面那坨code存的檔名是什麼,後面的test就是你編譯出來的執行檔檔名,也是任意取。
編譯完應該會多出一個test的elf檔,這時候讓他跑起來就會出現hello world了。
就可以看到他的shellcode
或是加上-i參數可以看到c語言字串形式的shellcode
程式設計師未對buffer長度做檢查,導致駭客可以控制程式流程或是改變數的值。
依照buffer位置可以分成:
這些函式可能導致buffer over:
buffer overflow的memory長這樣
首先是正常的memory
先開一個char buf[20]的空間
但是你放入了100個a,可以發現ebp和ret addr都被蓋成aaaa了
當你要return的時候,pop ebp就把eip的值改成0x61616161(aaaa)了
這時候就成功的控制了eip的值了
return address被改成0x61616161,但是記憶體裡面沒有這個位址,所以就segment fault了
那如果不要亂輸入a,輸入攻擊者的攻擊代碼位址,是不是就能為所欲為了呢?
這邊要注意x86是little-endian,address要反過來輸入
例子:
假設要填入0x080484fd
就要輸入\xfd\x84\x04\x08
或是用pwntool的p32()可以直接把你的address轉成little-endian的形式。
接下來就是要如何讓程式跳到我們想跳的地方(shellcode)
首先要先找到我們想跳的地方的函式的位址,可以用
去看這隻程式所有函式的位址(或是objdump)。
然後要去找你要塞多少byte才能到你的目標函式,也就是你要覆蓋的函式的前面都要填滿東西,這邊可以用pwntool內建的工具cyclic。
進到python環境先打:
然後就會出現一大坨字串,像這樣:
再把這坨餵到程式的輸入裡,然後用gdb看他的EIP是哪四個字元,我用Lab3示範
其實不一定是EIP,看你要對程式哪邊操作去決定要填到哪裡,這邊看到EIP的值是aava
接下來用cyclic_find()這個函式去找offset(填充的長度)要多少?
可以看到offset是82。
netcat用法:
Ex:
這邊的意思就是把你的ret2sc的binary放到你的localhost的8888port上,所以你nc loalhost:8888會執行這支binary
打$ nc -v localhost 8888
就可以連過去囉
開始練習 Lab3
記憶體位址隨機變化
每次執行程式stack、heap、library位址都不一樣
Linux核心的東西
cat /proc/sys/kernel/randomize_va_space
這個指令可以看到ASLR的隨機化強度
分別有0、1、2
用$ ldd /bin/ls
可以看到執行時載入的library位址都不一樣
大原則:可寫不能執行;可執行不能寫
可以用gdb裡面的vmmap看process的權限
用gcc編譯的時候預設不會開啟,加上-fPIC -pie就可以開啟
沒開:data段、code段固定
有開:data段、code段跟著ASLR
隨機生成亂數,function call的時候塞入,function return的時候檢查值有沒有變動
該亂數稱為canary
預設是開啟的,能有效的防止stack overflow
canary會放在tls區段裡面的tcbhead_t結構中,在x86/x64架構下恆有一個暫存器指向tls區段裡面的tcbhead_t結構,在x86是gs;在x64是fs,取canary的值會直接以fs/gs取。
由於ASLR機制,每次function的位置都不一樣
Dynamic Linking的程式在執行過程中,有些library的函式到結束都不會用到,因此為了節省時間,Lazy binding的機制會在第一次call function的時候再去找function位置進行binding,而不是一次把所有函式都找出來,這樣的效率比較高。
library的位置載入後才決定,所以在無法在編譯的時候知道位置
GOT是一個函式指標陣列,存library中function的位置
由於Lazy Binding機制的關係,程式在呼叫function的時候不會填上函式位置,而是填上一段PLT位置的code
GOT主要分成
後面是.so函式引用位置
假設現在在call 一個叫做foo()的函式
第一次call foo()
在.got.plt裡面會寫成foo@plt+6
+6就是 jmp *(foo@GOT) 的長度
因為foo是第一次被call所以會直接執行push index,跳過 jmp *(foo@GOT) 這行
接下來就開始執行Lazy Binding,這邊index根據你的function不同會有不同的值,push index後就jump到PLT0的code段。
PLT0在所有PLT的最上方,push *(GOT+4) 這邊+4是因為32位元一個pointer是4byte
push *(GOT+4) 主要是把link_map push進去
再來 jmp *(GOT+8) 是jmp dl_runtime_resolve()
到這邊總結一下就是call了dl_runtime_resolve(),並且把link_map和index放進去
->dl_runtime_resolve(link_map,index)
接下來進到dl_runtime_resolve後
最核心的function就是call_fix_up()
這個function會把函式真正的位置找出來,並且填回.got.plt(GOT)
然後ret 0xc就是return回foo()
第二次call foo()
這次不會跳過第一行jmp *(foo@GOT),這次會直接跳到.got.plt(GOT)裡面,由於第一次已經把位置填好了,所以會直接跳過去,不會再執行下面的push index和jmp PLT0
or
GOT Hijacking主要的思路就是由於Lazy Binding的機制GOT必須要可以寫入,剛剛實作時把foo()填進GOT的時候把foo()改成system()就可以達到控制程式流程的效果了。
GOT的防禦機制RELRO
RELRO分成三種:
一般程式很難有system這種可以直接得到shell的function、DEP/NX的保護機制也會讓你無法用shellcode去執行,但大部分程式在Dynamic Linking的情況下都會載入libc,libc有很多好用的function,例如:system、execve…
由於ASLR的關係,每次libc的載入位置都不一樣,通常都需要leak出libc的base address,然後加上offset就能導到你要的funtion。
可以leak出libc base位置的地方有:
一般情況下ASLR都是整個library image一起移動,因此只要leak出libc其中一個位址後,就可以推得全部function的位址。
可以用
library大概長這樣,由於ASLR的關係,記憶體的位址會隨機,但是整個library一起隨機,他們的相對位置不會變(即使隨機,還是不會把他們的排序打亂,距離library_base的address(offset)也是固定的)
也就是說只要知道你需要用到的function的offset還有library_base的位置,就可以把你需要用到的function位址求出來。
要先確認library的版本,可以用
像這樣,可以看到是libc.so.6
然後用
就可以看到有很多printf
可以找到我們要的printf的offset是00053de0
然後還需要找到system的位置,一樣用
那接下來只需要把printf的位置扣掉printf的offset再加上system的offset就可以得到system的位置了
公式:
libc_base_addr + printf_offset = printf_addr
libc_base_addr + system_offset = system_addr
已知printf_addr、printf_offset、system_offset就可以先算出libc_base_addr,然後再加上system_offset得到system_addr
成功拿到system位置後就可以複寫return位置,跳到system得到shell。
這邊要注意要多空一格,因為我們用的是ret不是call,一般call之後會push一個return address到stack中,function會空一格再取參數。
總之就是會有一個return address在stack上,要記得空著。
補充:
"/bin/sh"字串位置可以在libc找到;system參數只要"sh",也可以只找"sh"這個字串就好。
最後做Lab4練習看看吧~
這題的作法很多,我自己摸出來的做法是:
首先先看source code
會發現程式的邏輯就是把你輸入的字串和password比對,一樣的話就印出flag。
大概了解題意後就開始動工
我上網查過一些作法是leak出password,然後輸入password就能印出flag了。
我這邊的做法是下斷點在if判斷的instruction,先反組譯get_flag()函式
會在裡面看到組合語言的cmp,就相當於source裡面的if判斷式,把斷點下在這行(0x08048720),然後執行起來。
這邊會要你輸入magic,就隨便輸入就好了,反正後面會再做修改
可以看到程式跑到斷點,在做cmp的instruction了,這裡在比較edx和eax的值,eax是你輸入的magic,edx是password,這邊我的想法就是把其中一個的值改成另一個的值就過這條判斷式了!
那就用set這個指令去改
這樣就成功拿到flag囉
這題就是自己寫shellcode,去做read、write、open
題目可以去pwnable.tw找orw這題,
檔案的位置在/home/orw/flag,用剛剛的hello world的模板來改,最後的shellcode長這樣
首先前面做的都差不多,先jmp到hello然後call write的時候會把下一行的字串push到stack頂端,進到write的時候先寫open()的shellcode(10~13):
首先先
把stack頂端(剛剛push進去的字串)存到ebx,第31行要記得改成題目說的檔案位置/home/orw/flag。
在syscall的document中可以看到open的ebx要放file name,這邊pop ebx就成功設定好ebx這個參數了。
這邊要注意ebx要放絕對路徑
接下來
這邊是設定eax為5,5是open這個system call的numer,在剛剛的document中都可以看到
接下來
ecx放flag
這邊flag放0應該是指read-only
ref
不太確定
文章裡面說一定要有這個參數,我刪掉後還是可以拿到flag(X
接下來
這是呼叫system call的意思,這時候就會根據eax的值去決定要做那個function,檢查eax,ebx,ecx那些參數。
到這邊open()的Assembly就做完了
接下來(15~19行)是做read()
首先
剛剛open()完會回傳fd(file description)也就是這個檔案的描述到
eax,read()的ebx是放fd的,所以第一行先把eax的值(fd)給ebx
接下來
ecx是放buffer的地方,那就把esp給ecx就可以了
buffer是指兩個程式傳遞東西的速度不同,需要一個緩衝區去放待處裡的檔案的地方,這邊就隨便放esp就可以了,沒特別指定要放哪裡。
接下來
edx是放size的地方,這邊沒說size多少,就隨便放(不要太小)。
接下來
這跟剛剛一樣,讓system call可以知道要呼叫read()
以上是read()的部分
最後是write()(21~24)的部分
首先
eax會回傳讀取的長度,這邊write的size也是放在edx,所以就把eax的值給edx。
接下來
ebx放fd,給1代表stdout輸出。
這邊應該不陌生了,就是system call write()
到這邊這題就差不多了,最後別忘記exit()
離開程式。
到這邊shellcode就大功告成了~
pwntool的話簡單的介紹一下:
首先
還有最後的
最後用python去跑這個腳本就可以拿到flag囉!!
$ ncat -ve ./ret2sc -kl 6969
先把題目架好
這題是ret2sc,先看source code
可以看到這邊buf的size是20,但是下面用gets讀入,gets是一個很危險的函式,因為gets可以一直讀入東西,不會有長度的限制。
所以攻擊點在gets(buf)這邊。
整體的攻擊思路就是先輸入shellcode到name,然後在buf的地方overflow跳到前面name的地方去執行shellcode。
先檢查保護機制,先進到gdb之後再打這個
可以看到幾乎都是關的,那就不用再特別處裡bypass的部分
一開始先找offset要塞多少,用剛剛提到的cyclic先產生一大坨字串
然後我塞到輸入buf的部分,也就是"Try your best:"輸入的地方
然後用gdb去看EIP會發現是"iaaa"
再用$ cyclic_find("iaaa")
去看offset的長度是32
那就可以開始寫exploit了!!
基本模板先寫好
然後要先找到name這個function的address,這是我要跳到的function
可以用gdb或是nm
找到name的位置在0x804a060
知道offset要塞多少,也知道要跳的目標函式位址,就可以開始寫exploit(攻擊腳本)了
完成~
先看c的code
這題會要你輸入一個address,然後看address的內容,攻擊的注入點在Print_message裡面的strcpy,可以做到buffer overflow。
先做buffer overflow
首先要先餵一個10進位的address,先隨便找一個GOT的位置(我用put)。
要記得先轉成10進位,直接在python打就可以了(我這邊是用python3,應該1,2,3都可)要改成hex的形式才能轉喔0804a01c->0x804a01c
轉完0x804a01c(16進位)->134520860(10進位)
把這個10進位的數字餵進去會得到0xf7e46290
檢查一下0xf7e46290是不是put
確定ok後
接下來要找多少bytes會蓋到return address,這邊可以用之前提過的cyclic()或是直接在gdb裡面用pattc [數字]產生一大串bytes,然後用crashoff去看是在多少的時候crash的,這邊兩種做法都提供
先產生bytes,到python裡面import pwntools
產出字串先複製起來
接下來回到gdb,一開始還是一樣填入10進位的位置,然後填入剛剛產出的字串
像這樣
這時候去看EIP裡面是什麼
會發現是'paaa'
回到python,用這個指令就可以知道要塞多少
可以得到是60
這是gdb裡面的指令,先打
用法就是pattc [數字],一樣可以產出一串byte
接下來把程式跑起來然後填入put位置和剛剛產出來的bytes
然後打
就可以知道要塞多少囉
一樣是60
到這邊就可以開始寫exploit了
這邊有不懂的程式碼可以參考上面的exploit,上面有註解很仔細
主要分成四個部分
這邊put_got的位置可以用$ objdump -R ret2lib
找到
送出記得轉成字串
這邊用strip取字串,然後轉成16進位,要小心:後面還有一個空格,要記得recv
puts、system的offset可以用$ objdump -T /lib32/libc.so.6 | grep puts
、$ objdump -T /lib32/libc.so.6 | grep system
找到
然後上面有提到那個公式,總之就是推出system的address
最後找到程式裡面"sh"字串的位置,可以用ROPgadget找(我是用這個);或是ida的string view好像也可以找到
ROPgadget的找法就是ROPgadget --binary ret2lib --string 'sh'
到這邊需要的資訊就都到齊了,最後payload再把他組起來就好了
最後一步
還記得剛剛有找過要塞60bytes才會buffer overflow,所以傳b'a'*60
然後要跳到system的位置,並且要隨便塞一個address,上面有提到要空一個return address,他會從return address下面4個bytes開始吃參數,所以就亂塞一個地址(0xdeadbeef好像是大家的習慣XD),然後把sh傳進去,這樣就會執行system('/sh'),成功拿到shell~
這題就解掉了~