FLAREON 2020 - WRITEUP PHẦN 2

>>

Phần 2

Chúc mọi người một giáng sinh vui vẻ 🎄
Cùng đến với phần 2 của series bài với 5 thử thách:

4 - report

1
Nobody likes analysing infected documents, but it pays the bills. Reverse this macro thrill-ride to discover how to get it to show you the key.

Ở challenge này, chúng ta có 1 file excel:

Mờ file report.xls bằng Microsoft Excel:

Khi làm việc với các file office, có 1 tool để extract VBA macro từ chúng, đó là tool “olevba”.

VBA macro: khi phải làm các công việc thường xuyên lặp đi lặp lại trong excel, bạn có thể viết 1 script để tự động các công việc đó. Script đó được veiét bằng Visual Basic for Applications (VBA).

Lợi dụng điều này, malware cũng thường hay chứa các VBA macro độc hại trong các file excel để tấn công người dùng.

olevba là một tool dùng để extract các VBA macro trong file excel.

1
C:\Users\admin\Desktop\Mat\4_-_report>olevba report.xls

Sau khi chạy xong, ta đã có được code VBA và sẵn sàng phân tích, nhưng ở dưới cùng có 1 dòng đáng để ý:

1
VBA Stomping was detected: the VBA source code and P-code are different, this may have been used to hide malicious code

Trước tiên ta cần hiểu về VBA source code và P-code, đoạn giải thích gốc nằm ở đây

1
2
3
4
5
6
7
It is not widely known, but macros written in VBA (Visual Basic for Applications; the macro programming language used in Microsoft Office) exist in three different executable forms, each of which can be what is actually executed at run time, depending on the circumstances. These forms are:

Source code. The original source code of the macro module is compressed and stored at the end of the module stream. This makes it relatively easy to locate and extract and most free DFIR tools for macro analysis like oledump or olevba or even many professional anti-virus tools look only at this form. However, most of the time the source code is completely ignored by Office. In fact, it is possible to remove the source code (and therefore make all these tools think that there are no macros present), yet the macros will still execute without any problems. I have created a proof of concept illustrating this. Most tools will not see any macros in the documents in this archive it but if opened with the corresponding Word version (that matches the document name), it will display a message and will launch calc.exe. It is surprising that malware authors are not using this trick more widely.

P-code. As each VBA line is entered into the VBA editor, it is immediately compiled into p-code (a pseudo code for a stack machine) and stored in a different place in the module stream. The p-code is precisely what is executed most of the time. In fact, even when you open the source of a macro module in the VBA editor, what is displayed is not the decompressed source code but the p-code decompiled into source. Only if the document is opened under a version of Office that uses a different VBA version from the one that has been used to create the document, the stored compressed source code is re-compiled into p-code and then that p-code is executed. This makes it possible to open a VBA-containing document on any version of Office that supports VBA and have the macros inside remain executable, despite the fact that the different versions of VBA use different (incompatible) p-code instructions.

... truncated

Như giải thích ở trên thì:

  • VBA macro có thể tồn tại ở 3 dạng khác nhau.
  • Dạng 1 là source code, source code sẽ bị nén lại và đặt ở cuối module. Và ta có thể bỏ source code khỏi module mà macro vẫn có thể chạy bình thường.
  • Dạng 2 là p-code: VBA macro sẽ được biên dịch thành p-code, là một dạng mã giả của Microsoft. Code này mới là code được thực thi thật sự, và code này cũng được đặt ở trong module.
  • Dạng 3 mình không đề cập ở đây.

Về VBA Stomping: có thể đọc thêm ở đây, nhưng cơ bản ta có thể hiểu là:

  • Khi lưu 1 file excel, nó sẽ chứa cả source code và p-code trong đó.
  • Nhưng source code và p-code này không match nhau, tức là p-code hoặc source code đã bị thay đổi.

Để khôi phục VBA code từ P-code, ta có thể dùng pcode2code (hoặc đọc p-code luôn).

1
C:\Users\admin\Desktop\Mat\4_-_report>pcode2code report.xls

So sánh code mới và code cũ (code mới ở bên trái)

Hàm rigmarole dùng để decrypt string:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function rigmarole(es As String, id_FFFE As String) As String
Dim furphy As String
Dim c As Integer
Dim s As String
Dim cc As Integer
furphy = ""
For i = 1 To Len(es) Step 4
c = CDec("&H" & Mid(es, i, 2))
s = CDec("&H" & Mid(es, i + 2, 2))
cc = c - s
furphy = furphy + Chr(cc)
Next i
rigmarole = furphy
End Function

List string decrypt được là:

1
2
3
4
5
6
7
8
9
10
11
12
AppData
\Microsoft\stomp.mp3
play
FLARE-ON
Sorry, this machine is not supported.
FLARE-ON
Error
winmgmts:\\.\root\CIMV2
SELECT Name FROM Win32_Process
vbox
WScript.Network
\Microsoft\v.png

Mình tạo 1 file excel mới, thêm VBA macro mới vào, copy các Form y chang từ file excel cũ để debug, ta sẽ phân tích hàm folderol.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
If GetInternetConnectedState() = False Then
MsgBox "Cannot establish Internet connection.", vbCritical, "Error"
End
End If

Set fudgel = GetObject(rigmarole(onzo(7)))
Set twattling = fudgel.ExecQuery(rigmarole(onzo(8)), , 48)
For Each p In twattling
Dim pos As Integer
pos = InStr(LCase(p.Name), "vmw") + InStr(LCase(p.Name), "vmt") + InStr(LCase(p.Name), rigmarole(onzo(9)))
If pos > 0 Then
MsgBox rigmarole(onzo(4)), vbCritical, rigmarole(onzo(6))
End
End If
Next

