Fascination
article thumbnail

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


1.0.1. # 프롤로그

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

 

 

1.0.2. # 문제 풀이 - 0

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

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

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

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

1. 메인함수를 찾아낸다

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

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

 

 

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

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

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

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

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

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

 

 

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

1. 스택을 확장함

<c++ />
#1 1400012F0 | push rdi | 1400012F2 | sub rsp,30 |

 

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

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

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

<c++ />
char* buf = malloc(0x10);

 

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

<c++ />
#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 함수가 최적화가 적용된 형태로 기억해두는 것이 좋음

<c++ />
memset(buf, 0, 0x10);

 

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

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

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

<c++ />
sub_140001060("input: "); // sub_140001060 is function like printf

 

5. _fgetchar를 호출함

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

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

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

 

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

<c++ />
#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 |

- 간단하게 바꿔본 결과

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

- 더 간단하게 표현한 결과

<c++ />
buf[0] = _fgetchar();

 

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

<c++ />
#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에서는 각각 고정된 값을 넣는다는 것

<c++ />
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!을 출력함

<c++ />
#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>] |

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

<c++ />
if(sub_1400011E0(buf)) { puts("correct!"); } else { puts("wrong!"); }

 

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

<c++ />
#9 1400013F1 | xor eax,eax | 1400013F3 | add rsp,30 | 1400013F7 | pop rdi | 1400013F8 | ret

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

<c++ />
return 0;

 

 

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

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

<c++ />
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!을 출력하게 됨

 

 

1.0.6. # 문제 풀이 - 4, sub_1400011E0  분석

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

 

 

1.0.7. 문제 풀이 - 5, sub_1400011E0 분석

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

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

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

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

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

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

<c++ />
int sub_1400011E0(char* arg1) { ... }

 

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

<c++ />
#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로 정함

<c++ />
int tmp2 = 0; int tmp1 = sub_1400010C0(arg1[tmp2]);

 

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

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

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

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

<c++ />
int counter = 0; int tmp1 = sub_1400010C0(arg1[counter]); counter++;

 

 

1.0.8. # 문제 풀이 - 6, sub_1400011E0

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

<c++ />
#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로 점프를 뛰게 됨

<c++ />
if(arg1[counter] == 0) { ... } else { ... }

 

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

<c++ />
#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가 되어야 함

<c++ />
if(arg1[counter] == 0) { if(tmp1 == 0x5b) return 1; else return 0; } else { ... }

 

 

1.0.9. # 문제 풀이 - 7, sub_1400011E0 분석

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

<c++ />
#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으로 명명함

<c++ />
int tmp3 = arg1[counter];

 

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

<c++ />
#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로 명명

<c++ />
int tmp4 = sub_1400010C0(arg1[counter + 1]);

 

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

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

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

<c++ />
int tmp5 = tmp3;

 

 

1.0.10. # 문제 풀이 - 8, sub_1400011E0 분석

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

<c++ />
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]) 값을 비교하며 다음과 같이 분기함

<c++ />
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])에 저장

<c++ />
#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로 이동함

<c++ />
tmp1 += tmp4;

 

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

<c++ />
#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로 이동함

<c++ />
tmp1 -= tmp4;

 

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

<c++ />
#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로 다시 이동

<c++ />
tmp1 *= tmp4;

 

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

<c++ />
#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로 이어짐

<c++ />
tmp1 /= tmp4;

 

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

<c++ />
#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로 점프

<c++ />
counter += 2; goto #4;

 

 

1.0.11. # 문제 풀이 - 9, sub_1400011E0 분석

- 지금까지 분석한 결과

<c++ />
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; } }

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

<c++ />
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 없이 반복문으로 표현 가능

- 다시 정리한 코드

<c++ />
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문의 탈출 조건임을 알 수 있음

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

<c++ />
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; }

 

 

1.0.12. # 문제 풀이 - 10, sub_1400011E0 분석

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

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

<c++ />
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; }

 

 

1.0.13. # 문제 풀이 - 11, sub_1400010C0  분석

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

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

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

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

<c++ />
int sub_1400010C0(char arg1) { ... }

 

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

<c++ />
#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라 명명

<c++ />
int tmp1 = 0; int tmp2 = arg1; tmp2 -= 0x65;

 

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

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

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

<c++ />
if(tmp2 > 0x14) goto #4;

 

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

<c++ />
#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>] |

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

<c++ />
sub_140001060("wrong input!\n"); exit(0);

 

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

<c++ />
#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 | ...

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

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

- 다시 정리

<c++ />
goto 0x140000000 + [0x140000000 + tmp2 * 4 + 0x118

 

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

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

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

<c++ />
return tmp1;

 

 

1.0.14. # 문제 풀이 - 12, sub_1400010C0 분석

<c++ />
goto 0x140000000 + [0x140000000 + tmp2 * 4 + 0x1184]

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

- 코드의 의미

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

- tmp2를 0x10이라고 가정

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

<c++ />
1400011C4 3F 11 00 00 67 11 00 00 0D 11 00 00 67 11 00 00 ?...g.......g...

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

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

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

<c++ />
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의 값에 따라 어디론가 점프를 뛰는 코드임

 

 

1.0.15. # 문제 풀이 - 13, sub_1400010C0 분석

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

<c++ />
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을 출력하는 코드)로 점프를 뛰는 부분

 

 

1.0.16. # 문제 풀이 - 14, sub_1400010C0 분석

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

<c++ />
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 구문과 비슷하다나는 것을 알 수 있음

 

 

1.0.17. # 문제 풀이 - 15, sub_1400010C0 분석

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

<c++ />
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번 라인을 삭제한 코드

<c++ />
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; }

 

 

1.0.18. # 문제 풀이 - 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
<c++ />
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; }

 

 

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

- 지금까지 분석한 내용

<c++ />
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!을 출력

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

<c++ />
? d w s q a w d u ? * 2 - 1 + 2 * 7

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

<c++ />
((91 / 7) - 2 + 1) / 2 = 6

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

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

- 실행결과

 

 

1.0.20. # 문제 풀이 - 18, +a

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

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

profile

Fascination

@euna-319

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