Post

Flare-On 2024: checksum

Introduction

This reversing challenge was part of the yearly Flare-On 2024 CTF by Mandiant. The challenge contained a Windows executable which required the user to fill in several checksums, which would then lead to decrypting and writing the flag as a .JPG to your host. The executable had anti-tampering techniques in place in order to prevent easier solutions than intended.

Initial behaviour

Running file checksum.exe gives us the following output:

1
2
└─$ file checksum.exe
checksum.exe: PE32+ executable (console) x86-64, for MS Windows, 15 sections

We are dealing with a 64 bit Windows PE which we will run to see it’s initial behaviour:

Startup

The executable prompts the user for several simple math equations, followed by a checksum without an equation. Upon entering any (wrong) checksum, the application terminates, which is the same result as submitting an incorrect equation.

IDA

Import table

A good habit to have is to check the import table as the first thing you do. This will help you understand the behaviour of the executable, and what it’s capable of doing.

Import table

In this import table we notice several interesting functions being used, CreateFileA (used to create or open files on disk) such as WriteFile (usually writes data to files on disk) and VirtualAlloc (used to allocate memory). With this info, we can assume the executable will create a create a new or modify an existing file on our host.

Function table

It’s also a good habit to inspect the function table at the beginning, this can give away behaviour in case the functions aren’t obfuscated. In this case, when Go compiles a binary, it links all the code directly, including functions from the Go runtime and any external packages used.

functions

As you can see in the image above, there are +- 1378 functions present in the executable, where the majority are just Go runtime functions. However, usually it is good to search for main, start or entryto find our starting point. For Go, the entry point is usually called main_main.

Main

In this case, there is also main_a and main_b. We will come back later to this.

Executable flow

main_main

Inital main

Navigating to main_main we notice the behaviour we encountered upon running the executable. The amount of times the checksum equation gets prompted is a random number ranging from 1 to 5, plus 3.

Copying

When debugging, we can notice our checksum input follows the right code block; which, long story short, copies our data from one memory location to another using Golang multithreading.

Key length

Next, once we pass the checksum equations, our checksum input is being compared to see if its 32 bytes long. If it’s not, the executable terminates.

main_b

So how do we find the correct checksum? When we look back at our function table, we can tell main_b is used as a function to terminate our executable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  if ( result )
  {
    v24[0] = &RTYPE_string;
    v24[1] = runtime_convTstring(a3, a4, a3, a4, a5, a6, a7, a8, a9);
    v9 = os_Stdout;
    fmt_Fprintln(
      (unsigned int)go_itab__os_File_io_Writer,
      os_Stdout,
      (unsigned int)v24,
      1,
      1,
      v10,
      v11,
      v12,
      v13,
      v19,
      v21,
      v22,
      v23);
    return os_Exit(-559038737, v9, v14, 1, 1, v15, v16, v17, v18, v20);
  }
  return result;
}

That leaves us with main_a.

main_a

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
  if ( !a1 )
    a1 = &runtime_noptrbss;
  v26 = a1;
  v9 = runtime_makeslice((unsigned int)&RTYPE_uint8, a2, a2, a4, a5, a6, a7, a8, a9);
  v15 = v26;
  for ( i = 0LL; a2 > i; ++i )
  {
    a5 = v9;
    v17 = v15;
    v18 = i - 11 * ((__int64)((unsigned __int128)(i * (__int128)0x5D1745D1745D1746LL) >> 64) >> 2);
    v19 = v15[i];
    if ( v18 >= 0xB )
      runtime_panicIndex(v18, i, 11LL);
    v10 = "FlareOn2024bad verb '%0123456789_/dev/stdout/dev/stderrCloseHandleOpenProcessGetFileTypeshort write30517578125bad argSizemethodargs(reflect.SetProcessPrngMoveFileExWNetShareAddNetShareDeluserenv.dllassistQueuenetpollInitreflectOffsglobalAllocmSpanManualstart traceclobberfreegccheckmarkscheddetailcgocall nilunreachable s.nelems=   of size  runtime: p  ms clock,  nBSSRoots=runtime: P  exp.) for minTrigger=GOMEMLIMIT=bad m value, elemsize= freeindex= span.list=, npages = tracealloc( p->status= in status  idleprocs= gcwaiting= schedtick= timerslen= mallocing=bad timedivfloat64nan1float64nan2float64nan3float32nan2GOTRACEBACK) at entry+ (targetpc= , plugin: runtime: g : frame.sp=created by broken pipebad messagefile existsbad addressRegCloseKeyCreateFileWDeleteFileWExitProcessFreeLibrarySetFileTimeVirtualLockWSARecvFromclosesocketgetpeernamegetsocknamecrypt32.dllmswsock.dllsecur32.dllshell32.dlli/o timeoutavx512vnniwavx512vbmi2LocalAppDatashort buffer152587890625762939453125OpenServiceWRevertToSelfCreateEventWGetConsoleCPUnlockFileExVirtualQueryadvapi32.dlliphlpapi.dllkernel32.dllnetapi32.dllsweepWaiterstraceStringsspanSetSpinemspanSpecialgcBitsArenasmheapSpecialgcpacertracemadvdontneedharddecommitdumping heapchan receivelfstack.push span.limit= span.state=bad flushGen MB stacks, worker mode  nDataRoots= nSpanRoots= wbuf1=<nil> wbuf2=<nil> gcscandone runtime: gp= found at *( s.elemsize= B (";
    v11 = (unsigned __int8)aTrueeeppfilepi[v18 + 3060];
    *(_BYTE *)(a5 + i) = v11 ^ v19;
    v9 = a5;
    v15 = v17;
  }
  v20 = v9;
  v21 = encoding_base64__ptr_Encoding_EncodeToString(
          runtime_bss,
          v9,
          a2,
          a2,
          a5,
          (_DWORD)v10,
          v11,
          v12,
          v13,
          v23,
          v24,
          v25);
  if ( v20 == 88 )
    return runtime_memequal(
             v21,
             "cQoFRQErX1YAVw1zVQdFUSxfAQNRBXUNAxBSe15QCVRVJ1pQEwd/WFBUAlElCFBFUnlaB1ULByRdBEFdfVtWVA==");
  else
    return 0LL;
}