Đoạn code trên kiểm tra xem máy có được kết nối internet hay không, ngoài ra còn kiểm tra trong list process xem có process nào có chứa “vmw”, “vmt”, “vbox” trong tên không, đây chính là cơ chế anti-vm của nó. (ví dụ, “vmware” có chứa “vmw”, còn cụm “vbox” có thể được tìm thấy nếu bạn dùng virtualbox).

1
2
3
4
5
6
7
xertz = Array(&H11, &H22, &H33, &H44, &H55, &H66, &H77, &H88, &H99, &HAA, &HBB, &HCC, &HDD, &HEE)
Set groke = CreateObject(rigmarole(onzo(10)))
firkin = groke.UserDomain
If firkin <> rigmarole(onzo(3)) Then
MsgBox rigmarole(onzo(4)), vbCritical, rigmarole(onzo(6))
End
End If

Đoạn trên kiểm tra user domain có phải là “FLARE-ON” hay không, sau khi vượt qua các đoạn check, program sẽ decrypt 1 đoạn nhị phân và lưu ở “%appdata%\Microsoft\v.png”

1
thi5_cou1d_h4v3_b33n_b4d@flare-on.com

5 - TKApp

1
Now you can play Flare-On on your watch! As long as you still have an arm left to put a watch on, or emulate the watch's operating system with sophisticated developer tools.

Đề bài cho 1 file .tpk, sau 1 lúc google, mình phát hiện đây là file chạy trên hệ điều hành Tizen. Tải giả lập Tizen Studio về, chạy thử file, thì đây trông giống 1 app trên đồng hồ thông minh, có các chức năng định vị, đo nhịp tim, v.v…

Bài này không cần chạy file, chỉ cần phân tích tĩnh cũng có thể giải được.

Hai byte đầu của file .tpk là PK, nên mình đổi định dạng file thành “.zip” rồi giải nén.

Dùng “Detect it easy”:

File được nhận diện là .net, dùng “dnspy” để mở lên và phân tích.

dnspy là một tool dùng để phân tích các file .net.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void PedDataUpdate(object sender, PedometerDataUpdatedEventArgs e)
{
if (e.StepCount > 50U && string.IsNullOrEmpty(App.Step))
{
App.Step = Application.Current.ApplicationInfo.Metadata["its"];
}
if (!string.IsNullOrEmpty(App.Password) && !string.IsNullOrEmpty(App.Note) && !string.IsNullOrEmpty(App.Step) && !string.IsNullOrEmpty(App.Desc))
{
HashAlgorithm hashAlgorithm = SHA256.Create();
byte[] bytes = Encoding.ASCII.GetBytes(App.Password + App.Note + App.Step + App.Desc);
byte[] first = hashAlgorithm.ComputeHash(bytes);
byte[] second = new byte[] { /* a lot of bytes here */ };
if (first.SequenceEqual(second))
{
this.btn.Source = "img/tiger2.png";
this.btn.Clicked += this.Clicked;
return;
}
this.btn.Source = "img/tiger1.png";
this.btn.Clicked -= this.Clicked;
}
}
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
private bool GetImage(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(App.Password) || string.IsNullOrEmpty(App.Note) || string.IsNullOrEmpty(App.Step) || string.IsNullOrEmpty(App.Desc))
{
this.btn.Source = "img/tiger1.png";
this.btn.Clicked -= this.Clicked;
return false;
}
string text = new string(new char[]
{
// a lot of chars here, take from App.Password/Note/Step/Desc
});
byte[] key = SHA256.Create().ComputeHash(Encoding.ASCII.GetBytes(text));
byte[] bytes = Encoding.ASCII.GetBytes("NoSaltOfTheEarth");
try
{
App.ImgData = Convert.FromBase64String(Util.GetString(Runtime.Runtime_dll, key, bytes));
return true;
}
catch (Exception ex)
{
Toast.DisplayText("Failed: " + ex.Message, 1000);
}
return false;
}

Về cơ bản, chương trình lấy 4 string App.Password, App.Note, App.Step, App.Desc nối lại với nhau, tính SHA256 rồi lấy hash đó để giải mã file “Runtime.dll” với thuật toán AES256.

Để tìm xem App.Password được dùng ở chỗ nào, ta chuột phải vào App.Password trong hàm PedDataUpdate, chọn “Analyze”:

Sau đó ta có thể thấy nó được “set” (tức là được gán) ở hàm OnLoginButtonClicked:

Ta đến hàm này xem code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private async void OnLoginButtonClicked(object sender, EventArgs e)
{
if (this.IsPasswordCorrect(this.passwordEntry.Text))
{
App.IsLoggedIn = true;
App.Password = this.passwordEntry.Text;
base.Navigation.InsertPageBefore(new MainPage(), this);
await base.Navigation.PopAsync();
}
else
{
Toast.DisplayText("Unlock failed!", 2000);
this.passwordEntry.Text = string.Empty;
}
}

Hàm này lại dùng hàm IsPasswordCorrect, ta tới hàm này xem:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static byte[] Password = new byte[] {62, 38, 63, 63, 54, 39, 59, 50, 39};
private bool IsPasswordCorrect(string password)
{
return password == Util.Decode(TKData.Password);
}
public static string Decode(byte[] e)
{
string text = "";
foreach (byte b in e)
{
text += Convert.ToChar((int)(b ^ 83)).ToString();
}
return text;
}

