NC3 2019 was a CTF ran by the National Cyber Crime Center in Denmark. I participated as the organizer and team captain of ‘Holdet’, in which we finished first place.
If you’d like a more thorough explanation of a certain challenge, you are more than welcome to leave a comment and i will update the write-up as soon as possible.
I would like to say thanks to the members of holdet:
Below is a list of the challenges, sorted by their respective category and the amount of points we got from solving them.
The write-up is missing solution details for the following categories: Forensic, CTF and Misc. These are currently work-in-progress and will be published as soon as possible, if you can’t wait for my write-up, you can read a full write-up by queer agenda captain Astrid here
- Reversing
- C Skarp Ud (75 pt.)
- crackme_241219 (100 pt.)
- SHeLLK0D3 (350 pt.)
- crackme_231219 (375 pt.)
- intr0 (500 pt.)
- Den Røde Tråd (1000 pt.) – Unsolved
- Forensic
- rudolf (100 pt.)
- Juleønsker (125 pt.)
- Zip-Mareridt!!! (200 pt.)
- WordPerfekt (250 pt.)
- indrammet_julemand (350 pt.)
- Nisse IT support (450 pt.)
- CTF
- alle_kan_være_med (10 pt.)
- skru_op!! (25 pt.)
- Gæt Et Format (50 pt.)
- enplusfiretreds (70 pt.)
- Onion (75 pt.)
- Hardcore Crypto!! (125 pt.)
- crypto_dumper (250 pt.)
- Easy Pass (350 pt.)
- Analyse
- Hyggelæsning (400 pt.)
- Julemandens Juleanalyse (400 pt.)
- Boot2Root
- Misc
- En lille tur (75 pt.)
- Stop juletyven (175 pt.)
- H3X8 (175 pt.)
- Juleskibet (200 pt.)
Reversing
C Skarp Ud
This challenge consists of a very basic .NET application, where the flag is encrypted with a simple XOR key located at MainWindow::GetFlagString(). The application itself shows a form consisting of three sliders, where each of the slider values are used to generate the XOR key. This challenge can be solved by simply opening the .NET application using a decompiler like dnSpy and inputting the correct three keys into the form. The key is validated by the function MainWindow::TjekSliders(), which compares the input to the correct key values:
private string GetFlagString()
{
byte b = (byte)(this.slider1.Value + this.slider2.Value + this.slider3.Value);
byte[] array = new byte[]
{
51, 62, 78, 6, 30, 34, 14, 21, 28, 15, 13, 34, 8, 25, 24, 19, 34, 18, 31, 27, 8, 14, 22, 24, 15, 20, 19, 26, 34, 24, 15, 34, 28, 17, 17, 24, 15, 24, 25, 24, 34, 8, 25, 13, 28, 22, 22, 24, 9, 0
};
for (int i = 0; i < array.Length; i++)
{
array[i] ^= b;
}
return Encoding.UTF8.GetString(array);
}
private void TjekSliders()
{
this.sliderText1.Content = this.slider1.Value;
this.sliderText2.Content = this.slider2.Value;
this.sliderText3.Content = this.slider3.Value;
if (this.slider1.Value == 25.0 && this.slider2.Value == 99.0 && this.slider3.Value == 1.0)
{
this.kodeordsBoks.Text = this.GetFlagString();
return;
}
this.kodeordsBoks.Text = "-- FORKERT KOMBINATION --";
}
Flag: NC3{c_sharp_uden_obfuskering_er_allerede_udpakket}
crackme_241219
This challenge was a simple PE64 binary that compared the console arguments with the valid key, all you had to do was open the application in a disassembler and look at the comparison-instructions:
key = argv[1];
if ( *key != 'a'
|| key[1] != 'l'
|| key[2] != 'l'
|| key[3] != 'e'
|| key[4] != '_'
|| key[5] != 'e'
|| key[6] != 'l'
|| key[7] != 's'
|| key[8] != 'k'
|| key[9] != 'e'
|| key[10] != 'r'
|| key[11] != '_'
|| key[12] != 'j'
|| key[13] != 'u'
|| key[14] != 'l'
|| key[15] != 'e'
|| key[16] != 'n' )
{
result = 1;
}
else
{
std::printf("NC3{%s}\n", key, v8);
result = 0;
}
flag: NC3{alle_elsker_julen}
SHeLLK0D3
This challenge consisted of a text file with HEX values in it. When treated as x86 assembly, it was pretty clearly a simple xor algorithm which calculated the flag on to stack. To solve this challenge, you could execute the given assembly and break at the last instruction (which was an infinite loop). At that point the entire string will be at the top of the stack:

