This page has been machine-translated from the original page.
I participated in SECCON Beginners CTF 2023 (starting June 3) with team 0nePadding.
Final placement was 35th out of 778 teams.
We managed to hold a top-5 position for the first six hours or so, but then ran out of solvable problems and gradually slid down to 35th. Frustrating, but entirely a matter of skill — more practice needed.
As usual I focused on Rev and cleared it completely. This time I also tried my hand at Misc and Pwn.
This writeup covers the Rev and some of the Misc problems. The kernel exploit Pwn challenge deserves a separate, more detailed post.
Table of Contents
Half (Rev)
Let’s look up what kind of file a binary file is!
And how do we peek inside it…?
Running strings on the downloaded binary reveals the Flag.
Three (Rev)
You can’t find the flag just by glancing at the contents of this file!
Do you need a specialized tool to analyze binary files?
Decompiling with Ghidra shows that three data regions each define 4-byte-aligned values, and the Flag is assembled by taking one character at a time from each region.
The following solver script retrieves the Flag:
f1 = [ 0x63, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x72, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x7d, 0x00, 0x00, 0x00 ]
f2 = [ 0x74, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x79, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00 ]
f3 = [ 0x66, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00 ]
flag = ""
for i in range(0x31):
if i % 3 == 0:
flag += chr(f1[i//3*4])
elif i % 3 == 1:
flag += chr(f2[i//3*4])
elif i % 3 == 2:
flag += chr(f3[i//3*4])
print(flag)
# ctf4b{c4n_y0u_ab1e_t0_und0_t4e_t4ree_sp1it_f14g3}Poker (Rev)
Let’s play poker together! Apparently you get a flag if you accumulate enough points!
But when I try to run this binary file…? Let’s use a specialized tool that can inspect the internals while it’s running!
Running the binary shows a program that randomly wins or loses, causing the score to fluctuate:
Decompiling with Ghidra reveals that a function to retrieve the Flag executes once the total score reaches a value that is practically unachievable in normal play.
I could have patched memory at runtime with gdb, but it was easier to just patch the winning score threshold to 0 in Ghidra.
Running the patched binary means a single win triggers the Flag:
Leak (Rev)
Suspicious traffic was detected from the server!
On further investigation, a suspicious file was found. Please analyze it along with the traffic log.
Sensitive information may have been exfiltrated…?
Looking at the provided pcap file shows a byte sequence being sent to a C2 server.
This looks like some kind of encrypted byte sequence, so I analyzed the program in Ghidra.
0x8e,0x57,0xff,0x59,0x45,0xda,0x90,0x06,0x28,0xb2,0xab,0xfa,0x49,0x73,0x32,0x33,0x4a,0x73,0x29,0x41,0x3c,0x34,0xb7,0xf6,0x62,0x73,0x25,0x0f,0x95,0x40,0x16,0xfa,0x47,0xe9,0x22,0x8d,0xa5,0xcd,0x3d,0x53,0xee,0xb4,0xb3,0x51,0x8e,0xd2,0x89,0x93,0x5b,0xe0,0x59,0xcb,0xfb,0xb1,0x1bLooking at the decompiled output, the Flag encryption appears to be performed as follows:
At a glance it’s clear this is simply XOR-ing each Flag character with a key generated by a local function.
Manually deriving the per-character key generation was tedious, so I used the following gdb script to extract it.
Since the key generation does not depend on the input string, the extracted key can be used directly for decryption:
# gdb -x run.py
import gdb
from pprint import pprint
# pprint(dir(gdb))
BINDIR = "/home/ubuntu/Hacking/CTF/2023/sec4b/Rev/Leak"
BIN = "leak"
INPUT = "./in.txt"
OUT = "./out.txt"
BREAK = "0x555555555518"
gdb.execute('file {}/{}'.format(BINDIR, BIN))
gdb.execute('b *{}'.format(BREAK))
gdb.execute('run < {}'.format(INPUT, OUT))
key = []
while True:
# register
reg = int(gdb.parse_and_eval("$rcx"))
key.append(hex(reg))
print(key)
if not reg == "0x29":
gdb.execute("continue")
# ['0xed', '0x23', '0x99', '0x6d', '0x27', '0xa1', '0xe0', '0x32', '0x51', '0xed', '0xc5', '0xca', '0x16', '0x47', '0x46', '0x47', '0x2f', '0x1d', '0x5d', '0x70', '0xc', '0x5a', '0xe8', '0x82', '0x52', '0x2c', '0x51', '0x3b', '0xf4', '0x34', '0x49', '0x97', '0x73', '0x87', '0x7d', '0xef', '0xc0', '0xa5', '0xc', '0x3d', '0x8a', '0xeb', '0xc7', '0x65', '0xeb', '0x8d', '0xea', '0xe6', '0x29']XOR-ing the extracted key with the encrypted byte sequence from the pcap yields the Flag:
key=[0xed, 0x23, 0x99, 0x6d, 0x27, 0xa1, 0xe0, 0x32, 0x51, 0xed, 0xc5, 0xca, 0x16, 0x47, 0x46, 0x47, 0x2f, 0x1d, 0x5d, 0x70, 0xc, 0x5a, 0xe8, 0x82, 0x52, 0x2c, 0x51, 0x3b, 0xf4, 0x34, 0x49, 0x97, 0x73, 0x87, 0x7d, 0xef, 0xc0, 0xa5, 0xc, 0x3d, 0x8a, 0xeb, 0xc7, 0x65, 0xeb, 0x8d, 0xea, 0xe6, 0x29, 0xd4, 0x38, 0xfa, 0x95, 0xcc, 0x11]
enc=[0x8e,0x57,0xff,0x59,0x45,0xda,0x90,0x06,0x28,0xb2,0xab,0xfa,0x49,0x73,0x32,0x33,0x4a,0x73,0x29,0x41,0x3c,0x34,0xb7,0xf6,0x62,0x73,0x25,0x0f,0x95,0x40,0x16,0xfa,0x47,0xe9,0x22,0x8d,0xa5,0xcd,0x3d,0x53,0xee,0xb4,0xb3,0x51,0x8e,0xd2,0x89,0x93,0x5b,0xe0,0x59,0xcb,0xfb,0xb1,0x1b]
for i in range(len(enc)):
print(chr(key[i]^enc[i]),end="")
# ctf4b{p4y_n0_4ttent10n_t0_t4at_m4n_beh1nd_t4e_cur4a1n}Heaven (Rev)
I wrote a program to encrypt messages.
Try to decrypt it!
The challenge provides an ELF encryption binary and the output from encrypting the Flag:
$ ./heaven
------ menu ------
0: encrypt message
1: decrypt message
2: exit
> 0
message: ctf4b{---CENSORED---}
encrypted message: ca6ae6e83d63c90bed34a8be8a0bfd3ded34f25034ec508ae8ec0b7fDecompiling with Ghidra shows the encryption logic implemented as follows:
Since the message value itself is not modified, everything else can be ignored and we focus on:
do {
lVar11 = lVar10 + 1;
bVar2 = calc_xor(message[lVar10],uVar1);
message[lVar10] = sbox[bVar2];
lVar10 = lVar11;
} while (lVar7 != lVar11);
__printf_chk(1,"encrypted message: %02x",local_21);
print_hexdump(message,lVar7);From this decompiled output, the encryption and output flow is:
- A random 1-byte value
uVar1is generated in advance. - Each character of
messageis passed, along withuVar1, tocalc_xor. - The result of
calc_xoris used as an index into the hardcodedsbox, and replaces the character inmessage. - Finally,
uVar1and the hex-encoded encryptedmessageare printed.
From the output, the uVar1 used when encrypting the Flag is 0xca.
Since the sbox bytes are hardcoded, we just need to determine the implementation of calc_xor to decrypt the Flag.
However, attempting to analyze calc_xor in Ghidra did not yield a clear result:
So I fell back to objdump to get the disassembly:
I’m not entirely sure what this is doing, but it appears to store values defined from address 0x404181 onto the stack and then pop them with lret.
Analyzing with gdb revealed the following behavior:
- Decrement the received Flag character by 1.
- XOR the result from step 1 with
uVar1.
So I wrote the following reverse script to retrieve the Flag:
sbox = [ 0xc2, 0x53, 0xbb, 0x80, 0x2e, 0x5f, 0x1e, 0xb5, 0x17, 0x11, 0x00, 0x9e, 0x24, 0xc5, 0xcd, 0xd2, 0x7e, 0x39, 0xc6, 0x1a, 0x41, 0x52, 0xa9, 0x99, 0x03, 0x69, 0x8b, 0x73, 0x6f, 0xa0, 0xf1, 0xd8, 0xf5, 0x43, 0x7d, 0x0e, 0x19, 0x94, 0xb9, 0x36, 0x7b, 0x30, 0x25, 0x18, 0x02, 0xa7, 0xdb, 0xb3, 0x90, 0x98, 0x74, 0xaa, 0xa3, 0x20, 0xea, 0x72, 0xa2, 0x8e, 0x14, 0x5b, 0x23, 0x96, 0x62, 0xa4, 0x46, 0x22, 0x65, 0x7a, 0x08, 0xf6, 0x12, 0xac, 0x44, 0xe9, 0x28, 0x8d, 0xfe, 0x84, 0xc3, 0xe3, 0xfb, 0x15, 0x91, 0x3a, 0x8f, 0x56, 0xeb, 0x33, 0x6d, 0x0a, 0x31, 0x27, 0x54, 0xf9, 0x4a, 0xf3, 0xbf, 0x4b, 0xda, 0x68, 0xa1, 0x3c, 0xff, 0x38, 0xa6, 0x3e, 0xb7, 0xc0, 0x9a, 0x35, 0xca, 0x09, 0xb8, 0x8c, 0xde, 0x1c, 0x0c, 0x32, 0x2a, 0x0f, 0x82, 0xad, 0x64, 0x45, 0x85, 0xd1, 0xaf, 0xd9, 0xfc, 0xb4, 0x29, 0x01, 0x9b, 0x60, 0x75, 0xce, 0x4f, 0xc8, 0xcc, 0xe2, 0xe4, 0xf7, 0xd4, 0x04, 0x67, 0x92, 0xe5, 0xc7, 0x34, 0x0d, 0xf0, 0x93, 0x2c, 0xd5, 0xdd, 0x13, 0x95, 0x81, 0x88, 0x47, 0x9d, 0x0b, 0x1f, 0x5e, 0x5d, 0xa8, 0xe7, 0x05, 0x6a, 0xed, 0x2b, 0x63, 0x2f, 0x4c, 0xcb, 0xe8, 0xc9, 0x5a, 0xdc, 0xc4, 0xb0, 0xe1, 0x7f, 0x9f, 0x06, 0xe6, 0x57, 0xbe, 0xbd, 0xc1, 0xec, 0x59, 0x26, 0xf4, 0xb1, 0x16, 0x86, 0xd7, 0x70, 0x37, 0x4d, 0x71, 0x77, 0xdf, 0xba, 0xf8, 0x3b, 0x55, 0x9c, 0x79, 0x07, 0x83, 0x97, 0xd6, 0x6e, 0x61, 0x1d, 0x1b, 0xa5, 0x40, 0xab, 0xbc, 0x6b, 0x89, 0xae, 0x51, 0x78, 0xb6, 0xb2, 0xfd, 0xfa, 0xd3, 0x87, 0xef, 0xee, 0xe0, 0x2d, 0x4e, 0x3f, 0x6c, 0x66, 0x5c, 0x7c, 0x10, 0xcf, 0x49, 0x48, 0x21, 0x8a, 0x3d, 0xf2, 0x76, 0xd0, 0x42, 0x50, 0x58, 0x00 ]
enc = [0x6a, 0xe6, 0xe8, 0x3d, 0x63, 0xc9, 0x0b, 0xed, 0x34, 0xa8, 0xbe, 0x8a, 0x0b, 0xfd, 0x3d, 0xed, 0x34, 0xf2, 0x50, 0x34, 0xec, 0x50, 0x8a, 0xe8, 0xec, 0x0b, 0x7f]
# unk is a random 1-byte value
# bVar1 = calc_xor(message[k],unk);
# message[k] = sbox[bVar1];
# for i in range(256):
flag = ""
for e in enc:
bVar1 = sbox.index(e)
flag += chr((bVar1 ^ 0xca) + 1)
print(flag)YARO (Misc)
There may be malware on the server. Find it with your perfect signature.
A relatively rare YARA challenge — this got me excited!
The server accepts YARA rule text and returns the result of applying the submitted rules against the files under /root, including the matching rule names.
Multiple rules can be sent at once, and the server reports which rule names matched.
Since we can extract one character at a time, I first determined the character set and length of the Flag.
I started by binary-searching the Flag length using rules like:
$ctf4b_string = /.*ctf4b\{.{20,30}\}.*/
$ctf4b_string = /.*ctf4b\{.{25,30}\}.*/
$ctf4b_string = /.*ctf4b\{.{28,30}\}.*/
$ctf4b_string = /.*ctf4b\{.{28}\}.*/The Flag content inside the braces turned out to be 28 characters.
Next I determined the character set with rules like:
$ctf4b_string = /.*ctf4b\{[0-9|A-Z|a-z]{28}\}.*/
$ctf4b_string = /.*ctf4b\{[0-9|a-z|_]{28}\}.*/
$ctf4b_string = /.*ctf4b\{[A-Z|a-z|_]{28}\}.*/
$ctf4b_string = /.*ctf4b\{[0-9|A-Z|_]{28}\}.*/The Flag uses uppercase letters, lowercase letters, digits, and underscores.
I then wrote the following solver script to determine the Flag one character at a time:
from pwn import *
for i in range(28):
allrule = ""
p = remote("yaro.beginners.seccon.games", 5003)
for c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_":
c = c
R = "." * i
L = 28-1-i
sig = R + c + "[0-9|A-Z|a-z|_]{" + str(L) +"}"
if c == "_":
c = "ubar"
rule = "rule check_" + c +""" {
strings:
$ctf4b_string = /.*ctf4b\{""" + sig + """\}.*/
condition:
$ctf4b_string
}
"""
allrule += rule
r = p.recvuntil(b"rule:\n")
p.sendline(allrule.encode())
r = p.recvuntil(b"Not found: ./server.py\n")
r = p.recvline()
print(r)
p.close()
# b'Found: ./flag.txt, matched: [check_Y]\n'
# b'Found: ./flag.txt, matched: [check_3]\n'
# b'Found: ./flag.txt, matched: [check_t]\n'
# b'Found: ./flag.txt, matched: [check_ubar]\n'
# b'Found: ./flag.txt, matched: [check_A]\n'
# b'Found: ./flag.txt, matched: [check_n]\n'
# b'Found: ./flag.txt, matched: [check_0]\n'
# b'Found: ./flag.txt, matched: [check_t]\n'
# b'Found: ./flag.txt, matched: [check_h]\n'
# b'Found: ./flag.txt, matched: [check_3]\n'
# b'Found: ./flag.txt, matched: [check_r]\n'
# b'Found: ./flag.txt, matched: [check_ubar]\n'
# b'Found: ./flag.txt, matched: [check_R]\n'
# b'Found: ./flag.txt, matched: [check_3]\n'
# b'Found: ./flag.txt, matched: [check_4]\n'
# b'Found: ./flag.txt, matched: [check_d]\n'
# b'Found: ./flag.txt, matched: [check_ubar]\n'
# b'Found: ./flag.txt, matched: [check_O]\n'
# b'Found: ./flag.txt, matched: [check_p]\n'
# b'Found: ./flag.txt, matched: [check_p]\n'
# b'Found: ./flag.txt, matched: [check_0]\n'
# b'Found: ./flag.txt, matched: [check_r]\n'
# b'Found: ./flag.txt, matched: [check_t]\n'
# b'Found: ./flag.txt, matched: [check_u]\n'
# b'Found: ./flag.txt, matched: [check_n]\n'
# b'Found: ./flag.txt, matched: [check_1]\n'
# b'Found: ./flag.txt, matched: [check_t]\n'
# ctf4b{Y3t_An0th3r_R34d_Opp0rtun1ty}Wrap-up
The gap to the top teams is real, but it was still an enjoyable event.
Team 0nePadding has been growing a bit lately, which makes things more fun — looking forward to more competitions.