문제 설명
문제 풀이(x64dbg)
문제 파일을 x64dbg에서 열고 '모든 문자열 참조 검색' 기능을 통해 main 메소드를 찾은 결과는 다음과 같다.
이전 단계의 rev-basic 문제를 풀었다면 어디가 문자열 비교 함수인지 쉽게 알 수 있을 것이다.
140001000 주소로 들어가보자.
대충 어디서부터 어디까지가 문자열에 대한 연산을 수행하는지 보일 것이다.
140001024부터 140001053까지만 봐도 충분할 것 같다.
한줄씩 분석해보자.
1. movsxd rax,dword ptr ss:[rsp] : 스택의 값을 부호 확장하여 rax에 저장(rax는 1, 현재 루프 인덱스)
2. mov rcx,qword ptr ss:[rsp+20] : 스택의 [rsp + 20]에서 값을 rcx로 이동(사용자 입력의 시작 주소)
3. movzx eax,byte ptr ds:[rcx+rax] : rcx + rax에서 바이트를 읽어 eax에 제로 확장하여 저장(사용자 입력 문자 배열을 a라고 선언 할 때, a[i]의 값이라고 할수있음)
4. mov ecx,dword ptr ss:[rsp] : 스택의 [rsp]에서 값을 읽어 ecx에 저장
5. inc ecx : ecx++(루프 인덱스 증가)
6. movsxd rcx,ecx : ecx의 값을 부호 확장하여 rcx에 저장
7. mov rdx,qword ptr ss:[rsp+20] : 스택의 [rsp + 20]에서 값을 읽어 rdx에 저장
8. movzx ecx,byte ptr ds:[rdx+rcx] : rdx + rcx에서 바이트를 읽어 ecx에 제로 확장하여 저장(a[i + 1]라고 볼 수 있음)
9. add eax,ecx : eax와 ecx의 값을 더하여 eax에 저장(a[i] + a[i + 1]이라고 볼 수 있음)
10. movsxd rcx,dword ptr ss:[rsp] : 스택의 [rsp]에서 값을 부호 확장하여 rcx에 저장
11. lea rdx,qword ptr ds:[140003000] : 주소 0x140003000의 값을 rdx에 로드
12. movzx ecx,byte ptr ds:[rdx+rcx] : rdx + rcx에서 바이트를 읽어 ecx에 제로 확장하여 저장
13. cmp eax,ecx : eax와 ecx 비교(a[i] + a[i + 1]과 140003000[i]를 비교)
사용자 입력을 배열로 나타낸 것을 a라 하고 기존에 저장된 문자열을 byte_140003000이라고 할 때, 다음과 같은 일반항이 구해진다.
a[i] + a[i + 1] = byte_140003000[i]
그리고 이 식은 다음과 같이 변형될 수 있다.
a[i + 1] = byte_140003000[i] - a[i]
우리는 dump창을 통해 byte_140003000가 어떤 HEX값을 가지는지 알 수 있다. 하지만 a[i]는 어떻게 구할 수 있을까? 그냥 우리가 정해주면 된다. a는 결국에 FLAG 값이 될 것이고 문자열의 범위를 넘어서지 않을 것이다.
따라서 a[i]의 값을 임의로 지정해줄때 아스키코드 범위 내에서만 해주면 된다.
나는 그냥 a~Z까지가 그 범위라 생각하고 풀었다.
이건 손수 노가다로 풀기에 너무 오랜시간이 걸리기 때문에 파이썬 코드를 작성하여 풀었다. 내가 작성한 코드는 다음과 같다.
# byte_140003000 = [
# AD, D8, CB, CB, 9D, 97, CB, C4, 92,
# A1, D2, D7, D2, D6, A8, A5, DC, C7,
# AD, A3, A1, 98, 4C
# ]
byte_140003000 = [
173, 216, 203, 203, 157, 151, 203, 196, 146,
161, 210, 215, 210, 214, 168, 165, 220, 199,
173, 163, 161, 152, 76
]
sample = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
]
for s in sample:
temp = [ord(s)]
valid = True
for i in range(23):
next_value = byte_140003000[i] - temp[i]
if not (0 <= next_value <= 255):
valid = False
break
temp.append(next_value)
if valid:
result = "".join(chr(x) for x in temp)
print(result)
코드를 작성하며 적어도 문자열 하나는 제대로 나오겠지 했던 것 같다.
실행을 해보니 단 한 개의 문자열이 나왔다. 그리고 딱봐도 FLAG가 되기에 충분(?)해보였다.
FLAG :
DH{All_l1fe_3nds_w1th_NULL}
문제 풀이(IDA)
문제 파일을 IDA로 열어보자.
디컴파일 시키고 바로 문자열 비교 함수 내부로 들어가보았다.
__int64 __fastcall sub_140001000(__int64 a1)
{
int i; // [rsp+0h] [rbp-18h]
for ( i = 0; (unsigned __int64)i < 0x18; ++i )
{
if ( *(unsigned __int8 *)(a1 + i + 1) + *(unsigned __int8 *)(a1 + i) != byte_140003000[i] )
return 0LL;
}
return 1LL;
}
이미 x64dbg로 풀 때 함수 내부 구조를 확인해보았기 때문에 바로 이해가 되었다. 그리고 내가 작성했던 코드와 크게 다르지 않았다.
여기서 우리는 다음과 같은 일반항을 얻을 수 있다.
*(a1 + i + 1) + *(a1 + i) = byte_140003000[i]
그리고 다음과 같이 변형 될 수 있다.
*(a1 + i + 1) = byte_140003000[i] - *(a1 + i)
그 뒤의 풀이는 똑같이 코드를 작성하고 실행시켜보는 과정이기 때문에 생략하려고 한다.
왜 IDA로 풀기전에 x64dbg로 분석해보라는지 알 것 같다...
'Security > Reverse Engineering' 카테고리의 다른 글
rev-basic-8 풀이 (0) | 2025.03.05 |
---|---|
rev-basic-7 풀이 (0) | 2025.02.28 |
rev-basic-4 풀이 (0) | 2025.02.24 |
rev-basic-6 풀이 (0) | 2025.02.23 |
rev-basic-3 풀이(IDA) (0) | 2025.02.20 |