Với App.Password, ta tính ra được chuỗi “mullethat”. Ta tiếp tục làm tương tự (chuột phải -> analyze với App.Note, App.Step, App.Desc).

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
private void SetupList()
{
List<TodoPage.Todo> list = new List<TodoPage.Todo>();
if (!this.isHome)
{
list.Add(new TodoPage.Todo("go home", "and enable GPS", false));
}
else
{
TodoPage.Todo[] collection = new TodoPage.Todo[]
{
new TodoPage.Todo("hang out in tiger cage", "and survive", true),
new TodoPage.Todo("unload Walmart truck", "keep steaks for dinner", false),
new TodoPage.Todo("yell at staff", "maybe fire someone", false),
new TodoPage.Todo("say no to drugs", "unless it's a drinking day", false),
new TodoPage.Todo("listen to some tunes", "https://youtu.be/kTmZnQOfAF8", true)
};
list.AddRange(collection);
}
List<TodoPage.Todo> list2 = new List<TodoPage.Todo>();
foreach (TodoPage.Todo todo in list)
{
if (!todo.Done)
{
list2.Add(todo);
}
}
this.mylist.ItemsSource = list2;
App.Note = list2[0].Note;
}

Với App.Note, ta có thể thấy được nó là chuỗi “keep steaks for dinner”.

1
2
3
4
5
6
7
8
private void PedDataUpdate(object sender, PedometerDataUpdatedEventArgs e)
{
if (e.StepCount > 50U && string.IsNullOrEmpty(App.Step))
{
App.Step = Application.Current.ApplicationInfo.Metadata["its"];
}
// truncated ...
}

Với App.Step, nó chính là chuỗi “magic”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void IndexPage_CurrentPageChanged(object sender, EventArgs e)
{
if (base.Children.IndexOf(base.CurrentPage) == 4)
{
using (ExifReader exifReader = new ExifReader(Path.Combine(Application.Current.DirectoryInfo.Resource, "gallery", "05.jpg")))
{
string desc;
if (exifReader.GetTagValue<string>(ExifTags.ImageDescription, out desc))
{
App.Desc = desc;
}
return;
}
}
App.Desc = "";
}

Với App.Desc, ta có thể cài đặt thư viện ExifLib, viết lại một đoạn code như trên và lấy được chuỗi “water”, còn file ảnh “05.jpg” có thể lấy từ các file có được sau khi giải nén TKApp.tpk.

Tất cả mọi thứ đã có, ta viết đoạn script để decrypt file:

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
import hashlib
from base64 import b64decode
from Crypto.Cipher import AES

def sha256(text: str) -> tuple:
h = hashlib.sha256()
h.update(text.encode())
return h.hexdigest(), h.digest()

def aes_dec(data: bytes, key: bytes, iv: bytes) -> bytes:
key_size = 32
assert len(key) == key_size and len(iv) == 16
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
s = cipher.decrypt(data)
return s[:-ord(s[len(s)-1:])]

if __name__ == '__main__':
Password = "mullethat"
Note ="keep steaks for dinner"
Step = "magic"
Desc = "water"

final = Password + Note + Step + Desc

text = [Desc[2], Password[6], Password[4], Note[4], Note[0], Note[17], Note[18], Note[16],
Note[11], Note[13], Note[12], Note[15], Step[4], Password[6], Desc[1], Password[2],
Password[2], Password[4], Note[18], Step[2], Password[4], Note[5], Note[4], Desc[0],
Desc[3], Note[15], Note[8], Desc[4], Desc[3], Note[4], Step[2], Note[13], Note[18],
Note[18], Note[8], Note[4], Password[0], Password[7], Note[0], Password[4], Note[11],
Password[6], Password[4], Desc[4], Desc[3]
]
text = ''.join(text)
data = b''
with open('Runtime.dll', 'rb') as f:
data = f.read()
dec = aes_dec(data, sha256(text)[1], b'NoSaltOfTheEarth')
dec = b64decode(dec)
with open('out.bin', 'wb') as f:
f.write(dec)
print ('[+] Done')

Sau khi chạy xong đoạn code trên ta được 1 file mới, có chứa JFIF trong phần header.

Đổi tên file mới thành “out.jpg”, ta được ảnh sau:

1
n3ver_go1ng_to_recov3r@flare-on.com

6 - codeit

1
Reverse engineer this little compiled script to figure out what you need to do to make it give you the flag (as a QR code).

Ở bài này, chúng ta có 1 file .exe

“Detect it easy” đã nhận ra đây là file thực thi được viết bằng AutoIt. Chạy file thử:

Dùng “Exe2Aut” để convert file này về dạng code AutoIt, ta được:

Exe2Aut là một tool dùng để convert binary được viết bằng AutoIT về dạng source code của nó.

Có vẻ như file này đã bị obfuscated để làm khó việc RE. Đến đây có 2 lựa chọn, ta có thể dùng chức năng “Find and replace” của editor để sửa lại đống này, hoặc viết 1 đoạn regex để rename lại toàn bộ chúng. Ở đây mình chọn cách viết regex bằng python, lý do là vì:

  • Mình thấy tên biến sau khi source code bị obfuscate có quy luật, nên có thể dùng regex.
  • Giả sử có hai hàm, gọi là hàm A và hàm B cho dễ, hai hàm này đều có biến local tên là “x” chẳng hạn. Dùng “Find and Replace” để đổi tên biến “x”, thì ta sẽ thay đổi biến “x” ở cả hai hàm, còn dùng regex thì có thể thay đổi “x” của chỉ một hàm (hoặc cả hai hàm luôn cũng được).

Ý tưởng viết regex như sau:

Ví dụ, trong đoạn code có một số chỗ như sau:

1
2
3
Global $flavekolca = Number(" 0 ")
Global $flwecmddtc = Number(" 1 ")
...

