W3Challs - ProtectionSystem Ⅱ (Reversing, 14pts)
이번 문제는 이전에 풀었던 ProtectionSystem Ⅰ과 유사한 키젠 문제이다. 다만 이전 문제와 달리 password 뿐만 아니라 login값까지 받는 것이 실행했을 때 보이는 가장 큰 차이점이다. 아마 고정된 password를 가졌던 이전 문제와는 다르게 login값에 따라 password가 달라지는 형태를 가지는 것으로 보인다.
W3Challs에서 가면, 아래 [그림 2]와 같은 화면을 볼 수 있는데 조건에 맞는 user/password를 입력해야 플래그를 주는 것으로 보아 거의 확실하다.
아무튼 바로 기드라로 넘어가서 분석을 시작했다.
이번에도 Defined Strings를 이용해 메인 함수를 찾았다. 확실히 지금까지 풀었던 문제들과는 달리 함수가 많고 복잡하다. 하지만 다행히 이해하기 어려운 것들은 많지 않아서 어느정도 흐름을 파악할 수 있었다.
역시나 예상대로 login값에 따라 password가 달라지는 모습을 보인다. 대충 요약하자면 login과 password에 대해서 여러 조건이 달리고, 그 조건들을 모두 만족해야만 로그인에 성공한다.
그 조건들은 아래와 같다.
- login과 password의 길이는 모두 5-8자여야 함. (단, 서로 길이가 동일할 필요는 없음)
- login과 password의 ASCII Code의 합이 서로 동일해야 함. 예를 들어, login이 ‘user’일 경우 login의 ASCII Code의 합은 447(0x1BF)이 된다. 이 때 올바른 password를 구성하려면 password를 구성하는 문자들의 ASCII Code의 합 또한 447이 되어야 한다.
- 각 문자열에 쓰인 문자는 서로 달라야하며, login과 password 간 겹치는 문자가 있으면 안 된다. (login/password를 구성할 때 한 번 쓰인 문자는 다시 쓸 수 없다)
- 마지막으로 아래 [그림 4]의 코드에서 실행하는 연산 결과가 서로 일치해야 한다.
조건들을 하나씩 살펴보면 그다지 어려운 조건들은 아님을 알 수 있다. 다만 그 수가 적지 않다보니 시간이 많이 소요되었다. 기드라의 디컴파일링이 제대로 되지 않아서 수도 코드로 볼 수가 없어 어셈블리로 꾸역꾸역 따라가느라 고생 좀 했다…
[그림 4]의 경우, 단순히 설명하자면 문자열에서 한 글자씩 가져와 해당 글자의 index에 따라 연산을 하는데, 같은 index에서 login은 더하기(+) 연산을 한다면 password는 빼기(-) 연산을 하고, 그 다음 index에서는 빼기와 더하기를 하는 방식으로 서로 반대로 연산을 한다. 이 때 서로 같은 결과값을 내는 문자열이 요구된다.
위 [그림 5]는 가장 마지막으로 문자열을 검증하는 루틴인데, 이 역시 문자열에서 한 글자씩 가져와 해당 글자의 index에 따르며 login을 제외하고 password만 확인한다. 크게 3가지 방식으로 이뤄지며 그 방식은 아래와 같다.
- index가 0이 아닌 짝수일 경우 - password[index + 2] > password[index] 여야 한다.
- index가 홀수인 경우 - password[index + 2] < password[index] 여야 한다.
- 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)))
참고로 고정된 값이 아닌 여러 값이 나올 수 있지만 해당 값들이 모두 정답이니 그냥 입력하면 된다. 문제 프로그램에서 해당 값을 입력하면 아래 [그림 7]과 같이 로그인에 성공했다는 메시지를 볼 수 있다.
이제 W3Challs에 가서 똑같이 로그인을 하고, 플래그를 얻는 일만 남았다.
크게 놀라울 것은 아니지만 상당히 무미건조하게 플래그만 띄워준다. 아무튼 플래그를 복붙하면 14점을 얻을 수 있다!
여담으로… Keygen을 만들 때, 가능하다면 고정된 login 이름이 아닌 보다 다양한 조건에서 동작하게끔 만들고 싶었지만… 그렇게 만드려면 적지 않은 시간이 필요할 것 같아 일단은 플래그를 얻을 수 있는 정도에서만 갈음하려 한다.