[Reversing][x64dbg] crackme 분석

silver35·2022년 5월 16일
0

Reversing

목록 보기
1/2

프로그램 분석


1. 프로그램을 실행 시키면 input: 을 출력하고 2개의 숫자를 입력받고 그 결과에 따라 wrong! 또는 correct! 출력
2. main 함수에서 사용할 법한 함수로는 printf, puts등의 출력 함수 및 scanf 같이 입력을 받는 함수가 포함되어 있을 것이며 wrong!이 출력되기전에 입력받은 숫자 2개를 처리하는 부분이 있었을 것

main 함수 찾기 - 모듈간 호출 찾기

  1. 함수 호출 보기
  2. puts 함수 발견 및 더블클릭
  3. input 등의 내용이 보이는 것으로 보아 메인함수

main 함수 분석

1|140001200 | sub rsp,38                         |
2|140001204 | lea rcx,qword ptr ds:[140002230]   | 140002230:"input: "
2|14000120B | call <easy-crackme1.sub_140001070> |
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> |
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|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>]        |
6|140001253 | xor eax,eax                        |
6|140001255 | add rsp,38                         |
6|140001259 | ret                                |

1 : stack을 확장하는 코드로 이 함수에서는 0x38만큼 스택 사용을 알 수 있음. rsp는 stack pointer register로 스택은 높은 주소에서 낮은주소로 증가하기 때문에 0x38만큼 뺀 것

2 : input 문자열의 주소값을 rcx 레지스터에 저장 후(첫번째 인자에 input 전달) sub_140001070 함수 호출. lea(load effective address) 명령어는 좌변(레지스터)에 우변의 주소값을 저장

3 : 첫번째 인자에 %d %d문자열 주소를 넣고 두번째 인자에 rsp+0x24, 세번째 인자에 rsp+0x20을 넣고 sub_140001120를 호출. 이는 첫번째 인자를 보았을 때 scanf 함수라는 것을 추측할 수 있으며 rsp+0x24, rsp+0x20은 첫번째 숫자와 두번째 숫자가 4바이트 정수형임을 알 수 있음

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

5: sub_140001180 함수의 리턴값이 0이면 wrong!이 출력되고 아니면 1이면 안뛰어 correct!를 출력

6 : main 함수의 리턴값을 0으로 설정하고 스택을 정리 한후 리턴. xor은 각 비트가 서로다른 값일때 결과가 1이 되고 같은 값이면 0이 됨

결론 : 입력받은 값을 처리하는 부부은 sub_140001180이라는 것을 알 수있음

sub_140001180 함수분석


g를 누르면 그래프로 볼 수 있음

초록색 : jcc(jump is condition is met) 명령어에서 분기를 취했을 때 가는 노드
빨간색 : jcc 명령어에서 분기를 취하지 않았을 때 가는 노드
파란색 : 항상 분기를 취하는 노드

