Write up for pwnable challenges - tetctf 2019

  1. BabyFirst

Binary Infomation:

d4rkn3ss:babyfirst/ $ file babyfirst                                                                                                                                                                                   
babyfirst: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=6ea24d0d62a454fc636fc0fa0cd43044f7886e19, not stripped
d4rkn3ss:babyfirst/ $ checksec babyfirst                                                                                                                                                                               
[*] '.../tetctf/babyfirst/babyfirst'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

Bài này chương trình cho khá đơn giản, nên dễ dàng nhận ra lỗi buffer over flow ở hàm Play. Nhưng muốn đến được đó, trước hết phải bypass qua hàm Login đã.

Do sau khi đăng nhập username được copy vào biến user trên bss bằng hàm memcpy (hàm này k tự terminate string bằng null byte). Vây nên nếu ta đăng nhập 2 lần với username lần lượt là "bdmin", "a" thì sau đó ta sẽ có "admin" được lưu ở biến user. Như vậy đã bypass thành công hàm Login.

Tiếp đến chỉ cần khai thác lỗi buffer over flow, lên shell và cat flag là xong. Nhưng trước hết cần leak được các thông tin cần thiết đã.

Do sử dụng hàm `secure_read` để nhập dữ liệu (hàm này cũng không teminate string luôn) nên nếu ta nhập độ dài buffer thích hợp + không chứa '\n' thì ta có thể leak được ra các thông tin cần thiết như canary và địa chỉ base của glibc. Sau khi có được các thông tin trên, mình ghi đè save return address = địa chỉ one_gadget cho nhanh.Và vậy là lên shell và mình có được flag.

POC: https://github.com/Hi-Im-darkness/CTF/blob/master/tetctf2019/babyfirst/exploit.py

TetCTF{Y0U_4r3_N0T_Baby}

2. BabySandbox

Binary Infomation:

d4rkn3ss:sandbox/ $ file sandbox
sandbox: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=3cd8af04b6bc41bc5b119dead6d4869423945dca, stripped
d4rkn3ss:sandbox/ $ checksec sandbox                                                                                                                                                                                   
[*] '.../tetctf/sandbox/sandbox'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
d4rkn3ss:sandbox/ $ file program 
program: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=5042beceb3ccc21ad0da2841dc8acfdb248ccb84, stripped
d4rkn3ss:sandbox/ $ checksec program                                                                                                                                                                                  
[*] '.../tetctf/sandbox/program'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

Đề bài cho 2 file thực thi là file sandboxprogram. Qua RE cơ bản, ta biết file sandbox sẽ chạy file program và filter 1 số syscall trong quá trình chạy file program.Giờ mình tiến hành phân tích file program trước.

File program là 1 file được biên dịch kiểu static linking và được strip hết symbol.Bước đầu, ta tìm hàm main dựa vào hàm _start. Đổi têncác hàm cho dễ đọc ta được như sau:

Ta nhận ra ngay lỗi buffer over flow ở dòng 6. Nhưng do cuối hàm main, fd 0, 1 tương ứng với stdin và stdout đều đã bị close nên ta sẽ không thể leak được bất cứ thông tin gì. Vậy làm sao để khai thác ?

Do file program là static linking nên sẽ có hàm `dl_make_stack_executable` được implement. Hàm này khi thực thi sẽ thay đổi permission của stack thành rwx, tức có thể thực thi shellcode trên stack được. Nhưng với 1 bài mà symbol bị strip hết như bài này thì làm thế nào để xác định hàm `dl_make_stack_executable` là hàm nào. Cách của mình là tìm hàm mprotect trước. Hàm mprotect thì luôn luôn gọi sys_mprotect (IDA gán nhãn code cho dễ đọc, thực chất đó là syscall 0xa). Vậy nên ta có thể sử dụng tính năng search text của IDA với keyword là "sys_mprotect",ta sẽ tìm được hàm mprotect. Có hàm mprotect rồi thì tra xrefs của nó ta sẽ tìm được hàm `dl_make_stack_executable`.Đổi tên các hàm cho dễ đọc ta được như sau:

