Posts Angstrom CTF 2021
Post
Cancel

Angstrom CTF 2021

Better late than never! This is my late writeup for some challenges of the great Angstrom CTF 2021. I hope you enjoy it (and the memes) and let’s get started!!


Keysar v2

Keysar v2

Given the source code of the encryption process:

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
import string

with open("key.txt", "r") as f:
    shift = int(f.readline())
    key = f.readline()

with open("flag.txt", "r") as f:
    flag = f.read()


stdalph = string.ascii_lowercase
rkey = ""

for i in key:
    if i not in rkey:
        rkey += i
for i in stdalph:
    if i not in rkey:
        rkey += i
rkey = rkey[-shift:] + rkey[:-shift]

enc = ""
for a in flag:
    if a in stdalph:
        enc += rkey[stdalph.index(a)]
    else:
        enc += a

print(enc)

and the output of encrypting the flag:

1
quutcvbmy ft qii amtkm iqkd tx qjbqfbtm, fzwcw bd mt kqo q sww dztgiv sw qsiw ft xio. bfd kbmyd qcw ftt drqii ft ywf bfd xqf ibffiw stvo txx fzw yctgmv. fzw sww, tx utgcdw, xibwd qmokqo swuqgdw swwd vtm'f uqcw kzqf zgrqmd fzbma bd brhtddbsiw. owiitk, siqua. owiitk, siqua. owiitk, siqua. owiitk, siqua. ttz, siqua qmv owiitk! iwf'd dzqaw bf gh q ibffiw. sqcco! scwqaxqdf bd cwqvo! utrbmy! zqmy tm q dwutmv. zwiit? sqcco? qvqr? uqm otg swibwjw fzbd bd zqhhwmbmy? b uqm'f. b'ii hbua otg gh. ittabmy dzqch. gdw fzw dfqbcd. otgc xqfzwc hqbv yttv rtmwo xtc fztdw. dtcco. b'r wnubfwv. zwcw'd fzw ycqvgqfw. kw'cw jwco hctgv tx otg, dtm. q hwcxwuf cwhtcf uqcv, qii s'd. jwco hctgv. rq! b ytf q fzbmy ytbmy zwcw. otg ytf ibmf tm otgc xgpp. tk! fzqf'd rw! kqjw ft gd! kw'ii sw bm ctk 118,000. sow! sqcco, b ftiv otg, dfth xiobmy bm fzw ztgdw! zwo, qvqr. zwo, sqcco. bd fzqf xgpp ywi? q ibffiw. dhwubqi vqo, ycqvgqfbtm. mwjwc fztgyzf b'v rqaw bf. fzcww vqod ycqvw duztti, fzcww vqod zbyz duztti. fztdw kwcw qkakqcv. fzcww vqod utiiwyw. b'r yiqv b ftta q vqo qmv zbfuzzbawv qctgmv fzw zbjw. otg vbv utrw squa vbxxwcwmf. zb, sqcco. qcfbw, yctkbmy q rgdfquzw? ittad yttv. zwqc qstgf xcqmabw? owqz. otg ytbmy ft fzw xgmwcqi? mt, b'r mtf ytbmy. wjwcostvo amtkd, dfbmy dtrwtmw, otg vbw. vtm'f kqdfw bf tm q degbccwi. dguz q ztfzwqv. b ygwdd zw utgiv zqjw lgdf ytffwm tgf tx fzw kqo. b itjw fzbd bmutchtcqfbmy qm qrgdwrwmf hqca bmft tgc vqo. fzqf'd kzo kw vtm'f mwwv jquqfbtmd. sto, egbfw q sbf tx htrh... gmvwc fzw ubcugrdfqmuwd. kwii, qvqr, ftvqo kw qcw rwm. kw qcw! sww-rwm. qrwm! zqiiwiglqz! dfgvwmfd, xqugifo, vbdfbmygbdzwv swwd, hiwqdw kwiutrw vwqm sgppkwii. kwiutrw, mwk zbjw ubfo ycqvgqfbmy uiqdd tx... ...9:15. fzqf utmuigvwd tgc uwcwrtmbwd. qmv swybmd otgc uqcwwc qf ztmwn bmvgdfcbwd! kbii kw hbua tgclts ftvqo? b zwqcv bf'd lgdf tcbwmfqfbtm. zwqvd gh! zwcw kw yt. awwh otgc zqmvd qmv qmfwmmqd bmdbvw fzw fcqr qf qii fbrwd. ktmvwc kzqf bf'ii sw ibaw? q ibffiw duqco. kwiutrw ft ztmwn, q vbjbdbtm tx ztmwdut qmv q hqcf tx fzw zwnqytm yctgh. fzbd bd bf! ktk. ktk. kw amtk fzqf otg, qd q sww, zqjw ktcawv otgc kztiw ibxw ft ywf ft fzw htbmf kzwcw otg uqm ktca xtc otgc kztiw ibxw. ztmwo swybmd kzwm tgc jqibqmf htiiwm ltuad scbmy fzw mwufqc ft fzw zbjw. tgc fth-dwucwf xtcrgiq bd qgftrqfbuqiio utitc-utccwufwv, duwmf-qvlgdfwv qmv sgssiw-utmftgcwv bmft fzbd dttfzbmy dkwwf docgh kbfz bfd vbdfbmufbjw ytivwm yitk otg amtk qd... ztmwo! fzqf ybci kqd ztf. dzw'd ro utgdbm! dzw bd? owd, kw'cw qii utgdbmd. cbyzf. otg'cw cbyzf. qf ztmwn, kw utmdfqmfio dfcbjw ft brhctjw wjwco qdhwuf tx sww wnbdfwmuw. fzwdw swwd qcw dfcwdd-fwdfbmy q mwk zwirwf fwuzmtityo. kzqf vt otg fzbma zw rqawd? mtf wmtgyz. zwcw kw zqjw tgc iqfwdf qvjqmuwrwmf, fzw acwirqm. qufx{awowvuqwdqcrtcwibawdgsdfbfgfbtm}