1번 노드 : 첫번째 인자(ecx)와 두번째 인자(edx)를 각각 rsp+0x8, rsp+0x10에 저장함. 그리고 rsp에서 0x18을 빼기 때문에 rsp+0x8이 아닌 rsp+0x20, rsp+0x10이 아닌 rsp+0x28로 접근(16진수 기준

9번 노드 : 확장한 스택을 정리하고 ret

6, 7, 8번 노드 : 9번 노드와 연결된 노드들로 함수의 리턴값인 eax를 설정. 6번과 8번은 eax을 0으로 설정하며 7번은 eax를 1로 설정. 따라서, 무조건 7번 노드를 지나가야만 correct!가 출력. 즉, 1-2-3-4-5-7-9순서로 실행되면 1이 리턴됨

eax 레지스터는 함수의 리턴값이 저장되는 장소이며 이외에도 산술연산 등을 수행할 시에 임시로 값을 저장하는 장소

그러면 1-2-3-4-5-7-9순서로 실행되는 조건에 대해 알아보자

1번 노드 → 2번 노드

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

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 - rsp+0x28인자가 0x2000보다 작거나 같으면 1400011A4로 분기

3번 노드로 가기 위해서는 rsp+0x28(두번째인자)가 0x2000보다 작거나 같아야함

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 = 첫번째 인자 xor 두번째 인자 값 저장
cmp dword ptr ss:[rsp],6AE9BC ; [rsp]와 0x6ae9bc을 비교한다.
jne easy-crackme1.1400011F1 ; Jump short if not equal

imul 명령어는 정수 곱셈을 할 때 사용
div 명령어는 정수 나눗셈 할 때 사용하며 이 코드에서는 eax / 두번째 인자를 나눠서 edx에 몫을 저장
jne 명령어는 두 값이 같지 않으면 점프

요약하자면 아래와 같음

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

결국 3번 노드에서 4번 노드로 가위해서는 rsp값 = 첫 번째 인자 * 두 번째 인자가 0x6ae9bc여야함

4번 노드 → 5번 노드

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

rsp+4가 4인지 비교해 같지 않으면 1400011F1로 점프. 4번 노드에서 5번 노드로 가기 위해 rsp+4 = 첫번째 인자 / 두번째 인자가 4와 같아야함

5번 노드 → 7번 노드

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

rsp+8과 0x12f을 비교해 값이 같지 않으면 1400011F1으로 점프. 5번노드에서 7번 노드로 가기 위해 rsp+8이 = 첫 번째 인자 ^ 두 번째 인자가 0x12fc여야함

solve.py 작성

지금까지 구한 정보를 요약해 보면 아래와 같다.

  1. 첫번째 인자가 0x2000보다 작거나 같아야 함
  2. 두번째 인자가 0x2000보다 작거나 같아야 함
  3. 첫 번째 인자 * 두 번째 인자가 0x6ae9bc
  4. 첫번째 인자 / 두번째 인자가 4
  5. 첫 번째 인자 ^ 두 번째 인자가 0x12fc

x,y값을 구하기 위해 아래와 같은 python 코드를 작성했다.

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)

이렇게 하면 answer : 5678 1234가 출력된다.

코드를 더 효율적으로 짜보기 위해 xor 성질에 대해서 이해해보자

  1. A ^ B ^ B == A
  2. A ^ A = 0
  3. A ^ B = C 일때, C ^ A == B이고 C ^ B == A

문제에서는 첫번째 인자 ^ 두번째 인자 = 0x12fc이므로 두번째인자 = 첫번째인자 ^ 0x12fc라는 것을 알 수 있었다. 이를 기반으로 코드를 고쳐보자

for x in range(0x2000 + 1):
    y = x ^ 0x12fc
    if x * y != 0x6ae9bc:
        continue

    if x // y != 4:
        continue

    print('answer:', x, y)

실행이 더 빨리 되는 것을 알 수 있다.

아래는 오늘 디버깅한 프로그램의 소스코드 이다.

// easy-crackme.cpp
#include <stdio.h>
int check(unsigned int input1, unsigned int input2) {
    unsigned int tmp1;
    unsigned int tmp2;
    unsigned int tmp3;
    if (input1 > 0x2000 || input2 > 0x2000) {
        return 0;
    }
    tmp1 = input1 * input2;
    tmp2 = input1 / input2;
    tmp3 = input1 ^ input2;
    if (tmp1 == 0x6ae9bc && tmp2 == 4 && tmp3 == 0x12fc) {
        return 1;
    }
    else {
        return 0;
    }
}
int main() {
    unsigned int input1;
    unsigned int input2;
    printf("input: ");
    scanf_s("%d %d", &input1, &input2);
    if (check(input1, input2)) {
        puts("correct!");
    }
    else {
        puts("wrong!");
    }
    return 0;
}

분석를 마치며

오늘 dreamhack의 쉬운 crackme를 통한 디버거 사용법 - 1에 대해서 배웠다. 강의를 따라하면서 디버깅은 이렇게 하는구나라는 감(?)을 배운것같다. 실제 상용하는 프로그램은 이것보다 훨씬 복잡한 소스코드로 이뤄져있을 텐데, 더 열심히 해야겠다는 생각이 들었다~! 다음번에는 쉬운 crackme룰 통한 디버거 사용법 - 2를 강의를 따라하지 않고 혼자 분석해볼 생각이다.

참고자료)

로그인 | Dreamhack

0개의 댓글