Post

WACON 2023 Writeup

MISC - mic check

문제 사이트에 들어가자마자 Not Found 에러가 뜬다. 공지를 보니 의도된 거라고 해서 계속 찾아봤다.

Untitled

혹시나 해서 robots.txt를 이용해 접근할 수 있는 경로를 알아봤는데 다음과 같이 W/A/C/O/N/2/ 가 있었다.

Untitled

그래서 해당 경로에 가면 flag를 얻을 수 있을 것 같아서 접속해봤는데 달라는 flag는 안주고 Not Found 가 뜬다.

Untitled

이 이후로는 경로를 만지작 거리다가 혹시 보이는 거만 저렇게 보이고 status code는 다르지 않을까라고 생각해서 status code를 보았는데 http://58.225.56.196:5000/W/A/C/O/N/2 해당 경로에 접속했을 때 200 OK 코드를 받는 걸 볼 수 있었다.

Untitled

반대로 올바른 경로가 아니면 실제로 404 Not found가 뜬다.

/W/A/C/O/N/0 으로 접속 했을 때 /W/A/C/O/N/0 으로 접속 했을 때

그래서 status code가 200인 경로를 찾아서 flag 값을 완성시켜주었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import requests
import string

url = 'http://58.225.56.196:5000/W/A/C/O/N/2/0/2/3/{'

table = string.hexdigits

def go(_url):
    for i in table:
        url_tmp = _url + '/' + i
        print(url_tmp)
        res = requests.get(url_tmp)
        if res.status_code == 200:
            print(i)
            return i
    return '}'

while True:
    path = go(url)
    if path == '}':
        break
    url = url + '/' + path

url += '/}'
print(url)

FLAG : WACON2023{2060923e53fa205a48b2f9ad47d943c4}

Crypto - White arts(easy)

prob.py 소스코드를 주는데 Generator1, Generator2, Generator3를 통과하면 flag를 던져준다.

해당 문제는 gen(0) 모드인지 random(1) 모드인지 맞히면 다음단계로 넘어가는 형식이다. 그리고 q에 입력값을 저장하고 G.calc(q, inverse).hex() 를 통해 랜덤으로 정해진 모드를 통해 q값을 연산하고 출력한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import math
import os
from Generator import Generator1, Generator2, Generator3, Generator4, Generator5

query_left = 266

def guess_mode(G, query_num):
    for _ in range(query_num):
        q = bytes.fromhex(input("q? > "))
        inverse = input("inverse(y/n)? > ") == 'y'
        assert len(q) == G.input_size
        print(G.calc(q, inverse).hex())
    
    # Time to guess
    assert input("mode? > ") == str(G.mode) # 0(gen) / 1(random)
        
def challenge_generator(challenge_name, Generator):
    global query_left
    print(f"#### Challenge = {challenge_name}")
    query_num = int(input(f"How many queries are required to solve {challenge_name}? > "))
    query_left -= query_num
    for _ in range(40):
        G = Generator()
        guess_mode(G, query_num)

challenge_generator("Generator1", Generator1)
challenge_generator("Generator2", Generator2)

if query_left < 0:
    print("You passed all challenges for EASY but query limit exceeded. Try harder :(")
    exit(-1)

print("(Only for a junior division) Good job! flag_baby =", open("flag_baby.txt").read())

challenge_generator("Generator3", Generator3)

if query_left < 0:
    print("You passed all challenges for EASY but query limit exceeded. Try harder :(")
    exit(-1)

print("Good job! flag_easy =", open("flag_easy.txt").read())

challenge_generator("Generator4", Generator4)
challenge_generator("Generator5", Generator5)

if query_left < 0:
    print("You passed all challenges for HARD but query limit exceeded. Try harder :(")
    exit(-1)

print("(Only for general/global divisions) Good job! flag_hard =", open("flag_hard.txt").read())

Generator1

Generator1의 func_gen(self, q) 을 보면 입력 값의 하위 8bytes가 상위로 오게된다.

  • 따라서 본인이 입력한 하위의 8bytes가 상위의 8bytes로 표시되는 지 여부에 따라 mode를 구분할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Generator1:
    def __init__(self):
        self.mode = os.urandom(1)[0] & 1
        self.n = 8
        self.input_size = 2 * self.n
        self.RF_gen = RandomFunction(self.n)
        self.RF_random = RandomFunction(2 * self.n)
    
    def func_gen(self, q):
        L, R = q[:self.n], q[self.n:]
        L, R = R, xor(L, self.RF_gen.query(R))
        return L+R

    def func_random(self, q):
        return self.RF_random.query(q)

    def calc(self, q, inverse):
        assert inverse == False, "inverse query is not allowed for Generator1"
        ret_gen = self.func_gen(q)
        ret_random = self.func_random(q)
        if self.mode == 0:
            return ret_gen
        else:
            return ret_random

Generator2

