Fascination
article thumbnail

Reversing Engineering - 6. 쉬운 crackme를 통한 디버거 사용법 - 1


# 프롤로그

- 문제는 2개의 정수를 입력받아 값이 특정 조건을 만족하면 correct!을 출력하고 틀리면 wrong!을 출력함

 

 

# 문제 풀이 - 0

> 주어진 문제를 실행하면 input:  이라는 문자열이 출력됨

> 숫자 2개를 입력하면  wrong! 이라는 문자열을 출력함

- 추측

  > main함수에서 printf, puts 등의 출력 함수 및 scanf와 같이 입력을 받는 함수가 포함되었을 것임

  > wrong!이 출력되기 전 입력받은 숫자 2개를 처리하는 부분이 있을 것임

 

 

# 문제 풀이 - 1

- 다양한 방법으로 찾을 수 있지만 Az 버튼을 사용하여 input:  문자열을 찾아 main 함수를 발견

  > 처음 출력되는 input:  문자열로부터 입력받을 문자열의 형식을 지정하는 것으로 보이는 %d %d,

     정답 여부를 출력할 때 쓰는 것으로 보이는 correct!wrong!도 보임

 

 

# 문제 풀이 - 2

1. stack을 확장하는 코드. 이 함수에서는 0x38만큼 스택을 사용한다는 것을 알 수 있음

1|140001200 | sub rsp,38                         |

 

2. 첫 번째 인자에 input:  문자열의 주소를 넣고 sub_140001070 함수를 호출

2|140001204 | lea rcx,qword ptr ds:[140002230]   | 140002230:"input: "
2|14000120B | call <easy-crackme1.sub_140001070> |

* 내가 분석할 대상의 중요한 부분이 아니라면 적당히 추측하고 넘어가는 것이 시간을 단축하는데 있어 중요한 요소

 

3. 첫 번째 인자에 %d %d 문자열의 주소를 넣고, 두 번째 인자에 rsp+0x24, 세 번째 rsp+0x20을 넣고 sub_140001120 을 호출

3|140001210 | lea r8,qword ptr ss:[rsp+20]       |
3|140001215 | lea rdx,qword ptr ss:[rsp+24]      | rdx:EntryPoint
3|14000121A | lea rcx,qword ptr ds:[140002238]   | 140002238:"%d %d"
3|140001221 | call <easy-crackme1.sub_140001120> |

> 첫 번재 인자와 함수 호출 시점을 미뤄봤을 때 sub_140001120scanf 라는 것을 추측할 수 있음

> rsp+0x24rsp+0x20에는 각각 입력한 첫번째 숫자와 두번째 숫자가 4바이트 정수형으로 들어가는 것을 알 수 있음

 

4. 첫 번째 인자에 rsp+0x24, 두 번째 인자에 rsp+0x20을 넣고 sub_140001180을 호출

4|140001226 | mov edx,dword ptr ss:[rsp+20]      |
4|14000122A | mov ecx,dword ptr ss:[rsp+24]      |
4|14000122E | call <easy-crackme1.sub_140001180> |

 > 즉, 입력받은 두 숫자를 인자로 받음

 

5. sub_140001180 함수의 리턴값인 eax를 확인해 0이면 점프를 뛰어 wrong! 이 출력되고 1이면 점프를 안뛰어 correct!을 출력

5|140001233 | test eax,eax                       |
5|140001235 | je easy-crackme1.140001246         |
5|140001237 | lea rcx,qword ptr ds:[140002240]   | 140002240:"correct!"
5|14000123E | call qword ptr ds:[<&puts>]        |
5|140001244 | jmp easy-crackme1.140001253        |
5|140001246 | lea rcx,qword ptr ds:[14000224C]   | 14000224C:"wrong!"
5|14000124D | call qword ptr ds:[<&puts>]        |

> 이를 통해 sub_140001180 함수가 입력받은 숫자를 검사하는 함수라는 것을 확실하게 알 수 있음

 

6. main 함수의 리턴값을 0으로 설정하고 확장한 스택을 정리한 후 리턴

6|140001253 | xor eax,eax                        |
6|140001255 | add rsp,38                         |
6|140001259 | ret                                |

 

 

→ main 함수 분석 결과 입력받은 값을 처리하는 부분은 sub_140001180 이라는 것을 알 수 있었음

 

 

# 문제 풀이 - 3, sub_140001180 분석

- 주소를 기준으로 어셈블리어를 보여주는 형태는 이 함수처럼 분기문이 많은 함수를 보여주기에 적합하지 않음

- 그래프를 사용해서 보면 직관적으로 함수를 살펴볼 수 있음

  > 그래프로 보고 싶은 함수를 골라 오른쪽 클릭을 눌렀을 때 나오는 메뉴에서 그래프를 선택하거나 g를 누르면 됨

* 그래프 창이 이상하게 보이거나 함수의 시작 지점이 잘못된 부분이라면 모듈 분석이 안되었을 가능성이 높음

  > 모듈 분석을 다시해주고 나서 그래프 창을 보면 정상적으로 보임

 

 

# 문제 풀이 - 4, sub_14001180 분석

- 그래프는 노드(코드 부분)와 엣지(선)로 이루어져 있는데, 엣지의 색에 의미가 있음

  > 초록색: jcc 명령어에서 분기를 취했을 때 가는 노드

  > 빨간색: jcc 명령어에서 분기를 취하지 않았을 때 가는 노드

  > 파란색: 항상 분기를 취하는 노드

- 1번 노드(시작 부분)

  > 인자로 받은 eax(첫 번째 인자)와 edx(두 번째 인자)를 각각 rsp+0x8rsp+0x10에 저장

  > 이후 sub rsp, 0x18 명령어 때문에 이후 rsp를 통해 저장된 인자에 접근할 때는

     rsp+0x8이 아닌 rsp+0x20, rsp+0x10이 아닌 rsp+0x28 로 접근하게 됨 

- 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이 리턴되며 메인함수에서 correct!가 출력되게 만들 수 있음

 

 

# 문제 풀이 - 5, sub_140001180 분석

1번 노드 → 2번 노드

cmp dword ptr ss:[rsp+20],2000 ; rsp+0x20(첫번째 인자)과 0x2000을 비교한다
ja easy-crackme1.1400011A0 ; Jump short if above

- cmp 명령어 후 분기하는 것을 확인할 수 있음

- 2번 노드로 가기 위해서는 점프를 뛰지 말아야 하고(빨간색 선) ja 명령어이므로 rsp+0x20(첫번째 인자)가 0x2000보다 작거나 같아야 함

 

2번 노드 → 3번 노드

cmp dword ptr ss:[rsp+28],2000 ; rsp+0x28(두번째 인자)과 0x2000을 비교한다
jbe easy-crackme1.1400011A4 ; Jump short if below or equal

- 3번 노드로 가기 위해서는 점프를 뛰어야 하고(초록색 선) jbe 명령어 이므로 rsp+0x28(두 번째 인자)가 0x2000보다 작거나 같아야 함

 

 

# 문제 풀이 - 6, sub140001180 분석

3번 노드 → 4번 노드

- 어셈블리

mov eax,dword ptr ss:[rsp+20] ; eax = 첫 번째 인자
imul eax,dword ptr ss:[rsp+28] ; eax = eax * 두 번째 인자
mov dword ptr ss:[rsp],eax ; [rsp] = eax
xor edx,edx ; edx = 0
mov eax,dword ptr ss:[rsp+20] ; eax = 첫 번째 인자
div dword ptr ss:[rsp+28] ; eax = edx:eax / 두 번째 인자
mov dword ptr ss:[rsp+4],eax ; [rsp+4] = eax
mov eax,dword ptr ss:[rsp+28] ; eax = 두 번째 인자
mov ecx,dword ptr ss:[rsp+20] ; ecx = 첫 번째 인자
xor ecx,eax ; ecx = ecx ^ eax
mov eax,ecx ; eax = ecx
mov dword ptr ss:[rsp+8],eax ; [rsp+8] = eax
cmp dword ptr ss:[rsp],6AE9BC ; [rsp]와 0x6ae9bc을 비교한다.
jne easy-crackme1.1400011F1 ; Jump short if not equal

