Fascination
article thumbnail

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


# 프롤로그

- 문제는 어떤 값을 입력을 받아 특정 조건을 만족하면 correct!을 출력하고 틀리면 wrong!을 출력함

 

 

# 문제 풀이 - 0

- 처음 문제를 실행하면 input: 이라는 문자열이 출력됨

- 문제 지문에서 어떠한 형식으로 값을 입력받는지에 대해 알려주지 않았기 때문에 적당히 아무 문자열을 넣고 enter를 누르면 wrong!이 뜨는 것을 볼 수 있음

- 어떠한 형식으로 입력을 받는지는 모르지만, 문제의 형태가 이전 강의에서 풀이한 문제와 비슷

  > 다음과 같은 방식으로 분석할 수 있을 것임

1. 메인함수를 찾아낸다

2. 입력의 형식이 어떤 형식인지 알아낸다

3. 입력받은 값은 어떤 방식으로 처리하는지 알아낸다

 

 

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

- 문제 분석에 앞서 ctrl + a 또는 마우스 오른쪽 클릭 시 나오는 분석 - 모듈 분석을 통해서 모듈 분석을 해주어야 함

- 문자열 찾기를 통해 메인 함수를 찾을 수 있음

  > 문자열 분석을 실행하면 input: 이라는 문자열이 있는 것을 확인할 수 있음

  > 더블 클릭으로 해당 문자열을 참조하는 부분으로 이동하여 해당 함수를 살펴보면 correct!, wrong!

     의 문자열도 참조하는 부분을 볼 수 있음

 

 

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

1. 스택을 확장함

#1
1400012F0 | push rdi                           |
1400012F2 | sub rsp,30                         |

 

2. malloc(0x10)을 호출하고, 그 결과를 [rsp+0x20]에 저장함. 해당 값을 임시로 buf로 이름짓겠음

#2
1400012F6 | mov ecx,10                         |
1400012FB | call qword ptr ds:[<&malloc>]      |
140001301 | mov qword ptr ss:[rsp+20],rax      |

- 코드로 나타내면 아래와 같음

char* buf = malloc(0x10);

 

3. rdi에 buf 주소를 복사한 뒤, eax를 0으로 설정하고 ecx를 0x10으로 설정한 다음 rep stosb 명령어를 실행

#3
140001306 | mov rdi,qword ptr ss:[rsp+20]      |
14000130B | xor eax,eax                        |
14000130D | mov ecx,10                         |
140001312 | rep stosb                          |

> rep stosb 명령어를 찾아보면 Fill (E)CX bytes at ES:[(E)DI] with AL라고 나옴

> #3의 경우를 생각해보면 buf주소에 0x10 바이트만큼 0으로 채우는 명령어가 됨

> 이를 버퍼를 초기화 할 때 자주 쓰이는 memset 함수가 최적화가 적용된 형태로 기억해두는 것이 좋음

memset(buf, 0, 0x10);

 

4. 인자에 input: 주소를 넣고 sub_140001060를 호출

#4
140001314 | lea rcx,qword ptr ds:[140003250]   | 0000000140003250:"input: "
14000131B | call <easy-crackme2.sub_140001060> |

> 프로그램의 실행결과를 통해 sub_140001060을 분석하지 않아도 문자열을 출력하는 함수라는 것을 알 수 있음

sub_140001060("input: "); // sub_140001060 is function like printf

 

5. _fgetchar를 호출함

#5
140001320 | call qword ptr ds:[<&_fgetchar>]   |

> getchar와 동일한 작동을 하는 함수라는 것을 알 수 있음

> 이때 입력받은 값은 al에 저장되게 되는데 이 값은 140001334에서 사용하게 됨

 

6. 복잡해 보이지만 하나씩 분석해보면 간단함

#6
140001326 | mov ecx,1                          |
14000132B | imul rcx,rcx,0                     |
14000132F | mov rdx,qword ptr ss:[rsp+20]      | rdx:EntryPoint
140001334 | mov byte ptr ds:[rdx+rcx],al       |

- 간단하게 바꿔본 결과

ecx = 1
rcx = rcx * 0
rdx = buf // [rsp+0x20]
[rdx + ecx] = al // _fgetchar() return value

- 더 간단하게 표현한 결과

buf[0] = _fgetchar();

 

7. #6과 전부 비슷한 형태

#7
140001337 | mov eax,1                          |
14000133C | imul rax,rax,1                     | rax:EntryPoint
140001340 | mov rcx,qword ptr ss:[rsp+20]      |
140001345 | mov byte ptr ds:[rcx+rax],64       | 64:'d'
140001349 | mov eax,1                          |
14000134E | imul rax,rax,2                     | rax:EntryPoint
140001352 | mov rcx,qword ptr ss:[rsp+20]      |
140001357 | mov byte ptr ds:[rcx+rax],77       | 77:'w'
14000135B | mov eax,1                          |
140001360 | imul rax,rax,3                     | rax:EntryPoint
140001364 | mov rcx,qword ptr ss:[rsp+20]      |
140001369 | mov byte ptr ds:[rcx+rax],73       | 73:'s'
14000136D | mov eax,1                          |
140001372 | imul rax,rax,4                     | rax:EntryPoint
140001376 | mov rcx,qword ptr ss:[rsp+20]      |
14000137B | mov byte ptr ds:[rcx+rax],71       | 71:'q'
14000137F | mov eax,1                          |
140001384 | imul rax,rax,5                     | rax:EntryPoint
140001388 | mov rcx,qword ptr ss:[rsp+20]      |
14000138D | mov byte ptr ds:[rcx+rax],61       | 61:'a'
140001391 | mov eax,1                          |
140001396 | imul rax,rax,6                     | rax:EntryPoint
14000139A | mov rcx,qword ptr ss:[rsp+20]      |
14000139F | mov byte ptr ds:[rcx+rax],77       | 77:'w'
1400013A3 | mov eax,1                          |
1400013A8 | imul rax,rax,7                     | rax:EntryPoint
1400013AC | mov rcx,qword ptr ss:[rsp+20]      |
1400013B1 | mov byte ptr ds:[rcx+rax],64       | 64:'d'
1400013B5 | mov eax,1                          |
1400013BA | imul rax,rax,8                     | rax:EntryPoint
1400013BE | mov rcx,qword ptr ss:[rsp+20]      |
1400013C3 | mov byte ptr ds:[rcx+rax],75       | 75:'u'

> #6에서는 _fgetchar 함수의 결과를 넣었다면 #7에서는 각각 고정된 값을 넣는다는 것

buf[1] = 'd';
buf[2] = 'w';
buf[3] = 's';
buf[4] = 'q';
buf[5] = 'a';
buf[6] = 'w';
buf[7] = 'd';
buf[8] = 'u';

 

8. 첫 번째 인자에 buf([rsp+0x20])를 넣고 sub_1400011E0를 호출한 다음 그 결과가 0보다 크면 correct!, 0이면 wrong!을 출력함

#8
1400013C7 | mov rcx,qword ptr ss:[rsp+20]      |
1400013CC | call <easy-crackme2.sub_1400011E0> |
1400013D1 | test eax,eax                       |
1400013D3 | je easy-crackme2.1400013E4         |
1400013D5 | lea rcx,qword ptr ds:[140003258]   | 0000000140003258:"correct!"
1400013DC | call qword ptr ds:[<&puts>]        |
1400013E2 | jmp easy-crackme2.1400013F1        |
1400013E4 | lea rcx,qword ptr ds:[140003264]   | 0000000140003264:"wrong!"
1400013EB | call qword ptr ds:[<&puts>]        |

- 코드로 나타내면 아래와 같음

if(sub_1400011E0(buf)) {
    puts("correct!");
}
else {
    puts("wrong!");
}

 

9. 메인 함수의 리턴값을 0으로 설정하고 스택을 정리한 뒤 리턴함

#9
1400013F1 | xor eax,eax                        |
1400013F3 | add rsp,30                         |
1400013F7 | pop rdi                            |
1400013F8 | ret

- 코드로 나타내면 아래와 같음

return 0;

 

 

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

- 분석한 것을 정리하면 아래와 같음

int main() {
	char* buf = malloc(0x10);
	memset(buf, 0, 0x10);
	sub_140001060("input: "); // sub_140001060 is printf like function
	buf[0] = _fgetchar();
	buf[1] = 'd';
	buf[2] = 'w';
	buf[3] = 's';
	buf[4] = 'q';
	buf[5] = 'a';
	buf[6] = 'w';
	buf[7] = 'd';
	buf[8] = 'u';
	if(sub_1400011E0(buf)) {
		puts("correct!");
	}
	else {
		puts("wrong!");
	}
	return 0;
}

> 입력은 한 글자(_fgetchar)만을 받게 되며 그 위에 정해진 문자열(dwsqawdu)을 이어붙인 다음

sub_140011E0를 호출하여 그 결과를 가지고 correct!wrong!을 출력하게 됨

 

 

# 문제 풀이 - 4, sub_1400011E0  분석

- 그래프의 크기 상 디스어셈 결과를 가지고 설명할 것이지만, 그래프 창을 띄워놓고 흐름이 어떤 식으로 진행되는지 살펴보는 것이 좋음

 

 

문제 풀이 - 5, sub_1400011E0 분석

1. 인자로 받은 rcx를 스택에 저장하고 스택을 확장

#1
1400011E0 | mov qword ptr ss:[rsp+8],rcx      |
1400011E5 | sub rsp,48                        |

> 스택에 저장될 때는 [rsp+8]에 저장됐지만, 스택을 확장하면서 rsp의 값이 바뀌기 때문에

확장 이후부터는 [rsp+8]이 아닌 [rsp+0x50]로 접근해야 해당 인자에 접근할 수 있음

> 강의에서는 임의로 인자를 arg1이라 명명함

> 이때 arg1이 메인함수에서 문자열 포인터가 들어왔기 때문에 char* 타입이라는 것을 알 수 있음

