Post

SherpaCTF 2024 - Writeups

This is a writeup for some misc, AI and reverse engineering challenges from SherpaCTF 2024. Instead of coping and limiting myself to only forensics challenges, I had to reignite my rusty reverse engineering and misc skills to solve a couple challenges for the team. Huge shoutout to my teammates for staying up all night together to solve certain challenges that gave us the winning lead. We managed to persevere and achieve 1st place 🥇 and also win the most creative presentation award.

Odd 1 Out [Misc]

Question: There’s an imposter among the rest of the vroom vrooms, what is it trying to say?

Flag: SHCTF24{800_D035N7_3X157}

We are given a CAN bus traffic log file to investigate. Knowing the author to be a car hacking lover, I knew that this challenge was gonna be heavily related to it in some way.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(1732112773.463086) vcan0 166#D0320036
(1732112773.463270) vcan0 039#002A
(1732112773.463472) vcan0 158#0000000000000037
(1732112773.463659) vcan0 161#000005500108003A
(1732112773.463886) vcan0 191#010090A1410021
(1732112773.466068) vcan0 133#0000000089
(1732112773.466152) vcan0 136#000200000000000C
(1732112773.466208) vcan0 13A#000000000000000A
(1732112773.466265) vcan0 13F#0000000500000000
(1732112773.466320) vcan0 164#0000C01AA8000022
(1732112773.466376) vcan0 17C#0000000010000003
(1732112773.466434) vcan0 18E#00004D
(1732112773.466489) vcan0 1CF#80050000000F
(1732112773.466567) vcan0 1DC#0200000C
(1732112773.466622) vcan0 183#000000100000100A
(1732112773.470024) vcan0 143#6B6B00C2
(1732112773.470118) vcan0 095#800007F400000035
(1732112773.470170) vcan0 1A4#000000080000002F
---SNIP---

Doing some research, it seems like the name of the protocol was identified to be CAN protocol and the format of the logs were structured as:

1
(<TIMESTAMP>) <INTERFACE> <CAN INSTRUCTION ID>#<CAN INSTRUCTION DATA>

So I generated a Python script to categorize the logs according to their respective IDs using regex for better visibility.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import re

d = {}

f = open("chal_candump.log","r")
g = open("processed.log","w+")

for line in f:
    s = re.sub(r"(.+) (.+) (.+)\#(.+)", r"\1\t\3\t\4", line)
    q = s.split("\t")
    if q[1] not in d:
        d[q[1]] = [s]
    else:
        d[q[1]] += [s]

for e in d:
    for x in d[e]:
        g.write(x)
    g.write("\n\n\n")
g.close()

Analyzing the categorized logs, majority of the IDs seem to be normal CAN instructions according to the manual. However, I noticed that the instruction data from ID 800 had unique data that weren’t registered as any CAN instruction.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
---SNIP---
(1732112776.419827)	800	89504E470D0A1A0A
(1732112776.631090)	800	0000000D49484452
(1732112776.841308)	800	0000010100000015
(1732112777.375321)	800	080600000046DC5B
(1732112777.616813)	800	C400000001735247
(1732112778.087349)	800	4200AECE1CE90000
(1732112778.469675)	800	000467414D410000
(1732112778.660493)	800	B18F0BFC61050000
(1732112779.062727)	800	0009704859730000
(1732112779.464265)	800	12740000127401DE
(1732112780.011491)	800	661F7800000F1249
(1732112780.496732)	800	444154785EED9C07
(1732112780.855791)	800	D0144513861BC5AC
(1732112781.303553)	800	88220A06C08C8228
(1732112781.865495)	800	660B25086240CC88
(1732112782.339586)	800	48212AA652C11C00
---SNIP---

Extracting and decoding the data, the flag can be obtained.

car1

IN MY HEADDD [Misc]

Question: https://youtube.com/clip/UgkxrLa5UkwGkGiDT3IZoZUS6jDORSAjrCiQ?si=JKvLpTN-mHsA4snO FR But seriously, something is in my head, can you find it?

Flag: SHCTF24{uN_D05_7R35_CU47r0_IN_My_H34D_24/7}

We are given a PCAP file to investigate. Looking through the packets, several different protocols can be identified including ICMPv6, mDNS and TCP.

head1

Analyzing further, multiple TCP packets sent to from port 8888 to 9999 seem to be dropped. This was pretty suspicious and since the description mentioned something about headers, I analyzed each TCP packet carefully to identify unique header fields.