Mình thấy tên biến (bên trái dấu bằng) lúc nào cũng gồm 10 ký tự (flavekolca, flwecmddtc, …), hơn nữa tên biến luôn bắt đầu bằng chuỗi fl. Còn vế phải dấu bằng thì luôn là Number(", tiếp đến là một dấu cách, rồi đến một hằng số, rồi theo sau đó là một dấu cách và "). Nhận ra được quy luật trên, mình có thể dùng đoạn regex sau để match các đoạn string như trên:

1
template = rb'(?P<name>\$fl[a-z]{8}) = Number\(" (?P<num>[0-9]+) "\)'

Sau đó mình chỉ việc replace lại thành cái gì đó đơn giản hơn là xong, ví dụ mình sẽ dùng python để sửa đoạn trên thành:

1
2
Global $global_0_0 = 0
...

Ở trên chỉ là ý tưởng, còn code mình sẽ để ở file đính kèm. Sau khi chạy code xong, ta được file source code AutoIt mới, dễ đọc hơn nhiều.

Đoạn code mới không quá dài, ta có thể đọc và hiểu các function của nó. Về cơ bản, chương trình nhận input từ người dùng, sau đó tạo ra mã QR dưới dạng hình ảnh rồi hiện lên màn hình.

func_08, chương trình nhận vào input của người dùng để xử lý, sau đó đưa data đã được xử lý vào func_05.

func_05, chương trình lấy computer_name (ở chỗ số 6), sau đó encrypt nó ở hàm func_04 (số 7). Tiếp theo, đoạn computer_name được mã hóa sẽ được tính hash để làm key cho bước CryptDecrypt ở dưới (số 5).

  • Hash được dùng trong bài này là 32780 (CALG_SHA_256) (số 2) , và thuật toán mã hóa được dùng là 24 (AES) (số 1).
  • Ở hình trên, đoạn code có đánh số 3 chính là khởi tạo $local_5 để lưu trữ data bị mã hoá, còn đoạn code đánh số 4 là gán $local_5 vào struct để chuẩn bị decrypt.
  • Điều kiện để có thể đi vào trong nhánh If cuối cùng (số 8 ở trong hình trên) đó chính là, sau khi decrypt data chứa ở $local_5, thì 5 byte đầu phải là "FLARE" và 5 byte cuối cùng là "ERALF".

Và cuối cùng, ta phải xem lại hàm func_04, vì nó biến đổi computer_name trước khi tính hash.

Đoạn code trên biến đổi $arg_0 dựa vào nội dung của file .bmp (file này được drop ra trong lúc chương trình xử lý ảnh QR).

Tóm lại tới lúc này ta biết được rằng input từ người dùng không có tác dụng gì đối với việc lấy flag, quan trọng là computer_name phải thoả mãn những điều sau:

  • key = sha256(func_04(computer_name))
  • data = AES.decrypt(encrypted_data, key, IV = 0000000...)
  • data.startswith(b'FLARE') == True
  • data.endswith(b'ERALF') == True

Tuy nhiên chúng ta không thể biết được computer_name là gì, cũng không thể bruteforce dựa trên 2 điều kiện “FLARE” và “ERALF” vì AES không bị “Known plain-text attack”.

Vì không có điều kiện nào để giải ra computer_name nên mình đoán rằng:

1
func_04(computer_name) == computer_name

Tức là, computer_name sẽ là một chuỗi đặc biệt mà sẽ không bị biến đổi khi qua hàm func_04. Đến đây mình sẽ bruteforce để tìm ra computer_name như sau:

  • Mình nhận thấy hàm func_04 biến đổi chuỗi bằng cách biến đổi từng ký tự một, nên mình sẽ bruteforce một ký tự một lần.
  • Thuật toán: với mỗi ký tự trong computer_name, cho c chạy trong tập ký tự, nếu bruteforce_function(c) == c thì “chọn” c, trong đó bruteforce_function là hàm biến đổi chuỗi, nói thẳng ra nó chính là hàm func_04 (có sửa đổi 1 chút).

Code bruteforce:

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
# python3

chars = '0123456789abcdefghijklmnopqrstuvwxyz-'

def ror8(n: int, i: int) -> str:
i = i % 8
n = n & 0xFF
return (n >> i) + ((n << (8 - i)) & 0xFF)

def brute_one(index: int) -> str:
assert index >= 1 # AutoIT uses 1-based index .
with open('sprite.bmp', 'rb') as f:
f.read(54) # discard
data = f.read()
start = 7 * (index - 1)
tmp = 0
for i in range(7):
tmp += ((data[start + i] & 1) << (7 - i))
tmp = ror8(tmp, 1)
for c in chars:
if ord(c) == tmp:
return c
return ''

if __name__ == '__main__':
r = ''
for i in range(16):
r = r + brute_one(i + 1)
print (r)
1
aut01tfan1999

Sau đó, ta patch hàm func_03 (hàm này lấy computer_name), cho hàm này luôn return "aut01tfan1999".

Chạy thử chương trình, nhập 1 chuỗi bất kỳ, rồi bấm “Can haz code?”, ta sẽ nhận được 1 ảnh QR, quét ảnh này ta được:

1
L00ks_L1k3_Y0u_D1dnt_Run_Aut0_Tim3_0n_Th1s_0ne!@flare-on.com

7 - re_crowd

1
2
3
4
5
Hello,

Here at Reynholm Industries we pride ourselves on everything. It's not easy to admit, but recently one of our most valuable servers was breached. We don't believe in host monitoring so all we have is a network packet capture. We need you to investigate and determine what data was extracted from the server, if any.

Thank you

Ở bài này chúng ta có 1 file .pcap, dùng wireshark để xem file này:

Ở đây mình thấy gói số 7 có protocol là HTTP, nên mình sẽ extract các http object capture được như hình sau:

Sau khi extract hết ra, ta có được file “5c” là file nặng nhất (11 kb), mở nó lên bằng Google chrome, ta được một webpage về đoạn chat của một số nhân viên trong công ty.

Ở gần cuối của đoạn chat có 1 đoạn nói chuyện khá thú vị

Theo hình trên thì Jen đã lưu danh sách account ở “C:\accounts.txt”, nên mình đoán là, máy công ty đã bị nhiễm malware này, con malware này đã lấy cắp file “accounts.txt” và gửi về cho attacker.

Bây giờ ta sẽ vào phân tích các gói tin trong wireshark.

Ở trên là một gói tin được gửi đến server, phương thức PROPFIND cũng khá lạ so với mình, nên mình đã google để tìm PROPFIND và tìm được một số bài viết hay ho.

Trong loạt bài trên có phân tích rõ về lỗi Buffer Overflow dẫn đến RCE. Vì vậy ta chỉ cần tập trung vào đoạn shellcode bắt đầu từ “VVYAIAI…”.

Đoạn shellcode này chỉ bao gồm các kí tự chữ số, chữ thường và chữ in hoa nên mình nghĩ ngay tới Alphanumeric shellcode. Đoạn shellcode trên được tạo ra bằng thư viện alpha2.

Ta viết 1 đoạn code để decode đoạn shellcode này (lưu ý đoạn shellcode này là Unicode shellcode):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# python3

if __name__ == '__main__':
with open('shell.txt', 'rb') as f:
text = f.read().decode('utf-16le')
text = text[text.find('ZBABABABABkMAGB9u4JB') + len('ZBABABABABkMAGB9u4JB')::]
r = ''
for i in range(len(text) // 2):
x, y = ord(text[2*i]), ord(text[2*i+1])
c, d = (x & 0xF0) >> 4, x & 0xF
e, f = (y & 0xF0) >> 4, y & 0xF
b = f
a = d + e
while a >= 16:
a = a - 16
r += chr((a<<4) | b)
with open('dec.bin', 'wb') as f:
f.write(bytearray([ord(i) for i in r]))
print ('Done')

Trong đó, shell.txt chính là đoạn shellcode copy ra từ file .pcap.

Tiếp theo ta dùng blobrunner để chạy và debug đoạn shellcode mới.

blobrunner là tool dùng để chạy 1 đoạn shellcode, dùng để debug các shellcode dễ dàng hơn.

shellcode mới có 1 pattern lặp đi lặp lại rất nhiều (push arg, push arg, push hash, call ebp):

Chỗ “call ebp” sẽ nhảy đến hàm ở 0x10000006, hàm này sẽ tính hash của các hàm Windows API, so sánh với hash được push lên stack. Sau khi tìm được hàm có hash tương ứng, shellcode sẽ nhảy tới hàm Windows API đó qua lệnh “jmp eax”.

Đoạn shellcode này không dài quá, nhưng mình lười đọc nên đã làm như sau:

  • Breakpoint tại “jmp eax”.
  • Chạy shellcode.
  • Log lại những hàm và tham số được gọi để có cái nhìn tổng quát về chương trình.

Và thứ tự các hàm và tham số được gọi như sau:

  • LoadLibraryA , "ws2_32".
  • WSAStartup.
  • WSASocketA, AF_INET, SOCK_STREAM, …
  • WSAConnect, (random number) socket handle, sockaddr (family: AF_INET, port: 4444, IP: 192.168.68.21).
  • Đến đây chương trình không chạy nữa, lý do là vì nó cố gắng kết nối đến 192.168.68.21:4444 nhưng không có server nào ở địa chỉ này cả.

Ta thử tìm port 4444 trong wireshark.

Có 1 gói tin từ 192.168.68.21:4444 gửi về server với độ dài phần data là 1243 bytes.

Vậy ta dựng lại server để giả lập gói tin đó, làm lại 1 lần như trên, nhưng đến đoạn WSAConnect thì patch IP thành 127.0.0.1 để theo dõi tiếp xem nó gọi thêm hàm WinAPI nào.

Code server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# python3
import socket
from binascii import a2b_hex

if __name__ == '__main__':
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', 4444))
s.listen(5)
c, v = s.accept()
data = '9c5c4f52a4b1037390e4c88e...' # truncated
data = a2b_hex(data)
c.send(data)
c.close()
s.close()
print ('[+] Done')

Chạy lại và quan sát danh sách các hàm WinAPI được gọi (chỉ liệt kê hàm được gọi sau WSAConnect):

  • recv, (random number above) socket handle, buffer, 4, 0.
  • VirtualAlloc, lpBase = 0, size = 0x4D7, MEM_COMMIT, PAGE_EXECUTE_READWRITE.
  • recv, (random number above) socket handle, buffer, 0x4D7, 0.
  • Đến đây chương trình lại không chạy nữa, đã đến lúc ta phải phân tích đoạn shellcode này.

Sau khi nhận 0x4D7 byte, thì dữ liệu đó được đưa vào trong hàm mà mình đặt tên là “decr” ở 0x10000143. Ta thử xem hàm “decr” này có gì.

Đoạn code trên chính là RC4.

Mẹo nhận biết RC4: nhìn thấy đoạn code tương tự như for (i = 0; i < 256; ++i) a[i] = i , thì gần như chắc chắn là RC4, đó chính là 1 trong các bước tạo key của RC4.

Trong đoạn code trên, dòng asm ở địa chỉ 0x10000144 chính là pattern nói ở trên.

Key ở lần RC4 này là “killervulture123” , được lấy ở thanh ghi esi (hình ở dưới):

Sau khi RC4 decrypt đoạn data, thì ta nhận được một đoạn shellcode mới, đoạn shellcode này cũng thực hiện việc dynamically resolve các function để phục vụ việc gọi các hàm WinAPI. Sau đó, nó thực hiện 1 số công việc hay ho:

Đầu tiên là CreateFile C:\\accounts.txt với quyền đọc (GENERIC_READ). Ta cũng fake file “C:\accounts.txt” để program có thể đọc.

Tiếp theo, ReadFile để đọc 0x100 bytes từ file này.

Sau đó đoạn data vừa được đọc bởi hàm ReadFile lại được đưa vào 1 hàm khác để mã hoá, và hàm này rất quen thuộc:

Không biết bạn đọc có thấy pattern "for (i = 0; i < 256; ++i) a[i] = i" trong đoạn trên không nhỉ ^^!.

Lần này key được push trên stack: “intrepidmango”.

Tiếp theo, một socket mới lại được tạo ra, connect tới 192.168.68.21:1337

Như vậy, nội dung của file sẽ được RC4 encrypt trước khi gửi về port 1337 cho attacker.

Tóm lại, luồng thực thi của chương trình như sau:

  • Connect tới 192.168.68.21:4444 để nhận 1243 bytes dữ liệu.
  • Dữ liệu vừa nhận được sẽ được giải mã RC4 bằng key “killervulture123”.
  • Đoạn dữ liệu vừa giải mã được chính là một shellcode.

Sau đó, đoạn shellcode trên sẽ được thực thi, luồng thực thi của đoạn shellcode như sau:

  • Đọc file “C:\accounts.txt”.
  • Dữ liệu vừa đọc sẽ được mã hoá RC4 bằng key “intrepidmango”.
  • Dữ liệu vừa được mã hoá sẽ được gửi đến server ở 192.168.68.21:1337.
  • Kết thúc.

Vậy việc cuối cùng cần làm là, lấy data được gửi (ở port 1337) trong wireshark ra, decrypt và lấy flag.

1
2
3
4
5
6
7
8
9
10
# python3
from binascii import a2b_hex
from Crypto.Cipher import ARC4

if __name__ == '__main__':
key = b'intrepidmango'
data = a2b_hex(b'truncated ...')
rc4 = ARC4.new(key)
d = rc4.decrypt(data)
print (d)

Run:

1
b'roy:h4ve_you_tri3d_turning_1t_0ff_and_0n_ag4in@flare-on.com:goat\r\nmoss:Pot-Pocket-Pigeon-Hunt-8:narwhal\r\njen:Straighten-Effective-Gift-Pity-1:bunny\r\nrichmond:Inventor-Hut-Autumn-Tray-6:bird\r\ndenholm:123:dog'
1
h4ve_you_tri3d_turning_1t_0ff_and_0n_ag4in@flare-on.com

Fact: vì RC4 là thuật toán mã hoá đối xứng nên ta có thể lấy file bị mã hoá trong wireshark ra để làm giả file “C:\accounts.txt”, khi đó program sẽ mã hoá file này 1 lần nữa và cho ra luôn file gốc.

8 - Aardvark

1
Expect difficulty running this one. I suggest investigating why each error is occuring. Or not, whatever. You do you.

Ở challenge này ta có 1 file .exe, giải nén file này và chạy thử (windows 7 VM):

Dùng Detect it easy để nhận diện file:

Đến đây ta mở file bằng IDA để tìm xem nguyên nhân gây ra lỗi “socket failed” như trên hình là gì.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

// truncated ...
// ...
v8 = socket(1, 1, 0);
v6 = v8;
if ( v8 == -1i64 )
{
MessageBoxA(0i64, "socket failed", "Error", 0x10u);
v9 = "Error creating Unix domain socket";
LABEL_16:
MessageBoxA(0i64, v9, "Error", 0x10u);
goto LABEL_17;
}
// truncated ...
}

