ImaginaryCTF Round 12 Writeup

July 22nd 2021

ImaginaryCTF Round 12 Writeups

A short month this time around

Sanity Check Round 12

You copy and paste the flag:

ictf{Round_12_Sanity_Check}

smth here

We get :4E7L6oD*049p==b?8b=D07EHN and due to the fact that there are only printable characters (and also because it was a day 2 challenge) I guessed it was just a ROT47, which was in fact correct, so doing a rot47 again gives us the flag:

ictf{e@sY_chAll3ng3ls_ftw}

Slow Internet

We get the site router, and a hint that it is a default "default business router"

We visit the webpage and see a loginpage, so a quick google for Business Wireless Gateway default admin yields us the default credentials for cusadmin here.

So we try both passwords and with CantTouchThis we get the flag ictf{w3b_and_0s1nt???_gr3at_c0mb0!}

Quintessentially Quick Quiche

We get the site quiche and when we visit it tells us to use http/3. My first solution was with a python script which was way too complicated, you can just reload the page in firefox (Because after the initial load a header is set that firefox should use HTTP/3 for it).

So we get the flag ictf{who_knew_http_over_udp_could_be_so_quic(k)}

Caesar Tart

We see that there's a function caesar which applies a key to a character, we also see that the key is 5 to 10 characters long and we just loop over that. This encryption is called vigenere and there exist online solvers for it. I used dcode and told it to find something containing the word ICTF. It then pretty quickly found a solution, but I had to copy the recovered key to another website to get the full plaintext:

(I added spaces for your convenience)

HEY IF YOURE READING THIS MESSAGE I SUPPOSE THIS MEANS YOUVE SUCCESSFULLY DECRYPTED IT EH NEAT TODAY I WANNA TALK ABOUT APPLE TARTS DID YOU KNOW APPLE TARTS WERE MADE OUT OF APPLES BONKERS RIGHT I HEARD PEOPLE EVEN PUT SUGAR INTO IT SUGAR THIS ISR EALLY CRAZY ANYWAY EVEN THOUGH THERE ARE APPLES IN APPLE TARTS THEY ARE STILL DELICIOUS ENOUGH THIS MADE MEH UNGRY I WANNA EAT ONE NOW THINK ABOUT THES MELL THAT WAFTS THROUGH THE AIR INTO YOUR NOSE IMAGINE THE DELIGHTFUL GOLDEN COLORATION OF THE TART IM SERIOUSLY RUNNING OUT OF THINGS TO TELL IN HERE SO I GUESS WITHOUT FURTHER ADO HERES YOUR FLAG ICTFMORELIKEVIGENERETART

thus we get ictf{MORELIKEVIGENERETART}

Glitchy Video

We get a .wav file together with a pretty explicit hint to look at it, so I opened the file in audacity and switch to the spectrogram view. There is the a string, which looks like rot encoded text, so I just try all rotations on the string nhyk{34xd_4zi10_xy3l4s0lw4umd} and with a rotation of 21, I get the flag:

ictf{34sy_4ud10_st3g4n0gr4phy}

Web Flagchecker

We get the site flagchecker. There we get the sourcecode. The first line which catches the eye is actually just there to print the code, but that's fairly quickly discovered once we decoded \x3cpre\x3e\x3ccode\x3e%s\x3c/code\x3e\x3c/pre\x3e to be <pre><code>%s</code></pre>. So we look at the other function and see that we format the string which is then passed into the render_template_string function, which means we can make the query be something which will be interpreted by that function. But first we need to pass all the previous conditions, so our query must start with "ictf" and end with "}", also we need to have the exact same length as the flag, so we first try a few lengths (I just did a quick 3 lines script to get the check page with a few "a"s) to find the correct length. Then we put in the stuff to make the other check happy and finally we put a "}}{{flag}} somewhere in there to include the flag variable, so our final payload looks like this: /check?flag=ictf{"}}aaaa{{flag}} and this gives us the flag (After the inital Falseaaaa:

ictf{@llth3br@ck3t5}

overflowwwwwwwww

