본문 바로가기

Wargame/pwnable.kr

[Rookies] crypto1

문제 포인트만 잡고나면 쉬운데, 암호를 잘 모르는 나에게는 약간 접근하기 힘든 문제였다. 포인트 잡는데에 4시간?? 5시간?? 정도 헤맸던 것 같다. 

 

crypto1 문제는 CBC 블럭 암호의 운용방식을 잘 알아야 풀 수 있다.

 - 400명 이상 푼 문제는 PoC 코드를 공개하겠다.

 - 이 글에는 문제 풀이와 답이 있으니, 실력 증진을 원하는 분들은 충분히 삽질한 후 읽어주시길 바란다.

 

1. 코드흐름

====================================================

server.py

server.py 부터 보자.

AES128_CBC(msg)함수는 client로부터 날아온 패킷을 받아와서 AES128_CBC mode로 복호화한다.

(이 때 사용되는 키, IV(Initialization Vector)는 공개되지 않아 우리가 알 수 없다.)

그리고 복호화 후 패킷 뒤에 붙은 패딩값(0x00)들을 전부 제거한 뒤 리턴한다.

 

authenticate(e_packet)함수는 AES128_CBC 함수의 리턴 패킷값을 받아와서 id, pw, cookie가 일치하는지 확인한다.

패킷을 parsing하는 과정에서 id, pw, cookie를 split('-')[0], [1], [2]로 구분하는게 특징이다.

 

client.py

다음으로 client.py 이다.

sanitize(arg) 함수는 우리가 입력한 id, pw의 문자열을 필터링한다.

숫자, 영문자, 그리고 특수문자('-', '_') 외의 나머지 문자열을 입력하였을 경우, False를 리턴하고 프로그램이 종료된다.

 

AES128_CBC(msg) 함수는 특정 키, IV값을 이용해 패킷을 AES128_CBC mode로 암호화한다.

패킷은 우리가 입력한 id, pw, 특정 cookie로 구성된다.

 패킷: id-pw-cookie

중간에 '-' 문자가 있는데, 이는 서버에서 id, pw, cookie를 쉽게 parsing하기 위해 쓰였다.

 

request_auth(id, pw) 함수는 패킷을 id-pw-cookie 형태로 묶은 뒤 AES128_CBC mode로 암호화하여 서버에 보낸다. 이후 서버에서 받아온 값을 리턴한다.

request_auth(id, pw) 함수는 서버에서 받아온 값을 리턴한 후 cred 변수에 넣는다.

리턴값은 0, 1, 2로 구성되며 상세내용은 server.py의 authenticate() 함수에서 확인하시길 바란다.

리턴이 0일 경우, 인증되지 않은 유저로.

리턴이 1일 경우, guest 유저로.

리턴이 2일 경우, admin 유저로 인식한 후 flag 값을 준다.

 

 

2. 고려사항

====================================================

1. server.py, client.py의 key, iv, cookie = 'erased. but there ...' 은 문제서버에서 실제로 사용된 값이 아니다.

2. server.py에서 id, pw, cookie를 split('-')를 통해 구분한다.

3. client.py에서 특수문자 중 '-'의 입력을 허용한다.

4. client.py에서 우리가 입력한 값을 id-pw-cookie로 묶어서 AES128_CBC mode로 암호화한 후, 암호화된 값을 print해준다.

 

3. 필요 지식

====================================================

1. CBC 블럭 암호의 동작 원리를 알아야 한다.

관련 주소:

 

블록 암호 운용 방식 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 암호학에서 블록 암호 운용 방식(영어: block cipher modes of operation)은 하나의 키 아래에서 블록 암호를 반복적으로 안전하게 이용하게 하는 절차를 말한다.[1][2] 블

ko.wikipedia.org

 

 

4. 문제 풀이

====================================================

server.py 의 authenticate 함수 중..

이 문제는 쿠키값만 알아내면 'admin'의 pw를 계산할 수 있고, 이를 통해 'admin' 인증을 할 수 있게되어 flag를 얻을 수 있다.

근데 쿠키값을 어떻게 알아내야 하나.

 

문제의 핵심은 1. 패킷의 구조, 2. CBC 블럭 암호 운용 방식이다. 이걸 잘 알면 문제를 쉽게 풀 수 있다.

 

먼저 패킷의 구조를 보자.

패킷은 id-pw-cookie + zero padding(0x00)으로 이루어져있다.

여기서 패딩이란 패킷의 CBC 블럭 암호를 위해 부족한 부분을 특정 값으로 채워주는 건데, 이 문제에서는 '0x00'을 패딩값으로 사용했다.

 

패킷 구성 예를 들면 다음과 같다.

block_size가 16일 때, id-pw-cookie + zero padding은 총 3개의 블럭에 들어간다. 3번째 블록을 보면 알겠지만 쿠키를 채우고 난 뒤 나머지 부분이 0x0으로 다 채워져있다.

 

다음으로, CBC 블록암호 운용방식을 살펴보자.

"""

암호 블록 체인 (cipher-block chaining, CBC) 방식은 1976년 IBM에 의해 개발되었다.[4] 각 블록은 암호화되기 전에 이전 블록의 암호화 결과와 XOR되며, 첫 블록의 경우에는 초기화 벡터가 사용된다. 초기화 벡터가 같은 경우 출력 결과가 항상 같기 때문에, 매 암호화마다 다른 초기화 벡터를 사용해야 한다.

"""