This function looks much more promising. At first we can notice a really strong string which contains “junk”. This is due to Golang not using null-terminated strings, and IDA can’t really tell where the real string ends. In our case, we can make an educated guess and say the real string is v10 = FlareOn2024

main_a appears to take an argument, which is then being used to perform operations such as bitshifting and a XOR operation.

At the end of the function, the processed data is being compared to a base64string: cQoFRQErX1YAVw1zVQdFUSxfAQNRBXUNAxBSe15QCVRVJ1pQEwd/WFBUAlElCFBFUnlaB1ULByRdBEFdfVtWVA==

Using a python script, we decode the b64 encoded string XOR the result with the key.

1
2
3
4
5
6
7
8
9
10
11
12
import base64

b64string = "cQoFRQErX1YAVw1zVQdFUSxfAQNRBXUNAxBSe15QCVRVJ1pQEwd/WFBUAlElCFBFUnlaB1ULByRdBEFdfVtWVA=="
key = "FlareOn2024"

decoded = base64.b64decode(b64string)
result = bytearray()

for i in range(len(decoded)):
    result.append(decoded[i] ^ ord(key[i % len(key)]))

print(result.decode(errors='ignore'))

This gives us the following result, which also appears to be 32 bytes in size (remember the checksum input check!): 7fd7dd1d0e959f74c133c13abb740b9faa61ab06bd0ecd177645e93b1e3825dd

Retrieving the flag

Upon entering this string into the checksum prompt, the executable terminates. However, further inspecting IDA, we can see what really happens.

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
  v226.len = os_UserCacheDir();
  v216 = len;
  main_b(v111, v89, (unsigned int)"Fail to get path...", 19, v100, v112, v113, v114, v115, v151, v170, *(__int64 *)v186);
  v116 = v226.len;
  v121 = runtime_concatstring2(
           0,
           v226.len,
           v216,
           (unsigned int)"\\REAL_FLAREON_FLAG.JPG",
           22,
           v117,
           v118,
           v119,
           v120,
           v152,
           v171,
           *(__int64 *)v186,
           *(__int64 *)&v186[8]);
  v122 = v215[0];
  v126 = os_WriteFile(
           v121,
           v116,
           (int)v226.ptr,
           v214[0],
           v215[0],
           420,
           v123,
           v124,
           v125,
           v153,
           v172,
           *(_slice_uint8 *)v186,
           0);

The application retrieves os_UserCacheDir, which on Windows equals to C:\Users\<username>\AppData\Local. -> If it errors retrieving the directory, the application terminates. -> If it retrieves the directory succesful, it writes the decrypted flag as a .jpg file to the folder.

Upon opening the .jpg, we find the flag:

flag

FLAG: [email protected]

Extra

Since entering the initial checksums was very tedious, I decided to patch it so it only had to be entered once. This actually made the executable work improperly, which I suspect is either due to CRC, or possibly the ChaCha cipher (which I didn’t fully look into). So patching was not helpful unfortunately :@

This post is licensed under CC BY 4.0 by the author.