int sub_1400011E0(char* arg1) {
...
}

 

2. 변수([rsp+0x24], 4바이트)를 0으로 초기화한 후 8바이트로 부호 있는 확장(movsxd)을 거친 후 arg1의 인덱스로 사용하여 값을 가져옴

#2
1400011E9 | mov dword ptr ss:[rsp+24],0       |
1400011F1 | movsxd rax,dword ptr ss:[rsp+24]  |
1400011F6 | mov rcx,qword ptr ss:[rsp+50]     |
1400011FB | movzx ecx,byte ptr ds:[rcx+rax]   |
1400011FF | call <easy-crackme2.sub_1400010C0>|
140001204 | mov dword ptr ss:[rsp+20],eax     |

> 그 후 가져온 값을 첫 번째 인자로 넣어 sub_1400010C0을 호출한 다음,

sub_1400010C0 함수의 리턴값을 [rsp+0x20]에 저장

> [rsp+0x20]을 tmp1, [rsp+0x24]를 tmp2로 정함

int tmp2 = 0;
int tmp1 = sub_1400010C0(arg1[tmp2]);

 

3. tmp2([rsp+0x24])를 가져와 1을 증가한 후 다시 저장

#3
140001208 | mov eax,dword ptr ss:[rsp+24]     |
14000120C | inc eax                           |
14000120E | mov dword ptr ss:[rsp+24],eax     |

> 이러한 형태의 어셈블리어가 보이면 해당 변수가 반복문에 사용되는 카운터일 가능성이 매우 높음

> 따라서 편의를 위해 tmp2 변수의 이름을 counter로 변경할 것임

int counter = 0;
int tmp1 = sub_1400010C0(arg1[counter]);
counter++;

 

 

# 문제 풀이 - 6, sub_1400011E0

4. counter([rsp+0x24])를 가져와서 arg1([rsp+0x50])의 인덱스로 사용하여 값을 가져온 다음 해당 값이 0인지 확인

#4
140001212 | movsxd rax,dword ptr ss:[rsp+24] |
140001217 | mov rcx,qword ptr ss:[rsp+50]    |
14000121C | movzx eax,byte ptr ds:[rcx+rax]  |
140001220 | test eax,eax                     |
140001222 | je easy-crackme2.1400012CA       |
...

> 만약 0일 경우 1400012CA로 점프를 뛰게 됨

if(arg1[counter] == 0) {
...
}
else {
...
}

 

5. #4에서 arg1[counter]==0 일 때 이 부분으로 오게 됨

#5
1400012CA | cmp dword ptr ss:[rsp+20],5B  | 5B:'['
1400012CF | jne easy-crackme2.1400012DB   |
1400012D1 | mov dword ptr ss:[rsp+30],1   |
1400012D9 | jmp easy-crackme2.1400012E3   |
1400012DB | mov dword ptr ss:[rsp+30],0   |
1400012E3 | mov eax,dword ptr ss:[rsp+30] |
1400012E7 | add rsp,48                    |
1400012EB | ret

> 해당 부분은 tmp1([rsp+0x20])의 값이 0x5b(91)일 경우 1을, 아닐 경우 0을 리턴하는 코드

> 이 문제에서 sub_1400011E0 함수가 1을 리턴해야 메인함수에서 correct!을 출력하기 때문에

tmp1의 값은 0x5b가 되어야 함

if(arg1[counter] == 0) {
    if(tmp1 == 0x5b)
        return 1;
    else
        return 0;
}
else {
...
}

 

 

# 문제 풀이 - 7, sub_1400011E0 분석

6. #4에서 arg1[counter]==0 을 만족하지 않을 때 이 부분으로 오게 됨

#6
140001228 | movsxd rax,dword ptr ss:[rsp+24]   |
14000122D | mov rcx,qword ptr ss:[rsp+50]      |
140001232 | movzx eax,byte ptr ds:[rcx+rax]    |
140001236 | mov dword ptr ss:[rsp+34],eax      |

> counter([rsp+0x24])를 가져와서 arg1([rsp+0x50])의 인덱스로 사용하여 값을 가져온 다음

해당 값을 [rsp+0x34]에 넣음

> 편의를 위해 [rsp+0x34] 값을 tmp3으로 명명함

int tmp3 = arg1[counter];

 

7. counter([rsp+0x24])를 가져와 1 증가한 후 arg1([rsp+0x50])의 인덱스로 사용하여 값을 가져옴

#7
14000123A | mov eax,dword ptr ss:[rsp+24]      |
14000123E | inc eax                            |
140001240 | cdqe                               |
140001242 | mov rcx,qword ptr ss:[rsp+50]      |
140001247 | movzx ecx,byte ptr ds:[rcx+rax]    |
14000124B | call <easy-crackme2.sub_1400010C0> |
140001250 | mov dword ptr ss:[rsp+2C],eax      |

> 이후 해당 값을 첫 번째 인자로 하여 sub_1400010C0을 호출

> 그리고 함수의 결과값을 [rsp+0x2c]에 저장