위 문장은 위키피디아에서 따왔다. 빨간 글씨가 핵심인데, 저 부분을 잘 이용하면 IV를 이용하는 부분에서 Cookie값을 얻어올 수 있을 것이다. 왜냐면, crypto1 문제는 암호화에 사용된 IV값이 항상 동일하기 때문에 취약점이 발생할 수 있다.

 

CBC 블럭 암호 방식은

1. AES 암호화시, 첫번째 블록의 평문과 IV값이 항상 같다면 항상 같은 암호문을 생성한다.

2. 평문 혹은 IV 값이 1바이트라도 바뀌게 될 경우 암호문 16바이트에 영향이 가게되고, 전혀 다른 16바이트 암호문을 생성한다.

3. 2.로 인해 첫번째 암호문이 바뀌게 될 경우, 두번째 암호문에도 영향이 가서 전혀 다른 암호문이 생성되고, 3번째..n번째 블록까지 연달아(chaining) 영향이 간다.

4. 즉 중간에 1바이트라도 바뀌게 되면, 바뀌 부분부터 맨 끝 블록까지 영향이 가게되어 전혀 다른 암호문이 생성된다.

5. 이는 AES 암호의 Confusion(혼돈), Diffusion(확산)의 특성으로 인해 오염된 패킷(패킷이 바뀐 부분)이 존재하는 부분부터 모든 부분에 영향이 가는 것이다. AES 암호의 운용원리를 자세히 알고 싶으신 분은 구글링하시길.

 

이 점을 알면 문제를 풀 수 있다.

테스트를 해보자.

 

1. 첫번째 블록은 IV값이 고정되어 있기 때문에 동일한 평문을 입력하면 항상 값이 동일하게 나온다.

확인 결과, 값이 일치한다.

 

2. 1바이트라도 바꾸게 된다면?

값이 전부 바뀐다.

16개의 문자(1234567890abcdef)를 쓰고 그 다음 17번째 문자를 a, b로 주면서 테스트하면 2번째 블록 16바이트가 모두 바뀌는 걸 볼 수 있다.

 

 

그럼 문제를 어떻게 풀어야하나.

쿠키값을 1바이트씩 빼오면서 문자를 하나하나 brute force하는 방법밖에 없다.

이런 느낌이다.

블록사이즈 단위로 계속 암호문을 비교해가면서 쿠키값을 모두 찾은 뒤, 쿠키값을 이용해 pw를 계산하고 서버에 전송하면 flag를 얻을 수 있다.

 

아래는 내가 작성한 코드이다. python3으로 돌려야 실행이 잘 된다.

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
59
60
61
62
63
64
65
66
67
68
69
70
from pwn import *
import hashlib
 
 
context(log_level='error')
# clear the message of socket open and closed that occurs through pwntool
 
def connect_crypto(id, pw): # connect 'nc pwnable.kr 9006'
    r = remote('pwnable.kr'9006)
    r.recvuntil('ID\n')
    r.sendline(id)
    r.recvuntil('PW\n')
    r.sendline(pw)
    return r
 
 
find_str = "1234567890abcdefghijklmnopqrstuvwxyz-_"
real_cookie = ""
count = 2 # start '-' count to find cookie within IV range.
block_count = 1  # compare size of block: 16
ID = 'admin'
 
 
#==================== find cookie ========================#
print ("[*] find cookie :: [", end="")
while True:
    r = connect_crypto("-" * (15 - (count%16)), "")
    r.recvuntil("(")
 
    if (count % 16== 0:
    # if compared all current blocks?
        block_count += 1
    # go next block
 
    comp_str1 = r.recv(16 * block_count * 2)
    # 16 = block_size || block_count = number of block
    # 2 = To calculate the exact size of the string
    r.close()
 
    for cookie in find_str:
        r = connect_crypto("-" * ((15 - (count%16))+2+ real_cookie + cookie, "")
        # real_cookie = the real cookie value found so far
        # cookie = brute force value
 
        r.recvuntil("(")
        comp_str2 = r.recv(16 * block_count * 2)
        # 16 = block_size || block_count = number of block
        # 2 = To calculate the exact size of the string
        r.close()
 
        if comp_str1 == comp_str2:  # if found real_cookie?
            real_cookie += cookie
            print (cookie, end='')
            count += 1
            break
 
#================== authenticate 'admin' ======================#
    if comp_str1 != comp_str2: # when no more cookie value exist
        print ("]")
        PW = str(hashlib.sha256((ID+real_cookie).encode('utf-8')).hexdigest())
        # calculate pw hash
        r = connect_crypto(ID, PW) # authenticate it!
        print ("[*] authenticate admin")
        print ("  [-] send ID: admin")
        print ("  [-] send pw: " + PW)
        r.recvuntil("here is your flag\n")
        print ("[*] flag :: " + str(r.recv())[2:-5])
        # r.recv()[2:-5] = remove b' and \n\n'
        r.close()
        exit()
cs

'Wargame > pwnable.kr' 카테고리의 다른 글

[Grotesque] maze  (2) 2020.10.12
[Grotesque] aeg  (2) 2020.10.03
[Rookies] note  (0) 2020.09.10
[Toddler] fd  (0) 2020.09.06
[Grotesque] asg  (0) 2020.08.15