Dreamhack의 강의 문제 중 하나인 easy-crackme1을 풀어 보도록 하자.
프로그램을 실행시켜보면 input:이라는 문자열이 출력되고 숫자 2개를 입력해보니 wrong!이라는 문자열을 출력하는 것을 볼 수 있다.
이는 input:의 출력과 숫자 2개를 입력받고 무언가 받은 숫자를 처리하는 코드를 실행한 다음 결과에 따라 correct나 wrong을 출력한다는 것을 추측해볼 수 있다.
두 번째로는 main함수에서 printf, puts 등의 출력 함수 및 scanf같이 입력을 받는 함수가 포함되었을 것을 추측할 수 있다.
x64 dbg로 프로그램을 열어본 후 모듈 간 호출 찾기 기능을 사용해 앞서 추측한 함수들이 있는지 찾아보면 상단에 puts함수가 보인다.
puts함수를 따라가 보면 input문자열, 입력받을 문자열의 형식을 지정하는 것으로 보이는 % d % d, 정답 여부를 출력할 때 쓰는 것으로 보이는 correct! 와 wrong! 이 보인다. 메인 함수를 분석해보도록 하자.
1|0000000140001200 | 48:83EC 38 | sub rsp,38 |
2|0000000140001204 | 48:8D0D 25100000 | lea rcx,qword ptr ds:[140002230] | 0000000140002230:"input: "
2|000000014000120B | E8 60FEFFFF | call <easy-crackme1.sub_140001070> |
3|0000000140001210 | 4C:8D4424 20 | lea r8,qword ptr ss:[rsp+20] |
3|0000000140001215 | 48:8D5424 24 | lea rdx,qword ptr ss:[rsp+24] |
3|000000014000121A | 48:8D0D 17100000 | lea rcx,qword ptr ds:[140002238] | 0000000140002238:"%d %d"
3|0000000140001221 | E8 FAFEFFFF | call <easy-crackme1.sub_140001120> |
4|0000000140001226 | 8B5424 20 | mov edx,dword ptr ss:[rsp+20] |
4|000000014000122A | 8B4C24 24 | mov ecx,dword ptr ss:[rsp+24] |
4|000000014000122E | E8 4DFFFFFF | call <easy-crackme1.sub_140001180> |
5|0000000140001233 | 85C0 | test eax,eax |
5|0000000140001235 | 74 0F | je easy-crackme1.140001246 |
5|0000000140001237 | 48:8D0D 02100000 | lea rcx,qword ptr ds:[140002240] | 0000000140002240:"correct!"
5|000000014000123E | FF15 440F0000 | call qword ptr ds:[<&puts>] |
5|0000000140001244 | EB 0D | jmp easy-crackme1.140001253 |
5|0000000140001246 | 48:8D0D FF0F0000 | lea rcx,qword ptr ds:[14000224C] | 000000014000224C:"wrong!"
5|000000014000124D | FF15 350F0000 | call qword ptr ds:[<&puts>] |
6|0000000140001253 | 33C0 | xor eax,eax |
6|0000000140001255 | 48:83C4 38 | add rsp,38 |
6|0000000140001259 | C3 | ret |
1. sub rsp, 38은 스택을 확장하는 코드로 0x38만큼 스택을 사용한다.
2. 첫 번째 인자 rcx에 input: 문자열의 주소를 넣고 sub_140001070 함수를 호출한다. 이 함수는 printf나 printf와 비슷한 함수라는 것을 알 수 있기 때문에 내부 함수 분석은 하지 않겠다.
3. 첫 번째 인자 rcx에 %d %d 문자열 주소를 넣고, 두 번째 인자 rdx에 rsp+24, 세 번째 인자에 rsp+20을 넣고 sub_140001120을 호출한다. 이것은 입력받은 두 숫자를 인자로 받고 첫 번째 숫자와 두 번째 숫자가 4바이트 정수형으로 들어간다는 것도 알 수 있다.
4. 첫 번째 인자에 rsp+24, 두 번째 인자에 rsp+20을 넣고 sub_140001180을 호출한다. 즉 입력받은 두 숫자를 인자로 받는다.
5. sub_140001180함수의 리턴 값인 eax를 확인해 0이면 점프를 하여 wrong1 이 출력되고 1이면 점프를 하지 않고 correct! 를 출력한다. 이를 통해 sub_140001180 함수가 입력받은 숫자를 검사하는 함수라는 것을 확실하게 알 수 있다.
6. main함수의 리턴 값을 0으로 설정하고 스택을 정리한 후 리턴한다.
앞서 main함수를 분석해본 결과 입력 값을 처리하는 부분이 sub_140001180이라는 것을 알 수 있다. 해당 함수를 분석해보자.
x64 dbg는 그래프 모드를 지원한다 g버튼을 통해 그래프 모드로 전환해보면 노드(코드 부분)와 에지(선)로 이루어져 있는데, 에지의 색에는 각각의 의미가 있다.
- 초록색 : jcc 명령어에서 분기를 취했을 때 가는 노드
- 빨간색 : jcc 명령어에서 분기를 취하지 않았을 때 가는 노드
- 파란색 : 항상 분기를 취하는 노드
- 1번 노드(시작 부분)
인자로 받은 ecx(첫 번째 인자)를 rsp+8, edx(두 번째 인자)를 rsp+10에 저장한다.
이후 sub rsp, 18을 통해 rsp+8이 아닌 rsp+0x20, rsp+10이 아닌 rsp+28로 접근하게 된다.
- 9번 노드
함수의 끝 노드이다. 확장항 스택을 정리하고 ret을 한다.
- 6, 7, 8번 노드
9번 노드와 연결된 노드들이다. 자세히 보면 이 노드들이 함수의 리턴 값인 eax를 설정하는 것을 볼 수 있다.
6, 8은 eax를 0으로 7번은 eax를 1로 설정한다.
메인 함수에서 sub_140001180이 1을 리턴했을 때 correct! 가 출력된다는 것을 생각했을 때 6, 8번 노드를 지나가면 안 되고 무조건 7번 노드를 지나가야만 된다는 사실을 알 수 있다.
correct! 을 출력하는 함수의 흐름은 1 -> 2 -> 3 -> 4 -> 5 -> 7 -> 9이다.
1번 노드 -> 2번 노드
첫 번째 분기문은 1번 노드이다. 1번 노드 끝 부분을 보면 cmp명령어 후 분기하는 것을 볼 수 있다.
000000014000118C | 817C24 20 00200000 | cmp dword ptr ss:[rsp+20],2000 |
0000000140001194 | 77 0A | ja easy-crackme1.1400011A0 |
2번 노드로 가기 위해서는 점프를 하지 말아야 하고 rsp+0x20(첫 번째 인자)가 2000보다 작거나 같아야 한다.
ja - a > b (Jump short if above)
2번 노드 -> 3번 노드
0000000140001196 | 817C24 28 00200000 | cmp dword ptr ss:[rsp+28],2000 |
000000014000119E | 76 04 | jbe easy-crackme1.1400011A4 |
3번 노드를 가기 위해서는 점프를 해야 하기 때문에 rsp+0x28(두 번째 인자가) 2000보다 작거나 같아야 한다.
jbe - a <= b (Jump short if below or equal)
3번 노드 -> 4번 노드
1|00000001400011A4 | 8B4424 20 | mov eax,dword ptr ss:[rsp+20] |
2|00000001400011A8 | 0FAF4424 28 | imul eax,dword ptr ss:[rsp+28] |
3|00000001400011AD | 890424 | mov dword ptr ss:[rsp],eax |
4|00000001400011B0 | 33D2 | xor edx,edx |
5|00000001400011B2 | 8B4424 20 | mov eax,dword ptr ss:[rsp+20] |
6|00000001400011B6 | F77424 28 | div dword ptr ss:[rsp+28] |
7|00000001400011BA | 894424 04 | mov dword ptr ss:[rsp+4],eax |
8|00000001400011BE | 8B4424 28 | mov eax,dword ptr ss:[rsp+28] |
9|00000001400011C2 | 8B4C24 20 | mov ecx,dword ptr ss:[rsp+20] |
10|00000001400011C6 | 33C8 | xor ecx,eax |
11|00000001400011C8 | 8BC1 | mov eax,ecx |
12|00000001400011CA | 894424 08 | mov dword ptr ss:[rsp+8],eax |
13|00000001400011CE | 813C24 BCE96A00 | cmp dword ptr ss:[rsp],6AE9BC |
14|00000001400011D5 | 75 1A | jne easy-crackme1.1400011F1 |
1. eax = rsp+0x20(첫 번째 인자)
2. eax = eax * rsp+0x28(두 번째 인자)
3. [rsp] = eax
4. edx = 0
5. eax = rsp+0x20(첫 번째 인자)
6. eax = edx:eax / rsp+0x28(두 번째 인자)
7. [rsp+4] = eax
8. eax = rsp+0x28(두 번째 인자)
9. ecx = rsp+0x20(첫 번째 인자)
10. ecx = ecx ^ eax
11. eax = ecx
12. [rsp+0x8] = eax
13. [rsp]와 0x6ae8bc만큼 비교한다.
14. jne - a != b (Jump short if not equal)
위 코드를 정리해보면
1. [rsp] = 첫 번째 인자 * 두 번째 인자
2. [rsp+4] = 첫 번째 인자 / 두 번째 인자
3. [rsp+8] = 첫 번째 인자 ^ 두 번째 인자
4. [rsp] != 0x6ae8bc
3번 노드에서 4번 노드로 가기 위해서는 첫 번째 인자 * 두 번째 인자가 0x6ae8bc이여야 한다.
4번 노드 -> 5번 노드
00000001400011D7 | 837C24 04 04 | cmp dword ptr ss:[rsp+4],4 |
00000001400011DC | 75 13 | jne easy-crackme1.1400011F1 |
[rsp+4]와 4를 비교한다 jne이기 때문에 rsp+4가 4이어야 한다.
5번 노드 -> 7번 노드
00000001400011DE | 817C24 08 FC120000 | cmp dword ptr ss:[rsp+8],12FC |
00000001400011E6 | 75 09 | jne easy-crackme1.1400011F1 |
[rsp+8]과 0x12fc를 비교한다 jne이기 때문에 0x12fc이여야 한다.
이제 지금 까지 구한 조건으로 모든 경우의 수를 탐색하는 코드를 작성해 보면 답을 구할 수 있다.
# easy-crackme1.py
for x in range(0x2000 + 1):
for y in range(0x2000 + 1):
if x * y != 0x6ae9bc:
continue
if x // y != 4:
continue
if x ^ y != 0x12fc:
continue
print("Answer:", x, y)
좀 더 개선된 코드
#easy-crackme1
for x in range(0x2000 + 1):
y = x ^ 0x12fc
if x * y != 0x6ae9bc:
continue
if x // y != 4:
continue
print("Answer:", x, y)
xor은 특이한 성질을 가지고 있는데 이를 수식으로 간단하게 정리하면
A ^ B ^ B == A
A ^ A == 0
A ^ B == C 일 때 C ^ A == B이고 C ^ B == A이다.
위 문제에서 첫 번째 인자 ^ 두 번째 인자 == 0x12fc였으니 0x12fc ^ 첫 번째 인자 == 두 번째 인자라는 것을 알 수 있다.
이는 y = x ^ 0x12fc이다.
첫 번째 인자 5678 두 번째 인자 1234 인 것을 알았으니 프로그램에 입력해보자.
답을 입력해보면 correct! 가 출력되는 것을 볼 수 있다.
'Reversing' 카테고리의 다른 글
Base Relocation Table (0) | 2021.12.30 |
---|---|
PE File Format (0) | 2021.12.15 |
윈도우 기초 (0) | 2021.04.22 |
x64 (0) | 2021.01.29 |
Reverse Engineering (0) | 2021.01.28 |
댓글