> 편의를 위해 [rsp+0x2c]를 tmp4로 명명

int tmp4 = sub_1400010C0(arg1[counter + 1]);

 

8. 아까 [rsp+0x34]에 저장했던 값을 [rsp+0x28]에 저장

#8
140001254 | mov eax,dword ptr ss:[rsp+34]      |
140001258 | mov dword ptr ss:[rsp+28],eax      |

> 편의를 위해 [rsp+0x28] 값을 tmp5라고 명명

int tmp5 = tmp3;

 

 

# 문제 풀이 - 8, sub_1400011E0 분석

9. 이 부분은 분기문들의 집합임

14000125C | cmp dword ptr ss:[rsp+28],61   | 61:'a'
140001261 | je easy-crackme2.14000127A     | -> #10
140001263 | cmp dword ptr ss:[rsp+28],64   | 64:'d'
140001268 | je easy-crackme2.14000129E     | -> #12
14000126A | cmp dword ptr ss:[rsp+28],66   | 66:'f'
14000126F | je easy-crackme2.1400012AD     | -> #13
140001271 | cmp dword ptr ss:[rsp+28],73   | 73:'s'
140001276 | je easy-crackme2.14000128C     | -> #11
140001278 | jmp easy-crackme2.1400012BA    | -> #14

> tmp5([rsp+0x28]) 값을 비교하며 다음과 같이 분기함

if(tmp5 == 'a') goto #10;
else if(tmp5 == 'd') goto #12;
else if(tmp5 == 'f') goto #13;
else if(tmp5 == 's') goto #11;
else goto #14;

 

* #10 ~ #13 부분을 살펴보면 전부 각각의 동작을 한 다음 #14로 이동하는 것을 확인할 수 있음

 

10. tmp4([rsp+0x2c])와 tmp1([rsp+0x20]) 값을 가져와 두 값을 더한 후 다시 tmp1([rsp+0x20])에 저장

#10
14000127A | mov eax,dword ptr ss:[rsp+2C]  |
14000127E | mov ecx,dword ptr ss:[rsp+20]  |
140001282 | add ecx,eax                    |
140001284 | mov eax,ecx                    |
140001286 | mov dword ptr ss:[rsp+20],eax  |
14000128A | jmp easy-crackme2.1400012BA    | -> #14

> 이후 #14로 이동함

tmp1 += tmp4;

 

11. tmp4([rsp+0x2c])와 tmp1([rsp+0x20]) 값을 가져와 두 값을 뺀 다음 다시 tmp1([rsp+0x20])에 저장

#11
14000128C | mov eax,dword ptr ss:[rsp+2C]  |
140001290 | mov ecx,dword ptr ss:[rsp+20]  |
140001294 | sub ecx,eax                    |
140001296 | mov eax,ecx                    |
140001298 | mov dword ptr ss:[rsp+20],eax  |
14000129C | jmp easy-crackme2.1400012BA    | -> #14

> 이후 #14로 이동함

tmp1 -= tmp4;

 

12. tmp4([rsp+0x2c])와 tmp1([rsp+0x20]) 값을 가져와 두 값을 곱한 다음 다시 tmp1([rsp+0x20])에 저장

#12
14000129E | mov eax,dword ptr ss:[rsp+20]  |
1400012A2 | imul eax,dword ptr ss:[rsp+2C] |
1400012A7 | mov dword ptr ss:[rsp+20],eax  |
1400012AB | jmp easy-crackme2.1400012BA    | -> #14

> 이후 #14로 다시 이동

tmp1 *= tmp4;

 

13. tmp4([rsp+0x2c])와 tmp1([rsp+0x20]) 값을 가져와 두 값을 나눈 다음 다시 tmp1([rsp+0x20])에 저장

#13
1400012AD | mov eax,dword ptr ss:[rsp+20]  |
1400012B1 | cdq                            |
1400012B2 | idiv dword ptr ss:[rsp+2C]     |
1400012B6 | mov dword ptr ss:[rsp+20],eax  | -> #14

> 이후 #14로 이어짐

tmp1 /= tmp4;

 

14. counter([rsp+0x24])를 가져와 2를 더한 후 다시 counter에 저장

#14
1400012BA | mov eax,dword ptr ss:[rsp+24]  |
1400012BE | add eax,2                      |
1400012C1 | mov dword ptr ss:[rsp+24],eax  |
1400012C5 | jmp easy-crackme2.140001212    | -> #4

> 이후 #4로 점프

counter += 2;
goto #4;

 

 

# 문제 풀이 - 9, sub_1400011E0 분석

- 지금까지 분석한 결과

int sub_1400011E0(char* arg1) {
    int counter = 0;
    int tmp1 = sub_1400010C0(arg1[counter]);
    counter++;
label1: // #4
    if(arg1[counter] == 0) {
        if(tmp1 == 0x5b)
            return 1;
        else
            return 0;
    }
    else {
        int tmp3 = arg1[counter];
        int tmp4 = sub_1400010C0(arg1[counter + 1]);
        int tmp5 = tmp3;
        if(tmp5 == 'a') tmp1 += tmp4; goto label2;
        else if(tmp5 == 'd') tmp1 *= tmp4; goto label2;
        else if(tmp5 == 'f') tmp1 /= tmp4; goto label2;
        else if(tmp5 == 's') tmp1 -= tmp4; goto label2;
        else goto label2;
label2: // #14
        counter += 2;
        goto label1;
    }
}

