inndy-pwn101

2018-05-19

安安,第二篇文

這次是想紀錄一下之前聽木棍大大的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為例:

  1. INTEL X86、DEC VAX 使用 LITTLE-ENDIAN 設計;
  2. HP、IBM、MOTOROLA 68K 系列使用 BIG-ENDIAN 設計;
  3. 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我們可以看到

作法就是把usernameplay_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沒有
而跟p32u32一樣,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')

這個就是幫我們找returnjmp 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就像這個樣子: