Bạn cũng có thể tải xuống challenge ở đây: load.zip
Zip sẽ bao gồm 1 file:
load
Tải xuống, giải nén và chúng ta bắt đầu!
1. Tìm lỗi
Trước tiên, ta sẽ sử dụng file để kiểm tra thông tin cơ bản:
$ file load
load: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1128f7b9cbf10c5208e4624339366761018789ca, for GNU/Linux 3.2.0, not stripped
Đây là tệp 64-bit không bị ẩn code. Tiếp theo, ta sẽ sử dụng checksec để kiểm tra tất cả các lớp bảo vệ của file:
$ checksec load
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Wow, tất cả các lớp đều được bật. Cuối cùng, ta sẽ dịch ngược file bằng ghidra để hiểu được cách chương trình hoạt động.
Hàm main() không có gì thú vị. Nó chỉ nhận đầu vào và sau đó so sánh để thực hiện các hàm tương ứng.
Trong hàm see_balance(), chúng ta có thể thấy rằng có một lỗi Format string tại printf():
Tại hàm init_pack(), nó kiểm tra xem biến toàn cục b có bằng 249 hay không. Nếu nó bằng nhau thì hãy nhảy vào hàm lift_pack(), và có vẻ như nó đọc một file được gọi là flag_maybe và sau đó lưu nó vào stack.
Mình vừa tìm đến đó đã lấy được cờ nên phần còn lại bỏ qua =)))
2. Ý tưởng
Với lỗi Format string, ta có thể thay đổi dữ liệu của biến toàn cầu b thành 249 và khi đó, cờ sẽ được đọc và lưu trữ trên ngăn xếp. Ta sẽ sử dụng lại lỗi này để leak cờ ra ngoài.
Để thay đổi b, chúng ta cần địa chỉ base của binary vì PIE enabled. Sau đó, mọi thứ sẽ trở nên dễ dàng hơn.
Tóm lược:
Leak địa chỉ base
Thay đổi b
Lấy cờ
3. Khai thác
Giai đoạn 1: Leak địa chỉ base
Đầu tiên, ta sẽ kiểm tra xem tại %p thứ mấy sẽ in ra vị trí đầu của dữ liệu được nhập:
Chúng ta có thể thấy rằng tại địa chỉ 0x7ffe2e91e380 của stack có chứa địa chỉ của __libc_csu_init, vì vậy ta sẽ leak địa chỉ đó và tính toán để lấy địa chỉ base của binary.
Offset của địa chỉ __libc_csu_init sẽ nằm ở vị trí %18$p (6 + 12 = 18). Payload sẽ thay đổi thành:
Chạy script và ta leak địa chỉ của địa __libc_csu_init thành công. Sau đó thực hiện tính toán và ta có địa chỉ base của binary:
Bây giờ chúng ta sẽ chuyển sang giai đoạn tiếp theo: Thay đổi b!
Giai đoạn 2: Thay đổi b
Với địa chỉ base của binary, ta có thể lấy được địa chỉ của b một cách dễ dàng. Như ta đã biết, offset của phần đầu dữ liệu nhập vào nằm ở %6$p và vì đây là tệp 64 bit nên ta cần đặt địa chỉ ở cuối (nếu đặt địa chỉ b ở đầu với null byte trong địa chỉ thì printf() sẽ dừng thực thi tại null byte và giá trị b sẽ không thay đổi).
Ta kiểm tra xem nếu chúng ta nhập đầy đủ 100 byte vào see_balance() thì stack trông như thế nào:
Vậy, ta sẽ muốn đặt địa chỉ của b ở cuối dữ liệu nhập vào tại địa chỉ 0x7ffda58eb9a0 + 0x8. Chỉ bằng cách đếm, ta biết được offset là 6 + 11 = 17.
Có điều ta cần phải lưu ý, ta không nên sử dụng %<k>$n để viết giá trị. Do đó, ta sẽ sử dụng biểu mẫu chuẩn là %n. Ví dụ đoạn mã sau sẽ giống như%17$p`:
# Each '%' will count 1
payload = b'%c'*15
payload += b'%c%p'
Vì vậy, trước tiên ta sẽ đặt địa chỉ của b vào cuối dữ liệu đầu vào:
payload = b'%c'*15
payload += b'%c%p' # We will use this to change b
payload = payload.ljust(0x58) # Padding
payload += p64(exe.sym['b'])
Và ta sẽ muốn ghi 249 byte b bằng cách thay đổi số byte đưa vào ở dòng thứ 2 của payload bên trên. Payload mới sẽ trông như thế này:
payload = b'%c'*15
payload += '%{}c%n'.format(249 - 15).encode() # We will use this to change b
payload = payload.ljust(0x58) # Padding
payload += p64(exe.sym['b'])
Tại sao mình lại viết 249 - 15, đó là bởi vì %n sẽ viết số lượng byte trước %n vào vị trí của b (sử dụng %p sẽ cung cấp cho chúng ta địa chỉ chính xác của b và sử dụng %n sẽ ghi số byte trước nó vào b).
Payload cho giai đoạn 2 như sau:
p.sendlineafter(b'2) Upgrade Pack', b'1')
payload = b'%c'*15
payload += '%{}c%n'.format(249-15).encode() # We will use this to change b
payload = payload.ljust(0x58, b'P') # Padding
payload += p64(exe.sym['b'])
p.sendlineafter(b'Wanna upgrade pack?', payload)
Chạy script và kiểm tra trong GDB, ta có thể thấy giá trị b đã được thay đổi thành 249:
Vậy giờ ta sẽ chuyển sang giai đoạn cuối cùng: Lấy cờ!
Giai đoạn 3: Lấy cờ
Sau khi chúng tôi đã thành công giai đoạn 2, ta chỉ việc chọn tùy chọn thứ hai Upgrade Pack để đọc và đưa cờ lên stack. Ta sẽ tạo cờ giả để kiểm tra:
Pragyan CTF 2022 - Portal
Liên kết gốc: https://ctf.pragyan.org/
Bạn cũng có thể tải xuống challenge ở đây: load.zip
Zip sẽ bao gồm 1 file:
Tải xuống, giải nén và chúng ta bắt đầu!
1. Tìm lỗi
Trước tiên, ta sẽ sử dụng
file
để kiểm tra thông tin cơ bản:Đây là tệp 64-bit không bị ẩn code. Tiếp theo, ta sẽ sử dụng
checksec
để kiểm tra tất cả các lớp bảo vệ của file:Wow, tất cả các lớp đều được bật. Cuối cùng, ta sẽ dịch ngược file bằng ghidra để hiểu được cách chương trình hoạt động.
Hàm main() không có gì thú vị. Nó chỉ nhận đầu vào và sau đó so sánh để thực hiện các hàm tương ứng.
Trong hàm see_balance(), chúng ta có thể thấy rằng có một lỗi Format string tại printf():
Tại hàm init_pack(), nó kiểm tra xem biến toàn cục
b
có bằng 249 hay không. Nếu nó bằng nhau thì hãy nhảy vào hàm lift_pack(), và có vẻ như nó đọc một file được gọi làflag_maybe
và sau đó lưu nó vào stack.Mình vừa tìm đến đó đã lấy được cờ nên phần còn lại bỏ qua =)))
2. Ý tưởng
Với lỗi Format string, ta có thể thay đổi dữ liệu của biến toàn cầu
b
thành 249 và khi đó, cờ sẽ được đọc và lưu trữ trên ngăn xếp. Ta sẽ sử dụng lại lỗi này để leak cờ ra ngoài.Để thay đổi
b
, chúng ta cần địa chỉ base của binary vìPIE enabled
. Sau đó, mọi thứ sẽ trở nên dễ dàng hơn.b
3. Khai thác
Giai đoạn 1: Leak địa chỉ base
Đầu tiên, ta sẽ kiểm tra xem tại
%p
thứ mấy sẽ in ra vị trí đầu của dữ liệu được nhập:Chạy script và ta biết vị trí bắt đầu của chuỗi được nhập nằm ở
%6$p
:Ta sẽ kiểm tra stack ngay tại lệnh prinft có lỗi Format String đó để kiếm giá trị bất kỳ thuộc range địa chỉ của binary:
Chúng ta có thể thấy rằng tại địa chỉ
0x7ffe2e91e380
của stack có chứa địa chỉ của__libc_csu_init
, vì vậy ta sẽ leak địa chỉ đó và tính toán để lấy địa chỉ base của binary.Offset của địa chỉ
__libc_csu_init
sẽ nằm ở vị trí%18$p
(6 + 12 = 18
). Payload sẽ thay đổi thành:Chạy script và ta leak địa chỉ của địa
__libc_csu_init
thành công. Sau đó thực hiện tính toán và ta có địa chỉ base của binary:Bây giờ chúng ta sẽ chuyển sang giai đoạn tiếp theo: Thay đổi
b
!Giai đoạn 2: Thay đổi
b
Với địa chỉ base của binary, ta có thể lấy được địa chỉ của
b
một cách dễ dàng. Như ta đã biết, offset của phần đầu dữ liệu nhập vào nằm ở%6$p
và vì đây là tệp 64 bit nên ta cần đặt địa chỉ ở cuối (nếu đặt địa chỉb
ở đầu với null byte trong địa chỉ thì printf() sẽ dừng thực thi tại null byte và giá trịb
sẽ không thay đổi).Ta kiểm tra xem nếu chúng ta nhập đầy đủ 100 byte vào see_balance() thì stack trông như thế nào:
Chạy script và ta kiểm tra stack:
Vậy, ta sẽ muốn đặt địa chỉ của
b
ở cuối dữ liệu nhập vào tại địa chỉ0x7ffda58eb9a0 + 0x8
. Chỉ bằng cách đếm, ta biết được offset là6 + 11 = 17
.Có điều ta cần phải lưu ý, ta không nên sử dụng
%<k>$n
để viết giá trị. Do đó, ta sẽ sử dụng biểu mẫu chuẩn là%n. Ví dụ đoạn mã sau sẽ giống như
%17$p`:Vì vậy, trước tiên ta sẽ đặt địa chỉ của
b
vào cuối dữ liệu đầu vào:Và ta sẽ muốn ghi 249 byte
b
bằng cách thay đổi số byte đưa vào ở dòng thứ 2 của payload bên trên. Payload mới sẽ trông như thế này:Tại sao mình lại viết
249 - 15
, đó là bởi vì%n
sẽ viết số lượng byte trước%n
vào vị trí củab
(sử dụng%p
sẽ cung cấp cho chúng ta địa chỉ chính xác củab
và sử dụng%n
sẽ ghi số byte trước nó vàob
).Payload cho giai đoạn 2 như sau:
Chạy script và kiểm tra trong GDB, ta có thể thấy giá trị
b
đã được thay đổi thành 249:Vậy giờ ta sẽ chuyển sang giai đoạn cuối cùng: Lấy cờ!
Giai đoạn 3: Lấy cờ
Sau khi chúng tôi đã thành công giai đoạn 2, ta chỉ việc chọn tùy chọn thứ hai
Upgrade Pack
để đọc và đưa cờ lên stack. Ta sẽ tạo cờ giả để kiểm tra:Ta có thể thấy rằng cờ được viết trên stack rất gần với rsp. Vì vậy, ta chỉ cần sử dụng Format string ở tùy chọn 1 để leak cờ:
Mình đã thử với
%s
nhưng không thành công nên sử dụng%p
thay thế. Chạy script và ta có thể lấy được cờ giả:Vậy ta chỉ cần chạy trên máy chủ và nhận được cờ dưới dạng hex, sau đó khôi phục nó và ta lấy được cờ.
Full code: solve.py
4. Lấy cờ
Mình đã viết đoạn mã này để chuyển đổi cờ ở định dạng hex thành văn bản như dưới đây:
Convert.py
``` #!/usr/bin/python while True: hx = input('> ') if '0x' in hx: hx = hx.replace('0x', '') tx = '' for i in range(0, len(hx), 2): tx += chr(int(hx[i:i+2], 16)) print(tx[::-1]) ```
Cờ là
p_ctf{W3ll_1t_W4s_3aSy_0n1y}