BKCTF 2023
Solutions for some challenges in BKCTF 2023
BKCTF 2023
BKCTF là giải mà mình lần đầu tiên được tham gia onsite. Host là câu lạc bộ BKSEC của Trường Đại học Bách khoa Hà Nội, nơi đào tạo kỹ thuật hàng đầu tại Việt Nam, là niềm mơ ước của biết bao thế hệ học sinh, sinh viên trong nước. Mình nhớ tới BKSEC vì có biết một số anh chị rất khủng và có tiếng tăm trong ngành như anh chung96vn, chị lanleft, anh hacmao, …
Sau một năm, mình muốn chơi lại giải này để xem thử trình độ của mình đã tiến bộ được chút nào hay chưa. Đề bài mình chơi vẫn đang được mở trên web Cookie Hân Hoan, các bạn hoàn toàn có thể vào chơi và tận hưởng bộ đề theo mình nghĩ là khá thú vị.
Let’s get started
rev/BabyStack
- Given files: BabyStack.zip
- Difficulty: Hard
- Description: Stack up to the moon. Flag format:
BKSEC{}
Solution
Theo quan điểm cá nhân của mình, bài này không thực sự quá khó. Nếu ai đã từng có một chút kinh nghiệm làm các dạng bài StackVM thì sẽ thấy bài này khá nhẹ nhàng. Mình sẽ cố gắng đi chi tiết từng thao tác nhỏ để các bạn mới có thể dễ dàng tiếp cận. Happy hacking …
Overview & Clean code
Đề bài cho chúng ta một file PE 64 bit StackVM.exe
với mã giả dài hơn 300 dòng, chủ yếu là khai báo và gán giá trị cho các biến.
Sau khi nhìn tổng quan, ta thấy chương trình khởi tạo cho vm một loạt bytecode như thế này
Tiếp theo, chương trình cho nhập vào Buffer
và kiểm tra kích thước xem có bằng 20 không.
|
|
Đầu tiên, chúng ta phải đi định nghĩa lại kích thước của mảng bytecodes[]
và Buffer[]
để chương trình nhìn gọn gàng hơn.
Đặt lại cho mảng Buffer[]
có kích thước 20 bytes và đổi tên thành input[]
.
và mảng bytecodes[]
là 400 bytes.
Tại sao mình tính được kích thước là 400 bytes. Vì
bytecodes
bắt đầu từv24 [rsp+60h]
, kết thúc ởv131 [rsp+1E8h]
, vậy nên 0x1E8 - 0x60 + 8 = 400
Okay, chương trình đã ngắn hơn một xíu rồi. Tiếp tục quan sát đoạn code dưới đây, ta thấy chương trình sử dụng vtable. Hiểu một cách đơn giản, vtable như là một cái bảng chứa các hàm, chương trình cần dùng hàm nào thì nhảy vào đó mà lấy.
Ở đây mình sẽ tạo 1 struct cho vtable có kích thước 40 byte, đúng bằng kích thước của v19
. Double click vào vtable
, bôi đen toàn bộ các hàm, chuột phải và create struct. Đặt tên cho struct này là struct_vtable
, tên các field mình vẫn giữ nguyên, sau này khi phân tích kỹ càng hơn mình sẽ rename sau.
Thường những bài StackVM, mọi thao thác đều diễn ra trên cùng một stack. Và có 2 thứ không thể thiếu đó là:
stack_base
: địa chỉ gốc stackstack_esp
: địa chỉ đỉnh ngăn xếp
Nhìn vào mã giả, mình đoán chắn chắn v19[4]
là stack_base
và v19[3]
là stack_esp
. Còn v19[1]
và v19[2]
chưa rõ nên mình không định nghĩa.
Tạo tiếp một struct struct_vm
như sau
và ép kiểu cho field đầu tiên là *struct_vtable
mà chúng ta đã định nghĩa ở phía trên.
Right click v19
, nhấn Convert to Struct * ...
và chọn struct_vm
để sửa lại cấu trúc cho v19
.
Analyze
Chúng ta có thể thấy input
được load vào mảng bytecodes[]
như sau:
|
|
Nếu chú ý, ta có thể thấy các bytecodes
chứa input
liền kề nhau từng đôi một. Vậy rất có thể, chương trình sẽ đi xử lý từng cặp một của input
.
Đoạn xử lý chính của chương trình nằm ở đây
|
|
Tóm tắt đoạn code trên như sau:
- Nếu
[idx + 1] == 6
thì- Instruction sẽ có kích thước 4 byte, bắt đầu từ
[idx]
tới[idx + 3]
- Value sẽ là sự kết hợp giữa
[idx + 2]
và[idx + 3]
- Được xử lý bởi hàm
PUSH
- Instruction sẽ có kích thước 4 byte, bắt đầu từ
- Nếu
[idx + 1] != 6
thì:- Instruction sẽ có kích thước 2 byte, bắt đầu từ
[idx]
tới[idx + 1]
- Dựa vào
[idx + 1]
mà có 7 lựa chọn để gọi hàm xử lý:CMP = 0
XOR = 1
ADD = 2
SUB = 3
SHL = 4
SHR = 5
POP = 7
AND = 8
- Instruction sẽ có kích thước 2 byte, bắt đầu từ
Dưới đây là minh họa cho việc mình rename và retype cho hàm PUSH
. Các hàm khác các bạn làm tương tự thì code sẽ clean hơn rất nhiều.
|
|
Solve
Sau khi đã hiểu cách thức hoạt động, mình đã lấy toàn bộ giá trị của mảng bytecodes[]
và viết một đoạn code Python nhỏ để xem chương trình đang thực hiện những thao tác gì.
|
|
Mình sẽ thử phân tích một phần nhỏ kết quả thu được đầu tiên với input
= abcdefghiklm12345678
|
|
PUSH
3 số 0x1, 0xC0D, 0x8 vào stack,ESP
sẽ ở 0x8SHR
là dịch phải: 0xC0D » 0x8 = 0xC,ESP
sẽ là 0xCPUSH
2 số 0x2238, 0xFF00 vàAND
với nhau. Kết quả là: 0x2238 & 0xFF00 = 0x2200ADD
sẽ cộng 2 số đầu tiên trên stack: 0x2200 + 0xC = 0x220CPUSH
0x6261 là 2 byte đầu tiên củainput
XOR
0x220C ^ 0x6261 = 0x406DCMP
kết quả trên với0x694E
Phía trên chỉ là toàn bộ phỏng đoán của mình. Để kiểm chứng, mình debug và check ở hàm CMP
xem logic trên có thực sự đúng không.
Correct…
Với việc dump được ra các instruction, chúng ta hoàn toàn có thể giải tay ra được flag. Nhưng để tiết kiệm thời gian, mình sẽ chỉ đặt breakpoint ở hàm XOR
và hàm CMP
để lấy các kết quả cuối cùng.
Một số lưu ý nhỏ:
- Ta thấy trong đống
bytecodes[]
kia, 2 byte0x01, 0x00
đại diện cho lệnhCMP
. Vậy chắc chắn trước đó sẽ là lệnhPUSH
giá trịcipher
để so sánh kết quả đã xor. Từ đó ta không cần đặt breakpoint ở hàmCMP
nữa. - Việc thực hiện 1 loạt biến đổi rồi xor với 2 byte
input
ta không cần quan tâm. Chỉ cầnF9
và xem thử có input của mình không. Nếu có thì đó là giá trị chính xác.
|
|
Flag thu được là BKSEC{C0nGratul4t31}
rev/Reality
- Given files: reality.zip
- Difficulty: Medium
- Description: A simple reversing challenge… Flag format:
BKSEC{}
Solution
Đề bài cho chúng ta một file PE32 reality.exe
. Khi decompile file này, IDA không cho chúng ta mã giả. Mình sẽ đi đọc mã assembly kết hợp debug để xem chương trình đang làm gì.
Về tổng quan, chương trình cho nhập input và luôn nhảy vào block có exception khi chúng ta debug.
Nhìn ở block bên cạnh, mình thấy có một chuỗi khá khả nghi BKSEECCCC!!!
. Thử xem qua hàm sub_401220
được gọi trong block này, ta thấy đây đơn giản chỉ là một hàm xor input với key là chuỗi phía trên.
|
|
Mình debug và sửa EIP
cho nó trỏ vào khối này. Ta thấy được rất nhiều bytecode ở dưới, mình dùng make code thì thấy đó là những phép biến đổi rất phức tạp.
Ta thấy ở loc_40131F
là câu lệnh
|
|
nghĩa là cứ đến đây nó sẽ bị lặp vô tận. Mình nhận ra có gì đó sai sai nên đã ấn d
để tách hết thành từng bytecode và ấn c
để make code lại. Kết quả thu được như sau
Yeah, đến đây thì rõ ràng rồi. Chương trình check xem ta có đang debug không. Nếu có sẽ nhảy vào đống tính toán phức tạp kia, ngược lại sẽ nhảy tới loc_401AD5
.
Tại sao mình biết đây là anti-debug, câu trả lời các bạn có thể xem ở stackoverflow.
Ở loc_401AD5
chỉ là gán giá trị cho cipher[]
. Vậy chúng ta chỉ cần lấy mảng này xor ngược lại với key phía trên là có được flag.
|
|
Flag thu được là BKSEC{e4sy_ch4ll_but_th3r3_must_b3_som3_ant1_debug??}
rev/Checker
- Given files: checker.zip
- Difficulty: Easy
- Description: a checker ran with rice tree. Flag format:
BKSEC{}
Solution
Updating …
pwn/File Scanner
- Given files: bkctf2023-file-scanner.zip
- Difficulty: Medium
- Description: The most powerful tool maybe the worst :(. Flag format:
BKSEC{}
Solution
Chương trình tạo 1 số random 16 byte và bắt chúng ta phải nhập chính xác số random đó.
|
|
Ta hoàn toàn có thể bypass hàm strncmp
với input \n
.
Dễ thấy ý đồ của tác giả là muốn sử dụng kỹ thuật File Structure Attack. Ở option 4, chương trình có bug BOF như sau
|
|
Từ biến name
, ta hoàn toàn overwrite được filePtr
lẫn fileContent
. Vậy mình hoàn toàn fake được file structure
và vtable
sao cho toàn bộ hàm trong vtable
đều là system
.
Để leak được libc
, chúng ta có thể mở /proc/self/maps
hoặc /proc/self/syscall
.
Full exploit
|
|
Flag của bài toán là BKSEC{fSoP_1s_n0t_2_hArd_4_u_d4e2411f244126da3242265c90e10c46}