문제

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

문제풀이 - 0

input 에 숫자 2개를 입력하면 wrong!이라는 문자열을 출력하는 것을 확인할 수 있음

  • 추측할 수 있는 사실
  1. 프로그램은 input:을 출력하고, 숫자 2개를 입력받고, 무언가 받은 숫자를 처리하는 코드를 실행한 다음, 그 결과에 따라 correct!wrong!을 출력하는 순서로 실행된다

  2. main함수에서 사용할 법한 함수로는 printf, puts 등의 출력 함수 및 scanf같이 입력을 받는 함수가 포함되었을 것, wrong!이 출력되기 전 부분에 입력받은 숫자 2개를 처리하는 부분이 있을 것이다

문제 풀이 - 1, 메인함수 찾기

  • 모듈 간 호출 찾기기능

-> 호출한 함수들의 리스트 볼 수 있음

  • 메인 함수 확인

    -> 간단하게 참조하는 문자열만 보면 처음 출력되는 input: 문자열부터 입력받을 문자열의 형식을 지정하는 것으로 보이는 %d %d, 정답 여부를 출력할 때 쓰는 것으로 보이는 correct!wrong!도 보임

문제 풀이 - 2, 메인함수 분석

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. 스택을 확장하는 코드입니다. 이 함수에서는 0x38만큼 스택을 사용한다는 걸 알 수 있습니다.
  2. 첫 번째 인자에 input: 문자열의 주소를 넣고 sub_140001070함수를 호출합니다. sub_140001070내부로 들어가서 분석할 수도 있지만, 여기서는 프로그램의 동작과 인자만으로도 printf이거나 printf와 비슷한 함수라는 것을 충분히 알 수 있기 때문에 내부 함수 분석은 넘어가도 됩니다. 이와 같이 내가 분석할 대상의 중요한 부분이 아니라면 적당히 추측하고 넘어가는 것이 분석 시간을 단축하는 데 있어 중요한 요소 중 하나입니다.
  3. 첫 번째 인자에 %d %d문자열의 주소를 넣고, 두 번째 인자에 rsp+0x24, 세 번째 인자에 rsp+0x20을 넣고 sub_140001120를 호출합니다. 첫 번째 인자와 함수 호출 시점을 미뤄봤을 때 sub_140001120scanf라는 것을 추측할 수 있습니다. 그리고 rsp+0x24rsp+0x20에는 각각 입력한 첫번째 숫자와 두번째 숫자가 4바이트 정수형으로 들어간다는 것도 알 수 있습니다.
  4. 첫 번째 인자에 rsp+0x24, 두 번째 인자에 rsp+0x20을 넣고 sub_140001180를 호출합니다. 즉 입력받은 두 숫자를 인자로 받습니다.
  5. sub_140001180함수의 리턴값인 eax를 확인해 0이면 점프를 뛰어 wrong! 이 출력되고 1이면 점프를 안 뛰어 correct!을 출력합니다. 이를 통해 sub_140001180함수가 입력받은 숫자를 검사하는 함수라는 것을 확실하게 알 수 있습니다.
  6. main함수의 리턴값을 0으로 설정하고 확장한 스택을 정리한 후 리턴합니다.

문제 풀이 - 3, sub_140001180 분석

분기문이 많은 함수

  • x64dbg는 그래프로 보기 기능

    -> 그래프로 보고 싶은 함수를 골라 오른쪽 클릭을 눌렀을 때 나오는 메뉴에서 그래프를 선택하거나, 혹은 간단하게 g를 누르면 그래프 창이 뜸

    • 그래프: 노드(코드 부분) + 엣지(선)
      • 엣지 색
        1. 초록색: jcc명령어에서 분기를 취했을 때 가는 노드
        2. 빨간색: jcc명령어에서 분기를 취하지 않았을 때 가는 노드
        3. 파란색: 항상 분기를 취하는 노드

문제 풀이 - 4, sub_140001180 분석

image

  • 1번 노드(시작 부분)

    인자로 받은 ecx(첫 번째 인자)와 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!가 출력되게 만들 수 있습니다

문제 풀이 - 7, sub_140001180 분석

  • 4번 노드 → 5번 노드

    네 번째 분기문은 4번 노드입니다.

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번 노드

    다섯 번째 분기문은 5번 노드입니다.

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

​ 3번 노드에서 설정한 [rsp+8]가 0x12fc인지 비교하고 점프합니다. 7번 노드로 가려면 명령어가 jne이고 빨간 선이니 [rsp+8]이 0x12fc이여야 합니다.

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

  • 구한 조건

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

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

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

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

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

    => 위를 바탕으로 모든 경우의 수를 탐색하는 코드 작성

      
    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)
    

    출처: https://dreamhack.io/