FLAREON 2020 - WRITEUP PHẦN 1

>>

Mở đầu

Flare-on CTF

Flare-on là sân chơi CTF hàng năm của những người chơi hệ nhị phân 👨‍💻, reverse engineer, các nhà nghiên cứu về mã độc và bảo mật. Một kỳ Flare-on CTF diễn ra trong vòng 6 tuần, thông thường gồm 11 challenge tương ứng với 11 level đầy đủ các thể loại và platform (Windows, Linux, Python, VBA, .NET, …)

Vì sao nên chơi Flare-on

  • Các challenge này được Fireeye mô tả là “một trong những vấn đề mà team Fireeye phải đối mặt hằng ngày”, tức là những challenge này đều lấy ý tưởng từ những vấn đề có thật (có bài dùng cả malware thật), nên đề bài sẽ có cảm giác “real-world” hơn so với các bài RE trong các đề thi CTF khác.
  • Nếu bạn là 1 người chơi RE thì không nên bỏ qua Flare-on, nhất là với các bạn mới chơi RE. Bởi vì, đề Flare-on tuy khó, các bạn có thể không giải hết các challenge, nhưng trong vòng 6 tuần đó bạn sẽ học được nhiều thứ mới (như tính kiên trì chẳng hạn 😄 !!)
  • Sau khi kì thi kết thúc, Fireeye sẽ đưa ra lời giải chi tiết cho các challenge, bạn có thể tham khảo cách họ làm. Ngoài ra trên mạng cũng có rất nhiều write-up cho các bạn tham khảo, từ đó bạn có thể học hỏi cách họ làm, học cách tiếp cận bài toán, dùng tool để giải bài, …

Mình bắt đầu chơi Flare-on từ năm ngoái, và cũng không giải được hết các challenge, tuy nhiên ít nhất mình đã không bỏ cuộc cho tới phút cuối cùng. Năm nay mình tiếp tục tham gia Flare-on và đã giải được đủ 11 bài, và đây là write-up cho 11 bài đó !

Phần 1

Mình sẽ chia 11 bài writeup ra thành nhiều phần khác nhau 😄, ở phần 1 này chúng ta cùng khởi động với 3 bài đầu tiên đó chính là: Fidler (solution), garbage (solution) và Wednesday (solution)

1 - Fidler

1
2
3
4
5
Welcome to the Seventh Flare-On Challenge!

This is a simple game. Win it by any means necessary and the victory screen will reveal the flag. Enter the flag here on this site to score and move on to the next level.

This challenge is written in Python and is distributed as a runnable EXE and matching source code for your convenience. You can run the source code directly on any Python platform with PyGame if you would prefer.

Như tiêu đề, bài này bao gồm file .exe và file source code .py.

Chạy file exe:

Chương trình yêu cầu nhập password, ta mở source code ra:

Ta dễ dàng lấy được password là ghost, nhập vào, ta sẽ chuyển sang màn hình tiếp theo:

Ta phải click vào con mèo 100 tỷ lần để lấy được flag, hoặc ta có thể xem chương trình tạo flag như nào để có thể lấy được nó.

Ta có thể mở source code của game này ra để đọc, đó chính là file fidler.py, lướt qua nhìn tên các hàm thì có một hàm liên quan tới “flag”, đó là hàm decode_flag:

1
2
3
4
5
6
7
8
9
10
11
12
def decode_flag(frob):
last_value = frob
encoded_flag = [1135, 1038, 1126, 1028, 1117, 1071, 1094, 1077, 1121, 1087, 1110, 1092, 1072, 1095, 1090, 1027,
1127, 1040, 1137, 1030, 1127, 1099, 1062, 1101, 1123, 1027, 1136, 1054]
decoded_flag = []

for i in range(len(encoded_flag)):
c = encoded_flag[i]
val = (c - ((i%2)*1 + (i%3)*2)) ^ last_value
decoded_flag.append(val)
last_value = c
return ''.join([chr(x) for x in decoded_flag])

Sau đó select chữ "decode_flag" trong notepad++, thì nó sẽ tự select tất cả các chỗ khác có chứa "decode_flag"

Ta có thể thấy 1 reference khác của "decode_flag" ở hàm victory_screen, ta lại tiếp tục select chữ "victory_screen" để tìm reference:

Ta lại tiếp tục làm như trên với hàm game_screen:

Vậy là ta đã biết hàm game_screen được gọi như nào, tuy nhiên điều đó cũng không quá quan trọng vì ta sẽ chỉ quan tâm tới hàm decode_flag.

Chỉ có duy nhất 1 chỗ gọi hàm decode_flag, đó là chỗ này:

1
2
3
4
5
6
7
8
9
# ... truncated 
while not done:
target_amount = (2**36) + (2**35)
if current_coins > (target_amount - 2**20):
while current_coins >= (target_amount + 2**20):
current_coins -= 2**20
victory_screen(int(current_coins / 10**8))
return
# truncated ...