RF_gen.query(R) 을 보면 아래와 같이 q를 통해서 만들어 진 랜덤 값은 self.domain_cache[x] 에 저장되는데 이는 q값이 달라지지 않으면 항상 같은 값을 반환한다.

1
2
3
4
5
def query(self, q):
    x = q
    if x not in self.domain_cache:
        self.domain_cache[x] = os.urandom(self.n)
    return self.domain_cache[x]
  • 1 라운드에서 입력값 16바이트를 입력
  • 2 라운드에서는 1라운드의 출력 값 중 상위 8바이트를 2 라운드의 상위 8바이트로 그리고 1 라운드의 입력 값 중 하위 8바이트를 2라운드의 하위 8바이트로 입력해준다.
  • 그리고 나서 2라운드의 출력 값 중 상위 8바이트의 값이 1라운드 입력 값 중 상위 8바이트 값과 비교해서 동일한지 여부를 따진다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Generator2:
    def __init__(self):
        self.mode = os.urandom(1)[0] & 1
        self.n = 8
        self.input_size = 2 * self.n
        self.RF_gen = RandomFunction(self.n)
        self.RF_random = RandomFunction(2 * self.n)
    
    def func_gen(self, q):
        L, R = q[:self.n], q[self.n:]
        L, R = R, xor(L, self.RF_gen.query(R))
        L, R = R, xor(L, self.RF_gen.query(R))
        return L+R

    def func_random(self, q):
        return self.RF_random.query(q)

    def calc(self, q, inverse):
        assert inverse == False, "inverse query is not allowed for Generator2"
        ret_gen = self.func_gen(q)
        ret_random = self.func_random(q)
        if self.mode == 0:
            return ret_gen
        else:
            return ret_random

Generator3

Generator3은 inverse를 사용할 수 있다.

  • 1, 2라운드 모두 같은 값을 입력한다.
  • 1라운드에서는 상위 8바이트 2라운드에서는 하위 8바이트가 동일한 지 여부를 판단한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Generator3:
    def __init__(self):
        self.mode = os.urandom(1)[0] & 1
        self.n = 8
        self.input_size = 2 * self.n
        self.RF_gen = RandomFunction(self.n)
        self.RF_random = RandomPermutation(2 * self.n)
    
    def func_gen(self, q, inverse):
        if not inverse:
            L, R = q[:self.n], q[self.n:]
            L, R = R, xor(L, self.RF_gen.query(R))
            L, R = R, xor(L, self.RF_gen.query(R))
            L, R = R, xor(L, self.RF_gen.query(R))

        else:
            L, R = q[:self.n], q[self.n:]
            L, R = xor(R, self.RF_gen.query(L)), L
            L, R = xor(R, self.RF_gen.query(L)), L
            L, R = xor(R, self.RF_gen.query(L)), L

        return L+R

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from pwn import *

context.log_level = 'debug'
p = remote('175.118.127.63', 2821)

def gen1():
    p.sendlineafter(b'Generator1? > ', '1')
    for i in range(40):
        payload = '11' * 16
        p.sendlineafter(b'q? > ', payload)
        p.sendlineafter(b'inverse(y/n)? > ', b'n')
        result = p.recvline().strip().decode()
        if result[:16] == payload[:16]:
            p.sendlineafter(b'mode? > ', b'0')
        else:
            p.sendlineafter(b'mode? > ', b'1')

def gen2():
    p.sendlineafter(b'Generator2? > ', '2')
    for i in range(40):
        payload = '00' * 16
        p.sendlineafter(b'q? > ', payload)
        p.sendlineafter(b'inverse(y/n)? > ', b'n')
        result = p.recvline().strip().decode()

        payload = result[:16] + '00' * 8
        p.sendlineafter(b'q? > ', payload)
        p.sendlineafter(b'inverse(y/n)? > ', b'n')
        result = p.recvline().strip().decode()

        if result[:16] == '00' * 8:
            p.sendlineafter(b'mode? > ', b'0')
        else:
            p.sendlineafter(b'mode? > ', b'1')

def gen3():
    p.sendlineafter(b'Generator3? > ', '2')
    for i in range(40):
        payload = '00' * 16
        p.sendlineafter(b'q? > ', payload)
        p.sendlineafter(b'inverse(y/n)? > ', b'n')
        result1 = p.recvline().strip().decode()

        payload = '00' * 16
        p.sendlineafter(b'q? > ', payload)
        p.sendlineafter(b'inverse(y/n)? > ', b'y')
        result2 = p.recvline().strip().decode()

        if result1[:16] == result2[16:]:
            p.sendlineafter(b'mode? > ', b'0')
        else:
            p.sendlineafter(b'mode? > ', b'1')

gen1()
gen2()
gen3()

p.interactive()
1
Good job! flag_easy = WACon2023{930db8b4dedb8cb86f309521011a1039}
This post is licensed under CC BY 4.0 by the author.