- 위의 코드를 더 쉽게 정리할 수 있는 부분만 정리하면 다음과 같음

int sub_1400011E0(char* arg1) {
    int counter = 0;
    int tmp1 = sub_1400010C0(arg1[counter]);
    counter++;
label1: // #4
    if(arg1[counter] == 0) {
        return tmp1 == 0x5b;
    }
    else {
        int tmp3 = arg1[counter];
        int tmp4 = sub_1400010C0(arg1[counter + 1]);
        if(tmp3 == 'a') tmp1 += tmp4; goto label2;
        else if(tmp3 == 'd') tmp1 *= tmp4; goto label2;
        else if(tmp3 == 'f') tmp1 /= tmp4; goto label2;
        else if(tmp3 == 's') tmp1 -= tmp4; goto label2;
        else goto label2;
label2: // #14
        counter += 2;
        goto label1;
    }
}

> 9번째 라인부터 20번째 라인까지는 항상 label1로 돌아오게 되는 코드

> 이러한 형태의 코드는 goto 없이 반복문으로 표현 가능

- 다시 정리한 코드

int sub_1400011E0(char* arg1) {
    int counter = 0;
    int tmp1 = sub_1400010C0(arg1[counter]);
    counter++;
    while(1) {
        if(arg1[counter] == 0) {
            return tmp1 == 0x5b;
        }
        int tmp3 = arg1[counter];
        int tmp4 = sub_1400010C0(arg1[counter + 1]);
        if(tmp3 == 'a') tmp1 += tmp4; goto label2;
        else if(tmp3 == 'd') tmp1 *= tmp4; goto label2;
        else if(tmp3 == 'f') tmp1 /= tmp4; goto label2;
        else if(tmp3 == 's') tmp1 -= tmp4; goto label2;
        else goto label2;
label2: // #14
        counter += 2;
    }
}

> 6번째 ~ 8번째 라인의 코드가 while문의 탈출 조건임을 알 수 있음

- 위의 사실을 반영하여 다시 정리한 코드

int sub_1400011E0(char* arg1) {
    int counter = 0;
    int tmp1 = sub_1400010C0(arg1[counter]);
    counter++;
    while(arg1[counter]) {
        int tmp3 = arg1[counter];
        int tmp4 = sub_1400010C0(arg1[counter + 1]);
        if(tmp3 == 'a') tmp1 += tmp4; goto label2;
        else if(tmp3 == 'd') tmp1 *= tmp4; goto label2;
        else if(tmp3 == 'f') tmp1 /= tmp4; goto label2;
        else if(tmp3 == 's') tmp1 -= tmp4; goto label2;
        else goto label2;
label2: // #14
        counter += 2;
    }
    return tmp1 == 0x5b;
}

 

 

# 문제 풀이 - 10, sub_1400011E0 분석

- 이전 코드에서 8~12번째 라인을 살펴보면 항상 label2로 점프하며, 조건문에서 항상 tmp3를 변수로 사용

  > 이는 C언어 문법에서 switch문과 가장 가깝다고 생각할 수 있음

int sub_1400011E0(char* arg1) {
	int counter = 0;
	int tmp1 = sub_1400010C0(arg1[counter]);
	counter++;
	while(arg1[counter]) {
		int tmp3 = arg1[counter];
		int tmp4 = sub_1400010C0(arg1[counter + 1]);
		switch(tmp3){
			case 'a': tmp1 += tmp4; break;
			case 'd': tmp1 *= tmp4; break;
			case 'f': tmp1 /= tmp4; break;
			case 's': tmp1 -= tmp4; break;
		}
		counter += 2;
	}
	return tmp1 == 0x5b;
}

 

 

# 문제 풀이 - 11, sub_1400010C0  분석

1. 인자로 받은 cl을 [rsp+8]에 저장하고 스택을 확장

#1
1400010C0 | mov byte ptr ss:[rsp+8],cl            |
1400010C4 | sub rsp,38                            |

> [rsp+8]에 저장되었지만 스택이 확장되었기 때문에 이후 인자에 접근하려면 [rsp+0x40]으로 접근해야 함

> 편의를 위해 해당 변수를 arg1으로 명명

int sub_1400010C0(char arg1) {
...
}

 

2. [rsp+0x20]을 0으로 초기화하고 arg1([rsp+0x40])을 가져와 [rsp+0x24]에 넣고 다시 해당 값을 가져와 0x65만큼 뺀 다음 다시 [rsp+0x24]에 저장

#2
1400010C8 | mov dword ptr ss:[rsp+20],0           |
1400010D0 | movsx eax,byte ptr ss:[rsp+40]        |
1400010D5 | mov dword ptr ss:[rsp+24],eax         |
1400010D9 | mov eax,dword ptr ss:[rsp+24]         |
1400010DD | sub eax,65                            |
1400010E0 | mov dword ptr ss:[rsp+24],eax         |