, where ESP=008FF8AC
You can find the source code of the shellcode application on the nc3ctf2019 repository.
flag: NC3{x86_i_en_nøddeskal}
crackme_231219
This challenge was a simple ELF64 binary that calculated the input flag from executable’s name. By opening the application in IDA64, we quickly determined that the program calculated a key from the binary’s file name (without ./) and then uses that to determine the flag value. To solve this challenge, you have to figure out the correct key, rename the application and then run it:
puts("NC3 CRACKME :: 231219 (er det ikke snart jul??)");
puts("----------------------------------------------");
if ( argc == 2 )
{
unlock_key = 0;
// SKIP ./
file_name = &(*argv)[**argv == '.'];
file_name += *file_name == '/';
for ( file_name_index = 0; file_name_index != 14; ++file_name_index )
{
character = file_name[file_name_index];
unlock_key = 2 * (unlock_key + character);
if ( !character )
break;
if ( file_name[file_name_index] != (g_xor_table[(char)(g_index_table[file_name_index] - file_name_index)] ^ 1) )
break;
}
__printf_chk(1, "* Fik unlock key: %u (0x%X)\n", unlock_key, unlock_key);
result = 1;
if ( unlock_key )
{
puts("* Lad os prøve:");
__printf_chk(1, "NC3{");
calculate_flag(unlock_key);
puts("}");
result = 0;
}
}
else
{
puts("Denne crackme tager 1 parameter. Hilsen Drillenissen");
result = 2;
}
return result;
The file name key generation is very easy to emulate, you can simply calculate the given value by copying the two arrays (g_index_table, x_xor_table) from the PE and running the same algorithm as before:
constexpr std::uint8_t index_table[] = { 0x4, 0x1, 0x6, 0x7, 0x7, 0xA, 0x8, 0xB, 0xB, 0xF, 0xB, 0x12, 0xF, 0x15 };
constexpr std::uint8_t xor_table[] = { 0x68, 0x67, 0x74, 0x64, 0x6D, 0x6B, 0x60, 0x75, 0x6F };
std::printf("Calculating key...\n");
std::printf("Key: ");
std::uint32_t unlock_key = 0;
for (size_t i = 0; i != 14; i++)
{
const auto value = xor_table[index_table[i] - i] ^ 1;
unlock_key = 2 * (unlock_key + value);
std::printf("%c", value);
}
This will yield the key “lillejuleaften”, renaming the binary to said key and running it gives us the flag:

This is the boring solution though, if you continue to reverse the calculate_flag function, you find that the next algorithm is just as easy:
unsigned __int64 __fastcall calculate_flag(unsigned int unlock_key)
{
__int64 count; // rbx
__int16 unlock_key_u16; // ax
unsigned int shifted_unlock_key; // edi
char xor_value; // al
char next_count; // bp
int unlock_key_modif; // [rsp+14h] [rbp-24h]
unsigned __int64 stack_cookie; // [rsp+18h] [rbp-20h]
count = 0LL;
stack_cookie = __readfsqword(0x28u);
unlock_key_u16 = unlock_key;
shifted_unlock_key = unlock_key >> 16;
BYTE1(unlock_key_modif) = HIBYTE(unlock_key_u16);
HIWORD(unlock_key_modif) = (unsigned __int8)unlock_key_u16;
xor_value = 0x54;
LOBYTE(unlock_key_modif) = shifted_unlock_key;
while ( 1 )
{
next_count = count + 1;
if ( count != 7 )
break;
__printf_chk(1LL, "æ");
LABEL_3:
xor_value = byte_20E1[count++];
LOBYTE(shifted_unlock_key) = *((_BYTE *)&unlock_key_modif + (next_count & 3));
}
if ( (_BYTE)count == 0xC )
{
putchar('s');
goto LABEL_3;
}
putchar((unsigned __int8)xor_value ^ (unsigned __int8)shifted_unlock_key);
if ( (_BYTE)count != 0x21 )
goto LABEL_3;
return __readfsqword(0x28u) ^ stack_cookie;
}
As you can see, this algorithm is simply another array-lookup-xor method. 🙂
You might actually be able to copy paste the pseudo-code as it is, but i took the courtesy of rewriting the assembly’s logic to C++, which is more reliable than the automated decompiler.
You can find this on the repository
Flag: NC3{alle_glæder_sig_til_lillejuleaften}
intr0
This challenge was a GameCube ROM (.dol) which contained a demo scene sequence (The text has been modified in the video).
This one was actually pretty interesting, you could insert 1’s and 0’s to the screen’s displayed flag NC3{<->}, and when you hit enter it showed an url to a christmas music video.
Presumably, the solution would be to find the correct binary sequence, and then when you hint enter the flag would be revealed.
To figure out how this was done, our approach was to find the input handler and figure out how the input was treated. Since this was a PowerPC binary, we could not use IDA Pro (due to the fact that we have not purchased the PowerPC disassembler and decompiler), so we were stuck with Ghidra.
While looking at the main function (80006578) we noticed a few functions that resembled a basic is_key_pressed implementation:
uint main_input_related_2(uint mode)
{
if (3 < mode) {
return 0;
}
if ((&g_input_related_2_1)[mode * 4] == -1) {
return 0;
}
return (uint)(ushort)(&g_input_related_2)[mode * 8];
}
At the very bottom of the main function, this and similar table-lookup functions are used in conjunction with a function we quickly determined handled the event:
if (0x24 < var_input + 0x12) {
uStack188 = var_input ^ 0x80000000;
local_c0 = 0x43300000;
fVar3 = DAT_8020328c - DAT_80203274;
fVar2 = DAT_80203294 - DAT_8020327c;
fVar1 = (float)((double)CONCAT44(0x43300000,uStack188) - (double)DAT_801f684c) / DAT_801f6824;
DAT_80203274 = fVar1 * fVar3 + DAT_80203274;
DAT_8020327c = fVar1 * fVar2 + DAT_8020327c;
DAT_8020328c = fVar1 * fVar3 + DAT_8020328c;
DAT_80203294 = fVar1 * fVar2 + DAT_80203294;
}
var_input = main_input_related_2(0);
if (((var_input & 0x1000) == 0) ||
(flag_string = (char *)handle_enter(uVar12), flag_string == (char *)0x0)) {
var_input = main_input_related_2(0);
if ((var_input & 0x400) == 0) goto LAB_8000711c;
LAB_80007178:
main_inner_2(0);
LAB_80007180:
FUN_80007e30(&DAT_8002c820,DAT_80063d78,0);
}
else {
copy_string(acStack576,flag_string);
var_input = main_input_related_2(0);
if ((var_input & 0x400) != 0) goto LAB_80007178;
LAB_8000711c:
iVar5 = FUN_80007ebc();
if (iVar5 == 0) goto LAB_80007180;
}
This handle_enter function seemingly did some basic xor arithmetic on a global member with our input integer (that you input as binary before proceeding), then calculating a hash of the output string and *only* displaying it, if it is equal to 0xCD. This is a very short hash for what is presumably a 15+ char string, therefore we are going to have collision, but we’ll deal with those later. Here’s the handle_enter pseudo from Ghidra:
byte * handle_enter(uint key)
{
uint index;
int var3;
char key_arr [2];
g_displayed_string._0_4_ = (-(key >> 8) - 0x1f >> 1 & 0x7f) << 0x18; key_arr = (short)key; g_displayed_string._4_4_ = 0; index = 1; g_displayed_string._8_4_ = 0; var3 = 0x16; g_displayed_string._12_4_ = 0; g_displayed_string._16_4_ = 0; g_displayed_string._20_4_ = 0; do { g_displayed_string[index] = (byte)((uint)(byte)s_FLAG_encrypted[index] - (uint)(byte)key_arr[index & 1] >> 1) & 0x7f;
index = index + 1;
var3--;
} while (var3 != 0);
flag_hash = calculate_flag_hash();
if (flag_hash != 0xCD) {
g_displayed_string._0_4_ = s_bitly_xord[0] ^ 0x47474747;
g_displayed_string._4_4_ = s_bitly_xord[1] ^ 0x47474747;
g_displayed_string._8_4_ = s_bitly_xord[2] ^ 0x47474747;
g_displayed_string._14_2_ = g_displayed_string._14_2_ & 0xff;
g_displayed_string._12_4_ = CONCAT22(0x4a30,g_displayed_string._14_2_);
}
return g_displayed_string;
}
Now, i should’ve known better, but I unironically assumed that Ghidra could decompile these simple arithemtic expressions properly (spoiler alert: it couldn’t), so i had to recreate this entire function from powerpc to C++, which ended up being not that hard:
using flag_t = std::array<char, 0x18>;
inline flag_t get_string(std::uint32_t key)
{
constexpr std::uint8_t encrypted_flag[] =
{
0xE1, 0xC2, 0xED, 0xD2,
0x03, 0xDC, 0xF7, 0xBC,
0x0F, 0xF0, 0xEB, 0xBC,
0xF3, 0xBC, 0x19, 0xBA,
0xE5, 0xB8, 0xE5, 0xCC,
0xDF, 0xBC, 0xFF };
auto flag = flag_t();
// CALCULATE FIRST BYTE
std::uint32_t first_byte = key;
first_byte = ppc::__rlwinm(first_byte, 0x18, 0x8, 0x1f);
first_byte = -first_byte - 31;
first_byte = ppc::__rlwinm(first_byte, 0x1f, 0x19, 0x1f);
flag.at(0) = static_cast<std::uint8_t>(first_byte);
// BYTE SWAP AND TRUNCATE
auto key_truncated = _byteswap_ushort((std::uint16_t)key);
auto key_arr = reinterpret_cast<std::uint8_t*>(&key_truncated);
for (size_t index = 1; index < flag.size() - 1; index++)
{
const std::uint32_t key_delta = encrypted_flag[index] - key_arr[index & 1];
flag.at(index) = ppc::__rlwinm(key_delta, 0x1F, 0x19, 0x1F);
}
flag.at(flag.size() - 1) = 0x00;
return flag;
}
Now that i can properly calculate the flag from a given input value, we need to implement the hashing algorithm and then run a quick brute-force on any potential flags.
The hashing algorithm pseudo from Ghidra is:
uint calculate_flag_hash(void)
{
uint uVar1;
uVar1 = (g_displayed_string._0_4_ & 0x7f7f7f7f) + (g_displayed_string._4_4_ & 0x7f7f7f7f) ^
(g_displayed_string._0_4_ ^ g_displayed_string._4_4_) & 0x80808080;
uVar1 = (uVar1 & 0x7f7f7f7f) + (g_displayed_string._8_4_ & 0x7f7f7f7f) ^
(uVar1 ^ g_displayed_string._8_4_) & 0x80808080;
uVar1 = (uVar1 & 0x7f7f7f7f) + (g_displayed_string._12_4_ & 0x7f7f7f7f) ^
(uVar1 ^ g_displayed_string._12_4_) & 0x80808080;
uVar1 = (uVar1 & 0x7f7f7f7f) + (g_displayed_string._16_4_ & 0x7f7f7f7f) ^
(uVar1 ^ g_displayed_string._16_4_) & 0x80808080;
uVar1 = (uVar1 & 0x7f7f7f7f) + (g_displayed_string._20_4_ & 0x7f7f7f7f) ^
(uVar1 ^ g_displayed_string._20_4_) & 0x80808080;
return (uVar1 >> 0x18) + (uVar1 >> 0x10) + (uVar1 >> 8) + uVar1 & 0xff;
}
I recreated this by hand as well, but it is pretty dirty as I haven’t run it through llvm’s optimizer, and I can’t really be bothered since it works just fine:
inline std::uint32_t get_hash(flag_t flag)
{
const auto flag_integers = reinterpret_cast<std::uint32_t*>(flag.data());
std::uint32_t r8 = _byteswap_ulong(flag_integers[0]);
std::uint32_t r6 = _byteswap_ulong(flag_integers[1]);
std::uint32_t r7 = 0x7f7f7f7f;
std::uint32_t r0 = 0x80808080;
std::uint32_t r3 = r6 & r7;
r6 ^= r8;
r8 &= r7;
r8 += r3;
r6 &= r0;
r3 = _byteswap_ulong(flag_integers[2]);
r8 ^= r6;
r6 = r8 & r7;
std::uint32_t r10 = 0x18;
r8 ^= r3;
r3 &= r7;
r6 += r3;
r8 &= r0;
r3 = _byteswap_ulong(flag_integers[3]);
r8 ^= r6;
r6 = r8 & r7;
r8 ^= r3;
r3 &= r7;
r8 &= r0;
r10 = r6 + r3;
r6 = _byteswap_ulong(flag_integers[4]);
r10 ^= r8;
r3 = r10 & r7;
r10 ^= r6;
r6 &= r7;
r10 &= r0;
r3 += r6;
r10 ^= r3;
r8 = _byteswap_ulong(flag_integers[5]);
r3 = r10 & r7;
r7 &= r8;
r10 ^= r8;
r7 += r3;
r10 &= r0;
r10 ^= r7;
r8 = ppc::__rlwinm(r10, 16, 16, 31);
r3 = ppc::__rlwinm(r10, 8, 24, 31);
r3 += r8;
r8 = ppc::__rlwinm(r10, 24, 8, 31);
r3 += r8;
r3 += r10;
r3 = ppc::__rlwinm(r3, 0, 24, 31);
return r3;
}
It is essentially powerpc in C++, but LLVM optimizes and compiles this insanely well, so i won’t mess with it any further:

When that was all set and done, i set up a simple loop that iterated key values ranging from 0x00000 to 0x10000, printing all flags that were valid candidates:

Flag: NC3{CHIPTUNEZ_HELE_DECEMBER}
Den Røde Tråd
I actually had really high hopes for this one. The challenge consisted of a PE64 that seemed to be heavily obfuscated. The program took input parameters and transformed them into a given output. I began reversing this and quickly realized that they just copy-pasted the same dynamic function import macro (using PEB->LDR_MODULE iteration and a simple CRC check), making it seem hard to reverse, while in reality it was extremely simple. I extensively reversed the application, and whilst doing so creating a complete application clone in C++ with the help of Matti, but we really can’t find out how this was supposed to be solved. The constant 0x0F0B07030E0A06020D0905010C080400 used by the cipher is equal to that in Anubis SIMD variants, but we couldn’t find any other resemblance between the two ciphers.
At the end of the executable file, the below base64 string was appended:
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
This base64 string decodes to an OpenSSL (due to the Salted_ prefix) encrypted buffer, which we could not find any further information about (algorithm, etc.)
We thought that the solution would be to reverse the algorithm in the process, but this algorithm is *not* involutable, due to data loss in the SIMD instruction sequence, therefore reversing the algorithm is not possible.
The protections in the application could be handled by simply attaching x64dbg, using ScyllaHide to bypass the very poor attempt at throwing off debuggers by registering a vectored exception handler and continuously closing a protected handle.
Here is the relevant code in the entrypoint function which transforms the input array, before passing it on to the cipher algorithm:
// ADD EXCEPTION HANDLER
g_set_exception_handler = RtlAddVectoredExceptionHandler)(0, exception_handler);
if ( !g_set_exception_handler )
goto EXIT;
// CREATE A NEW THREAD FOR EACH INPUT (0x80) BYTE
thread_id = 0;
while ( true )
{
__asm { int 1; - internal hardware - SINGLE-STEP }
thread_handles[thread_id] = CreateThread(0, 0, handle_input_array, thread_id++, 0, 0);
if ( thread_id < 0x80 )
continue;
break;
}
This is the function that is executed 0x80 times to handle each input byte in the input array (zero initialized)
int __stdcall handle_input_array(void *thread_index)
{
__asm
{
vxorps xmm0, xmm0, xmm0
vcvtsi2sd xmm0, xmm0, esi
vaddsd xmm0, xmm0, ds:qword_4131D0[eax*8]
vmovsd [esp+2Ch+var_2C], xmm0
}
v16 = sin(thread_index) * 4096.0;
__asm { vcvttsd2si eax, qword ptr [ebp-18h] }
val_48 = gentable[(unsigned __int8)gentable_offset + _EAX];
v18 = input_table[thread_index % (unsigned __int8)byte_432110];
v19 = input_table[(thread_index + 1) % (unsigned __int8)byte_432110];
v20 = input_table[(thread_index + 2) % (unsigned __int8)byte_432110];
throw_mutex_exception();
g_transformed_input[thread_index] = v18 + v19 + (val_48 ^ v20);
return 0;
}
This function deliberately throws exceptions to throw off the debugger, but it doesn’t actually work on proper debuggers.
int throw_mutex_exception()
{
HANDLE mutex_handle; // eax MAPDST
mutex_handle = CreateMutexA(0, 0, "NuDetJulIgen");
if ( mutex_handle )
{
SetHandleInformation(mutex_handle, 2u, 2u); // HANDLE_FLAG_PROTECT_FROM_CLOSE
CloseHandle(mutex_handle);
}
return 0;
}
Interestingly enough, they’re trying to throw off output if a hypervisor is present by checking bit 31 of ECX of CPUID leaf 0x1:
signed int __stdcall exception_handler(_EXCEPTION_POINTERS* exception)
{
PCONTEXT ctx = exception->ContextRecord;
if ( ctx->Dr0 || ctx->Dr1 || ctx->Dr2 || ctx->Dr3 )
breakpoint_hit_count += 0x99;
else
ctx->Eip += 2;
__asm { vmovdqa xmm0, ds:xmmword_4131B0 }
_EAX = 1;
__asm
{
cpuid
vmovdqu [ebp+var_10], xmm0
}
gentable_offset += (_ECX >> 31) & 1; // USED BY handle_input_array
return -1;
}
Here is a perfect replica of the application in C++, input and output matches in all cases. Since no one solved this challenge, it could be a mistake on their end, or we’re just stupid.
alignas(32) const std::uint64_t xmm2_in[] = { 0x0101010101010101, 0x0101010101010101 };
alignas(32) const std::uint64_t xmm3_in[] = { 0x0D0905010C080400, 0x0F0B07030E0A0602 }; // { low, hi } - vpshufb mask
alignas(32) const std::uint64_t xmm4_in[] = { 0x0202020202020202, 0x0202020202020202 };
namespace red_thread
{
using flag_t = std::array<std::uint8_t, 0x80>;
__forceinline flag_t calculate_input(std::string_view string)
{
auto result = flag_t();
for (size_t i = 0; i < 0x80; i++)
{
// CONVERT TO DOUBLE, CALCULATE SIN AND CONVERT BACK
const auto index_double = _mm_cvtsi32_sd(__m128d{}, i);
const auto sin_value = sin(*(double*)&index_double) * 4096.f;
const std::uint32_t section_index = _mm_cvttsd_si32(*(__m128d*)&sin_value);
const std::uint8_t character1 = string[(i + 0) % 0x40];
const std::uint8_t character2 = string[(i + 1) % 0x40];
const std::uint8_t character3 = string[(i + 2) % 0x40];
const std::uint8_t table_val = red_thread::data_section[0x2000 + section_index];
result.at(i) = character1 + character2 + (table_val ^ character3);
}
return result;
}
__forceinline flag_t calculate_flag(char* input, flag_t output)
{
__asm
{
vmovdqa xmm2, xmmword ptr xmm2_in // xmm2 = 0x01010101010101010101010101010101
vmovdqa xmm3, xmmword ptr xmm3_in // xmm3 = 0x0F0B07030E0A06020D0905010C080400
vmovdqa xmm4, xmmword ptr xmm4_in // xmm4 = 0x02020202020202020202020202020202
}
for (size_t i = 0; i < 0x70; i += 0x10)
{
__asm
{
mov edx, i
vmovdqu xmm0, output[edx] // xmm0 = input_array_copy[edx]
vpsubd xmm0, xmm0, xmm2 // xmm0 = xmm0 - xmm2
vpsubw xmm1, xmm0, xmm4 // xmm1 = xmm0 - xmm4
vmovdqu xmm0, xmmword ptr input // xmm0 = input_arg
vpshufb xmm0, xmm0, xmm3 // shuffle xmm0 according to mask xmm3
vpaddb xmm0, xmm1, xmm0 // xmm0 = xmm1 + xmm0
vpalignr xmm0, xmm0, xmm0, 0Fh // xmm0 = "Concatenate xmm0 and xmm0, extract byte aligned result shifted to the right by constant value 0xF" https://www.felixcloutier.com/x86/palignr
vpshufb xmm0, xmm0, xmm3 // shuffle xmm0 according to mask xmm3
vmovdqu output[edx], xmm0 // input_array_copy[edx] = xmm0
}
}
return output;
}
}
It can also be found on the GitHub repository
Forensic
rudolf
Flag: NC3{JULERENSDYR!}
Juleønske
Flag: NC3{tystys_nissen}
Zip-Mareridt!!!
Flag: NC3{godt_det_er_overstået}
WordPerfekt
Flag: NC3{0KAY_okai}
indrammet_julemand
Flag: NC3{JULEMANDENS_RENSDYR_PÅ_TUR}
Nisse IT support
Flag: NC3{tak_du_fandt_den}
CTF
alle_kan_være_med
Flag: NC3{eksempel_på_et_flag}
skru_op!!
Flag: NC3{nice_godt_fundet}
Gæt Et Format
Flag: NC3{der_kan_bruges_mange_forskellige_formater}
enplusfiretreds
Flag: NC3{har_nogen_set_min_dobbelt_diskettestation??}
Onion
This challenge consisted of the following text:
Jeg *hjerte* Ron og julen! Ron og julen er n1ce! - Ukendt citat fra 1987
993357cf36a3008c0bfe4bb417daef1307340fe565fe4ccc215e5fd6805e88749f98acd5155baaeae4e4dce428e8c0e11870b03af8ab8ba2f41d40da6d0313e5b0612650a615873465dd0e6b15c7a0fda3bf80d2b81b1365c2bc35f675b5050dabdbdedf0f90cd32
Now, the way to solve this was to RC2 decrypt the above hex string with the key julen and no IV, then you get another hex string which you rc2 decrypt with the key Ron and no iv, which yields a base64 string that decodes to the flag.
Here’s a CyberChef project that calculates it for you. There really wasn’t any thought to this, we just kept trying to guess keys from the input string. We figured it would be RC2/RC4 due to the Ron Rivest reference, that’s all there is to it.
Flag: NC3{flere_lag_det_er_ingen_sag}
Hardcore Crypto!!
Flag: NC3{crypto_er_total_sejt__når_det_bruges_rigtigt}
crypto_dumper
Flag: NC3{DET_HER_ER_DA_NEMT}
Easy Pass
Flag: NC3{En_lille_nisse}
Analyse
Hyggelæsning
This challenge consisted of an excel document with a ton of russian words on it. By doing a word frequency analysis, we found the least frequent words:
ТОСКОВАЛ
МАРМЕЛАДОВА
ПPИДЕТ
Googling these words lead us to a book called Crime and Punishment, and since the challenge hint said “What are you reading?” we figured the flag was the name of the book. It was, in fact, not the name of the book… We decided to continue reading the Wikipedia page and found a paragraph about the first newspaper that posted the Crime and Punishment text, which was called Русский вестник, and that was the correct flag. :-\
Flag: NC3{Русский_вестник}
Julemandens Juleanalyse
This challenge contained a csv file of floating point values and corresponding characters. Doing a frequency analysis lead us to the fact that the floating point list
was a normal distribution, and we managed to recreate the floating point generation using mersenne-twister with seed=1, which means that they generated the flag after the fact.
Remembering from last year where there was a similar challenge, where you had to sort out RNG discrepancies, we tried to do all sorts of filtering and sorting and randomly stumbled on the sorting filter: abs(floating_value)>3 yielded a flag value:
Flag: NC3{NaughtyN1ceDeterminat0r!}
Boot2Root
To be completely honest, this was definitely not the intended way to solve these “pwn” challenges, but i still find it relevant to explain how it was done. These boot2root/pwn challenges will always vulnerable to these memory-hack challenges, where was proper box-pwning challenges are hosted remotely with no memory access. We did this last year and will continue to do so as long as you hand us virtual machine images for a “challenge”. The proper solution to this category can be found on my friend Astrid’s website
The virtual image was a snapshot taken *after* inputting the LUKS disk decryption slot key, which leaves out the good old GRUB root-boot we used last year, but you’re still perfectly capable of getting root by other means.
First, we converted the virtual memory image to VMWARE format, which gives us raw virtual memory access to the snapshot: nc3ctf2019_boot2root-Snapshot23.vmem
Then we dumped the LUKS key using findaes on the memory dump:
findaes.exe nc3ctf2019_boot2root-Snapshot23.vmem
which gave us some different keys, but the last one was a 512-bit key and we assumed that was the LUKS key:Searching nc3ctf2019_boot2root-Snapshot23.vmem
Found AES-256 key schedule at offset 0x50a55fc:
bf 38 7f 14 90 8a 0c 9f 0a ed 4d de 67 2a 95 e5 04 d4 c5 ac c9 d2 3e a8 b2 08 0a fa b7 73 05 e2
Found AES-256 key schedule at offset 0x526e5fc:
d6 47 73 09 2a 4b b1 bc 18 0c 7b 8c 88 e1 12 85 95 7d 80 5f f5 ad 7c a7 20 1f 3d aa 3c 2e 35 fc
Found AES-256 key schedule at offset 0xb4185fc:
b1 23 93 97 3c 5a 43 8d dd 6c b2 51 1f 53 1d 9b df 93 73 30 72 9e 84 96 1c 52 ee 79 be 57 ff 44
Found AES-128 key schedule at offset 0x17799030:
61 36 50 8d e0 5b 57 b9 d2 04 10 6f 08 cd 59 f0
Found AES-128 key schedule at offset 0x17799230:
76 00 43 ec 6b e0 16 3b 88 61 fd 2a 59 fd 09 04
NB: Key is likely part of a 256(?) bit composite:
76 00 43 ec 6b e0 16 3b 88 61 fd 2a 59 fd 09 04 61 36 50 8d e0 5b 57 b9 d2 04 10 6f 08 cd 59 f0
Found AES-256 key schedule at offset 0x194dd030:
01 88 97 60 c0 98 41 a7 79 59 94 c1 68 06 68 a1 d1 54 be d4 2f da 26 75 13 5a 63 2c 24 bc b9 e5
Found AES-256 key schedule at offset 0x194dd230:
8e 46 20 5c 65 73 e0 bb 57 81 c0 b0 ec 26 81 96 0a ba 84 f2 d2 ef 4f eb 6a b7 80 bd b5 d1 6d ba
NB: Key is likely part of a 512(?) bit composite:
8e 46 20 5c 65 73 e0 bb 57 81 c0 b0 ec 26 81 96 0a ba 84 f2 d2 ef 4f eb 6a b7 80 bd b5 d1 6d ba 01 8
8 97 60 c0 98 41 a7 79 59 94 c1 68 06 68 a1 d1 54 be d4 2f da 26 75 13 5a 63 2c 24 bc b9 e5
Then we added our own passphrase to the root user by using cryptsetup luksAddKey:
echo "8e 46 20 5c 65 73 e0 bb 57 81 c0 b0 ec 26 81 96 0a ba 84 f2 d2 ef 4f eb 6a b7 80 bd b5 d1 where /dev/sdb5 is the mounted virtual disk of the image.
6d ba 01 88 97 60 c0 98 41 a7 79 59 94 c1 68 06 68 a1 d1 54 be d4 2f da 26 75 13 5a 63 2c 24 bc b9 e5" > txt
xxd -r -p txt key
cryptsetup luksAddKey /dev/sdb5 --master-key-file key
Then entered our desired passphrase, and we had full root access within minutes 🙂
We solved most of the flags instantly by simply grepping for NC3{, leaving us with two challenges we had to go look for manually.
flag1
This flag was located on disk inside of the http server.
Flag: NC3{jeg_er_endelig_inde}
flag2
This flag was located on disk inside of the http server.
Flag: NC3{www-data__kan_det_hele}
flag3
Inside of /home/julenisse/.ecryptfs was a file called “husker”, which translates to reminder.
The contents of the file were four hashes:
Sikkerhed frem for alt!!
698e4cf3b5e00889f8b24543529b79ac
f05c8652de134d5c50729fa1b31d355b
c4a1d1c029de92575c768bff573239d2
f05c8652de134d5c50729fa1b31d355b
These hashes were simple MD5 hashes and we cracked them instantly using an online MD5 cracker:
Hash Type Result
698e4cf3b5e00889f8b24543529b79ac md5 glade
f05c8652de134d5c50729fa1b31d355b md5 jul
c4a1d1c029de92575c768bff573239d2 md5 dejlige
f05c8652de134d5c50729fa1b31d355b md5 jul
logging into lillenisse with the password gladejuldejligejul then granted you the flag on disk:
Flag: NC3{suid_suid_suid}
flag4
This flag was located on disk
Flag: NC3{jeg_er_virkelig_i_julestemning_nu}
flag6 – bonus
After grepping for “flag”, we found this file on disk:
"VI VIL HA' MERE!" ... Okay, så er der bonus flag:
Her er en AES-krypteret HEX streng:
a06dc1b6c4db7f33215627c0de116b3c1ec1f3e4408398618946d7930d592230970fd5b1e336202450b0f68044a8093055651f7b9c2488f86a46271bf56398d9
CBC er fin. Key og IV er det samme som til root. Her er en lille kodeordsliste:
jul
nisse
dejligt
2019
hos
længe
en
varer
dejligt
NC3
So we generated permutations where length was equal 32 and ran the hash through hashcat, which gave us the key:
julenhosNC3varerdejligtlænge2019
and the contents:
TkMze2IwbnVzaW5mb19fZm9yYnJ5ZGVsc2VfYmV0YWxlcl9zaWdfaWtrZX0=
Which decodes to:
Flag: NC3{b0nusinfo__forbrydelse_betaler_sig_ikke}
flag5
This flag was located on disk
Flag: NC3{r00t_r00t_dansemus_med_rigtig_seje_dansemoves}
Misc
En lille tur
Flag: NC3{GPS_NEMT}
Stop juletyven
Flag: NC3{Juletyvens_plan}
H3X8
Flag: NC3{0tte_mester}
Juleskibet
Flag: NC3{mellemgod_til_grader}