Solving RTN CTF challenges

kao

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.157637764

Let'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;j

Sure, 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 OK

OK, 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-256

Keywords "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           :F68BFB6D7B7A2F74BD47ECDB964CFCF390238AEB096402F7FFF3E18FECE92245

You'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.pdf

Put 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 see

AES-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!

8 thoughts on “Solving RTN CTF challenges

  1. Great job on solving the challenges, glad you enjoyed it πŸ™‚
    It surely won't be our last CTF!

  2. Hi kao, can you help me with unpacking this exe or some advice how to do it?

    download:
    {hidden link}.exe

  3. 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

Leave a Reply

  • Be nice to me and everyone else.
  • If you are reporting a problem in my tool, please upload the file which causes the problem.
    I can`t help you without seeing the file.
  • Links in comments are visible only to me. Other visitors cannot see them.

5  +   =  8