Reverse hàm `dl_make_stack_executable`, ta thấy để stack executable được, ta phải set giá trị __stack_prot = 7 và phải truyền địa chỉ libc_stack_end làm tham số thứ nhất khi gọi hàm `dl_make_stack_executable`. Để làm được vậy, ta dùng các ROP gadgets sau:

# 0x00000000004150d4 : pop rax ; ret
# 0x00000000004b4181 : pop qword ptr [rax] ; add ah, ch ; ret

Với 2 ROP gadgets trên, ta có thể gán __stack_prot = 7. Sau đó dùng ROP gadget `0x0000000000400686 : pop rdi ; ret` để gán rdi = libc_stack_end, rồi sau cùng gọi hàm `dl_make_stack_executable`. Sau khi hàm được thực thi, stack được gán rwx, dùng tiếp ROP gadget `0x0000000000450524 : push rsp ; ret` để thực thi shellcode được ghi sẵn ngay sau ROP chain vừa rồi trong stack.

Giờ khoan hãy build shellcode,phân tích file sandbox trước đã.RE file thì thấy nó kiểm tra các syscall của mình và filter 1 vào syscall như sau.

  • syscall number = 59/322 (execve/execveat) thì chặn.
  • syscall number = 2/257/304 (open/openat/open_by_handle_at) thì kiểm tra tham số filename, nếu filename chứa xâu "flag" thì chặn.
  • syscall number = 56/57/58/86 (clone/fork/vfork/link) thì chặn nốt.

Vậy ta phải build 1 shellcode bypass qua các rules kia. Do tác giả build rule chưa chặt, nên có 1 vài cách có thể bypass qua các rule đó. Cách của mình làm để giải bài này thì không phải cách tác giả mong muốn. Cách intended thì 1 anh trong cùng team mình sẽ trình bày. Giờ cứ nói về cách của mình đã.

Idea của mình là lợi dụng việc rule không hề check arch, nên mình hoàn toàn có thể switch sang mode x86 để thực thi các syscall theo ý mình mà không cần quan tâm rule như nào. Các bước chi tiết để switch mode thì đã được 1 người anh của mình hướng dẫn rất chi tiết trong 1 bài writeup khác, nên mình không nhắc lại nữa. Các bạn có thể tham khảo ở link dưới đây.

https://github.com/phieulang1993/ctf-writeups/tree/master/2018/WhiteHatGrandPrix2018quals/giftshop_pwn01

Trước khi switch mode và chuyển còn trỏ eip sang vùng nhớ mới, ta cần copy shellcode từ stack qua vùng nhớ mới đó trước, để ngay sau khi switch mode xong, shellcode tại vùng nhớ mới có thể thực thi được ngay. Mình dùng lệnh assembly sau đây để thực hiện việc copy đó.

rep movs qword ptr [rdi], qword ptr [rsi]

Do stdinstdout của chương trình đều đã bị close, nên ta cần tạo 1 kết nối socket mới đến vps của mình. Và sau đó có thể open->read->write flag qua kết nối đó. Nhưng 1 vấn đề nữa nảy sinh là độ dài buffer mình được nhập là 0x200, qua cả tá công việc như trên thì buffer không còn đủ để viết hết shellcode open-read-write nữa. Nên mình phải read thêm shellcode từ vps vào, rồi sau đó mới có thể thực thi open-read-write được.

Các bạn tham khảo poc của mình để biết thêm chi tiết. :D

POC: https://github.com/Hi-Im-darkness/CTF/blob/master/tetctf2019/babysanbox/exploit.py

TetCTF{H4PPY->N3W->Y34R}

3. BabyHeap

d4rkn3ss:baby_heap/ $ file pwn03                                                                                                                                                                                       [23:25:03]
pwn03: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter .../lib/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=b5f987c24714e8d80308e7668bba50e2459e163a, not stripped
d4rkn3ss:baby_heap/ $ checksec pwn03                                                                                                                                                                                  
[*] '.../tetctf/baby_heap/pwn03'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
d4rkn3ss:baby_heap/ $ ./pwn03
WELCOME TO BABY HEAP
MENU
1.ALLOC
2.EDIT
3.REMOVE
4.SHOW
5.EXIT
YOUR CHOICE :

