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_140001120가 scanf 라는 것을 추측할 수 있음
> rsp+0x24와 rsp+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+0x8과 rsp+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)
'Hacking Tech > Reversing' 카테고리의 다른 글
[Dreamhack] Reversing Engineering - 7. 쉬운 crackme를 통한 디버거 사용법 - 2 (0) | 2021.12.24 |
---|---|
[Dreamhack] Reversing Engineering - 5. hello-world로 배우는 x64dbg 사용법 (0) | 2021.10.04 |
[Dreamhack] Reversing Engineering - 4. x64dbg (0) | 2021.10.03 |
[Dreamhack] Reversing Engineering - 3. puts("hello world!\n") → x86_64 asm (0) | 2021.10.03 |
[Dreamhack] Reversing Engineering - 2. x64 기초 (0) | 2021.09.19 |