Dock12/Approaching R2R from an RE point of view

Created by Cesare Pizzi Mon, 29 Jan 2024 17:20:37 +0100 Modified Mon, 29 Jan 2024 17:20:37 +0100

Here I am with another chapter of my CTF explorations. I stumbled in a very interesting challenge in the Insomni’hack Teaser 2024: TrompeLoeil.

I found it interesting because it touches a specific technique used to hide code, by abusing a .NET feature named R2R (Ready To Run).


Let’s spend few words about R2R in .NET: usually .NET executables contain what is called managed code, which is basically “code whose execution is managed by a runtime”, usually by the JIT Compiler. This code is stored in a form known as IL (Intermediate Language) and it is compiled when the program starts. In order to improve performances a new features called R2R has been introduced: to put it simple, the pre-compiled version of code is stored in the executable, and it is run directly instead of relying on the JIT Compiler. But guess what? You can in some way tamper the executable and change this pre-compiled code, so that the executed code will differ from the IL stored in the .NET app, confusing the Reverse Engineering activity. You can read more about R2R in this awesome article written by Jiri Vinopal @Checkpoint.

The Challenge

Here below the challenge description:

Something seems weird with this binary... Can you help me with it ?

Note: The application requires .NET runtime 8.0.0

Once expanded the application looks made by some different files, but the interesting ones are:


By inspecting the files, the first one looks like a C++ program which act as a launcher for the DLL, which is actually written in .NET.

Running the challenge

This is the output of the challenge:


As you can see there is a request for the flag, then some strings written for the checks and the answer at the end: Nope!

Looking at the code

The obvious choice for inspecting .NET code is dnSpy (or one of its forks, like dnSpyEx). Let’s start from the Main (entry point):


That looks similar to what we saw at runtime, but not exactly the same. For example, we don’t see any WriteLine for the Checking... string, which looks odd. May we what we are looking at it’s not exactly what has been run?

In the M Class we have a couple of other methods:


The first one is computing a SHA256 hash, the second one is just returning FALSE, and it looks like it not called at all.

So, what is going on here?

It seems we need to give R2R a try. In order to check if it is actually used, you can check the executable header and look for a Magic Number like 0x00525452 (RTR). Well, it’s a bit more complicated than that (look at the article above), but this is enough for our initial triage (CTFs need speed):


Now, how can I extract information about R2R? We don’t have many option for that at this time and my choice was to use R2RDump. Unfortunately we need to compile it by ourself, but it is not super hard to do and we can compile just the R2RDump.sln. Once done, let’s start our investigation:

R2RDump.exe -i Trompe-Loeil.dll --header --sc

This is extracting a lot of info, like R2R Header, Sections and details about the Methods. Looking for the Main method we see

void Trompe_Loeil.M.Main(string[])
Handle: 0x0600002A
Rid: 42
EntryPointRuntimeFunctionId: 46
Number of RuntimeFunctions: 1
Number of fixups: 8
    TableIndex 6, Offset 005A: "Guess the flag:" (STRING_HANDLE)
    TableIndex 6, Offset 005B: "Checking.\r" (STRING_HANDLE)
    TableIndex 6, Offset 005C: "Checking..\r" (STRING_HANDLE)
    TableIndex 6, Offset 005D: "Checking...\r" (STRING_HANDLE)
    TableIndex 6, Offset 005E: "Checking....\r" (STRING_HANDLE)
    TableIndex 6, Offset 005F: "49156db8ffcbf419b5777c28339b75ad6aaae115e3b5678437c10c2e4fb9e9f0" (STRING_HANDLE)
    TableIndex 6, Offset 0060: "Congratz!" (STRING_HANDLE)
    TableIndex 6, Offset 0061: "Nope!" (STRING_HANDLE)

Wow, there are some STRING_HANDLE referring to the Checking... strings…we are on the right track!

Dumping the Methods

Starting from Main we can now try to dump the R2R compiled methods

R2RDump.exe -i Trompe-Loeil.dll -q 42 -d

The 42 option passed to the command is row ID taken from the first dump (Rid). Again, we get a lot of info, and the disassembled code of the R2R method.

679: ff 15 19 7c 00 00         call    qword ptr [0xc298]    // void System.Threading.Thread.Sleep(int) (METHOD_ENTRY_REF_TOKEN)
467f: 48 8b 0d 6a 81 00 00      mov     rcx, qword ptr [0xc7f0] // "Checking....\r" (STRING_HANDLE)
4686: 48 8b 09                  mov     rcx, qword ptr [rcx]
4689: ff 15 81 7c 00 00         call    qword ptr [0xc310]    // void System.Console.WriteLine(string) (METHOD_ENTRY_REF_TOKEN)
468f: b9 e8 03 00 00            mov     ecx, 1000
4694: ff 15 fe 7b 00 00         call    qword ptr [0xc298]    // void System.Threading.Thread.Sleep(int) (METHOD_ENTRY_REF_TOKEN)
469a: 48 8b cb                  mov     rcx, rbx
469d: ff 15 55 7d 00 00         call    qword ptr [0xc3f8]    // bool Trompe_Loeil.M.T(string) (METHOD_ENTRY_DEF_TOKEN)
46a3: 85 c0                     test    eax, eax
46a5: 74 37                     je      0x46de
46a7: 48 8b cb                  mov     rcx, rbx
46aa: ff 15 40 7d 00 00         call    qword ptr [0xc3f0]    // string Trompe_Loeil.M.ComputeSha256Hash(string) (METHOD_ENTRY_DEF_TOKEN)