Ta để ý dòng:

1
v8 = socket(1, 1, 0); // socket(AF_UNIX, SOCK_STREAM, 0);

Theo mình biết thì AF_UNIX chỉ xuất hiện trên các hệ điều hành UNIX, nhưng ở các bản cập nhật Windows gần đây, Microsoft đã thêm nó vào hệ điều hành Windows 10, xem ở đây.

Vì vậy mình đã tải bản Windows 10 version 1909 về để chạy lại file này, và nhận được kết quả:

Ta quay lại IDA, xem strings window để tìm dòng trên:

Dùng xref để đi tới hàm sub_140001B10 (hàm này sử dụng chuỗi “CoCreateInstance failed”).

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
__int64 __usercall sub_140001B10@<rax>(__int64 a1@<rcx>, unsigned int a2@<edx>, __int64 a3@<r8>, __int128 *_XMM0@<xmm0>)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

v4 = 0;
v5 = a3;
ppv = 0i64;
v6 = a2;
v13 = 0i64;
v7 = a1;
if ( CoCreateInstance(&rclsid, 0i64, 4u, &riid, &ppv) )
{
MessageBoxA(0i64, "CoCreateInstance failed", "Error", 0x10u);
}
else if ( (*(*ppv + 96i64))(ppv, &v14) )
{
MessageBoxA(0i64, "GetDefaultDistribution failed", "Error", 0x10u);
}
else if ( (*(*ppv + 24i64))(ppv, &v14, 0i64, &unk_14001E028, &v13) )
{
MessageBoxA(0i64, "CreateInstance failed", "Error", 0x10u);
}
else
{
v11 = 0;
v10 = 0;
GetCurrentDirectoryW(0x105u, &Buffer);
v15 = 0i64;
v16 = 0i64;
v12 = *(*(*(__readgsqword(0x30u) + 96) + 32i64) + 16i64);
if ( (*(*v13 + 48i64))(v13, v7, v6, v5, 0, 0i64, &Buffer, 0i64, 0i64, 0, 0, &v15, &v12, 0, &v11, &v10) )
MessageBoxA(0i64, "CreateLxProcess failed", "Error", 0x10u);
else
v4 = 1;
}
if ( v13 )
(*(*v13 + 16i64))(v13);
if ( ppv )
(*(*ppv + 16i64))(ppv);
return v4;
}