Đây là 1 bài ctf về heap đúng theo format cổ điển với menu xen,xem,xóa,sửa :v . 1 lưu ý là file libc cung cấp là phiên bản 2.23 tức là không có tcache. Rồi giờ cùng bắt tay vào phân tích các hàm nào.

  • ALLOC: malloc 1 vùng nhớ với size luôn luôn là 0x98, tức là size của chunk sẽ là 0xa8 nên khi free sẽ rơi vào unsorted bin. Các vùng nhớ sau khi được cấp phát sẽ được lưu tại 1 mảng trên vùng nhớ bss.
  • EDIT: nhập thông tin cho pointer đã malloc. 8 byte đầu của vùng nhớ pointer trỏ đến được tách ra thành 1 biến long, còn lại là mảng char, được đặt tên là content. Nhập dữ liệu cho content bằng hàm `scanf` với format "%144s" tức vừa bằng size của buffer content đó.Trong trường hợp này, nếu người dùng đưa vào đúng 144 bytes, sẽ dẫn đến lỗi null byte off-byte-one.
  • REMOVE: free vùng nhớ được cấp phát trước đó, và xóa luôn pointer trỏ đến vùng nhớ đó khỏi mảng trên bss.
  • SHOW: show thông tin của 1 vùng nhớ. Do vùng nhớ được free hay malloc đều không clear data có sẵn, nên ta có thể dùng hàm SHOW này để leak các thông tin cần thiết.

Để khai thác, ta cứ đi từ dễ đến khó, nên ta sẽ leak data trước đã. Mình nhắm đến 2 thông tin cần lấy được đó là base address của heap và base address của glibc.Để có thể lấy được cả 2 thông tin này cùng 1 lúc ta sẽ tiến hành như sau: Đầu tiên malloc 4 chunk, sau đó free chunk với index= 0 và 2. Lúc này trong unsorted bin sẽ chứa 2 chunk,vậy nên trong 2 chunk, sẽ có 1 chunk có con trỏ fd trỏ vào main_arena+88, và bk trỏ vào chunk còn lại - tức 1 địa chỉ trên heap. Và bây giờ khi ta malloc 1 chunk mới,chunk như đã nhắc ở trên sẽ được lấy ra khỏi unsorted bin, và do không clear data trong chunk, nên khi dùng function SHOW, ta có thể leak được cả 2 thông tin mình cần.

Bước tiếp theo khó hơn là khai thác. Dựa vào 1 lỗi null byte off-byte-one kia, mình sẽ tạo ra các chunk overlap lên nhau. Như đã biết, khi 1 chunk được free (sẽ rơi vào unsorted bin), nó sẽ check 2 chunk liền kề phía trước và phía sau xem có gộp vào cùng nhau được không. Vì size của 1 chunk là 0xa8, nên ta không thể dùng null byte tràn đó để ghi đè lên size 1 chunk bình thường được (vì lúc đó size = 0 -> crash), nên mình nghĩ đến việc tấn công vào top_chunk. Vì size của top_chunk lớn nên hoàn toàn có thể ghi đè null byte vào cuối size.

Cho dễ hình dung, mình dùng 4 chunk có index từ 0 - 3, với chunk thứ 3 được malloc ngay liền kề top chunk. Khi ghi đè null byte vào size của top chunk tức PREV_INUSE bit của top_chunk = 0 tức chunk thứ 3 được coi là đã free. Vậy nên khi ta free chunk idx = 2, nó sẽ gộp 2 chunk thứ 2 và thứ 3 vào nhau (unlink chunk idx = 3) tức có thể overlap các chunk. Để được vậy , còn 1 điều kiện nữa cần thỏa mãn đó là :

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                     
malloc_printerr (check_action, "corrupted double-linked list", P);