We get a binary and a libc, we open the binary and see that main just reads a string and then compares a value with a constant (But that value is initialized to another variable and we never write to it, so binja actually optimized that code out), anyway, there is a heap bufferoverflow because the variable is also on the heap. So all we need to do is to overflow enough memory with the correct bytes and that value will be set. I am rather lazy, so I did not calculate any offsets, but just ran python -c "print('\x37\x13\x37\x13\x00\x00\x00\x00'*32+'cat flag.txt')" | ./overflowwwwwwwww with different values (instead of 32) until it worked. Then I switched to remote and hurray, we get the flag

ictf{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_h34p_c4n_b3_0v3rfl0w3d_t00}

how r u

We see python 2 and input (But not raw_input), we know we can just input code, which then will be eval'd. So we have one line to do anything. So we just launch a shell by doing __import__("os").system("sh") and get our shell and flag.

ictf{n3v3r_3v3r_3v3r_3v3r_u53_1npu7_1n_pyth0n2}

Arabica

We need to reverse engineer a java class. JD-GUI didn't support the java version, so I ended up using this online decompiler. After decompilation, we see that there are three phases. Other people realized you could skip the first two by just copying the code of phase 3, I however solved all the phases. Also I needed to install a java16 jvm and had to rename the file from Coffe.class to Arabica.class (Else it would complain that the classname and filename didn't match)

Phase 1: you enter a string and it needs to match ascii codes which are hardcoded, so just convert the ascii codes to text and you get hotcupofcoffee

Phase 2: We read an int and that int divided by the last int we gave it (or one in the first iteration) needs to be equal to the current iteration, so we just have to multiply the last input by the iteration number (which results in the factorials) So we just need to enter 1 2 6 24 120

Phase 3: We need this to return not zero, so we need to input a number which is not -1 and which when doubled and added 1 gives -1, so we first convert -1 into it's hex form, 0xFFFFFFFF then subtract one and shift one to the right to get 0x7FFFFFFF convert that back into decimal to get 2147483647 and use that to finally get our flag:

flag{i_4ctU411y_l1k3_T34_m0R3!}

Password recovery Pt. 1

We get a file of email transcript. The first email tells us that in each email following it, there is a password generated in python like this: b64encode(getrandbits(32).to_bytes(4, 'big') + getrandbits(32).to_bytes(4, 'big')).decode() also we need to generate the next 10 getrandbits(32) to get the flag. Luckily there's already pre-made solutions for that (Like randcrack) which takes 624 32-bit integers and then is able to predict following values. So I simply parsed the file, extracted all the passwords, fed them into randrcrack and got the flag:

from randcrack import *
from base64 import b64decode

with open("out.txt") as f:
    d=[b64decode(x.strip()) for x in f.read().split("\n")[3:] if x.strip() != ""]

rc = RandCrack()
for x in d:
    try:
        rc.submit(int.from_bytes(x[:4], 'big'))
    except ValueError:
        rc.predict_getrandbits(32)
    try:
        rc.submit(int.from_bytes(x[4:], 'big'))
    except ValueError:
        rc.predict_getrandbits(32)
print(hex(rc.predict_getrandbits(32*11)))

where out.txt was simply contained the passwords in correct order (Sorted them, just to make sure) Then I needed to swap the endianess and reverse the string, to get the flag:

ictf{Pr3d1ct_M3rs3nn3_By_S3tt1ng_St4t3}

ICICLE Jail

We get a new interpreter and a new spec and we need to read a file, but we can't use "read" and certain other strings in the icicle code (Which needs to be one single line long). Luckily there's also a new "exec" instruction. And by sheer coincidence, that will convert an int to a string if it's given an int as parameter. So we just use

readf r0, "flag.txt"
pr r0

convert the character to hex, then convert that into decimal to get exec 47059791966767526693337926357987238923915048615978680990013992970

and by inputting that on the remote we get:

ictf{3x3cut!v3_3x3cu+0r_3x3cut3s_3x3c}

Too Old

We see that we generate a random number between 10^7 and 10^8, which means that we have 24 to 27 bits, which is totally bruteforcable, so that's what I did:

