2021.01.07


W3Challs - ProtectionSystem Ⅱ (Reversing, 14pts)


[그림 1] 프로그램 실행 화면


이번 문제는 이전에 풀었던 ProtectionSystem Ⅰ과 유사한 키젠 문제이다. 다만 이전 문제와 달리 password 뿐만 아니라 login값까지 받는 것이 실행했을 때 보이는 가장 큰 차이점이다. 아마 고정된 password를 가졌던 이전 문제와는 다르게 login값에 따라 password가 달라지는 형태를 가지는 것으로 보인다.

W3Challs에서 가면, 아래 [그림 2]와 같은 화면을 볼 수 있는데 조건에 맞는 user/password를 입력해야 플래그를 주는 것으로 보아 거의 확실하다.

[그림 2] how to get the flag


아무튼 바로 기드라로 넘어가서 분석을 시작했다.

[그림 3] Main 함수 중 일부


이번에도 Defined Strings를 이용해 메인 함수를 찾았다. 확실히 지금까지 풀었던 문제들과는 달리 함수가 많고 복잡하다. 하지만 다행히 이해하기 어려운 것들은 많지 않아서 어느정도 흐름을 파악할 수 있었다.

역시나 예상대로 login값에 따라 password가 달라지는 모습을 보인다. 대충 요약하자면 login과 password에 대해서 여러 조건이 달리고, 그 조건들을 모두 만족해야만 로그인에 성공한다.

그 조건들은 아래와 같다.

  1. login과 password의 길이는 모두 5-8자여야 함. (단, 서로 길이가 동일할 필요는 없음)
  2. login과 password의 ASCII Code의 합이 서로 동일해야 함. 예를 들어, login이 ‘user’일 경우 login의 ASCII Code의 합은 447(0x1BF)이 된다. 이 때 올바른 password를 구성하려면 password를 구성하는 문자들의 ASCII Code의 합 또한 447이 되어야 한다.
  3. 각 문자열에 쓰인 문자는 서로 달라야하며, login과 password 간 겹치는 문자가 있으면 안 된다. (login/password를 구성할 때 한 번 쓰인 문자는 다시 쓸 수 없다)
  4. 마지막으로 아래 [그림 4]의 코드에서 실행하는 연산 결과가 서로 일치해야 한다.

[그림 4] login/password 검증 루틴 중 일부


조건들을 하나씩 살펴보면 그다지 어려운 조건들은 아님을 알 수 있다. 다만 그 수가 적지 않다보니 시간이 많이 소요되었다. 기드라의 디컴파일링이 제대로 되지 않아서 수도 코드로 볼 수가 없어 어셈블리로 꾸역꾸역 따라가느라 고생 좀 했다…

[그림 4]의 경우, 단순히 설명하자면 문자열에서 한 글자씩 가져와 해당 글자의 index에 따라 연산을 하는데, 같은 index에서 login은 더하기(+) 연산을 한다면 password는 빼기(-) 연산을 하고, 그 다음 index에서는 빼기와 더하기를 하는 방식으로 서로 반대로 연산을 한다. 이 때 서로 같은 결과값을 내는 문자열이 요구된다.

[그림 5] password 검증 루틴 중 일부


위 [그림 5]는 가장 마지막으로 문자열을 검증하는 루틴인데, 이 역시 문자열에서 한 글자씩 가져와 해당 글자의 index에 따르며 login을 제외하고 password만 확인한다. 크게 3가지 방식으로 이뤄지며 그 방식은 아래와 같다.

  1. index가 0이 아닌 짝수일 경우 - password[index + 2] > password[index] 여야 한다.
  2. index가 홀수인 경우 - password[index + 2] < password[index] 여야 한다.
  3. password[0] > password[-1:] 여야 한다.

이러한 조건들을 모두 만족하는 문자열을 생성해야만 로그인에 성공하고 플래그를 얻을 수 있다. 조건들을 일일이 다 확인하고 키 생성 코드 짠다고 시간 꽤나 잡아먹혔지만 어쨌든 완성은 했다. 아래 코드를 실행하면 login 이름이 ‘usera’일 때의 password를 얻을 수 있다.

import string
import random

def create_str(login):
    while True:
        ret = ''
        for i in range(len(login)):
            ret += random.choice(string.ascii_lowercase)

        if ascii_sum(ret) == ascii_sum(login) and verify_password(ret) == verify_login(login) and check_same_value(login, ret) == True and verify_password2(ret) == True: break

    return ret

def check_same_value(string_a, string_b):
    for a in string_a:
        if a in string_b: return False

    return True

def ascii_sum(string):
    result = 0
    for s in string:
        result += ord(s)

    return result

def verify_password(password):
    ret = ord(password[0])
    for i in range(len(password) - 1):
        if i % 2 == 0:
            ret = ret - ord(password[i + 1])

        else: # i % 2 == 1
            ret = ret + ord(password[i + 1])

    return ret

def verify_password2(password):
    len_p = len(password)
    for i in range(1, len(password) - 2):
        if i % 2 == 1: # s[i + 2] < s[i]
            if ord(password[i + 2]) >= ord(password[i]): return False

        else: # (2, 4) s[i + 2] > s[i]
            if ord(password[i + 2]) <= ord(password[i]): return False
        
    return ord(password[0]) > ord(password[-1:]) # s[0] > s[-1:]
    

def verify_login(login):
    ret = ord(login[0])
    for j in range(len(login) - 1):
        if j % 2 == 1:
            ret = ret - ord(login[j + 1])

        else:
            ret = ret + ord(login[j + 1])

    return ret

login = 'usera'

print("login : %s / password : %s" % (login, create_str(login)))

[그림 6] Keygen 실행 결과


참고로 고정된 값이 아닌 여러 값이 나올 수 있지만 해당 값들이 모두 정답이니 그냥 입력하면 된다. 문제 프로그램에서 해당 값을 입력하면 아래 [그림 7]과 같이 로그인에 성공했다는 메시지를 볼 수 있다.

[그림 7] Good password and login


이제 W3Challs에 가서 똑같이 로그인을 하고, 플래그를 얻는 일만 남았다.

[그림 8] Flag


크게 놀라울 것은 아니지만 상당히 무미건조하게 플래그만 띄워준다. 아무튼 플래그를 복붙하면 14점을 얻을 수 있다!

[그림 9] 풀이 완료!


여담으로… Keygen을 만들 때, 가능하다면 고정된 login 이름이 아닌 보다 다양한 조건에서 동작하게끔 만들고 싶었지만… 그렇게 만드려면 적지 않은 시간이 필요할 것 같아 일단은 플래그를 얻을 수 있는 정도에서만 갈음하려 한다.