> 편의를 위해 [rsp+0x20]를 tmp1, [rsp+0x24]를 tmp2라 명명

int tmp1 = 0;
int tmp2 = arg1;
tmp2 -= 0x65;

 

3. tmp2([rsp+0x24])를 가져와 0x14보다 크면 #4(140001167)로 점프

#3
1400010E4 | cmp dword ptr ss:[rsp+24],14          |
1400010E9 | ja easy-crackme2.140001167            | -> #4

- 코드로 나타내면 아래와 같음

if(tmp2 > 0x14) goto #4;

 

4. 첫 번째 인자로 wrong input!\n 문자열의 주소를 지정한 후 sub_140001060 를 호출한 다음 exit 함수를 호출

#4
140001167 | lea rcx,qword ptr ds:[140003240]      | 140003240:"wrong input!\n"
14000116E | call <easy-crackme2.sub_140001060>    |
140001173 | xor ecx,ecx                           |
140001175 | call qword ptr ds:[<&exit>]           |

> 따라서 이 부분으로 실행 흐름이 오면 안된다는 것을 알 수 있음

sub_140001060("wrong input!\n");
exit(0);

 

5. 살짝 어려울 수 있는 부분

#5
1400010EB | movsxd rax,dword ptr ss:[rsp+24]      | 
1400010F0 | lea rcx,qword ptr ds:[140000000]      |
1400010F7 | mov eax,dword ptr ds:[rcx+rax*4+1184] |
1400010FE | add rax,rcx                           | 
140001101 | jmp rax                               |
...

- 어셈블리어 하나씩 의사 코드로 변경

rax = tmp2 // [rsp+24]
rcx = 0x140000000
eax = [rcx+rax*4+0x1184]
rax += rcx
goto rax

- 다시 정리

