Earlier this month, my friend Washi invited me to take a part in a small CTF competition. It was a first time his team was making something like that, so I did not know what to expect. I must say that RTN CTF was a great success and I really really enjoyed it.
Challenges and official solutions have already been published on RTN Github and few people have already made great and detailed writeups:
https://github.com/CodeOfDark/CTF-Writeups/tree/master/RTN/2021
https://holly-hacker.github.io/ctf-writeups/2021-01_RTNCTF/Index.html
https://zsr2531.github.io/writeups/RTN_2021/
I have no intention of repeating what's already been said and done, so I'll just add a few personal notes about how I solved the most interesting challenges. π
Lambdastic
Challenge description says:
I heard you liked Monads.
You're given a .NET assembly and decompiled code looks really intimidating:
CodeOfDark somehow managed to have the code beautified. I'm guessing he decompiled the entire project and pasted it into Visual Studio - but that's just a guess:
It looks better but is still pretty unreadable.
How to attack such a challenge?
Official solution suggests using static analysis and figuring out what each class does. That's slow and painful process - and during CTFs I prefer not to analyze things in details.
Solution by CodeOfDark notes the SafeDiv operation. But it does a terrible job explaining what happens next:
we get the multiple lines of output
SafeDiv 11761312493529 / 1000000000 = 11761.312493529 SafeDiv -552867629889483 / 250000000 = -2211470.519557932 SafeDiv 25988818539409441 / 250000000 = 103955274.157637764Let's try to simplify it.
x = unknown r = (x * x) / 1000000000 n = (-161210479 * x) / 250000000 s = 103955274.157637764 y = r + n + s //should be 0
I have no idea how tracing values passed to SafeDiv allows us to find and simplify multiplication ops. But maybe that's just me.. π
Anyways, just like other solvers, I noticed that all the calculations are done on decimals. So, I put breakpoints on math operations dealing with decimals:
and started writing down all the math operations that were happening.
== input 01 == 1*1 = 1 0+1 = 1 1*256 = 256 1*1 = 1 1/1000000000 = 0.000000001 0.000000001 + 0 = 0.000000001 -161210479 * 1 = -161210479 -161210479 / 250000000 = -0.644841916 -0.644841916 + 0.000000001 = -0.644841915 25988818539409441 / 250000000 = 103955274.157637764 103955274.157637764 + -0.644841915 = 103955273.512795849 COMPARE 103955273.512795849 == 0 == input 02 == 2*1 = 2 0+2 = 2 1*256 = 256 2*2 = 4 4 / 1000000000 = 0.000000004 0.000000004 + 0 = 0.000000004 -161210479 * 2 = -322420958 -322420958 / 250000000 = -1.289683832 -1.289683832 + 0.000000004 = -1.289683828 25988818539409441 / 250000000 = 103955274.15763776 103955274.157637764 + -1.289683828 = 103955272.867953936 COMPARE 103955272.867953936 == 0
I know, these notes look like crap. Next time I'll make better ones. π
But actually that's all you need - two inputs, two results and you can see what operations are being done. Now it's a matter of converting these values into an equation.
Start by using the obvious constants:
103955274.157637764 + (-1.289683828) = 0
and work your way backwards by gradually replacing all the calculations:
(25988818539409441 / 250000000) + (-1.289683828) = 0 (25988818539409441 / 250000000) + (-1.289683832 + x*x/1000000000) = 0 (25988818539409441 / 250000000) + (-322420958 / 250000000 + x*x/1000000000) = 0 (25988818539409441 / 250000000) + (x*-161210479 / 250000000 + x*x/1000000000) = 0
Now we have an equation that we can feed to Wolfram Alpha:
Convert the result to hex and you have the flag!
Espresso
This is a Java challenge. Or is it?
You're provided with both Windows EXE file and Linux ELF. Both binaries seem to be simple wrappers around JVM engine. Wrappers load JVM and execute a built-in JAR file, passing as argument the input string and a hardcoded string "94z3PpMpaFhjUjcjMUauourjxcajIbKpxcTi-MlT".
.text:0000000140003993 mov rax, [rdi] .text:0000000140003996 lea r9, java_class_file .text:000000014000399D xor r8d, r8d .text:00000001400039A0 mov [rsp+38h+var_18], 446h ; size of java class file .text:00000001400039A8 lea rdx, aCcRtnTeamEspre ; "cc/rtn_team/Espresso" .text:00000001400039AF mov rcx, rdi .text:00000001400039B2 mov r10, [rax+28h] .text:00000001400039B6 call r10 ; load class file from buffer .text:00000001400039B9 mov rsi, rax ... .text:0000000140003A3B lea rdx, a94z3ppmpafhjuj ; 94z3PpMpaFhjUjcjMUauourjxcajIbKpxcTi-MlT .text:0000000140003A42 mov rbx, rax .text:0000000140003A45 mov r8, [rcx+538h] .text:0000000140003A4C mov rcx, rdi .text:0000000140003A4F call r8 ; 000007FEEF1B1F60 ... .text:0000000140003A93 mov rax, [rdi] .text:0000000140003A96 lea r9, aLjavaLangStrin ; "([Ljava/lang/String;)V" .text:0000000140003A9D lea r8, aMain ; "main" .text:0000000140003AA4 mov rdx, rsi .text:0000000140003AA7 mov rcx, rdi .text:0000000140003AAA mov r10, [rax+388h] .text:0000000140003AB1 call r10 .text:0000000140003AB4 mov r9, rbx .text:0000000140003AB7 mov r8, rax .text:0000000140003ABA mov rdx, rsi .text:0000000140003ABD mov rcx, rdi .text:0000000140003AC0 call execute_main ; run "main" method
I extracted JAR file and decompiled it. It looked like a horribly complicated and broken way to compare two strings:
package cc.rtn_team; public class Espresso { public Espresso() { } private static Boolean brewCoffee(Byte byte1, Short short1, Boolean boolean1) { byte byte0 = byte1.byteValue(); if(boolean1.booleanValue()) byte0++; return Boolean.valueOf(byte0 == short1.shortValue()); } public static void main(String args[]) { if(args == null || args.length <= 1 || args[0] == null || args[1] == null || args[0].length() != args[1].length()) { System.exit(1); return; } for(int i = 0; i < args[0].length(); i++) { if(brewCoffee(Byte.valueOf((byte)args[0].charAt(i)), Short.valueOf((short)args[1].charAt(i)), Boolean.valueOf(i % 2 == 0)).booleanValue()) { System.exit(1 + i); return; } } } }
I figured out that string "84y3OpLp`FgjTjbjLU`unuqjwc`jHbJpwcSi,MkT" is the one that should pass all the tests. But it doesn't look like a flag and challenge isn't accepting it. Why?!
The only possible explanation is that EXE file somehow messes with Java internals, JIT compilation or something like that.
How it's done? I had no idea and I had no intention of becoming an expert in JVM internals.
Since Java class file was totally unprotected, I replaced it with with my own, modified version:
package cc.rtn_team; public class Espresso { public Espresso() { } private static Boolean brewCoffee(Byte byte1, Short short1, Boolean boolean1) { byte byte0 = byte1.byteValue(); if(boolean1.booleanValue()) byte0++; System.out.println(byte0); // <--- my debug System.out.println(short1.shortValue()); // <--- my debug return Boolean.valueOf(byte0 == short1.shortValue()); } public static void main(String args[]) { for(int i = 0; i < args[0].length(); i++) { if(brewCoffee(Byte.valueOf((byte)args[0].charAt(i)), Short.valueOf((short)args[1].charAt(i)), Boolean.valueOf(i % 2 == 0)).booleanValue()) { System.exit(1 + i); return; } } } }
As you can see, I added some debug output and removed checks for string lengths.
Assuming that the flag should begin with "RTN{", I ran my modified EXE file, passing "RTN{" in the command-line and... Got a success message!
C:\Program Files\AdoptOpenJDK\jdk-11.0.9.101-hotspot\bin\client>espresso.windows-patched.exe RTN{ Welcome in the RTN coffee lounge! Your choice was RTN{ 57 57 52 52 122 122 51 51 ))) ((( +-----+ | |] An excellent choice! `-----' Enjoy your stay! ___________ `--------- '
Now it was a matter of bruteforcing 5th byte of the flag, then 6th byte of the flag, and so on.. Since I'm lazy, I automated it using a simple C# code:
class Program { static string eval(string valuez) { Process p = new Process(); p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardOutput = true; p.StartInfo.FileName = @"espresso.windows-patched.exe"; p.StartInfo.Arguments = valuez; p.Start(); string output = p.StandardOutput.ReadToEnd(); p.WaitForExit(); return output; } static void Main(string[] args) { string charmap = "1234567890-={}_+/qwertyuiopasdfghjklzxcvbnm,./QWERTYUIOPASDFGHJKLZXCVBNM"; string flag = ""; for (int i = 0; i < 40; i++) { for (int j = 0;jSure, it's not the prettiest of solutions but - hey, it works! π
In my opinion, this was THE best challenge in the CTF, so I really suggest you read the official writeup about the challenge and how it was done. It's a great read!
Nonsence
You're given 5 PDF documents and 5 matching SIG files. You also get 2 PUB files that contain public keys used to sign the documents. Your task is to somehow obtain a private key and use to to fetch flag from the server. Challenge description hints on improper use of cryptography:
...
security system has problems. Two of our employees did not follow the protocol to the letter
...
The IT department said something about SHA 256 and public keys. Personally I have a hard time making heads and tails out of this nonsense.I'm not a crypto wizard and for sure I wouldn't be able to solve it on my own. But this is a typical CTF challenge which suffers from a simple weakness. Namely, there are gazillions of CTF writeups available on the internet and you just need to google for the proper keywords. But I'm getting ahead of myself.
Let's start with the very basics - see if the signatures match the PDFs:
C:\>openssl dgst -sha256 -verify berry.pub -signature document01.sig document01.pdf Verified OKOK, so signature is a match, no tricks here!
Let's see what's inside PUB key:
C:\>openssl pkey -inform PEM -pubin -in berry.pub -text -noout Public-Key: (256 bit) pub: 04:7d:ed:aa:7d:d4:83:74:71:68:98:a1:6c:d8:2e: 7a:f9:ea:93:06:20:b7:41:5c:2c:70:3b:2e:1d:ea: 82:9c:41:15:af:14:b3:f5:cf:69:bc:23:85:02:49: 22:07:97:ed:52:8e:58:15:54:de:26:18:d9:73:da: 7a:05:5f:67:50 ASN1 OID: prime256v1 NIST CURVE: P-256Keywords "prime256v1" and "NIST CURVE" tells us it's using elliptic curve cryptography. I have no idea how that works - but I can google!
Let's search for CTF solutions covering that specific elliptic curve "NIST CURVE: P-256".
Right in the first page there is a link to Ployka PWNer - a full explanation of the problem, possible solutions and a python script that can forge a private key. Now I just need to update script with proper data and run it.
C:\>openssl asn1parse -inform der -in document01.sig 0:d=0 hl=2 l= 70 cons: SEQUENCE 2:d=1 hl=2 l= 33 prim: INTEGER :FA4696CDC7C525A5D481C3E1878514B2996517DDB074D3B391C27AB50E57FF9E 37:d=1 hl=2 l= 33 prim: INTEGER :AE505FA68243E251762A1C56B324F32D5B08081CE2A18EF48E32B704CB2E88A0 C:\>openssl asn1parse -inform der -in document02.sig 0:d=0 hl=2 l= 69 cons: SEQUENCE 2:d=1 hl=2 l= 33 prim: INTEGER :AAF620088A0956BD58E951579202A8ACC52B3AFA4FD73FFD308BC6EFFD1EFC5C 37:d=1 hl=2 l= 32 prim: INTEGER :2241BF9F6635DAE771AE07CA545FC180436626F6D331D093738FB69B15849786 C:\>openssl asn1parse -inform der -in document03.sig 0:d=0 hl=2 l= 68 cons: SEQUENCE 2:d=1 hl=2 l= 32 prim: INTEGER :684F825B589CCDDF23AC8486C5554221BA43F98E1655868068A3ED1702AE0D40 36:d=1 hl=2 l= 32 prim: INTEGER :43550D2BD3280D6FD0076F33F277B11EB6D612911E7594FF6D6AE11AB7DB4D7D C:\>openssl asn1parse -inform der -in document04.sig 0:d=0 hl=2 l= 70 cons: SEQUENCE 2:d=1 hl=2 l= 33 prim: INTEGER :AAF620088A0956BD58E951579202A8ACC52B3AFA4FD73FFD308BC6EFFD1EFC5C 37:d=1 hl=2 l= 33 prim: INTEGER :C7683319B5783021B687968EDB4A41CA82CF809B3547905FA3C3001822DFA1CE C:\>openssl asn1parse -inform der -in document05.sig 0:d=0 hl=2 l= 70 cons: SEQUENCE 2:d=1 hl=2 l= 33 prim: INTEGER :D41A2A4DF54906EC2D5F6ABBD16B47C98EC746F90AA79B9AD5FCFE12138631B6 37:d=1 hl=2 l= 33 prim: INTEGER :F68BFB6D7B7A2F74BD47ECDB964CFCF390238AEB096402F7FFF3E18FECE92245You'll notice that document02.pdf and document04.pdf signatures are suffering from the reused nonce.
As a final step, script requires SHA256 hashes of the documents:
C:\>sha256sum document02.pdf \dfa7bb194e89fc2737440983d90c0fe74a90e702b48b9df98012fb423044dc15 *document02.pdf C:\>sha256sum document04.pdf \a2c3bdeb3601b632f44d13f5f9876db2262a045f6d81e25208d092d76c68777f *document04.pdfPut it all together, run it and you'll get a forged private key. Magic! π
Ssssecret
This is the only challenge I didn't solve. It looked like a riddle:
I want to share a secret,
A secret dear to me.
But the secret to keeping a secret
Is to never give out the key.So I broke it up in little pieces
As small as they can be.
Then after that you wouldn't expect
I handed them out for free.Follow up these pieces
Understand them to a degree
At the very root of it all
The answer you will seeAES-ECB('6eeb7683e9242ed56080e847c2f94cfc50634c29a3a50f14b31e7e3f97e34df0', key)
(1, d8877abe8c795a869b1fe28ed9a328a4)
(2, 1a1c134fc5c474899ee9d573b8895388)
(3, da143ab7a656e69a91b22414de846d0e)
(4, e4c6d40f50bd5f37d5c2f5e356a0af94)
(5, f344401d9a20a8ea1f08598434bfbf8e)I truly hate this type of challenges - there is zero skill involved, you just need to guess what challenge author meant and/or try random stuff until you succeed. As Radium explained in his solution:
As soon as I saw this, I immediately recognized what this challenge was about; Shamirβs Secret Sharing Scheme.
Dude, I'm so happy for you! π Unfortunately I was not aware of Shamir's Secret Sharing Scheme, so I approached the challenge as a language riddle.
We get 5 hex-strings that look like hashes. Perhaps they are MD5s? Rainbow table lookup didn't yield any results. What about MD4? MD2? Some other hashing algo?
Since the encryption used is AES, key should be 8,16 or 32bytes. Challenge says "..never give out the key. So I broke it up in little pieces As small as they can be." Could it be that key is part of the riddle, split into bytes or short sequences?
"Follow up these pieces Understand them to a degree" - could it be I need to recognize some sort of hash? So, I ran all the hashing algorithms I could think of, on all parts of the riddle and found nothing.
"At the very root of it all" - could it be that these numbers are squares/cubes/x to the power of y? I factorized all the numbers and found nothing.
After I ran out of ideas, I said "f*ck it! If someone can guess the answer, so be it. I don't care."
Key takeaways
While I was reading solutions, I was surprised how much people rely on Cyberchef. Some challenges like Dragons even expected you to use Cyberchef to decode affine cypher (thankfully it was not the only way to solve it!)
Python is everywhere, I really should learn more of it.. Some of the published solutions were so pretty, whereas mine looked like crap. This is especially true when it comes to using Z3 solver.
Perhaps, I could have solved Ssssecret if I spent more time googling and trying to figure out what secret sharing methods exist. I must pay more attention to those small hints hidden in the challenge descriptions.
Conclusion
RTN CTF was a very nice, small competition featuring relatively easy challenges. It was really fun to play and the reward was also very satisfying.
Have fun and till next time!
Great job on solving the challenges, glad you enjoyed it π
It surely won't be our last CTF!
Sounds great! π I'm already looking forward to that!
Hey man, do you have any Enigma Protector unpacker?
No, I don't.
Hi kao, can you help me with unpacking this exe or some advice how to do it?
download:
{hidden link}.exe
You can use Virtual File System Editor to extract all data files. You will not get main EXE file, but I'm guessing you're not interested in it anyway.
Thanks a lot, but i need main exe file too. Is there anyway to unpack it?
hello kao,
this is great fun reading your words. it is like Β»i'm HAL 9000(...), but i can googleΒ«, π
appreciate it, good games only.
thanks for the trees and thank to you