安安,第二篇文
這次是想紀錄一下之前聽木棍大大的pwn基礎,做個筆記,不得不說pwn真的蠻有趣的, 只是無奈門檻真的好高….
在看文章前建議先看一下這篇
https://blog.csdn.net/zhu2695/article/details/16813425
http://godleon.blogspot.com/2008/02/indirect-addressing-indirect-address.html
https://bash.cyberciti.biz/guide/What_is_a_Process%3f
https://blog.csdn.net/gatieme/article/details/51594439
http://nmc.nchu.edu.tw/linux/process.htm
[1]array
這一題比較簡單,提示有說 control EIP to 0x80485a6 to get a shell!
就是把EIP指標指向0x80485a6
這個位置就好了
剛開始開程式會是這個樣子
看source code
可以看到他已經幫我們寫好 /bin/sh/ 了
提示有說明更改index=20的位置就可以修改EIP指向的位置了, 所以我們只要把EIP指到/bin/sh那個function的記憶體位置我們就可以拿到shell了
最後小提醒還是要進行overflow讓程式crash才能喔,如果是正常退出的話不會成功
[2] overflow
這一題比較難,但個人覺得有趣多了
一樣,執行的畫面是這樣
提示說題目已經幫我們把jmp esp
這段機器碼塞入程式裡了,我們要藉由overflow讓EIP 停在jmp esp
的記憶體位置,好讓它指向我們的shell code執行
先用gdb打開應用程式讓它overflow,觀察記憶體狀態:
可以發現stack都被’a’占滿了,而上面的0x61616161 拿去比對acsii table 可以知道是 ‘a’ (或是你也可以用python的chr(0x61)去解更快)
ok, 現在overflow成功後又有另一個問題,我要怎麼知道我該段在哪裡去塞jmp? 做法是把aaaaa替換成一個有順序的ascii,看他斷在哪裡就知道哪裡該塞了 可以利用python快速達成
利用 open('bin','wb').write(s)
就可以把ascii塞進bin檔案裡
用xxd打開來看就會是這個樣子
要注意0x0a是換行符號,這會造成我們overflow失敗,所以必須把0a那段改成其他數字
再利用gdb insert 一次,
$gdb r < bin
然後看一下結果
可以發現EIP的位置最後指向0x2f2e2d2c
,你可能會有疑問 為什麼會這樣排列呢? 這種反著排的排列的方式叫做Little Endian,反而言之就是Big Endian
以目前常見的CPU為例:
- INTEL X86、DEC VAX 使用 LITTLE-ENDIAN 設計;
- HP、IBM、MOTOROLA 68K 系列使用 BIG-ENDIAN 設計;
- POWERPC 同時支援兩種格式,稱為 BI-ENDIAN。
至於為什麼叫做Little Endian和Big Endian呢? 可以參考這裡的最後一段
好,我們現在知道改哪一段就可以改變EIP的位置了,那要改到哪裡呢?
使用objdump -d -M intel ./overflow | less
切到main欄位可以看到我們目標的記憶體位置,
你可能會有疑問怎麼每個hex長度都不同,這是x86的特性,我們可以利用這點去截斷中間的指令,下面就用實作來說明吧
利用gdb x/i [address]
可以查看該記憶體位址的指令,x=要輸出記憶體的值,i=instruction,另還有8b=輸出8個位元 16b=16個位元…
透過剛剛objdump的圖可以數出來0xe4ff的記憶體位置在哪(0x8048595)
所以我們就修改bin的hex,把0x2f2e2d2c
改成0x95850408
(因為Little Endian的緣故) 這樣我們就可以確定他會執行stack內的東西了,
先用xxd bin > bin.hex
把bin匯出成hex檔,把0x2f2e2d2c
改成0x95850408
然後在下面放入我們要執行的shellcode,這樣jmp esp
後就會去往下執行stack的第一行指令就是我們放在下面的shellcode
google “ /bin/sh shellcode “ 就可以得到簡易的shellcode,放在下面後就大概是這個樣子
再利用cat bin - | overflow
最後就可以得到我們要的shell
用cat -的原因是,這個指令可以在輸入完成後不會斷開,繼續保持input的模式,如果沒加後面的減號,等於我們一拿到shell就馬上斷開了,那沒什麼用
你可能會有另一個疑問,為什麼不用gdb的 r < bin 來輸入呢?
原因是,我們這個行為是讓程式crash並且去叫(執行) /bin/sh ,而gdb只會去分析overflow這個程式,並不會跟著程式一起去執行 /bin/sh ,所以當gdb看到overflow停止執行了 當然就exit了
[3] guess-number
這題我覺得是比較簡單的一題
先看執行:
可以知道是一猜數字遊戲,總共有3條命,只要有1條命猜對五次就算贏 但我們不可能傻傻地真的去猜,當然是要嘗試破解遊戲
先嘗試overflow
雖然我們可以嘗試很多次,但是發現win的次數要剛好停在5才算成功,所以就不能無腦輸入來進行overflow了
打開source code我們可以看到
作法就是把username
和play_count
蓋掉,直接去修改wins的值
第一步就是我們要產出32(char)+4(int)個位元的字元
利用 echo + python 快速達成
echo `python -c 'print("a"*36)'` > payload
之後我們要把payload匯出成payload.hex,因為要把最後wins的5次加上去
把payload匯出成payload.hex並加上後面的5次:
利用cat把payload輸入進guess_number裡:
cat payload | ./guess_number
you win!
Pwntools
以下開始會利用一個工具來加速我們製作exploit code 的速度
pwntool的常用的範例如下
process
就是要開啟的程式,但pwn的題目幾本上都是要你nc過去他們的伺服器進行攻擊
所以這時候就會需要remote
進行連線
p32就是pack這個int轉換成機器碼
除此還有p64 p8 p16
等等,但基本上常用的就是p32和p64
相對於p32,u32就是把機器馬轉換回int,一樣也有u64 u8 16
send和sendline都是一樣是傳送數據的指令
差別在於sendline是傳送一行條指令,字串結尾會有\n
,而send沒有
而跟p32
、u32
一樣,send
的反向就是recv
,當然也有recvline
知道了這些之後就可以開始用pwntools了
以guess為例子
我們要先開啟程式來做作動
io = process('path/to/program')
然後塞入36個字元+贏5次
g = sendline('A'*36 + p32(5))
然後猜錯六次讓程式檢查我們贏幾次
for i in range(6):
a = sendline('0')
最後加一個interactive()
結尾
加這個的用意是,如果程式還有什麼沒吐出來的東西他會一起吐出來給你,
然後他也會有一個shell跟你交談,有點類似cat - 一樣
全部整合起來就會是這個樣子
以overflow為例子
這邊就直接貼code逐行講解(懶得再截圖了)
最一開始先把pwntools import 進來
再來
context(arch='i386', os='linux')
這個是宣告說這個應用程式要怎麼開他,是x86架構的,os是linux
elf = ELF('overflow')
這一行就是以elf檔案的格式搭配我們上面宣告好的context 去打開程式
elf有一個很方便的function
ret = next(elf.search('\xc3'))
jmp_esp = next(elf.search('\xff\xe4')
這個就是幫我們找return
和jmp esp
這兩個的位址在哪
可以透過ELF去解析後直接給我們結果
再來就是shell
這行的指令了
還記得我們在上面手動打overflow的時候要自己去找/bin/sh
的shellcode嗎?
現在pwntools就有內建好shellcode給我們使用了
使用shellcraft.sh()
然後用asm包住把它變成machine code
因為shellcraft.sh()
出來的是組合語言,我們必須用asm把輸出成machine code才能執行
所以~我們的payload就是
a*32 + p32(ret) * 10 + p32(jmp_esp) + shell
前面的a32故意不塞滿是為了塞return還可以理解,那為什麼 return 要做十次呢?
其實return做不只十次也可以,這邊的ret還蠻重要的,畢竟是能不能成功到jmp_esp很重要的點
只能多不能少,因為多出來的return只會在原地執行,消耗完後就會開始執行jmp_esp
大概是這樣
後面的三行就不贅述了
leak
沉寂許久之後終於又開始寫了
leak的特點是─直接告訴你stack位置、沒有system function、沒有strcpy給你存bin_sh、有buffer overflow、可洩漏任意address內容
只能透過程式的資訊洩漏取得system的address
首先先看source code吧
可以看到有一個menu,然後有一個argument叫做ununsed
他也如其名並沒有被用到
再來看看執行的樣子
選第一個他會吐給你一個address,這個address就是unused的address
所以你可以再選2,把1的address餵給他會得到0xcafecafe的字串
第3個就是我們熟悉的buffer overflow
首先可以用gdb去開他,先用r把他跑起來,再ctrl+c中斷,再下vmm(或vmmap)去看他的virtual memory
上面可以一個區段的開頭與結尾,以及他的權限,和來自哪個檔案
看到第一段的Perminsion(Perm)是可讀可執行,來源又是leak,可能程式碼的區段在那
第二段是僅可讀,那可能就是一些唯獨的資料,可能靜態的字串就在這裡
第三段是可讀可寫,可能一些global value或是GOT table在這裡
同樣的東西你可以再我們的libc和ld.so都找的到,還有stack的address
然後這個address你可以用gdb表示出來
4b代表4個bytes,x代表hex,所以x/4bx = 我要print 4個bytes,用hex的方式
回到正題,上面得知,我們可以leak出shared library的address,但是shared library的address是dynamic的
所以我們需要的是程式內的address,雖然說shared library的address會變動,但是他們function與function之間的距離是不變的
也就是說我們可以在shared library內找到puts & system & /bin/sh 的address,得到他們之間的相對距離後
就能在程式執行時加上相對位置得到當時執行的位置了
下面直接細說:
我們第一步要先構造 system(bin/sh)
這個東西
那這個東西拆成兩個部分,system的部分可以去libc裡面找到
至於/bin/sh有兩個方法,第一個方法是可以自己把/bin/sh
字串丟進stack內,畢竟都知道stack的位置了
第二個方法是libc裡面也有/bin/sh的字串可以找
這時候又要請到偉大的gdb了,gdb可以直接用searchmem
來幫我們找位置在哪
這樣就拿到/bin/sh在libc裡面的address了
然後我們要怎麼拿到puts呢,可以上GOT表去找
如何看GOT表內的資料呢?可以用objdump
去看到
objdump -R ./leak
你可能會好奇puts的這個address是什麼,用gdb去看看這個address
可以知道這個address(0x0804a018)會指向shared library裡面的puts
而p puts
也有相同的功能
用以上方法我們可以很容易地知道system的shared library的位置
做到這裡有一個疑問,那我可以用p去找”/bin/sh”的字串嗎?
答案是不行的,下面是結果
所以可以理解為─gdb 裡面的p
是用來找function的address,searchmem
拿來找字串的
好了,現在我們有binsh & puts & system 的address,可以來構造我們的payload了
以下是exploit code:
rop2
source code:
程式碼看起來很簡單,恩 很恐怖
可以看到他只給我們一個gets和1格buffer
看起來很恐怖
執行起來會是這個樣子
其中gets我們只經知道可以利用這個進行buffer overflow
再來就是想該怎麼構成system("/bin/sh")
透過objdump -R
可以得知他有用到puts
這個libc function
So~ 解法就是我們透過Bof去call 他在binary的puts,把puts在GOT的內容print出來
puts在GOT的內容就會是puts的libc位置
藉此取得libc的地址
然後後面的東西就跟leak差不多了
這邊有個地雷點,之前都傻傻得,現在才知道原來io.recvline
會從第一行開始接收(如果是第一次用的話)
所以如果要recv puts的libc address必須先把第一行那個Just什麼的先recv之後再設變數去接收libc address
io.recvline()
recv = io.recvline()
putsInlibc = u32(recv[:4])
第一行就是把Just接收掉,那因爲他是垃圾所以我們不用任何變數去接他
第三行就是用變數接收puts libc的address,爲什麼要recv[:4]呢
因爲linker
除了address之外還會丟一些東西給GOT table
(算是一個記號用來標示這是puts的libc address)
但我們不需要這些記號,所以只接收前四個byte,然後用u32去把他轉成address,
因爲他預設從libc吐回來給GOT的位置會是以byte的形式回傳
不信你看:
用u32取得puts 在libc的位置後再來就是算出system
和/bin/sh
的位置了
這部分跟leak一樣就不多贅述
最後開始構造我們的payload,先是透過Bof呼叫puts
顯示puts在GOT的內容
用recv去接他,然後算出system
和/bin/sh
的位置,之後我們要重新跳回去main
再注入一次,這裏有一點要注意,再次注入的offset不會是第一次注入的offset
因爲call 他的是pwntools不是start_main
,這時候可以透過gdb.attach(io)
,
這樣可以用pwntools呼叫gdb來attach,然後下一行io.sendline(cyclic(100))
可以
透過pwntools呼叫cyclic
產生gadget來知道新的offset
exploit code就像這個樣子: