본문 바로가기
Reversing

easy-crackme1

by Anatis 2021. 2. 20.

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

댓글