head2

Spending a few minutes and my sanity, I managed to identify one unique header field between the TCP packets. It seems that each TCP request had a unique identification in them which are written in hexadecimal.

head3

So by extracting every identification value and decoding them, the flag can be obtained.

head4

head5

into the matrix [AI]

Question: The Rebel Alliance has hidden a secret message in a distance matrix, camouflaged within the chaos of coordinates. Your task is to use multidimensional scaling (MDS) to uncover the hidden flag from the matrix, revealing the encoded message. With the Force guiding you, decipher the pattern, extract the flag, and restore balance to the galaxy. Can you navigate the matrix and uncover the truth before the Sith do?

Flag: SHCTF24{Intr0_t0_ML}

We are given a NPY file to investigate. This was a pretty easy AI challenge since I have done this before from Hack the Box. Essentially, NPY files are basically binary files that store numpy arrays. So by using the same Python script, the flag can be obtained.

1
2
3
4
5
6
7
8
9
10
11
12
import numpy as np
import matplotlib.pyplot as plt
from sklearn.manifold import MDS

data = np.load('matrix.npy')
mds = MDS(n_components=2, dissimilarity='precomputed', random_state=42)
X = mds.fit_transform(data)
X[:, 1] = -X[:, 1]

plt.scatter(X[:, 0], X[:, 1])
plt.title("Mirrored Graph (y-axis)")
plt.show()

matrix1

I am speed [RE]

Question: https://youtu.be/yryIJGVOovU?si=WIRVJ-xUkb5PJ1y-&t=52 this is good song. Anyways, the flag is somewhere in the code. Its all there.

Flag: SHCTF24{c47ch_m3_1f_y0u_c4n_3012}

We are given a PyInstaller generated executable file to investigate. So, the first thing to do was to extract the contents of the executable file to analyze it further.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
└─$ python pyinstxtractor.py ../iamspeed.exe 
[+] Processing ../iamspeed.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 3.10
[+] Length of package: 817506 bytes
[+] Found 10 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: iamspeed.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python 3.10 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: ../iamspeed.exe

You can now use a python decompiler on the pyc files within the extracted directory

The PYC files can then be decompiled and analyzed using any Python decompiler, in my case I used PyLingual. Analyzing the decompiled iamspeed.pyc file, it seems that it was trying to decrypt the hex string 5c404344470d1b1b445c5b45404145581a56401b766472604d 1000 times using a different key for each iteration.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import sys
import time

def xor_string(input_string, key):
    return ''.join((chr(ord(char) ^ ord(key[i % len(key)])) for i, char in enumerate(input_string)))

def to_hex(input_string):
    return ''.join((f'{ord(char):02x}' for char in input_string))

def from_hex(hex_string):
    return ''.join((chr(int(hex_string[i:i + 2], 16)) for i in range(0, len(hex_string), 2)))
checkmeout = '5c404344470d1b1b445c5b45404145581a56401b766472604d'
decoded_input = from_hex(checkmeout)
for i in range(1000):
    key = str(i)
    decrypted = xor_string(decoded_input, key)
    sys.stdout.write(f'\rDecrypted String: {decrypted}0')
    sys.stdout.flush()
    time.sleep(0.01)
print()
print('Did you checked properly? Its somewhere on the web..')

However, since there was a time sleep function, the execution of the whole program will not stop which causes the output to be hardly visible. So, create a new Python script that mimics the script but ensure it outputs every decrypted string.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import sys

def xor_string(input_string, key):
    return ''.join((chr(ord(char) ^ ord(key[i % len(key)])) for i, char in enumerate(input_string)))

def to_hex(input_string):
    return ''.join((f'{ord(char):02x}' for char in input_string))

def from_hex(hex_string):
    return ''.join((chr(int(hex_string[i:i + 2], 16)) for i in range(0, len(hex_string), 2)))

checkmeout = '5c404344470d1b1b445c5b45404145581a56401b766472604d'
decoded_input = from_hex(checkmeout)

results = []
for i in range(1000):
    key = str(i)
    decrypted = xor_string(decoded_input, key)
    results.append(f'Decrypted String with key {key}: {decrypted}')

print('\n'.join(results))
print('Did you check properly? It\'s somewhere on the web...')

Running and analyzing the output, one of the decrypted string seem to be a URL that redirects to a text file in Google Drive. The text file seem to be another hex string.

