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).
Intro
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 ?
TrompeLoeil.zip
Note: The application requires .NET runtime 8.0.0
Once expanded the application looks made by some different files, but the interesting ones are:
Trompe-Loeil.exe
Trompe-Loeil.dll
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)
[snip]
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.
[snip]
...
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)
...
[snip]
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]
...
[snip]
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 like5800FF34666A237551656C5A5B604463595E62585A5C4C5E5C5B6056595B4F5D5B5B5D565A59525B
- 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).
Outro
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!