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 >