Marathon CTF was a great CTF organized by CyberTalents during the whole month (1 Mar. - 31 Mar. 2021) but I participated during only the last 4 days and I got the 28th place (same points ad 25th) as I solved all Reverse challenges but one, and some of Web, Crypto, Forensics challenges and Here’s the writeup for some of these challenges!
Python Art
Well, This challenge I really liked because It’s more like a reverse challenge or at least for me! :D
So this website basically draws “whatever” I typed like this:
Since the challenge name is “Python Art” I thought of Django or Flask, I wanted to see the HTML source using inspect element and there was an interesting comment inside the <pre> tag under the drawn text the comment was <!-- test is not defined -->
or something like that so I guessed the comment is a result of eval()! I tired 2+3 as input and the result in the comment was 5 so YUP, that’s eval() function messing with me I KNEW IT!
Because I’m a reverse guy I overthought it so I tried to get the flag variable but it was not defined, I thought it might be in a text file and tried to read flag.txt
but didn’t work either, then I tried to execute compile() function inside the eval function but none of that worked but that made me read about code objects in Python so I had fun.
since the URL is http://18.196.160.18:3333/send
I tried the input send
and the comment had this response <!-- function send at 0xfff..... -->
so that’s the address of the function in memory, huh!
so to get the properties and methods of the function send I tried dir(send)
and the response was
1
['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__doc__', '__format__', '__get__', '__getattribute__', '__globals__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']
And by taking a look at func_globals by typing send.func_globals
.. BOOM!:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
'DEFAULT_FONT': 'standard',
'art': <function art at 0x7f2ce3b7b4d0>,
'Flask': <class 'flask.app.Flask'>,
'app': <Flask 'app'>,
'aprint': <function aprint at 0x7f2ce3b7b450>,
'DECORATION_COUNTER': 195,
'randart': <function randart at 0x7f2ce3b7b550>,
...
lots of gibberish stuff
...
'__file__': 'app.py',
'art_param': <module 'art.art_param' from '/usr/local/lib/python2.7/site-packages/art/art_param.pyc'>
...
'decor_dic': <module 'art.decor_dic' from '/usr/local/lib/python2.7/site-packages/art/decor_dic.pyc'>, 'request': <Request 'http://18.196.160.18:3333/send' [POST]>, 'ART_VERSION': '5.1', 'ART_COUNTER': 700
So this is a Flask
web application written in app.py
file and there’s functions like art
, aprint
, randart
, etc.. which belong to the art python library! All makes sense now! :D
Since I have the filename of source code I grabbed it open('app.py','r').read()
and styled it a little:
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
#!/usr/bin/python
from flask import Flask
from flask import render_template
from flask import request
from art import *
app = Flask(__name__, static_url_path='/static')
@app.route('/')
def index():
return render_template('index.html')
@app.route('/send', methods = ['POST'])
def send():
text = request.form.get('name')
art = text2art(str(text),font='block',chr_ignore=True)
try:
cmd = eval(text)
except NameError as e:
return render_template('index.html', cmd=e , art= art)
return render_template('index.html', cmd=cmd , art=art)
if __name__ == '__main__':
app.run(host='0.0.0.0')
so as I thought the comment is using eval() function and all looks good, Now it’s time to get the flag!
I typed __import__('glob').glob("*")
to discover what’s around (which I should’ve done at first BTW xD):
1
<!-- ['app.py', 'flag', 'templates', 'static'] -->
Okay it’s flag
file, not flag.txt
! then by reading that file open('flag', 'r').read()
I’ve got the flag:
ASM (4SM)
I do love assembly! taking a look at 4SM.asm:
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
flag_checker:
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
xor DWORD PTR [rbp-4], 133337
sar DWORD PTR [rbp-4], 3
add DWORD PTR [rbp-4], 1337
sub DWORD PTR [rbp-4], 137
mov edx, DWORD PTR [rbp-4]
mov eax, edx
add eax, eax
add eax, edx
mov DWORD PTR [rbp-4], eax
cmp DWORD PTR [rbp-4], 1128648
jne .L2
mov eax, 1
jmp .L3
.L2:
mov eax, 0
.L3:
pop rbp
ret
.LC0:
.string "Enter the secret number: "
.LC1:
.string "%d"
.LC2:
.string "Correct number :D"
.LC3:
.string "Wrong number :p"
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
mov eax, DWORD PTR [rbp-4]
mov esi, eax
mov edi, OFFSET FLAT:.LC1
mov eax, 0
call __isoc99_scanf
mov eax, DWORD PTR [rbp-4]
mov edi, eax
call flag_checker
mov DWORD PTR [rbp-8], eax
cmp DWORD PTR [rbp-8], 0
je .L5
mov edi, OFFSET FLAT:.LC2
call puts
jmp .L6
.L5:
mov edi, OFFSET FLAT:.LC3
call puts
.L6:
mov eax, 0
leave
ret
The flow goes as follows:
- starting from main
- “Enter the secret number: “ message get printed using printf function
- getting user’s input through scanf function with %d format
- flag_checker function is called “to check the flag obviously”
- doing some math:
((((x ^ 133337) >> 3) + 1337) - 137) * 3
where x is the user’s input - comparing so the result should be equal to 1128648
NOTE: the ^
is XOR and >>
is shift arithmetic right
I had the equation ((((x ^ 133337) >> 3) + 1337) - 137) * 3 = 1128648
so by solving it:
x = ((((1128648/3) + 137) - 1337) << 3) ^ 133337 = 3133337
The flag: flag{3133337}
Jett
I was given an assembly source like the previous challenge:
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
.LC0:
.string "Enter the flag:"
.LC1:
.string "%25s"
.LC2:
.string "Wrong Flag :("
.LC3:
.string "Correct Flag : %s"
main:
push rbp
mov rbp, rsp
sub rsp, 160
mov DWORD PTR [rbp-160], 150
mov DWORD PTR [rbp-156], 204
mov DWORD PTR [rbp-152], 233
mov DWORD PTR [rbp-148], 31
mov DWORD PTR [rbp-144], 211
mov DWORD PTR [rbp-140], 75
mov DWORD PTR [rbp-136], 212
mov DWORD PTR [rbp-132], 74
mov DWORD PTR [rbp-128], 193
mov DWORD PTR [rbp-124], 215
mov DWORD PTR [rbp-120], 85
mov DWORD PTR [rbp-116], 176
mov DWORD PTR [rbp-112], 132
mov DWORD PTR [rbp-108], 215
mov DWORD PTR [rbp-104], 57
mov DWORD PTR [rbp-100], 222
mov DWORD PTR [rbp-96], 38
mov DWORD PTR [rbp-92], 75
mov DWORD PTR [rbp-88], 2
mov DWORD PTR [rbp-84], 139
mov DWORD PTR [rbp-80], 75
mov DWORD PTR [rbp-76], 21
mov DWORD PTR [rbp-72], 215
mov DWORD PTR [rbp-68], 21
mov DWORD PTR [rbp-64], 229
mov edi, OFFSET FLAT:.LC0
call puts
lea rax, [rbp-48]
mov rsi, rax
mov edi, OFFSET FLAT:.LC1
mov eax, 0
call __isoc99_scanf
mov DWORD PTR [rbp-4], 0
jmp .L2
.L5:
mov eax, DWORD PTR [rbp-4]
cdqe
movzx eax, BYTE PTR [rbp-48+rax]
movsx eax, al
imul eax, eax, 137
cdq
shr edx, 24
add eax, edx
movzx eax, al
sub eax, edx
mov DWORD PTR [rbp-8], eax
mov eax, DWORD PTR [rbp-4]
cdqe
mov eax, DWORD PTR [rbp-160+rax*4]
cmp DWORD PTR [rbp-8], eax
je .L3
mov edi, OFFSET FLAT:.LC2
call puts
mov eax, 0
jmp .L6
.L3:
add DWORD PTR [rbp-4], 1
.L2:
cmp DWORD PTR [rbp-4], 24
jle .L5
lea rax, [rbp-48]
mov rsi, rax
mov edi, OFFSET FLAT:.LC3
mov eax, 0
call printf
mov eax, 0
.L6:
leave
ret
The execution flow:
- starting from main
- allocating the stack and storing some bytes
- printing “Enter the flag:” message to the screen using puts
- reading user’s input to the address of [rbp-48] using scanf with %25s format
- jumping to .L2 label
- while loop executing while counter [rbp-4] < 25:
- reading each byte from the user’s input (let’s call it
x
) - doing the math:
y[i] = (byte)[((x[i] * 137) >> 24) + (x[i] * 137)]
- known_values[i] = y[i] - ((x[i] * 137) » 24)
- increment counter
- reading each byte from the user’s input (let’s call it
The full equation is known_bytes[i] = (byte)[(x[i] * 137) >> 24) + (x[i] * 137)] - ((x[i] * 137) >> 24)
and since (x[i] * 137 >> 24)
for printable characters is always zero
it can be reduced to: known_bytes[i] = (byte)(x[i] * 137) = (x[i] * 137) % 256
and I could brute force them and that’s one solution
The other solution is simply using modular inverse to get the flag:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/env python3
from Crypto.Util.number import inverse
known_bytes = [150, 204, 233, 31, 211, 75, 212, 74, 193, 215, 85, 176, 132, 215, 57, 222, 38, 75, 2, 139, 75, 21, 215, 21, 229]
flag = ""
for a in known_bytes:
inv = inverse(137, 256)
original = inv * a % 256
flag += chr(original)
print(flag)
And I got the flag: flag{34zy_m0d_1nv3rs3-_-}
HiKi
I was given an apk file so I decompiled it to jar to see the source code using dex2jar
tool and there were two interesting source files MainActivity.class and EncDec.class:
By reading the code it’s obvious that it’s doing AES Decryption to a pre-encrypted value and the key is the pin entered by the user concatenated with Cyb3rT4l3nt5
Here’s the trick the pin in code should be 6 digits or less and since Cyb3rT4l3nt5
length is 12 and since we’re talking about a small key then It’s 128-bit key which is 16 bytes we know 12 bytes so the pin should be 4 digits! (so the key is on the format xxxxCyb3rT4l3nt5
)
The pin has 10k possibilities I wrote a python script to brute force it and simulate the decryption process:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/env python3
import base64
from Crypto.Cipher import AES
known_key = "Cyb3rT4l3nt5"
enc = "blJ9s0JObmk7Qi7vfe98RWN3snqSc79GZTC+8IaHuV052R3Q43SH8vXhjQt7E5m3"
b64_enc = base64.b64decode(enc)
for pin in range(0, 10000):
try:
key = str(pin).zfill(4) + known_key
cipher = AES.new(key.encode('utf-8'), AES.MODE_ECB)
msg = cipher.decrypt(b64_enc).decode('utf-8')
print("key = {}, flag = {}".format(key, msg))
except:
continue
C1ph3r
Last but not least! This is the most challenge I enjoyed so far in the Marathon CTF and you will figure out why!
I was given this 32-bit binary and by analyzing it in IDA and taking a look at the strings:
There are a lot of messages that shows the binary is corrupted and need to be fixed also in the API imports IsDebuggerPresent
function from KERNEL32 Library so I’m expecting ant-debugging techniques!
I went to "Enter the flag:\n"
reference:
After of pushing data to the stack it calls some functions one of them XORs these bytes with 0x44 (I named it XOR_44) which result in the string just for your attention, nothing here is real
and yeah just another rabbit hole :”D
Another note from the code overview is that to approach a function it calls a function that jumps to the desired function
I headed back to the beginning to the main function and started tracing statically the flow which was full of exceptions and debugger traps int 3
instructions to make breakpoints and another important note is Pseudocode is not working so we’re talking assembly here which is cool tbh ;)
My approach here was:
- hitting a breakpoint at the start of try..catch exception
- modifying EIP to the call instruction (to get out of there ASAP :D)
- repeating 1 and 2 until reaching finding the treasure (a cool function)
Let’s dive in starting from main:
There were 2 intermediate functions and then I reached this function which seems to be cool the exception start was on the right block but I changed the flow to the highest block above where the magic begins:
Here’s a closer look to the upper part the first part basically prints “Enter the flag:” and accepts user’s input (32 characters) and I could see clearly 2 loops in there one inside another:
the first loop iterates while count_1 < 32 (count increases by two) and allocates memory using the standard memset function with size 0x1B28 let’s call that buff
and the second loop iterates while count_2 < 0xD94 (which is half of 0x1B28):
The second loop is doing encryption and after it finishes it’s using memcmp standard function to compare it to a pre-defined memory chunk
Putting pieces together the first loop takes two characters from the user’s input at a time, decrypt it then comparing the result with the pre-stored chunks of memory so I dumped those 16 chunks using 3 lines of IDApython:
1
2
3
4
start = 0x00CACB60
for counter in range(16):
idc.savefile("mem" + str(counter).zfill(2) + ".bin", 0, start + (counter * 0x1B28), 0x1B28)
Now the decryption process:
- val = two bytes of the user’s input
- loop from zero to 0xD94:
- if val % all_bytes[i] != 0 increment counter
- otherwise buf[i] += 1 and val = val / all_bytes[i]
Where all_bytes are stored constants in memory to see if these constants divides the value of 2 bytes
So again I made a 2 lines IDApython script to get the all_bytes from memory:
1
2
3
all_bytes_addr = 0x00CAB038
idc.savefile("all_bytes.bin", 0, all_bytes_addr, 0x1B28)
Okay enough with the analysis let’s talk code this is the encryption simulation in python for first 2 letters of the flag "fl" = 0x666c
which its result was equal to mem00.bin (the first chunk of memory):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# encrpytion simulation
buf = [ 0 for _ in range(0xD94)]
val = 0x666C
count_2 = 0
while count_2 < 0xD94:
this_byte = all_bytes[count_2]
if val % this_byte != 0:
count_2 += 1;
else:
buf[2 * count_2] += 1
val = int(val / this_byte)
for i in buf:
print(i, ' ', end='')
Output: 2 0 1 0 1 0 0 0 0 0 0 0 0 0 1 0 1 … 0 (which is identical to mem00.bin which holds the first 2 bytes’ factorization of the flag)
Let’s sum up, I had the constants and how many times each constant divides the 2 bytes value from the flag so to get those bytes I should calculate the Greatest Common Divisor (GCD) for the 16 values (32 bytes flag) and this way I should have the flag!! so I made a python script to do that for me:
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#!/bin/env python3
import struct
from Crypto.Util.number import long_to_bytes
all_bytes = []
# reading all_bytes from the memory dump to array as unsigned short values in little endian
with open("all_bytes.bin", "rb") as f:
while True:
buf = f.read(2)
if not buf:
break
all_bytes.append(struct.unpack('<H', buf)[0])
flag_bytes = []
# looping through the 16 memory dumps for the encrypted 2 letters flag
for count_1 in range(16):
# reading memory dump
with open("mem/mem" + str(count_1).zfill(2) + ".bin", "rb") as f:
memdump = f.read()
# reseting counters
count_2 = 0
result = 1
# getting the divisors from the memory dump
for ptr in range(0, len(memdump)):
if memdump[ptr] > 0:
# how many times the divisor divides the value
times = memdump[ptr]
#print("@count_2 = %i -> 0x%x" % (count_2, times))
# multiplying the divisors to get the GCD (greatest common divisor)
for _ in range(times):
result *= all_bytes[count_2]
elif ptr % 2 == 1:
count_2 += 1
flag_bytes.append(result)
# decoding the flag
flag = ""
hex_bytes = ''.join([hex(x)[2:] for x in flag_bytes])
flag = long_to_bytes(int(hex_bytes, 16)).decode('utf-8')
print(flag)