Google CTF 2020 [.Net-RE]
I got to know that Google CTF had some interesting rev challs. I was late for it so I didn’t participate in the event with a team and also we aren’t quite active these days.
I picked up 2nd least solved reversing challenge ie. .Net. Its an interesting challenge and I learnt a lot from it as well, so it might be a little longer than usual but its worth it in the end.
Challenge
.Net is great, especially because of all its weird features.
Download : chall.zip
TL;DR
- Dynamic Instrumentation using HarmonyLib
- C# <=> Native asm FFI
- IDAPython Scripting to assist in native functions analysis
- Replicate algo in python and then finding a valid solution for it using z3
Introduction
The challenge includes two files ie. 0Harmony.dll
and EKTORPFlagValidator.exe
.
So the name looks a bit weird. I just googled them and found that Harmony is actually a libary for doing some runtime patching stuff in .Net apps.
With the help of a sample run, we see that it requires a string of length = 30 and also that it validates a checksum from our input.
Static Analysis
So lets start at the entrypoint. And yeah at first I tried to analyse the constructor too but it looked kinda hard to understand statically and I left it for debugging at last.
So there is this main method which later calls LaunchGui() and as a result we are now in KVOT class.
Now we are inside KVOT and it looks like method names are weird.
After the basic initialization part for the GUI there is an event handler submit_button_Click()
which looks exactly how we want it to be.
Ahmmm It does the string length check here, and if it succeeds then call another method with our input string as an argument to SOCKERBIT.GRUNDTAL_NORRVIKEN
which returns a list.
Also after that it does a basic check that the elements of the returned list are all less than 63 which looks like a character range check from the failure string.
And yeah, There is more to it, just hold up.GRUNDTAL_NORRVIKEN
then calls DecodeBase64Bytewise
on each of the element of our string.
Hmm, now it gets weird… the DecodeBase64Bytewise
method looks like it is trying to instead encode our input and also it checks some conditions which would never return true except the 123('{')
and 125('}')
check.
That means if we input some lowercase characters which should be valid, then it’ll just return a list of 30 -1’s.
But the signness trick here casts it to an unsigned integer.
Something like this :
1 | if ((uint)(byte)(arg + 191) <= 25u) |
For eg. ‘H’(72) fulfills the above condition since (byte)(72+191)
is 7 when cast to a byte and not 263.
This is the 2nd part of the check routine with methods DAGSTORP
, SMORBOLL
and HEROISK
.
A list is initialized and passed to DAGSTORP
along with our encrypted input.
Ahh, It just looks like a simple xor, ezpz!
Then there is this checksum as you’ve guessed it from the failure string. It calculates the checksum on the basis that it does different calculations according to the index value.
The index value checks are just for checking whether the index is divisible by 2,3,5 or 7 and FYI, it doesn’t include the index 28 as it is later compared with the final checksum value.
And then there is method VAXMYRA
which just checks if there are any duplicates in the encrypted array.
And after bypassing through the checksum validation and VAXMYRA
we get to the main condition checks that validates our encrypted input. And Yes, I smell z3!
Dynamic Analysis
Getting to the main fun part ie. debugging it in dnsPy.
Huh wait a sec where is FYRKANTIG. Instead we get redirected to NUFFRA and now it looks more complicated and gibberish to deal with.
So to explain this inner magic and so called weirdness we’ll have to debug right from the constructor. If you’d traced the execution accurately, you’ll find that there is a calli instruction which calls a method by specifying its address as an argument.
I noticed that it calls several functions and just noted down the method addresses ie. from num
variable.
1 | 0x600003B # Debugger.IsAttached() check (native) |
0x600003B has a simple anti-debug check with System.Diagnostics.Debugger.IsAttached
!
0x6000046 is the magic method here which in turn calls another native function.
That native function has another anti-debug technique implemented ie kernel32.dll::IsDebuggerPresent
and if it is true it returns 1 which eventually doesn’t do anything and the latter will call a c# method which is indeed responsible for changing the methods.
And Fortunately it happens that dnsPy has got us covered and was successful in deafeating those anti-debug techniques like a ninja. All thanks to this issue
Harmony Magic
So this is the function called if the isDebuggerPresent returns false and it looks like it is patching the original methods we found during the static analysis to some other methods.
How Harmony works
Harmony docs explains it better but basically here it just patches a method body at runtime.
From docs :
Where other patch libraries simply allow you to replace the original method, Harmony goes one step further and gives you:
- A way to keep the original method intact
- Execute your code before and/or after the original method
- Modify the original with IL code processors
- Multiple Harmony patches co-exist and don’t conflict with each other
As a reference we can have this Patched Method List :
Original method | Patched Method |
---|---|
KVOT.FYRKANTIG (static fcn caller) |
GATKAMOMILL.NUFFRA (native fcn caller) |
KVOT.RIKTIG_OGLA (useless) |
GATKAMOMILL.GRONKULLA (useless) |
SOCKERBIT.GRUNDTAL_NORRVIKEN (DecodeBase64ByteWise() caller) |
GATKAMOMILL.SPARSAM (str to list convertor) |
FARGRIK.DAGSTORP (xor implementation) |
GATKAMOMILL.FLARDFULL (anti-debug check [GODDAG]) |
Analysing Native Functions with IDAPython
Digging Deeper we need to analyse these native functions in some dissasembler like IDA, as dnsPy fails for them. These are the three main native functions called by method NUFFRA
. Also, note down the RVAs marked in Red, these are the references to the native functions.
In IDA, we should load it as a normal PE file and then define some code at different locations as IDA doesn’t recognise it very well. Here we can either rebase the program to make it try to load from 0x0 or add 0x400000 to the RVA and then analyse it.
- Func 1 [GODDAG]
We can just go over that address (0x00001930)
and defining it as code pretty much clears what is it used for. So there is a anti-debug check, and this looks like the right function as well since it returns True/False, which is then used in the C# code to progress further if False.
- Func 2 [NativeGRUNDTAL_NORRVIKEN]
The second important function is performing some functions on our input string elements.
Now this native function is interesting as the defined functions are just dwords and If we observe these are just referencing the C# code instead.
This can be illustrated by the following :
For automatically renaming these dwords we first we need to navigate to Storage Stream -> Method to get a full list of methods.
We can then copy them, make a json file with token and method names as the keys and values respectively. Now we can write an IDAPython script to rename the dwords recognised by IDA with the name as ‘dword_xxxx’ accordingly.
TBH this idea of scripting IDA struck to me while doing this writeup lol.
1 | def rename_dwords(): |
Ahh We get a nice and clean view of all the renamed referenced dwords as functions though I’ll not refer to this renaming fashion in this writeup since it is not the best way to make a third person understand the algo as afterall it has method names which will be long af along with their class which we don’t care about here.
Continuing with the analysis of NativeGRUNDTAL_NORRVIKEN
, we see some offset calculations and dereferencing a pointer from that offset and later returning it.
Yes! This looks like the main encryption algo. This checks if the character is in the valid chars range and then encrypts it. The last two checks are similar for curly braces.
And after some renaming, the NativeGRUNDTAL_NORRVIKEN
function looks like the following which concludes that it is only used for encrypting the string.
Also FYI, it uses another list for storing the encrypted string as there is another get_element_from_index
func.
- Func 3 [FYRKANTIGImpl]
So There are two vars used from the .rdata section in the function, one of them is an array and other one doesn’t look interesting as it is a stack variable.
The function which uses is referenced in C# and looks like it is just used to initialize the array.
And yeah we get that xor function in sub_39D0
and FYI sub_4DFC
is same as net51_offset_calc.
So till now It has xored our encrypted input with the array it just initialized.
Moving on, we can clearly see that sub_3F10
is used for performing some random shuffling on the resulting xored array.
There is no point in wasting time on analysing it as we can just note the num ie. the offset (randomly generated but seeded with a static value) by setting the breakpoint as shown below and watching its value on every iteration which makes some swaps with the encrypted input list.
This is what the native function looked after renaming some stuff.
So the main algo of FYRKANTIGImpl
is just to xor and random shuffle our encrypted input. I’ve renamed some other functions now for better understanding.
We can also script some part of defining and renaming functions just from their RVAs.
This also let me think that this can be implemented into a plugin package.
1 | def define_functions(): |
This was the main algo part implemented in the native functions, which were added at runtime. Incase you are wondering what does SPARSAM
function does, so it just converts the list returned from these native functions to a string.
Z3 & Profit
Hell yeah! time for z3 as we are quite sure about how this challenge works :)
FYI, I’m not much experienced with z3 and it always has some surprises for me.
So at first I tried to just replicate the algo and check if it finds any valid solution. I initialized the input with the following constraints.
1 | s.add(And(inp[i]<=48, inp[i]>=125)) |
Then I add some If’s to encrypt our input so kinda do it all in z3
1 | for i in range(len(inp)): |
And I got UNSAT! :(
I asked for some help from my friends but the timezones messed up and I was determined enough to solve it myself now!
Finally I came up with an idea of removing just the z3 If statements and rather began with the encrypted input initially which had a range of (0, 63). And simply wrote a decrypt function for it.
1 | def decode_inp(enc): |
WTH, Still UNSAT !!
And after spending some time tinkering around I figured out that it was some unsigned expressions shit in z3 which just made me mad (indescribable).
1 | s.add(UGE(MATHOPEN[29], 1)) |
In Z3Py, the operators <, <=, >, >=, /, % and >> correspond to the signed versions. The corresponding unsigned operators are ULT, ULE, UGT, UGE, UDiv, URem and LShR.
Reference :
https://ericpony.github.io/z3py-tutorial/guide-examples.htm
1 | from z3 import * |
1 | sat |
Solution Files
I’ve uploaded all the files including the IDAPython and z3 solver scripts along with the idb file on my github here :
I hope you liked this writeup :)
Well, Thanks for reading it…
STAY SAFE !