1
2
3
4
5
6
7
8
9
10
11
└─$ python decrypt.py
---SNIP---
Decrypted String with key 444: htwps9//phoqtuql.bt/BPFTy
Decrypted String with key 445: htvps8//qhoptupl.ct/CPFUy
Decrypted String with key 446: htups;//rhostusl.`t/@PFVy
Decrypted String with key 447: https://shorturl.at/APFWy
Decrypted String with key 448: ht{ps5//|ho}tu}l.nt/NPFXy
Decrypted String with key 449: htzps4//}ho|tu|l.ot/OPFYy
Decrypted String with key 450: huspr=/.thnuttul/ft.FPGPy
Decrypted String with key 451: hurpr</.uhnttttl/gt.GPGQy
---SNIP---

speed1

Similarly, just place the hex string into the the same script to obtain the real flag from the output.

1
2
3
4
5
6
7
8
9
10
11
└─$ python flag.py
---SNIP---
Decrypted String with key 928: SIJTG;4zj46jh^d3^8f^p0tVc5g_2913t
Decrypted String with key 929: SIKTG:4zk46kh^e3^9f^q0tWc5f_2813u
Decrypted String with key 930: SHBTF34{b47bh_l3_0f_x0u^c4o_3112|
Decrypted String with key 931: SHCTF24{c47ch_m3_1f_y0u_c4n_3012}
Decrypted String with key 932: SH@TF14{`47`h_n3_2f_z0u\c4m_3312~
Decrypted String with key 933: SHATF04{a47ah_o3_3f_{0u]c4l_3212
Decrypted String with key 934: SHFTF74{f47fh_h3_4f_|0uZc4k_3512x
Decrypted String with key 935: SHGTF64{g47gh_i3_5f_}0u[c4j_3412y
---SNIP---

Rest in Peace [RE]

Question: On average, there are 170,790 deaths per day. What if you can just be like “sike, imna set my rest in peace day to another time”

Flag: SHCTF24{n0b0dy_c4n_dr46_m3_d0wn_1337}

We are given an executable file to investigate. Decompiling and analyzing the main function, two encoded flag parts can be easily identified.

rip1

rip2

Being the lazy ass I am, I analyzed the strings of the executable file and managed to identify a key and a function called reversed_hex.

rip4

rip3

At that point, it was pretty obvious how the flag was encrypted.

rip5

X [RE]

Question: Its a windows world

Flag: SHCTF24{cdc5df19b407428c78ae9ae69f7f2fac}

We are given an executable file to investigate. Decompiling and analyzing the “DialogFunc” function, it seems to be validating the user’s input in the dialog box and outputting a message based on it.

x1

Debugging it on IDA, the function logic seems to be true so far. So the objective now is to identify the right flag to input in the dialog box.

x2

Adding a breakpoint on the “DialogFunc” function call, the source code of this function shows that it was retrieving the user input and storing it in String and calls a function sub_7FF6CCD71140() with its return value stored in v4. A message is then sent to the dialog, combining the result of v4 with 1025.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
INT_PTR __fastcall DialogFunc(HWND a1, int a2, unsigned __int16 a3)
{
  int v4; // eax

  switch ( a2 )
  {
    case 272:
      return 1LL;
    case 273:
      if ( a3 == 2 )
      {
        EndDialog(a1, a3);
        return 1LL;
      }
      if ( a3 == 1 )
      {
        GetDlgItemTextA(a1, 1000, String, 99);
        v4 = sub_7FF6CCD71140();
        SendMessageA(a1, v4 + 1025, 0LL, 0LL);
        return 1LL;
      }
      break;
    case 1025:
      MessageBoxA(a1, "Nope !", "Please try again", 0x10u);
      return 1LL;
    case 1026:
      MessageBoxA(a1, "Congratulations !", "Please submit the flag", 0x40u);
      return 1LL;
  }
  return 0LL;
}

Stepping into the function sub_7FF6CCD71140(), the source code of this function shows that it was:

  1. Checking if the input string was at least 38 characters long.
  2. Validating the first 8 characters (String[0] to String[7]) using XOR with specific hex values.
  3. Validating the middle 32 characters (String[8] to String[39]) are compared against a predefined array (aBcb4ce08a36317) with each character incremented by 1.
  4. Checks that the last character (String[40]) results in ‘}’.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
__int64 sub_7FF6CCD71140()
{
  int i; // [rsp+0h] [rbp-58h]
  unsigned __int64 v2; // [rsp+8h] [rbp-50h]
  char v3[32]; // [rsp+18h] [rbp-40h] BYREF

  memset(v3, 0, sizeof(v3));
  v2 = -1LL;
  do
    ++v2;
  while ( String[v2] );
  if ( v2 < 0x26
    || (String[0] ^ 0x6B) != 56
    || (String[1] ^ 0x75) != 61
    || (String[2] ^ 0x65) != 38
    || (String[3] ^ 0x68) != 60
    || (String[4] ^ 0x74) != 50
    || (String[5] ^ 0x69) != 91
    || (String[6] ^ 0x6F) != 91
    || (String[7] ^ 0x77) != 12
    || (String[40] ^ 0x78) != 5 )
  {
    return 0LL;
  }
  qmemcpy(v3, &String[8], sizeof(v3));
  for ( i = 0; i < 32; ++i )
  {
    if ( v3[i] - 1 != aBcb4ce08a36317[i] )
      return 0LL;
  }
  return 1LL;
}

Checking the array aBcb4ce08a36317, the full value can be identified to be bcb4ce08a3/6317b67`d8`d58e6e1e`b

x3

At this point, I was stuck and could not solve this before the CTF ended. However, attempting the challenge at home, I noticed that it was actually pretty obvious. By reverse engineering the function sub_7FF6CCD71140(), the flag can be obtained.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def xor_decrypt(position, xor_value, result):
    return chr(result ^ xor_value)

def reconstruct_middle(aBcb4ce08a36317):
    return ''.join([chr(val + 1) for val in aBcb4ce08a36317])

def main():
    aBcb4ce08a36317 = [ord(c) for c in "bcb4ce08a3/6317b67`d8`d58e6e1e`b"]

    decrypted = [
        xor_decrypt(0, 0x6B, 56),  # String[0]
        xor_decrypt(1, 0x75, 61),  # String[1]
        xor_decrypt(2, 0x65, 38),  # String[2]
        xor_decrypt(3, 0x68, 60),  # String[3]
        xor_decrypt(4, 0x74, 50),  # String[4]
        xor_decrypt(5, 0x69, 91),  # String[5]
        xor_decrypt(6, 0x6F, 91),  # String[6]
        xor_decrypt(7, 0x77, 12),  # String[7]
    ]

    middle = reconstruct_middle(aBcb4ce08a36317)
    decrypted_40 = xor_decrypt(40, 0x78, 5)
    flag = ''.join(decrypted) + middle + decrypted_40
    print("Flag:", flag)

if __name__ == "__main__":
    main()
1
2
└─$ python x.py        
Flag: SHCTF24{cdc5df19b407428c78ae9ae69f7f2fac}

Plastik-Hitam-0.2 [RE]

Question:

Flag: SHCTF24{0ps_Pl@st1k_H1TAM_T3W@S}

We are given an executable file to investigate. This time the executable file seems to be packed using UPX, a common packing tool.

hitam1

However, UPX fails when trying to unpack it. This was pretty strange since DiE was able to detect the packer to be UPX already, so it should be unpackable with no issues.

hitam3

Doing some research online, it seems that the author had implemented an anti-UPX measure that ensures it would not unpacked using the official tool. Based on this blog, it mentioned that the UPX sections had to be fixed manually to ensure that UPX identifies the executable file to be a normally packed program.

hitam2

Fixing the UPX sections, the executable file can finally be unpacked and decompiled.

hitam4

Analyzing the decompiled executable file, two anti-debugger functions can be identified in the program (IsDebuggerPresent). There was also a long base64 string, but it was either encrypted heavily or a fake flag to throw us off.

hitam5

At this point, it was already midnight and I was pretty tired. So I went ahead and debugged the executable file to hope that the flag decrypts itself. Inside the debugger, the first thing was to obviously place a breakpoint on the IsDebuggerPresent function to ensure it can be bypassed during debugging.

hitam6

After that, keep running the program until it hits the added breakpoint. Simply change the RAX value will bypass the anti-debugger functions.

hitam7

Keep stepping over until the main function of the program is reached. Here, several calls to different functions can be identified. At this point, I was stuck and could not solve this before the CTF ended. However, attempting the challenge at home, I finally realized what step I overlooked. Looking at the function calls, one of them was a memcpy call that might contain the flag.

hitam8

I kid you not, the final step I missed during the CTF was literally placing a breakpoint on the call before running the program again. Doing so, the flag will appear. Note to self: pay attention to the function calls next time.

hitam9

This post is licensed under CC BY 4.0 by the author.