Dễ thấy current_coin phải lớn hơn hoặc bằng 2**36+2**35 và bé hơn 2**36+2**35+2**20 thì dòng victory_screen(int(current_coins / 10**8)) mới được gọi.

Khi đó ta viết lại đoạn code ở trên với frob=int((2**36+2**35)/10**8)

1
print (decode_flag(int((2**36+2**35)/10**8)))

Run:

1
idle_with_kitty@flare-on.com

2 - garbage

1
One of our team members developed a Flare-On challenge but accidentally deleted it. We recovered it using extreme digital forensic techniques but it seems to be corrupted. We would fix it but we are too busy solving today's most important information security threats affecting our global economy. You should be able to get it working again, reverse engineer it, and acquire the flag.

Ta giải nén và chạy thử file exe:

File đã bị corrupt đúng như đề bài nói. Tiếp theo ta dùng Detect it easy để lấy thêm thông tin về file.

Detect it easy là một tool dùng để xác định xem 1 file PE được biên dịch bằng compiler nào, được pack bằng packer nào, từ đó ta có thể có sử dụng các tool thích hợp để phân tích file đó.

File được nén bằng UPX, ta sẽ dùng đúng tool UPX để unpack file này.

UPX là một tool dùng để nén file executable, open source, bạn đọc có thể xem ở đây.

UPX không thể unpack file này, có thể là vì nó đã bị lỗi. Giờ ta mở file lên bằng PE-bear để xem phần nào trong file này bị lỗi.

PE-bear là một tool dùng để xem PE header của 1 file PE, bạn đọc có thể tải ở đây.

Ta thấy ngay phần section .rsrc bị lỗi (màu đỏ).

Ta phân tích tiếp: mục .rsrcRaw size0x124, tức là khi map file lên bộ nhớ tại Virtual address 0x19000, phần data thật sự sẽ kết thúc ở 0x19123, trong khi ở hình trên thì phần Import Directory bắt đầu ở Virtual address 0x191DC, tức là nằm ngoài vùng data thật sự (3 vùng màu đỏ ở hình trên đều nằm ngoài vùng data).

Như vậy ta chỉ cần pad thêm vào file các byte null để nó có thể bao hết các Virtual address trên. Ta cần thêm vào 0x1929C+0x10-(0x19000+0x124) = 0x188 byte.

Nhưng chưa đủ, nếu ta thêm chỉ chừng đó byte thì chưa chắc file đã “hoạt động” được, vì dung lượng của file phải chia hết cho File alignment (tham khảo về PE struct tại đây).

Như hình trên, File alignment0x200. Vậy là sau khi thêm, ta phải đảm bảo là size của file này chia cho 0x200 dư 0, đoạn code dưới đây mình đã pad thêm cho tới bội của 0x2000x400.

1
2
3
4
5
6
7
8
9
# python3
if __name__ == '__main__':
with open('garbage.exe', 'rb') as f:
with open('fix.exe', 'wb') as f2:
data = f.read()
n = 0x9E00+0x400-0x9F23
data = data + b'\x00' * n
f2.write(data)
print ('[+] Done')

Sau khi chạy code trên, ta có file fix.exe và có thể được unpack bởi UPX

Ta mở file mới lên trong IDA và bấm F5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int __cdecl main(int argc, const char **argv, const char **envp)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

v12 = 741548835;
v13 = 1231306510;
strcpy(&v10, "nPTnaGLkIqdcQwvieFQKGcTGOTbfMjDNmvibfBDdFBhoPaBbtfQuuGWYomtqTFqvBSKdUMmciqKSGZaosWCSoZlcIlyQpOwkcAgw ");
v14 = 67771914;
v15 = 436344355;
v16 = 604530244;
strcpy(&v11, "KglPFOsQDxBPXmclOpmsdLDEPMRWbMDzwhDGOyqAkVMRvnBeIkpZIhFznwVylfjrkqprBPAdPuaiVoVugQAlyOQQtxBNsTdPZgDH ");
v17 = 745804082;
v18 = 255995178;
// truncated ...
sub_401000(&v28, 20, &v11, 0);
v3 = MEMORY[0x12418](v9, 0x40000000, 2, 0, 2, 128, 0);
sub_401045(&v9);
if ( v3 != -1 )
{
v8 = 0;
sub_401000(&v12, 61, &v10, v4);
MEMORY[0x123F8](v3, v9, 61, &v8, 0);
sub_401045(&v9);
MEMORY[0x12426](v3);
sub_401000(&v28, 20, &v11, v5);
MEMORY[0x12442](0, 0, v9, 0, 0, 0);
sub_401045(&v9);
}
v6 = MEMORY[0x123E4](-1);
MEMORY[0x12404](v6);
return 0;
}