As you can see at 467f there is the print of the last Checking... string, then a call to Trompe_Loeil.M.T at 469d. This was completely missing from the “managed code” version.

T method looks empty from dnSpy, but not from R2RDump:

3cbc: 33 c0                     xor     eax, eax
3cbe: 48 89 44 24 20            mov     qword ptr [rsp + 32], rax
3cc3: 48 8b d9                  mov     rbx, rcx
3cc6: ff 15 2c 85 00 00         call    qword ptr [0xc1f8]    // System.Collections.Generic.List`1<string> (NEW_OBJECT)
3ccc: 48 8b f0                  mov     rsi, rax
3ccf: 48 8b ce                  mov     rcx, rsi
3cd2: ff 15 50 86 00 00         call    qword ptr [0xc328]    // void System.Collections.Generic.List`1<__Canon>..ctor() (METHOD_ENTRY)
3cd8: 48 8b 15 39 88 00 00      mov     rdx, qword ptr [0xc518] // "0x5800FF3466" (STRING_HANDLE)
3cdf: 48 8b 12                  mov     rdx, qword ptr [rdx]
3ce2: 48 8b ce                  mov     rcx, rsi
3ce5: 4c 8d 1d 64 84 00 00      lea     r11, [0xc150]         // void System.Collections.Generic.List`1<string>.Add(!0) (VIRTUAL_ENTRY)
3cec: 41 ff 13                  call    qword ptr [r11]
3cef: 48 8b 15 2a 88 00 00      mov     rdx, qword ptr [0xc520] // "0x2B43FF266D33001C454047122733181F2B3B46211E28242C2A2E" (STRING_HANDLE)
3cf6: 48 8b 12                  mov     rdx, qword ptr [rdx]
3cf9: 48 8b ce                  mov     rcx, rsi
3cfc: 4c 8d 1d 4d 84 00 00      lea     r11, [0xc150]         // void System.Collections.Generic.List`1<string>.Add(!0) (VIRTUAL_ENTRY)
3d03: 41 ff 13                  call    qword ptr [r11]
3d06: 48 8b 15 1b 88 00 00      mov     rdx, qword ptr [0xc528] // "0x4F02FF3" (STRING_HANDLE)
3d0d: 48 8b 12                  mov     rdx, qword ptr [rdx]

A lot of things are going on here, loading strings, joining them, making comparisons…the method is not too complex, but may be we can do some dynamic analysis to speed up things.

Debug with x64dbg

x64dbg is a great debugger, even if not built exactly for .NET executables. But with a couple of tricks, we can jump directly into the interesting code.

We can begin starting the code:

dotnet Trompe-Loeil.dll

When the program reaches the Guess the flag: prompt, we can start x64dbg and attach it to the dotnet process. Now it’s just a matter of searching in Memory Map a sequence of opcodes we found interesting (for example the start of T method) 33 c0 48 89 44 24 20 48 8b d9 ff 15 2c. Once found, we can place a breakpoint, then insert the flag at the prompt and do our job in the debugger once breakpoint is hit!

The Flag check

By inspecting the code and with the help of the debugger it became quite easy to understand what was going on:

  • Initially the T method loads two arrays of hex strings. Then it combine them to create a sequence like 5800FF34666A237551656C5A5B604463595E62585A5C4C5E5C5B6056595B4F5D5B5B5D565A59525B
  • The above is done for all the elements (44)
  • Each char of the inserted flag is “hashed” by using the Trompe_Loeil.S.F7 method and then compared with the previous array of “hashes”

In order to solve the challenge is enough to provide a complete alphabet to the F7 method (inserting a flag like 0123456789ABC...), noting down the produced hashes and then compare them with the first array “hashes”, and the result is INS{Re4dy_2_Run_M4ster_4r3_S0_R34dy_2_Fl4g!} (44 chars).


I found this challenge extremely funny, because it allowed me to practice with what Jiri wrote in his article about R2R. I’m also happy that I found my way to do some quick and dirty dynamic analysis. This became part of my knowledge now and that’s why I think playing CTF is important: they are not “useless and unrealistic exercises”, but on the contrary they allow you to build your toolkit, in a funny way :-). Happy hacking!