goto 0x140000000 + [0x140000000 + tmp2 * 4 + 0x118

 

6. tmp1([rsp+0x20])을 리턴값으로 설정하고 스택을 정리한 후 리턴

#6
14000117B | mov eax,dword ptr ss:[rsp+20]         |
14000117F | add rsp,38                            |
140001183 | ret

- 코드로 나타내면 아래와 같음

return tmp1;

 

 

# 문제 풀이 - 12, sub_1400010C0 분석

goto 0x140000000 + [0x140000000 + tmp2 * 4 + 0x1184]

- 0x140000000의 의미는 언뜻 보면 무엇인지 알 수 없지만 지금까지 분석한 코드가 전부 0x14000...으로 시작하는 걸 통해 분석하고 있는 프로그램의 베이스 주소라는 것을 눈치챌 수 있음

- 코드의 의미

  > 0x140000000 + tmp2 * 4 + 0x1184에서 4바이트 값을 가져와서 다시 0x140000000를 더한 후 점프해라

- tmp2를 0x10이라고 가정

  > 0x140000000 + tmp2 *4 + 0x1184의 값은 0x1400011c4임

1400011C4    3F 11 00 00 67 11 00 00 0D 11 00 00 67 11 00 00  ?...g.......g...

> 앞 4바이트를 리틀엔디안으로 읽어오면 0x113f

> 다시 이 값에 0x140000000을 더하면 0x14000113f임

> 해당 값으로 가보면 다음과 같은 코드가 있음

14000113F | mov dword ptr ss:[rsp+20],7 |
140001147 | jmp easy-crackme2.14000117B | -> #6

> tmp1([rsp+0x20])을 7로 설정한후 #6 부분으로 점프

> #6 부분은 tmp1을 리턴하는 코드

 

→ 정리하자면 goto 0x140000000 + [0x140000000 + tmp2 * 4 + 0x1184] 코드는, 0x140001148에 있는 점프 테이블을 통해 tmp2의 값에 따라 어디론가 점프를 뛰는 코드임

 

 

# 문제 풀이 - 13, sub_1400010C0 분석

- 0x140001184에 있는 점프테이블을 하나씩 따라가 보면 전부 오른쪽과 같은 코드 영역으로 점프를 뛰는 형태임을 알 수 있음

140001103 | mov dword ptr ss:[rsp+20],1 |
14000110B | jmp easy-crackme2.14000117B | -> #6
14000110D | mov dword ptr ss:[rsp+20],2 |
140001115 | jmp easy-crackme2.14000117B | -> #6
140001117 | mov dword ptr ss:[rsp+20],3 |
14000111F | jmp easy-crackme2.14000117B | -> #6
140001121 | mov dword ptr ss:[rsp+20],4 |
140001129 | jmp easy-crackme2.14000117B | -> #6
14000112B | mov dword ptr ss:[rsp+20],5 |
140001133 | jmp easy-crackme2.14000117B | -> #6
140001135 | mov dword ptr ss:[rsp+20],6 |
14000113D | jmp easy-crackme2.14000117B | -> #6
14000113F | mov dword ptr ss:[rsp+20],7 |
140001147 | jmp easy-crackme2.14000117B | -> #6
140001149 | mov dword ptr ss:[rsp+20],8 |
140001151 | jmp easy-crackme2.14000117B | -> #6
140001153 | mov dword ptr ss:[rsp+20],9 |
14000115B | jmp easy-crackme2.14000117B | -> #6
14000115D | mov dword ptr ss:[rsp+20],0 |
140001165 | jmp easy-crackme2.14000117B | -> #6

- 모두 tmp1([rsp+0x20])을 어떤 값으로 설정한 다음 #6으로 점프를 뛰는 코드

- tmp2의 값에 따라 어떤 값이 tmp1에 들어가는지 살펴보면 다음과 같음

tmp2 tmp1
0 3
1 x
2 x
3 x
4 8
5 x
6 x
7 x
8 x
9 x
10 9
11 0
12 1
13 4
14 x
15 5
16 7
17 x
18 2
19 x
20 6

* x: #4(wrong input을 출력하는 코드)로 점프를 뛰는 부분

 

 

# 문제 풀이 - 14, sub_1400010C0 분석

- 지금까지 분석한 sub_1400010C0을 정리하면 다음과 같음

1 int sub_1400010C0(char arg1) {
2 	 int tmp1 = 0;
3	 int tmp2 = arg1;
4	 tmp2 -= 0x65;
5	 if(tmp2 > 0x14) goto fail;
6	 if     (tmp2 ==  0) goto set3;
7	 else if(tmp2 ==  4) goto set8;
8	 else if(tmp2 == 10) goto set9;
9	 else if(tmp2 == 11) goto set0;
10	 else if(tmp2 == 12) goto set1;
11	 else if(tmp2 == 13) goto set4;
12	 else if(tmp2 == 15) goto set5;
13	 else if(tmp2 == 16) goto set7;
14	 else if(tmp2 == 18) goto set2;
15	 else if(tmp2 == 20) goto set6;
16	 else goto fail; // tmp2 == 1, 2, 3, 5, 6, 7, 8, 9, 14, 17, 19
17 set0:
18 	 tmp1 = 0; goto ret;
19 set1:
20	 tmp1 = 1; goto ret;
21 set2:
22	 tmp1 = 2; goto ret;
23 set3:
24 	 tmp1 = 3; goto ret;
25 set4:
26	 tmp1 = 4; goto ret;
27 set5:
28	 tmp1 = 5; goto ret;
29 set6:
30	 tmp1 = 6; goto ret;
31 set7:
32	 tmp1 = 7; goto ret;
33 set8:
34   tmp1 = 8; goto ret;
35 set9:
36	 tmp1 = 9; goto ret;
37 ret:
38	 return tmp1;
39 fail:
40	 sub_140001060("wrong input!\n");
41	 exit(0);
42 }

> 6 ~ 38번째 라인이 점프 테이블 관련 코드

> 특정 값들(0, 4, 10, 11, 12, 13, 15, 16, 18, 20)은 전부 tmp1을 어떤 값으로 설정하게 되고,

나머지 값들은 fail 라벨로 점프를 뛰는 것을 확인할 수 있음

> C언어에서의 switch-case-default 구문과 비슷하다나는 것을 알 수 있음

 

 

# 문제 풀이 - 15, sub_1400010C0 분석

- switch-case-default 구문으로 바꾼 코드

1  int sub_1400010C0(char arg1) {
2     int tmp1 = 0;
3     int tmp2 = arg1;
4     tmp2 -= 0x65;
5     if(tmp2 > 0x14) goto fail;
6     switch(tmp2) {
7          case  0: tmp1 = 3; break;
8          case  4: tmp1 = 8; break;
9          case 10: tmp1 = 9; break;
10         case 11: tmp1 = 0; break;
11         case 12: tmp1 = 1; break;
12         case 13: tmp1 = 4; break;
13         case 15: tmp1 = 5; break;
14         case 16: tmp1 = 7; break;
15         case 18: tmp1 = 2; break;
16         case 20: tmp1 = 6; break;
17         default: goto fail
18     }
19     return tmp1;
20  fail:
21      sub_140001060("wrong input!\n");
22      exit(0);
23  }

> 6번 라인의 조건문을 삭제해도 됨

→ 0x14(20)보다 큰 값이 switch문으로 들어가면 무조건 default구문으로 이동하기 때문

 

- 6번 라인을 삭제한 코드

int sub_1400010C0(char arg1) {
	int tmp1 = 0;
	int tmp2 = arg1;
	tmp2 -= 0x65;
	switch(tmp2) {
		case  0: tmp1 = 3; break;
		case  4: tmp1 = 8; break;
		case 10: tmp1 = 9; break;
		case 11: tmp1 = 0; break;
		case 12: tmp1 = 1; break;
		case 13: tmp1 = 4; break;
		case 15: tmp1 = 5; break;
		case 16: tmp1 = 7; break;
		case 18: tmp1 = 2; break;
		case 20: tmp1 = 6; break;
		default: {
			sub_140001060("wrong input!\n");
			exit(0);
		}
	}
	return tmp1;
}

 

 

# 문제 풀이 - 14, sub_1400010C0 분석

- tmp2의 변수와 0x65를 빼는 부분을 삭제하면 코드가 더 간결해짐

  > 즉, 이 함수는 어떤 문자 한 글자를 받아 다음과 같은 규칙에 따라 숫자를 리턴하는 함수

q 1
w 2
e 3
r 4
t 5
y 6
u 7
i 8
o 9
p 0
int sub_1400010C0(char arg1) {
	int tmp1 = 0;
	switch(arg1) {
		case 0x65: tmp1 = 3; break; // e
		case 0x69: tmp1 = 8; break; // i
		case 0x6f: tmp1 = 9; break; // o
		case 0x70: tmp1 = 0; break; // p
		case 0x71: tmp1 = 1; break; // q
		case 0x72: tmp1 = 4; break; // r
		case 0x74: tmp1 = 5; break; // t
		case 0x75: tmp1 = 7; break; // u
		case 0x77: tmp1 = 2; break; // w
		case 0x79: tmp1 = 6; break; // y
		default: {
			sub_140001060("wrong input!\n");
			exit(0);
		}
	}
	return tmp1;
}

 

 

# 문제 풀이 - 17, 코드 해석

- 지금까지 분석한 내용

int sub_1400010C0(char arg1) {
	int tmp1 = 0;
	switch(arg1) {
		case 0x65: tmp1 = 3; break; // e
		case 0x69: tmp1 = 8; break; // i
		case 0x6f: tmp1 = 9; break; // o
		case 0x70: tmp1 = 0; break; // p
		case 0x71: tmp1 = 1; break; // q
		case 0x72: tmp1 = 4; break; // r
		case 0x74: tmp1 = 5; break; // t
		case 0x75: tmp1 = 7; break; // u
		case 0x77: tmp1 = 2; break; // w
		case 0x79: tmp1 = 6; break; // y
		default: {
			sub_140001060("wrong input!\n");
			exit(0);
		}
	}
	return tmp1;
}
int sub_1400011E0(char* arg1) {
	int counter = 0;
	int tmp1 = sub_1400010C0(arg1[counter]);
	counter++;
	while(arg1[counter]) {
		int tmp3 = arg1[counter];
		int tmp4 = sub_1400010C0(arg1[counter + 1]);
		switch(tmp3){
			case 'a': tmp1 += tmp4; break;
			case 'd': tmp1 *= tmp4; break;
			case 'f': tmp1 /= tmp4; break;
			case 's': tmp1 -= tmp4; break;
		}
		counter += 2;
	}
	return tmp1 == 0x5b;
}
int main(){
	char* buf = malloc(0x10);
	memset(buf, 0, 0x10);
	sub_140001060("input: "); // sub_140001060 is function like printf
	buf[0] = _fgetchar();
	buf[1] = 'd';
	buf[2] = 'w';
	buf[3] = 's';
	buf[4] = 'q';
	buf[5] = 'a';
	buf[6] = 'w';
	buf[7] = 'd';
	buf[8] = 'u';
	if(sub_1400011E0(buf)) {
		puts("correct!");
	}
	else {
		puts("wrong!");
	}
	return 0;
}

> 메인함수에서 사용자로부터 입력을 한 글자 받아 dwsqawdu 문자열과 합친 다음, sub_1400011E0을 호출

> 각 문자열을 숫자와  +*/-로 변환하여 연산을 진행

> 그 다음 연산의 결과가 0x5b(91)과 같다면 correct!을 출력

- 사용자의 입력을 ?로 표시하고 각 글자를 변환

? d w s q a w d u
? * 2 - 1 + 2 * 7

- 위의 계산 결과 값이 91이어야 하고 연산은 사칙연산 규칙을 따르지 않고 무조건 앞에서부터 연산하는 방식이기 때문에 역으로 계산해보면 6이 나오는 것을 알 수 있음

((91 / 7) - 2 + 1) / 2 = 6

> 6을 다시 문자열로 변환하면  y가 됨

> 사용자가 y를 입력하면 모든 조건이 충족되어 correct!가 출력됨

- 실행결과

 

 

# 문제 풀이 - 18, +a

- 정석적인 풀이대로라면 sub_1400011E0을 분석한 것을 바탕으로 어떤 입력을 넣어야 통과할지를 알아내 문제를 풀어야하지만, 한 글자만을 입력받기 때문에 간단한 스크립트를 이용해 0x00부터 0xff까지 256번 프로그램을 실행하면서 한 글자씩 모두 입력해보는 것으로도 문제를 풀 수 있음

- 이와 같이 문제의 의도와는 별개로 일명 '꼼수'를 이용해서 푸는 경우 굉장히 시간을 단축할 수 있어서 어떻게 하면 좀 더 쉽게 문제를 풀 수 있을지 생각하는 것도 좋은 리버싱 실력 향상 방법 중 하나임

profile

Fascination

@euna-319

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