Ta thấy có một số chỗ MEMORY[0x?????], đó là vì khi pack file bằng UPX, phần Import data nằm ở đó (và bị phá huỷ), nên khi ta thêm null byte vào, nó sẽ không chính xác, tuy nhiên điều này cũng không quá quan trọng.

Hai hàm sub_401000sub_401045 lần lượt là hai hàm set key và decrypt data bằng phép xor cơ bản, ta dùng script sau để solve.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# python3
from struct import pack

def p32(n: int) -> bytes:
return pack('<I', n)

def dec(data: bytes, key: bytes) -> bytes:
key_len = len(key)
r = b''
for i in range(len(data)):
r = r + (data[i] ^ key[i % key_len]).to_bytes(1, 'little')
return r

if __name__ == '__main__':
d1, d2 = [], []

d1.append(741548835)
d1.append(0x49643F0E)
d1.append(67771914)
d1.append(436344355)
d1.append(604530244)
d1.append(745804082)
d1.append(255995178)
d1.append(224677950)
d1.append(387646557)
d1.append(84096534)
d1.append(134815796)
d1.append(237248867)
d1.append(1479808021)
d1.append(981018906)
d1.append(1482031104)
d1.append(84)

d2.append(989990456)
d2.append(874199833)
d2.append(1042484251)
d2.append(1108412467)
d2.append(1931350585)

d1 = b''.join([p32(i) for i in d1])
d2 = b''.join([p32(i) for i in d2])

k1 = b'nPTnaGLkIqdcQwvieFQKGcTGOTbfMjDNmvibfBDdFBhoPaBbtfQuuGWYomtqTFqvBSKdUMmciqKSGZaosWCSoZlcIlyQpOwkcAgw '
k2 = b'KglPFOsQDxBPXmclOpmsdLDEPMRWbMDzwhDGOyqAkVMRvnBeIkpZIhFznwVylfjrkqprBPAdPuaiVoVugQAlyOQQtxBNsTdPZgDH '

print (dec(d1,k1))
print (dec(d2,k2))

Run:

1
2
b'MsgBox("Congrats! Your key is: C0rruptGarbag3@flare-on.com")\x00Fqv'
b'sink_the_tanker.vbs\x00'

3 - Wednesday

1
Be the wednesday. Unlike challenge 1, you probably won't be able to beat this game the old fashioned way. Read the README.txt file, it is very important.

Đề bài cho 1 game 2D, ta mở lên chạy thử

Game chỉ đơn giản là vượt qua các chướng ngại vật (ngồi xuống hoặc nhảy để né vật thể).

Tuy nhiên, có những chỗ như hình trên, nhìn bằng mắt thì phải ngồi xuống để vượt qua, nhưng khi ngồi xuống thì character của chúng ta die ngay lập tức, hoặc có chỗ nhìn thì đáng ra phải nhảy lên, nhưng thật ra phải ngồi xuống mới qua được.

Sau 1 thời gian ngồi chơi game, mình phát hiện ra rằng:

  • Game không thay đổi vị trí các vật thể sau các lần chết, để đạt được 8 điểm đầu tiên thì trạng thái lần lượt là: ngồi, ngồi, nhảy, nhảy, ngồi, ngồi, ngồi, nhảy.
  • Chỉ có thể nhảy hoặc ngồi mới vượt qua được vật thể, đứng yên là die.

Vậy chỉ có 2 trạng thái là có thể giúp ta vượt qua vật thể, đến đây mình nghĩ ngay tới số nhị phân, vì số nhị phân cũng chỉ có 2 chữ số là 0 và 1. Với loạt trạng thái như ở trên thì số nhị phân có thể là: 00110001 hoặc 11001110.

Tìm thử dãy binary 00110001 trong memory ta được:

Ở trên hình, mình đã select 296 byte, giá trị của các byte này chỉ là “00” hoặc “01”. Ta có thể nghĩ ngay tới số nhị phân vì số nhị phân cũng được biểu diễn từ các con số 0 và 1. Vì vậy đoạn code dưới đây mình đã ghép 8 byte lại với nhau, rồi in ra màn hình. Lý do mình ghép 8 mà ko phải là 7 hay 9 là vì mỗi byte được tạo nên từ số nhị phân có 8 chữ số.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# python3

if __name__ == '__main__':
d = [0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01,
0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01,
0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00,
0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01,
0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01,
0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00,
0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00,
0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01,
0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01,
0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01,
0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01,
0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01,
0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01,
0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01,
0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01,
0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00,
0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01,
0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00,
0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00,
0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01,
0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01,
0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00,
0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01,
0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01,
0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01,
0x01, 0x00, 0x01, 0x01, 0x00, 0x01
]
assert len(d) % 8 == 0
s = ""
for i in range(len(d) // 8):
tmp = 0
for j in range(8):
tmp = tmp | (d[8*i+j] << (7 - j))
s = s + chr(tmp)
print (s)

Run:

1
1t_i5_wEdn3sd4y_mY_Dud3s@flare-on.com