Ở đây ta thấy có string “CreateLxProcess failed” nên ta thử tìm Github hàm này:

Sau một lúc xem qua các kết quả thì mình thấy:

  • Tên repository này là “WSLReverse”, (WSL là “Windows Subsystem for Linux” Link).
  • Link github.

Vậy khả năng cao file bị lỗi là do chưa cài đặt WSL, nên ta cài đặt WSL trên Windows 10 , rồi chạy lại file:

Ta được 1 game Tic-tac-toe, mà computer đã đi trước 1 nước ở ngay giữa, tức là gần như chúng ta chỉ có thể hòa hoặc thua, chơi thử 1 vài trận, điều ở trên được xác nhận.

Quay lại IDA để phân tích code.

Đầu tiên, chương trình tạo ra 1 socket(AF_UNIX, ...) để lắng nghe:

1
2
3
4
5
6
7
8
9
10
11
v8 = socket(1, 1, 0);
v6 = v8;
if...
if ( bind(v8, &name, 110) == -1 )
v10 = "bind failed";
else
{
if ( listen(v6, 0x7FFFFFFF) != -1 )
goto LABEL_12;
v10 = "listen failed";
}

Tiếp theo chương trình thực hiện hàm sub_140012B0, trong hàm này, nó lấy resource ra và ghi ra thư mục “%tmp%”.

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
__int64 sub_1400012B0()
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

v11 = &v13;
v12 = 0i64;
NumberOfBytesWritten[0] = 0;
v0 = 0;
v1 = -1i64;
if ( !GetTempFileNameA(".", PrefixString, 0, &FileName) )
// ...
wsprintfA(&v13, "%s", &FileName);
*sub_140003268((__int64)&v13, '\\') = '/';
v1 = (__int64)CreateFileA(&FileName, 0x40000000u, 0, 0i64, 3u, 0x80u, 0i64); // GENERIC_WRITE
// ....
v3 = FindResourceA(0i64, (LPCSTR)0x12C, (LPCSTR)0x100);
v4 = v3;
// ...
v6 = SizeofResource(0i64, v3);
v7 = LoadResource(0i64, v4);
v5 = v7;
// ...
v8 = LockResource(v7);
if ( WriteFile((HANDLE)v1, v8, v6, NumberOfBytesWritten, 0i64) && NumberOfBytesWritten[0] == v6 )
{
CloseHandle((HANDLE)v1);
FreeResource(v5);
sub_140001930((__int64)&v13, 1u, (__int64)&v11); // <---- here
v0 = 1;
}
// ...
}

Tiếp theo chương trình nhảy vào hàm sub_140001930, hàm này sẽ dựa vào số build của Windows để tiếp tục thực thi:

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
if ( GetVersionExA(&VersionInformation) )
{
if ( VersionInformation.dwBuildNumber >= 0x42EE )
{
if ( VersionInformation.dwBuildNumber == 17134 )
{
sub_140001AB0();
sub_140001B10(v3, v5, v4, 0i64);
return 0i64;
}
if ( VersionInformation.dwBuildNumber == 17763 )
{
sub_140001AB0();
sub_140001D60(v3, v5, v4);
return 0i64;
}
if ( VersionInformation.dwBuildNumber - 18362 <= 1 )
{
sub_140001AB0();
sub_140001FB0(v3, v5, v4);
return 0i64;
}
if ( VersionInformation.dwBuildNumber - 19041 <= 1 || VersionInformation.dwBuildNumber > 0x4A62 )
{
sub_140001AB0();
sub_1400021E0(v3, v5, v4);
return 0i64;
}
}

Máy mình là Windows 10 build 18363, nên mình sẽ tiếp tục đi vào hàm sub_140001FB0:

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
__int64 __fastcall sub_140001FB0(__int64 a1, unsigned int a2, __int64 a3)
{
v3 = 0;
v4 = a3;
ppv = 0i64;
v5 = a2;
v6 = a1;
if ( CoCreateInstance(&rclsid, 0i64, 4u, &riid, &ppv) )
{
MessageBoxA(0i64, "CoCreateInstance failed", "Error", 0x10u);
}
else if ( (*(*ppv + 88i64))(ppv, &v18) )
{
MessageBoxA(0i64, "GetDefaultDistribution failed", "Error", 0x10u);
}
else
{
v17 = 0i64;
v16 = 0i64;
v20 = 0i64;
v19 = 0i64;
v15 = 0i64;
v14 = 0i64;
v13 = 0i64;
v12 = 0i64;
v7 = *(*(*(__readgsqword(0x30u) + 96) + 32i64) + 16i64);
GetCurrentDirectoryW(0x105u, &Buffer);
v10 = 0;
v9 = 0;
if ( (*(*ppv + 112i64))(ppv,&v18,v6,v5,v4,&Buffer,0i64,0i64,0,L"root",
v9,v10,v7,&v19,&v22,&v21,&v17,&v16,&v15,&v14,&v13,&v12) )
{
MessageBoxA(0i64, "CreateLxProcess failed", "Error", 0x10u);
}
else
{
v3 = 1;
}
}
if ( ppv )
{
(*(*ppv + 16i64))(ppv);
}
return v3;
}

Trong đoạn code trên:

1
2
if ( (*(*ppv + 112i64))(ppv,&v18,v6,v5,v4,&Buffer,0i64,0i64,0,L"root",
v9,v10,v7,&v19,&v22,&v21,&v17,&v16,&v15,&v14,&v13,&v12) )

Đoạn code này gọi hàm trong vtable của object C++, ta vào link respository trên, đọc file LxssUserSession.h

Ta có thể thấy sự tương đồng, vậy ta có thể kết luận đây là hàm CreateLxProcess.

Vậy hàm CreateLxProcess làm gì ?

Ở windows 10, khi WSL ra đời, ta đã có thể chạy các file executable (ELF) trên hệ điều hành linux, ví dụ như cat, ls, …

Nhưng đường dẫn tới file này nằm ở đâu ? Ta nhớ lại lúc nãy chương trình có lấy resource của nó rồi ghi ra “%tmp%”, nên ta dùng “Resource hacker” extract resource này ra xem thử:

Resource hacker là tool dùng để xem và sửa phần resource của file PE, tải ở đây.

Ta thấy 1 file bắt đầu với \x7FELF, chính là file executable của hệ điều hành linux. Ta extract file này ra và bỏ vào IDA:

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

addr.sa_data[13] = 0;
v47 = __readfsqword(0x28u);
v43 = 0LL;
memset(&v42, 0, 0x58uLL);
addr.sa_family = 1;
strcpy(addr.sa_data, "496b9b4b.ed5");
v3 = socket(1, 1, 0);
fd = v3;
if ( v3 < 0 )
{
perror("socket");
}
else
{
v4 = &addr;
v5 = (unsigned int)v3;
if ( connect(v3, &addr, 0x6Eu) >= 0 )
// ...
}
// ...
}