Trước khi unlink các chunk thứ 3, libc sẽ kiểm tra điều kiện xem bk của chunk FDfd của chunk BK của chunk đó có cùng trỏ tới nó hay không. Nếu không thỏa mãn sẽ abort. Để bypass đoạn này khá dễ, vì ta đã leak được heap base addr từ trước, nên ta hoàn toàn có thể fake các chunk BK, FD đó được.

Sau khi gộp 2 chunk 2, 3 vào với nhau, ta malloc thêm 1 lần, lúc này ta có trong unsorted bin có chunk thứ 3, mặc dù nó chưa hề được free. Đến bước này, chỉ cần fake con trỏ bk của chunk thứ 3, ta có thể malloc vào 1 địa chỉ tùy ý. Và mình chọn malloc vào phần data của chunk có idx = 1, mục đích để mình có thể toàn quyền chỉnh sửa header hay data của chunk mới malloc này. Và tất nhiên trước khi malloc vào đó, mình phải tạo ra các fake header để bypass các check của libc khi malloc.

Sau khi đạt được mục đích, mình dùng 1 phương pháp tấn công heap là House of Orange ,ghi đè IO_list_all để lên shell cat flag.

POC: https://github.com/Hi-Im-darkness/CTF/blob/master/tetctf2019/babyheap/exploit.py

TetCTF{Roi_Ai_Cung_Bo_Anh_Di}

4. Easy Web Server.

Bài này là bài khiến mình bế tắc nhất của giải này.

d4rkn3ss:webpwn/ $ file service
service: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=76da79c3bd1cfd31e22f441bf4637a4a528e8a7d, stripped
d4rkn3ss:webpwn/ $ checksec service
[*] '.../tetctf/webpwn/service'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

Đề cho 1 webserver api viết bằng c++ và file binary service đó đã bị strip hết symbol (ác mộng thật)... Ác mộng thì cũng phải đối mặt thôi. Cùng bắt tay vào phân tích.

Bài này mình sẽ đi nhanh vào phần exploit luôn, bỏ qua phần RE, fuzz, tâm linh các kiểu :v. Dạo qua các tính năng của app, ta thấy phải đăng nhập dưới quyền admin thì mới có thể dùng hết các tính năng. Password của admin thì được đặt ở đường dẫn /secret/password nên sure là không được. RE hàm login thấy dài chứa buffer password là 256, nên mình cứ thử nhập pass là 'a'*256 xem sao, và bùm :v.

Giờ có password rồi.Đăng nhập admin thôi và tìm cách đọc file /secret/flag là được. Và tất nhiên không thể đọc trực tiếp từ url "GET /secret/flag" được. Nên mình tìm cách đọc file này từ 1 hàm nào đó khác.Và hàm đọc file thì có hàm "GET /info?ip=xx.xx.xx.xx", nên tập trung vào hàm này.RE hàm này ta thấy đường dẫn file mà app sẽ mở được tạo ra từ hàm `snprintf` như sau:

snprintf(&s, 0x100uLL, "info/%s/%s.txt", v26, v25);

Trong đó v26 và v25 đều là tham số giá trị của tham số ip mà mình gửi lên. Vậy nếu thông thường mình gửi lên là "GET /info?ip=127.0.0.1" thì file được mở sẽ là "info/127.0.0.1/127.0.0.1.txt". Vậy giờ chỉ cần s trỏ đến file flag, ta sẽ có được nội dung flag.

Chú ý kỹ hơn chút ta thấy hàm `snprintf` có truyền vào 1 tham số nữa là 0x100 tức length tối đa của buffer s, vậy nếu tham số ip mình truyền lên có độ dài lớn hơn 0x100 thì xâu s trả về chỉ có chứa tham số v26 mà không chứa tham số v25. Tận dụng điều này và sử dụng path truncation, mình có thể đưa s trỏ đúng vào file flag.

Payload: GET service.chung96vn.cf:8080/info?ip=42.112.135.99/../..////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////secret/flag