from multiprocessing import Pool
def brute(a):
    for key in range(a&(~0xFF)|(0xe6^ord('i')), a+10**8//16,256):
        ok=key
        f = bytes.fromhex("e649fd7458fb36acb341346324635da87427d8d25f5c8b7665b921052727bf730f1c0273d00c23217873")
        out = b""
        for c in f:
            out += bytes([c ^ (key&0xff)])
            key = int("{:016d}".format(key**2)[4:12])
        if b"ictf" in out:
            print(ok)
            print(out)
with Pool(16) as p:
    p.map(brute, [10**8//16*i+10**7 for i in range(16)])

the script runs for about 2 second on my machine and gives me the flag. The intended solution is a bit smarter than this and also looks at the second character before fully decrypting, but meh, good enough.

ictf{f0r_l00p5_r1ght_th3r3_4nd_3v3ryWh3r3}

smells Like Stereo Bits

So we get the hint of stereo bits and LSB in the title. To view the distorted image, I used this. (Just googled magic eye solver, because I can't see those damn things). After doing that we get G1R12B0, which I first thought was a password until I remembered the hint that LSB is involved, so I opened up stegsolve and opened the file, went to extract data and set the bit plane order to GRB, and check the indicated bits, bit 1 for G, bit 1 and 2 for R and bit 0 for B.

I then saw that there was a JFIF in the output, so I exported it as a .jpg and looking at that gave us the flag:

ictf{90s_st3r30grams_r_k3wl}

ovvvvvvvvvverflow

We open the binary, see it's a bufferoverflow and enter a lot of a's, then it tells us that the command aaaaaaaaa was not found, so we play a bit around until we see that after 10 characters we overwrite the command that is run. So we enter aaaaaaaaaash to get a shell and read the flag file.

ictf{aAAaAAaaaAAAaaAAAaaAaaAaAAaaAAaAaAaaAAaaaAaAAAAAaAAaAAaAAAaAAAaash}

Regular Reversing

I can't say too much about this challenge, except that it's just tedious work and going through the "hints" again and again (I called each look ahead a hint and went through each one until I found something I could do; Solving it on paper was easier than on the computer)

There may or may not have been a lot of guessing involved towards the end (When I had the end of the string but didn't have the kn0w yet)

ictf{C4N'7_kn0w_2_much_4b0u+_R3Gul4r_3xpr3ss10n5}

Password recovery Pt.2

Again we are given passwords generated like in part 1, but this time we're missing pieces of it and need to get some part in the middle. The intended solution actually uses math to compute everything, I just asked z3 to solve it for me:

from base64 import b64decode
from z3 import *
from pwn import u32

with open("password.recovery.mbox") as f:
    d = [x.strip() for x in f.readlines() if x.strip() != ""]
lastnum = -1
nextpw = False
pws={}
minnum = -1
maxnum = -1
for l in d:
    if "Subject: " in l:
        lastnum = int(l.split("#")[-1])
        if lastnum < minnum or minnum == -1:
            minnum = lastnum
        maxnum = max(maxnum, lastnum)
    elif "We have assigned you" in l:
        nextpw = True
    elif nextpw:
        nextpw = False
        pws[lastnum] = b64decode(l)

### randrack inspired, but modified

MT = [BitVec(f"vec_{i}", 32) for i in range(624)] # initial state unknown
MTO = [x for x in MT]
index = 625 # initial state is to do regen

def regen():
    global MT
    global index

    N, M = 624, 397
    A = 0x9908b0df
    LO, HI = 0x7FFFFFFF, 0x80000000

    for kk in range(0, N-M):
        y = (MT[kk] & HI) | (MT[kk+1] & LO)
        MT[kk] = MT[kk+M] ^ LShR(y,1) ^ ((y&1)*A)
    for kk in range(N-M-1, N-1):
        y = (MT[kk] & HI) | (MT[kk+1] & LO)
        MT[kk] = MT[kk+(M-N)] ^ LShR(y,1) ^ ((y&1)*A)
    y = (MT[N-1]&HI)|(MT[0]&LO)
    MT[N-1] = MT[M-1] ^ LShR(y,1) ^ ((y&1)*A)
    index = 0

def extract():
    global index
    if index >= 624:
        regen()
    r = MT[index]
    r = r ^ LShR(r, 11)
    r = r ^ ((r << 7) & 0x9d2c5680)
    r = r ^ ((r << 15) & 0xefc60000)
    r = r ^ LShR(r, 18)
    index += 1
    return r&0xFFFFFFFF


def regen_int():
    global MT
    global index

    N, M = 624, 397
    A = 0x9908b0df
    LO, HI = 0x7FFFFFFF, 0x80000000

    for kk in range(0, N-M):
        y = (MT[kk] & HI) | (MT[kk+1] & LO)
        MT[kk] = MT[kk+M] ^ (y>>1) ^ ((y&1)*A)
    for kk in range(N-M-1, N-1):
        y = (MT[kk] & HI) | (MT[kk+1] & LO)
        MT[kk] = MT[kk+(M-N)] ^ (y>>1) ^ ((y&1)*A)
    y = (MT[N-1]&HI)|(MT[0]&LO)
    MT[N-1] = MT[M-1] ^ (y>>1) ^ ((y&1)*A)
    index = 0


def extract_int():
    global index
    r = MT[index]
    r = r ^ (r >> 11)
    r = r ^ ((r << 7) & 0x9d2c5680)
    r = r ^ ((r << 15) & 0xefc60000)
    r = r ^ (r >> 18)
    index += 1
    return r&0xFFFFFFFF

### Now solve it

solver = Solver()

start = minnum-(minnum%312)
for i in range(start, maxnum+1):
    x1 = extract()
    x2 = extract()

    expected = pws.get(i, None)
    if expected != None:
        solver.add(int.from_bytes(expected[:4],'big') == x1)
        solver.add(int.from_bytes(expected[4:],'big') == x2)

if solver.check() == sat:
    model = solver.model()
    MT = [model[x].as_long() if model[x] != None else 0 for x in MTO]
    solver.add(Or([MT[i] != MTO[i] for i in range(624)]))
    regen_int() # initial one
    regen_int() # the second one to get to the second state
    index = 0
    for i in range(2*9):
        extract_int()
    print(b"".join([bytes.fromhex(hex(extract_int())[2:].rjust(8,'0')) for i in range(9)]))
else:
    print("Nay")

This runs for about two minutes, but then finds the correct flag:

{St4t3_i+397_is_7rivi4lly_c0mpu73d}

Reversing Some Algorithms

Thanks to implementing a powermod function a few times, I recognized the method f immediatly to just be the python pow method, but recursive. Then in the main function we convert the flag into a number by just doing the same as converting to hex and then reading the number. Then we choose ten integers and they need to be equal to themselves when raised to flag*y, so we know that flag and y need to be modular inverse modulo phi(n), which is trivially computed to be 96933099098818028735695816363963142716310037576504750007863794238150071327948258222722982318041571631903896405924017042636201556382459909420519737582821842523796669050908979263786208808450207855351074945914505034111693323560346396010640647631560540364278925789581677681912012172605890035796657063975789700396 Then we can compute pow(y,-1,phi(n)) and get the flagnum. We just need to convert that to hex and then to characters again.

ictf{the_abuse_of_the_initials_of_rivest_shamir_and_adelman_is_criminal}

Puzzle

We get a unity game and need to somehow get the flag. We can either open the Csharp assembly file in a program like ILSpy and export the code, patch the code and build the dll in Visual studio to stop the pillars from rising. Alternatively we can extract the objects using something like AssetStudio and then we get a folder for each cube with a letter, with the letter in the folder. Unluckily using that method the letter y was duplicated (Because the cube exists twice but you can only see one of course)

ICTF{ARE_YOU_WINNING_SON_GGGGG}

ICICLE Golf

We had to implement some algorithms in ICICLE

my cat program (very quickly done and not optimized at all) looks like this:

loop:
    readstr r0
    strint r1, r0
    sub r1, r1, 1702390132
    jz r1, exit
    pr r0
    mov r1, 10
    intstr r1, r1
    pr r1
    j loop
exit:

My gcd looks like this

readint r0
readint r1
loop:
    jz r1, exit

    mov r3, r1
    mod r1, r0, r1
    mov r0, r3
    j loop
exit:
pr r0

And for the sort, I implemented a bubble sort in around 20 lines, sadly I don't have the code for that anymore. The code for the new insertion code will be pasted when the month is over :P

Converting those to base64 and inserting them online results in

ictf{wh@ts_c00l3r_th@n_b3!ng_c00l?_ICICLE!}

Password recover Pt. 3

Again we are given passwords, but this time we need to go back in time to get the flag. Luckily my z3 script was almost good to go, I had to fix a few things (Only related to the password reading and then the flag printing actually)

from base64 import b64decode
from z3 import *

with open("password.recovery.pt3.mbox") as f:
    d = [x.strip() for x in f.readlines() if x.strip() != ""]
lastnum = -1
nextpw = False
pws={}
minnum = -1
maxnum = -1
for l in d:
    if "Subject: " in l:
        lastnum = int(l.split("#")[-1]) // 2
        if lastnum < minnum or minnum == -1:
            minnum = lastnum
        maxnum = max(maxnum, lastnum)
    elif "We have assigned you" in l:
        nextpw = True
    elif nextpw:
        nextpw = False
        pws[lastnum] = b64decode(l)

#print(pws)

### randrack inspired, but modified

MT = [BitVec(f"vec_{i}", 32) for i in range(624)] # initial state unknown
MTO = [x for x in MT]
index = 625 # initial state is to do regen

def regen():
    global MT
    global index

    N, M = 624, 397
    A = 0x9908b0df
    LO, HI = 0x7FFFFFFF, 0x80000000

    for kk in range(0, N-M):
        y = (MT[kk] & HI) | (MT[kk+1] & LO)
        MT[kk] = MT[kk+M] ^ LShR(y,1) ^ ((y&1)*A)
    for kk in range(N-M-1, N-1):
        y = (MT[kk] & HI) | (MT[kk+1] & LO)
        MT[kk] = MT[kk+(M-N)] ^ LShR(y,1) ^ ((y&1)*A)
    y = (MT[N-1]&HI)|(MT[0]&LO)
    MT[N-1] = MT[M-1] ^ LShR(y,1) ^ ((y&1)*A)
    index = 0

def extract():
    global index
    if index >= 624:
        regen()
    r = MT[index]
    r = r ^ LShR(r, 11)
    r = r ^ ((r << 7) & 0x9d2c5680)
    r = r ^ ((r << 15) & 0xefc60000)
    r = r ^ LShR(r, 18)
    index += 1
    return r&0xFFFFFFFF


def regen_int():
    global MT
    global index

    N, M = 624, 397
    A = 0x9908b0df
    LO, HI = 0x7FFFFFFF, 0x80000000

    for kk in range(0, N-M):
        y = (MT[kk] & HI) | (MT[kk+1] & LO)
        MT[kk] = MT[kk+M] ^ (y>>1) ^ ((y&1)*A)
    for kk in range(N-M-1, N-1):
        y = (MT[kk] & HI) | (MT[kk+1] & LO)
        MT[kk] = MT[kk+(M-N)] ^ (y>>1) ^ ((y&1)*A)
    y = (MT[N-1]&HI)|(MT[0]&LO)
    MT[N-1] = MT[M-1] ^ (y>>1) ^ ((y&1)*A)
    index = 0


def extract_int():
    global index
    r = MT[index]
    r = r ^ (r >> 11)
    r = r ^ ((r << 7) & 0x9d2c5680)
    r = r ^ ((r << 15) & 0xefc60000)
    r = r ^ (r >> 18)
    index += 1
    return r

### Now solve it

solver = Solver()

start = minnum-(minnum%312) # align min num
end = maxnum
for i in range(start, end):
    x1 = extract()
    x2 = extract()

    expected = pws.get(i, None)
    if expected != None:
        solver.add(int.from_bytes(expected[:4],'big') == x1)
        solver.add(int.from_bytes(expected[4:],'big') == x2)

if solver.check() == sat:
    model = solver.model()
    MT = [model[x].as_long() if model[x] != None else 0 for x in MTO]
    #regen_int()
    #regen_int()
    index = 0
    x = b"".join([bytes.fromhex(hex(extract_int())[2:].rjust(8,'0')) for i in range(624)])
    print(x)
    flag = x[x.find(b"ictf"):]
    flag = flag[:flag.find(b"}")+1]
    print(flag)
else:
    print("Nay")

and after only 40 seconds this prints the flag.... Or rather almost the flag, it prints ictf{m3rs3nn3_tw1st3rs_c4n_b\xe8\x06\x02Pck_g3N3r4t3d}

But I just guessed the four missing characters to be 3_b4 because that made the most sense (And since almost all e are 3 and a are 4 I just guessed this first and it was actually correct).

I have no idea what's wrong with my solve script, but too lazy to debug that now.

ictf{m3rs3nn3_tw1st3rs_c4n_b3_b4ck_g3N3r4t3d}

Bullet Time

We get a C++ binary and there are ton's of functions calls to functions which seemingly do nothing. So I instead searched for strings and found that we have two functions which tell us what they are (A matrix times matrix method and a matrix times a vector method). And after staring at some functions for enough, I figured out that all elements are calculated modulo 0xFB and that we generate two matrices using random elements (After seeding with 0x1337), and then multiply the flag with that matrix and then write the output to a file. So I wrote a quick C file to print me the matrices:

#include <stdio.h>
#include <stdlib.h>

int main() {
    srand(0x1337);
    //for(int i = 0; i < 24*24; i++) {rand();} // if uncommented, we want to print the second matrix
    for(int i = 0; i < 24; i++) {
        printf("[");
        for(int j = 0; j < 24; j++) {
            printf("gf(0x%x),", ((rand()%0xfb)+0xfb)%0xfb);
        }
        printf("],\n");
    }
}

and then I went to sage and put everything into a Matrix:

gf=GF(0xFB)
m1=Matrix([<first_run>])
m2=Matrix([<second_run>])
x=Matrix([gf(i) for i in [0x81,0xa3,0x97,0xd3,0xb8,0xcd,0xb3,0x24,0x4e,0xc7,0x33,0x23,0x19,0x20,0xf1,0xe9,0xa8,0x27,0x6f,0x21,0x4e,0xe1,0x2c,0xc7]])
y=x*(m1*m2).inverse().transpose()
print("".join([chr(i) for i in y[0]]))

which gives us the flag:

ictf{r4nd0m_m4tr1x_mul+}

Ay Yi Yi

We are given a file to encode pngs into some custom format and also embed a string into the png. So we look at the icicle code and see that it first skips the first eight bytes then inserts one bit of the flag into the next byte (red) then skips a byte, inserts another bit into the next byte, then skips three bytes and repeats the process until the flag is zero.

So we just need to reverse that by extracting the bits and reading them. For that I modified the convertion function which takes the .iii file and converts it to a png like this:

def iii_to_png(fname):
    assert fname[-4:] == ".iii"
    iii = open(fname, 'rb').read()
    assert iii[:4] == b"\x7fIiI"
    iii = iii[4:]
    w = bytes_to_long(iii[:2])
    h = bytes_to_long(iii[2:4])
    iii = iii[4:]
    assert len(iii)%3 == 0
    png = Image.new("RGB", (w, h))
    bitstring = ""
    curstr = ""
    now = True
    for i in range(0, len(iii), 3):
        idx = i//3
        if now:
            curstr += str(iii[i+0]&0x1)+str((iii[i+2]>>2)&1)
            now = False
        else:
            now = True
        if len(curstr) == 8:
            bitstring += curstr[::-1]
            curstr = ""
        png.putpixel((idx//h, idx%h), (iii[i], iii[i+1], iii[i+2]))
    print(bitstring)
    png.save(fname[:-4] + ".png")

Then renaming the chall.iii.enc to chall.iii and then converting that to a png prints a binary string, which decoded yields the flag:

ictf{idky_yall_asked_for_this_but_here_you_go_have_fun}


< ImaginaryCTF Round 11 Writeup | PBCTF: NP - Not Password >