- 어셈블리를 빼고 정리

eax = 첫 번째 인자
eax = eax * 두 번째 인자
[rsp] = eax
edx = 0
eax = 첫 번째 인자
eax = edx:eax / 두 번째 인자
[rsp+4] = eax
eax = 두 번째 인자
ecx = 첫 번째 인자
ecx = ecx ^ eax
eax = ecx
[rsp+8] = eax
# [rsp]와 0x6ae9bc을 비교한다.

- 더 간단히 정리

[rsp] = 첫 번째 인자 * 두 번째 인자
[rsp+4] = 첫 번째 인자 / 두 번째 인자
[rsp+8] = 첫 번째 인자 ^ 두 번째 인자
# [rsp]와 0x6ae9bc을 비교한다.

- 결론: 3번 노드에서 4번 노드로 가기 위해서는 명령어가 jne이고 빨간색 선이므로 첫번째 인자 * 두번째 인자가 0x6ae9bc 이여야 함

 

 

# 문제 풀이 - 7, sub_140001180 분석

4번 노드 → 5번 노드

cmp dword ptr ss:[rsp+4],4 ; [rsp+4]와 4를 비교한다
jne easy-crackme1.1400011F1 ; Jump near if not equal

- 3번 노드에서 설정한 [rsp+4]가 4인지 비교하고 점프함

- 5번 노드로 가려면 명령어가 jne이고 빨간 선이므로 [rsp+4]가 4여야함

 

5번 노드 → 7번 노드

cmp dword ptr ss:[rsp+8],12FC ; [rsp+8]과 0x12fc를 비교한다
jne easy-crackme1.1400011F1 ; Jump near if not equal

- 3번 노드에서 설정한 [rsp+8]가 0x12c인지 비교하고 점프

- 7번 노드로 가려면 명령어가 jne이고 빨간선이므로 [rsp+8]이 0x12c이여야 함

 

 

# 문제 풀이 - 8, solve.py 작성

- 지금까지 구한 조건

  > 첫 번째 인자가 0x2000보다 작거나 같아야 한다

  > 두 번째 인자가 0x2000보다 작거나 같아야 한다

  > 첫 번째 인자 * 두 번째 인자가 0x6ae9bc 여야 한다

  > 첫 번째 인자 / 두 번째 인자가 4여야 한다

  > 첫 번째 인자 ^ 두 번째 인자가 0x12fc여야 한다

- solve.py

# $ python solve.py 
# answer: 5678 1234
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)

- 실행 결과

 

 

# 문제 풀이 - 9, solve.py 개선

- 문제 풀이 - 8에서 만든 solve.py는 실행해보면 답이 나오는데 시간이 좀 걸림

  > 모든 경우의 수를 하나씩 체크해야하는 방식으로 답을 찾기 때문

  > xor의 특이한 성질을 가지고 이를 개선할 수 있음

A^B^B == A

A^A == 0

A^B == C 일 때, C^A == B 이고 C^B == A이다

- 문제에서 첫 번째 인자 ^ 두 번째 인자 == 0x12fc 임을 알 수 있었음

  > 두 번째 인자 == 첫 번째 인자 ^ 0x12c 임을 알 수 있음

- solve.py

# $ python solve.py 
# answer: 5678 1234
for x in range(0x2000 + 1):
    y = x ^ 0x12fc
    if x * y != 0x6ae9bc:
        continue
    
    if x // y != 4:
        continue
        
    print('answer:', x, y)
profile

Fascination

@euna-319

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!