[CyberSecurityRumble 2021 - RE / Game Hacking] CSRunner
Collect green stuff, avoid red guys. Easy as pie, right? Not even your speedhacks will help you here! You might have to take a closer look and inspect it carefully. Have fun & good luck!
This challenge is a game, according to its description. Since I gave up on the challenge I was trying to do (NOdeBANKing), I went to help other teammates blocked on this challenge.
This game was using Unity3D as engine, where game logic is usually written in C# or other language that compiles to MSIL. So let’s find the GameAssembly.dll, but no luck it’s a native DLL and not a managed one… After opening it in IDA Free, I noticed “il2cpp_” references, and after some googling, figured out it was a tool that compile MSIL code to native x86 code.
I first thought that the game “logic” could actually be embedded in resources (the “data.unity3d” file in CSRunner_Data), but I failed to find a tool able to open the new UnityFS format, all I got was weird lz4 invalid stream issues coming from nowhere.
So, I looked for a tool able to reconstruct metadata of the “original” assembly, and finally came across Il2CppDumper, which even comes with an IDAPython script to rename funcs and add types in IDA Pro. I used that tool to generate “dummy” assemblies that could be opened in dnSpy, even if there was no MSIL code in the assemblies, but it’s a good starting point to understand the class structure.
And the tool also comes with IDAPython scripts to rename il2cpp-converted functions, which I could use since I managed to compile IDAPython and make it run on my IDA Free 7.6. I first explored Game methods, which only seemed to print debug messages stuff even if PickupHit
and PickupMiss
methods looked promising.
But there was another class with interesting name, DieOnCollision
, which seemed to handle collisions, I looked at the DieOnCollision$$OnCollisionEnter
and saw this:
tag = UnityEngine_GameObject__get_tag(gameObject, 0i64);
if ( System_String__op_Equality(tag, StringLiteral_4306, 0i64) )
{
if ( !byte_180A0BB4F )
{
sub_1801292D0(5488i64);
byte_180A0BB4F = 1;
}
instance = Game_TypeInfo->static_fields->instance;
if ( instance )
{
v9 = DieOnCollision_TypeInfo;
if ( (DieOnCollision_TypeInfo->_2.bitflags2 & 2) != 0 && !DieOnCollision_TypeInfo->_2.cctor_finished )
{
il2cpp_runtime_class_init(DieOnCollision_TypeInfo, v7);
v9 = DieOnCollision_TypeInfo;
}
PHRASES_ENEMY = v9->static_fields->PHRASES_ENEMY;
if ( PHRASES_ENEMY )
{
v11 = UnityEngine_Random__RandomRangeInt(0, PHRASES_ENEMY->max_length, 0i64);
v12 = v11;
if ( (unsigned int)v11 >= LODWORD(PHRASES_ENEMY->max_length) )
{
v32 = sub_180129250();
sub_1801293E0(v32, 0i64);
}
LABEL_15:
Game__GameOver(instance, PHRASES_ENEMY->m_Items[v12], 0i64);
return;
}
goto LABEL_50;
}
}
So I just had to force the if ( System_String__op_Equality(tag, StringLiteral_4306, 0i64) )
condition to be always false (patching the jz loc_1806E612D
at 0x1806e6078
to a jmp), and colliding with enemies would do nothing.
Then I gave the modified assembly to our team’s pro gamer (since I run Linux, and couldn’t manage to get the tool mentioned before on GameAssembly.so
of the Linux version), which was able to get the flag in two game tries.