I took a look at the source code this line caught my attention rkey = rkey[-shift:] + rkey[:-shift] and the couple lines before .. because these lines basically put the key first and then fill the string with the alphabet without repeating and then shifts the string .. Huh, sounds like a keyed Caesar cipher to me .. Yeah, because it’s!

just a meme

Another quick note is the output is long so you can apply a frequency analysis! and using this online tool site with auto solve I was able to retrieve the key qsuvwxyzbeairmthlcdfgjkpon and hence the flag: actf{keyedcaesarmorelikesubstitution}


Exclusive cipher

Exclusive cipher

The encoded cipher text:

1
ae27eb3a148c3cf031079921ea3315cd27eb7d02882bf724169921eb3a469920e07d0b883bf63c018869a5090e8868e331078a68ec2e468c2bf13b1d9a20ea0208882de12e398c2df60211852deb021f823dda35079b2dda25099f35ab7d218227e17d0a982bee7d098368f13503cd27f135039f68e62f1f9d3cea7c

Okay given XOR key is 5 bytes long (5 characters), the flag is somewhere in the message, fortunately we know also 5 characters of the flag actf{ and that XOR is bidirectional. So when I XOR the whole cipher text with actf{ 5 bytes, somewhere in the result lies the 5 bytes key!

Since I don’t know where exactly is the key I made this simple script to take 5 bytes chunks from start of the result at a time and try to decode the cipher text with it:

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
#!/bin/env python3

from Crypto.Util.number import long_to_bytes
from Crypto.Util.number import bytes_to_long
from pwn import xor

c = "ae27eb3a148c3cf031079921ea3315cd27eb7d02882bf724169921eb3a469920e07d0b883bf63c018869a5090e8868e331078a68ec2e468c2bf13b1d9a20ea0208882de12e398c2df60211852deb021f823dda35079b2dda25099f35ab7d218227e17d0a982bee7d098368f13503cd27f135039f68e62f1f9d3cea7c"

data_bytes = bytes.fromhex(c)

key = 0

after_xor = xor(data_bytes, b'actf{').hex()

i = 0

print(after_xor)

pos_keys = []

while i < len(after_xor):
    pos_keys.append(after_xor[i:i+10])

    i += 10

for pos_key in pos_keys:
    try:
        print(xor(data_bytes, bytes.fromhex(pos_key)).decode('utf-8'))
    except:
        continue

Exclusive cipher's script

Pretty straightforward! the flag: actf{who_needs_aes_when_you_have_xor}

just a meme


Jailbreak

Jailbreak

Jail? break? We need sherlock to solve this one!

just a meme

I found assembly is more comfortable to me than the pseudo code, so in the following picture we can see the main function which starts by allocating registers which will be used in deciding the flow of the program and comparisons:

  • r12d = 0
  • r13d = 1
  • r14d = 0
  • r15d = 0 which I knew by debugging

Then I faced a string obfuscation technique which is deobfuscated by a function I called later stringy which can be called indirectly using call_stringy (I named that too)

I got the strings using dynamic analysis by setting breakpoints before puts to read rdi register which holds the value that will be printed on the terminal, another nice solution I knew later is to reverse the stringy function and get the strings!

I made comments in the disassembly with those strings to make the reversing easier

Another note is these blue arrows coming back to this second block means that after each choice the flow goes back and starts from here so this is obviously a loop

first blocks

Next is a crossroad which checks if the input is look around if so it prints the scene information otherwise it continues to a second check, but before diving in I followed the right branch which happens if r12d != 0

two ways

Here it starts with a check if r12d == 1337 and the input is bananarama I get the flag otherwise if r12d is not 1337 I have two choices:

  • if input is press the red button then r12d += r12d
  • if input is press the green button then r12d += r12d + 1

buttons of right

So to sum up to get the flag r12d should equal 1337 which happens through pressing the buttons in the right order and the input should be bananarama

but first to make r12d != 0 I needed to dive in the left side of branches

After analyzing the left side I found that r12d can be equal to 1 if and only if the input is pry the bars open I commented that like as (((the destination)))

pry the bars open

But this happens only if r13b == 0 which is initialized to 1 in main if you remember! and to make that case the input should be throw the snake at at kmh and that like is commented as ((( the destination 2 ))) in the following image:

throw the snake at kmh

But also for this to happen there is a condition above r14b != 0 and to fulfill this the input should be pick the snake up which sets r14d = ebp that is non-zero value

pick the snake up

so going backwards is the correct steps to move to the right branch:

1
2
3
pick the snake up
throw the snake at kmh
pry the bars open

Now since I moved to the right branch to make r12d == 1337 through the increments in the green and red buttons I made a python script to figure that sequence out instead of factoring by hand to save time:

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
#!/bin/env python3

from random import randint

key = 1337
sum_all = 1
ops = []
sums = []

while True:
    select = randint(0, 1)

    ops.append(select)

    if select == 0:
        sum_all += sum_all +1

    else:
        sum_all += sum_all


    sums.append(sum_all)

    if sum_all == key:
        break

    if sum_all > key:
        sum_all = 1
        ops.clear()
        sums.clear()

# output of possible permutations
# [1, 0, 1, 1, 0, 0, 0, 1, 1, 0]

for op in ops:
    if op == 0:
        print("press the green button")
    else:
        print("press the red button")

And the result was:

factor script

And following that by bananarama to get the flag the full steps should be:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pick the snake up
throw the snake at kmh
pry the bars open
press the red button
press the green button
press the red button
press the red button
press the green button
press the green button
press the green button
press the red button
press the red button
press the green button
bananarama

Piping the payload to the shell server should give us the flag: actf{guess_kmh_still_has_unintended_solutions}

shell response

That was a nice challenge, Watson!


Infinity gauntlet

Infinity gauntlet

As a Marvel’s fan I wanted to solve this challenge so I can be an Avenger too! :D .. It’s more of a coding challenge like 20% is reversing and 80% of the problem is pure coding .. I’m talking about +100 lines of python code here!

At first glance I saw this simple encryption by reading the flag to variable s then XORing the i-th letter in the flag with i * 0x11 % 256 so don’t forget after getting the letters to XOR back with the corresponding value to get the original letter back!

start of code

Then there is an infinite loop (sounds like infinity war to me) that contains 2 types of equations foo and bar equations, foo with 2 known variables and one unknown (3 possible permutations) and bar with 3 known variables and one unknown (4 possible permutations)

foo code

bar code

The input is the unknown variable so I needed to solve the equation for each question and it should be correct to go to the next round otherwise it puts wrong and exits, but what’s the point of solving the functions?

The answer to the previous question is r variable that’s the unknown in these equations and this variable is as you can see in line 67 (if round number > 49) a random letter in the flag as the LSB and the MSB is random % len_flag + round_num and since we can have all but the random value (which can act as the index of the random letter in the flag) then reversing this we can have the letter and its position in the flag string!

Putting all pieces together, we need to:

  • identify the function type and which variable is the unknown
  • reversing the 7 equations to get the unknown (the r variable)
  • extracting MSB and LSB from r to get the letter and index
  • decrypting the letter with the XOR 0x11 * index

Note that I used static analysis to reverse the 7 equations + dynamic analysis to prove my reversing with numbers just to make sure

This is a python script I wrote to do all of that:

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#!/bin/env python3

from pwn import *
import re

len_flag = 49

flag = ["-" for i in range(len_flag)]

def decrypt_xor(pos, val):

    xor_value = 0x11 * pos % 256
    return chr(xor_value ^ ord(val))


def get_index(msb, round_):

    msb_hex = hex(msb)[2:]

    for i in range(0, len_flag):

        index_plus_r = hex(i % len_flag + round_)[-1 * len(msb_hex):]

        if index_plus_r == msb_hex:
            return i


def func_identify(q, round_num):

    if q.find("foo") != -1:
        q_type = 0 # foo

    elif q.find("bar") != -1:
        q_type = 1 # bar

    data = re.sub(r'^(foo|bar)\(|\) =|,|\n', "", q).split(' ')

    q_mark_pos = data.index('?')

    if q_type == 0:

        if q_mark_pos == 0:
            result = foo_1(int(data[1]), int(data[2]))

        elif q_mark_pos == 1:
            result = foo_2(int(data[0]), int(data[2]))

        elif q_mark_pos == 2:
            result = foo_3(int(data[0]), int(data[1]))

    else:

        if q_mark_pos == 0:
            result = bar_1(int(data[1]), int(data[2]), int(data[3]))

        elif q_mark_pos == 1:
            result = bar_2(int(data[0]), int(data[2]), int(data[3]))

        elif q_mark_pos == 2:
            result = bar_3(int(data[0]), int(data[1]), int(data[3]))

        elif q_mark_pos == 3:
            result = bar_4(int(data[0]), int(data[1]), int(data[2]))


    if round_num > 49 and len(hex(result)[2:]) == 4:

        # restoring the most significant byte
        msb = int(hex(result)[2:-2], 16) # 0xXX--
        index = get_index(msb, round_num)


        # restoring the least significant byte
        val = chr(int(hex(result)[-2:], 16)) # 0x--XX
        letter = decrypt_xor(index, val)

        # getting the letter if it's printable
        if (ord(letter) > 0x20 and ord(letter) < 0x39) or (ord(letter) >= 0x40 and ord(letter) <= 0x7d):
            print("@ %i -> letter = %s" % (index, letter))

            flag[index] = letter
            print(''.join(flag))

    return result

def foo_1(a, b):

    r = b ^ (a + 1) ^ 1337
    return r


def foo_2(a, b):

    r = (b ^ a ^ 1337) - 1
    return r

def foo_3(a, b):

    r = a ^ (b + 1) ^ 1337
    return r


def bar_1(a, b, c):

    r = c - (a * (b + 1))
    return r


def bar_2(a, b, c):

    r = (c - a) // (b + 1)
    return r


def bar_3(a, b, c):

    r = ((c - a) // b) - 1
    return r


def bar_4(a, b, c):

    r = a + b * (c + 1)
    return r


# main
s = process('./infinity_gauntlet')

s.recvline()
s.recvline()

round_num = 1

while True:

    res = s.recvline().decode('utf-8') # round number line

    if res.find("ROUND") != -1:
        round_num = int(re.sub("[^\d]", "", res))

    elif res.find("foo") != -1 or res.find("bar") != -1:
        r = func_identify(res, round_num)
        s.sendline(str(r))


s.close()

By running this on the shell server I watched as the flag was being formed

Infinity gauntlet flag

just a meme

The flag is: actf{snapped_away_the_end} I should’ve gone for the head tho!

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