1 lưu ý là ip mình truyền lên phải đúng là ip của mình, nếu không server sẽ trả về lỗi Internal Error.

TetCTF{Th4nk_4_pl4ying_my_ch4ll3ng3s}

5. fmt

d4rkn3ss:fmtstr/ $ file fmt                                                                                                                                                                                             
fmt: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/d4rkn3ss/Data/study/IT/Security/Tool/Libc/2.23/64bit/lib/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=2d6914e0f65a30412cdda27b07045fa6b6805856, not stripped
d4rkn3ss:fmtstr/ $ checksec fmt                                                                                                                                                                                        
[*] '.../tetctf/fmtstr/fmt'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
d4rkn3ss:fmtstr/ $ seccomp-tools dump ./fmt                                                                                                                                                                             [1:59:42]
Welcome to the loop
a
a
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x06 0x00 0x40000000 if (A >= 0x40000000) goto 0010
0004: 0x15 0x04 0x00 0x00000000 if (A == read) goto 0009
0005: 0x15 0x03 0x00 0x00000001 if (A == write) goto 0009
0006: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0008
0007: 0x06 0x00 0x00 0x0005000d return ERRNO(13)
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x00000000 return KILL

Chương trình có cấu trúc khá đơn giản:

Cùng phân tích:

Hàm `setup()` mở file flag và lưu fd tại 1 biến trên bss.Do cơ chế file decriptor luôn luôn tăng dần , nên fd tương ứng với file flag là 3.

Chương trình có áp dụng seccomp để filter 1 số syscall, nhìn vào rule được dump ở trên ta thấy chỉ có hàm read/write được allow, và nếu dùng hàm open, sẽ trả về ERRNO, còn các syscall khác đều sẽ bị kill.

Chỉ có duy nhất lỗi format string, nhưng chương trình lại enable cơ chế FORTIFY nên việc khai thác rất khó khăn.

Giờ nhờ google thôi.Kết quả mình tìm được đầu tiên là 1 paper trên phrack nói về việc bypass FORTIFY bằng int overflow, mình cố gắng làm theo nhưng kết quả không khả quan. Lại google tiếp, thậy may mắn mình đã tìm được 1 writeup từ giải hxp2017 nói về việc bypass FORTIFY bằng chính seccomp rule đã cài đặt.

https://pwning.re/2017/11/19/hxp-flag-store/

Như bài này, seccomp rule đã có sẵn open,nên sau khi seccomp rule được cài đặt, ta đã có thể dùng đc luôn '%n'. 1 lưu ý nữa là bạn phải sử dụng bản libc 2.23 vì nếu dùng libc 2.27 thì khi mở file, nó mặc định sẽ dùng openat thay cho open.

Hiện tại tuy đã có '%n' nhưng do seccomp được cài đặt, ta chỉ có thể dùng read, write. Do đã biết fd ứng với file flag đã mở= 3, nên mình sẽ dùng ROP chain để giải quyết nốt bài này. Trong chương trình có sẵn hàm readputs. Nên giờ mình chỉ cần leak stack address ra nữa là đủ.

Để xử lý phần ROP 1 cách nhanh gọn nhất, mình lựa chọn ghi đè save return address khi gọi hàm `__printf_chk` và ghi luôn ROP chain vào trong buffer format string. Khi gọi hàm `__printf_chk`, save return address là 0x400B1B, mình chọn ghi đè nó thành 0x400BA6 (add rsp, 8; pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; retn) tức chỉ cần ghi đè 1 byte cuối, payload lúc này sẽ là : fmtstr.ljust(56, 'a')+ropchain. Khá là nhanh gọn.

POC: https://github.com/Hi-Im-darkness/CTF/blob/master/tetctf2019/fmt/exploit.py

TetCTF{Anh_Dech_Can_Gi_Nhieu_Ngoai_Em}

Cảm ơn các bạn đã cố đọc hết bài writeup khá dài của mình. Và cuối cùng xin cảm ơn các anh tsu, chung96vnhocsama đã tổ chức 1 giải ctf với các challenges chất lượng.