Vậy chương trình mới được drop ra sẽ connect tới socket được tạo bởi file .exe.

Phân tích cả 2 file .exe và file ELF, ta có thể thấy đây là 1 game Tic-tac-toe theo cấu trúc Client-Server, nếu Server thắng, Client sẽ bắt đầu tạo 1 message từ /proc/modules, /proc/mounts, /proc/version_signature, … rồi gửi cho Server.

Nhưng như phân tích ở ban đầu, thì Server không bao giờ thắng được vì Client đã đi trước 1 nước ở ngay chính giữa, cách duy nhất để thắng chính là bằng cách nào đó patch Client để Server có cơ hội thắng.

Ở Server, khi ta click vào 1 ô, nó sẽ gửi tọa độ của ô đó (x,y) cho Client (hàm DialogFunc ở 0x140001000):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if ( qword_14001EA78[v22] == 32 )
{
::buf = a3 >> 4;
v23 = s;
byte_14001EA71 = a3 & 0xF;
qword_14001EA78[v22] = 79;
send(v23, &::buf, 2, 0); // <----- send X,Y here
recv(s, qword_14001EA78, 10, 0);
sub_140001520(v4);
if ( byte_14001EA81 )
{
recv(s, &buf, 64, 0);
MessageBoxA(v4, &buf, "Game Over", 0);
sub_1400014E0();
sub_140001520(v4);
}

Ở Client, trước khi gửi trạng thái của game cho Server, nó sẽ kiểm tra xem với nước đi đó thì có ai “thắng” không (trạng thái của game là 1 chuỗi 10 ký tự, trong đó 9 ký tự đầu là “XXOOXO…” tùy vào các nước đi, ký tự cuối để xác định xem ai là người thắng với trạng thái hiện tại)

1
2
3
4
5
6
7
8
9
10
11
12
13
// function "main" on Client
v4[3 * v6 + i] = 88;
byte_2020A9 = sub_14B0();
send(fd, &byte_2020A0, 0xAuLL, 0); // send state
v5 = byte_2020A9;
if ( byte_2020A9 )
break;
recv(fd, &unk_2020AA, 2uLL, 0);
v15 = &v4[3 * unk_2020AA + byte_2020AB];
if ( *v15 != 32 )
goto LABEL_5;
*v15 = 79;
v5 = sub_14B0(); // check if someone wins, return 'X', 'O', or 0

Vậy ta chỉ cần breakpoint ngay tại hàm send của Client, sửa State lại trước khi nó gửi để lừa Client.

Đầu tiên, mình dùng plugin gef cho gdb, tải ở đây.

Cách debug process chạy bởi WSL:

  • Chạy ttt2.exe.

  • Mở 1 windows terminal khác lên, gõ WSL để truy cập vào Linux Subsystem.

  • Gõ ps -aux để lấy list các process trong Linux Subsystem.

  • Ta thấy process đang chạy dưới tên “XXXX.tmp”, giờ ta attach debugger vào bằng lệnh “sudo gdb -p 9”, vì PID của nó là 9. (xem ở cột PID).

  • Gõ “vmmap” trong gdb để tìm base address của “XXXX.tmp”.

  • Như hình trên, base address sẽ là 0x00007fe9e6200000.

  • Vào IDA lấy Offset:

    1
    .text:0000000000000D47                 call    sub_14B0        ; check
  • Vậy Offset là 0xD47, ta gõ “b *0x00007fe9e6200000+0xD47” để đặt breakpoint ngay tại chỗ này.

  • Gõ c để chương trình tiếp tục chạy.

  • Bên Server, ta click vào bất kỳ ô nào (trừ ô giữa), khi đó Client đã dừng lại ngay tại breakpoint.

  • State ở Client là 1 mảng char, global, offset tại 0x2020A0, ta dùng lệnh sau để patch state: patch string 0x00007fe9e6200000+0x2020A0 "OOOOOOOOO" (9 chữ “O”).

  • Bấm c để cho Client chạy tiếp, ở Server ta nhận được 1 message.

1
c1ArF/P2CjiDXQIZ@flare-on.com