뉴비에욤

리버싱 기초 - IDA Pro을 이용한 실제 예제 분석 본문

securityxploded.com/IDA Pro

리버싱 기초 - IDA Pro을 이용한 실제 예제 분석

초보에욤 2015. 12. 28. 03:48

* IDA Pro Tutorial /튜토리얼 문서 = http://bob3rdnewbie.tistory.com/208

 

 

원문 : http://securityxploded.com/reversing-basics-ida-pro.php

 

 

 

소개

 

리버스 엔지니어링(이하 리버싱)은 정보 보호 연구를 하는 사람들에게는 매우 중요한 기술이다. 이번 튜토리얼에서는 간단한 크랙미 예제를 통하여 리버싱의 기본적인 컨셉을 보여줄 것이다.

 

이번 튜토리얼에서 사용하려는 크랙미는 바이너리 수정 코스에서 사용된 것이다. 리버싱의 강력함을 보여주기 위해 정적인 접근 방법을 이용하여 문제를 해결할 것이다. 이런 것들을 이해하기 위해서는 선수지식으로 약간의 어셈블리 지식, 디스어셈블러, 디버거에 대하여 알고 있어야 한다. 본인은 IDA 디스어셈블러를 사용할 것인데, 이는 시중에 있는 가장 강력한 디스어셈블러이다. 제작사인 HexRay(헥스레이)에서는 데모 버전을 제공하고 있다.

 

 

 

리버싱 - 기본 단계

 

리버싱은 쉬울수도 있고 어려울 수도 있다. 이는 분석 대상 바이너리에 따라 다른데 기본적으로 다음과 같은 단계를 거친다

1. 패커/프로텍터/암호화 탐지    =>   탐지되면 우선 언팩/복호화를 시도하고 imports 등을 복구함

2. 정적 분석    =>  실제 환경에서 실행하지 않고 어플리케이션의 알고리즘 로직 분석

3. 동적 분석    =>  바이너리를 실행하여 행위 분석

위 단계들은 리버싱의 가장 기본적인 단계이다. 종합적인 프로세스는 리버서(리버싱을 수행하는 분석가)의 목표에 따라 달라진다. 예를 들면 안티 바이러스 분석가들과 크래커들이 사용하는 기본 단계는 동일하지만 프로세스는 다르다.

 

 

 

몇 가지 핵심 정리

 

API( Application Programming Interface ) : 윈도우 환경에서는, 코드를 공유하는 것은 커뮤니케이션과 신뢰 관계의 핵심이다. 사용자 어플리케이션은 직접적으로 하드웨어를 컨트롤 하거나 윈도우 커널과 직접적으로 통신할 수 없다. 따라서 윈도우는 몇몇의 DLL(Dynamic Link LIbrary)들을 제공하는데 이 DLL 들은 사용자 어플리케이션에게 서비스를 제공하기 위해 몇몇의 함수를 익스포트하는데, 이 때 이런 함수를 API 라고 부른다. 따라서 API의 이해는 필수적이다.

 

 

리턴 값은 EAX 레지스터에 저장 : 모든 함수/API 들은 대부분 EAX 레지스터에 리턴 값이 저장된다. 예를 들어 strlen() 함수를 이용하여 주어진 문자열의 길이를 구하려고 하면, strlen() 함수는 길이를 EAX 레지스터에 저장하여 리턴한다.

 

 

 

 

IDA Pro를 이용하여 실제 예제 분석

 

이제 리버싱을 시작하여 크랙미 바이너리를 크랙할 것이다. 리버서는 다음과 같은 작업을 수행해야 한다.

1. Nag창 제거                              =>    설명 X ( 실력 향상을 위해 직접 해 보길.. )

2. 하드 코딩 된 패스워드 찾기      =>    설명 O

3. 키젠 작성                                =>    설명 X ( 실력 향상을 위해 직접 해 보길.. )

 

일단 대상 바이너리를 IDA Pro에서 로딩한다. 기본 옵션으로 열게 되면 IDA 에서 파일을 처리(읽기/분석 등)하는 것을 볼 수 있다. 이제 분석을 시작할 수 있는데, 중요한 작업 중 하나는 바로 Import/Export 함수 목록을 살펴봐서 얼마나/어떤 API가 해당 바이너리에서 사용되는지 보는 것이다. 그리고 어플리케이션을 실행하는데, 디버거에서 실행하는 것이 아니라 그냥 실행하는 것이다. 그리고 임의의 입력값을 이용하면 다음과 같은 메시지를 얻을 수 있다.

 

 

위 사진에서 보는 것처럼 분석 하는 크랙미 파일은 올바르지 않은 입력값에 대한 메시지 박스를 보여준다. "Sorry, please try again" 문자열은 상당히 많은 작업을 줄여주는 중요한 값이다. 분석 대상 바이너리에 따라 다르겠지만 이번 바이너리에서는 위 문자열을 시작 포인트로 사용할 수 있다. 그러나 IDA 에서는 start 함수를  보여주지만 "Sorry, Please try again"과 같은 에러 메시지는 볼 수 없다.

 

이제 리버서는 2가지 접근 방법을 이용할 수 있다. 하나는 start 함수로부터 호출을 트레이싱하여 우리가 원하는 "Sorry~~" 문자열이 있는 함수를 찾는 것인데, 예를 들면 "call sub_40102c" 주소로 이동하여 동일한 방법(트레이싱)을 이용하는 것이다. 또 다른 방법은 IDA 에서 보여지는 View 메뉴에서 function 탭으로 이동하여 각각의 함수를 조사하는 것이다. 대부분의 분석 시간을 줄이기 위해 두가지 방법을 혼합하여 사용한다.

 

 

위 사진에서 볼 수 있듯이 "sub_*"로 시작하는 함수 이름은 프로그래머에 의해 생성된 함수이다. 우리는 각 함수를 개별로 살펴보면서 이전에 알아낸 "Sorry, Please try again" 문자열을 어떤 함수에서 사용하는지 찾을 것이다. 이런 프로세스를 각 함수별로 실행하다보면 해당 문자열을 "sub_401178" 에서 찾을 수 있다.

 

 

위 사진에서 보듯이 해당 문자열이 평문으로 깔끔하게 저장되어 있다. 이제 백트레이싱을 하면서 해당 문자열을 사용하는 비교 명령어를 찾을 것이다. ( 위 사진은 subb_401178 함수로 이동한 뒤 내부 로직을 그래프로 본 화면임 )

 

 

첫 번째 박스를 보면 어플리케이션에서 "GetWindowTextA" API 함수를 호출하는 것을 볼 수 있다. 만약 해당 API를 잘 모르겠다면 "MSDN Win Api Reference Guide" 문서를 보거나 간단히 구글링을 하여 보면 된다. 참고로 가이드 문서는 함수의 파라미터 설명, 구조체, 예상 리턴 값 등을 보여준다.

 

 

  위 사진(api 레퍼런스 가이드)에서 보여주는 구조와 IDA에서 보여주는 구조를 비교해야 한다. 일단  IDA 에서 보여지는 "PUSH OFFSET STRING" 명령어를 통해 아마도 사용자의 입력을 받을 것이다. 그 다음 실행되는 두개의 명령어는 "LEA EAX, aHardCoded", "LEA EBX,STRING" 인데 이를 통해 하드코딩 된 문자열의 주소는 EAX 레지스터에 저장되고 사용자가 입력한 문자열의 주소는 EBX 레지스터에 저장된다. 그 다음 명령어를 보면 사용자 입력 문자열과 하드코딩 문자열을 비교하고 분기하고 있기 때문에,"aHardCoded" 라는 변수(의미상 변수)안에 실제 하드코딩된 비밀번호가 존재하는 것을 알 수 있다.

 

MOV CL, [EAX]                 ; 하드코딩 된 문자열 (반복문 1번 = 문자 1개)

MOV DL, [EBX]                 ; 사용자가 입력한 문자열 (반복문 1번 = 문자 1개)

CMP CL, DL                      ; 비교 (한 글자씩 비교 함)

JNZ SHORT_BAD_BOY         ; 동일하지 않을 경우 실패 메시지쪽으로 분기

INC EAX                           ; 동일한 경우 EAX 증가

INC EBX                           ; 동일한 경우 EBX 증가

JMP @Loop                       ; 반복문 계속 실행

 

위 루프 코드는 문자열을 비교하는 것으로써, 결국 "aHardCoded" 변수에 하드 코딩된 비밀번호가 존재하고 해당 비밀번호가 "HardCoded" 임을 알 수 있다.

 

이제 두 번째 문제를 해결할 차례이다. 일단 지금 분석하고 있던 함수(sub_401178)를 보면 하드코딩된 패스워드에 대한 정보만 있기 때문에 다른 함수로 이동하여 Name/Serial 관련 정보를 찾아야 한다. 이전에 했던 것처럼 함수 탭에서 유저 함수를 하나 하나 조사하다보면 "sub_4015e4" 함수에서 관련 정보를 찾을 수 있다.

 

이제 또 백트레이싱을 하면서 위 사진에서 보여지는 메시지 박스로 분기되는 곳을 찾아서 어떻게 하면 "Good job~" 쪽으로 분기되는지 봐야 한다. 현재 함수(sub_4015e4))의 시작 부분은 위 사진의 메시지 박스를 호출하는 주소일 확률이 높다. 왜냐하면 어플리케이션이 시작할 때 "GetWindowTextA" 함수를 호출하기 때문이다. 이전의 분석 과정을 통해 해당 함수의 용도를 알았으니 어플리케이션이 Name / Serial 입력을 기다리고 있다는 것을 알 수 있다.

 

 

코드 분석을 하다 보면 사용자가 아무런 값도 입력하지 않았을 경우 "Please enter username r please enter a Serial" 문자열과 함께 메시지 박스가 나타나는 것을 볼 수 있다. 따라서 임의의 값을 입력해야 하는데, 입력을 하면 간단한 계산 로직으로 이동되는데 이를 역분석 해야 한다.

mov eax, username_len            ; API 함수에 의해 사용자가 입력한 이름의 길이가 eax 레지스터에 저장

xor ecx, ecx                          ; ECX 레지스터 초기화(=0)

xor ebx, ebx

xor edx, edx

lea esi, username_store           ; 사용자 입력 이름의 주소가 esi 레지스터에 저장

           ; char name[] = "amit"

     ; ESI = name[0] = 'a'

 

lea edi, user_gene                  ; 계산 로직 이후에 생성되는 이름값을 edi 레지스터에 저장

; name_after_calc[] = name[0] ^ 2

 

mov ecx, 0Ah                         ; ECX 레지스터 = 10d = 0Ah

 

 

loop:

movsx eax, byte ptr [esi+ebx]         ; ESI = 사용자 입력 이름, EBX = 0

cdq                                           ; DWORD 자료형을 QuadWord 로 변환 (큰 의미 없음)

idiv ecx                                     ; EAX / ECX ( EAX / 10 )

xor edx, ebx                               ; EDX xor EBX (EBX=카운터 역활) (EDX=EDX^EBX)

add edx, 2                                  ; EDX = EDX + 2

cmp dl, 0Ah                                ; DL 레지스터 10d = 0Ah 비교

jl short loc_401646                        ; EDX < 10 이라면, loc_401646 으로 분기

sub dl, 0Ah                                ; EDX >= 10 이라면, DL - 10d = 0Ah

 

 

loc_401646:

mov [edi+ebx], dl                        ; DL 레지스터 값을 EDI 레지스터에 저장

inc ebx                                     ; EBX 증가 (EBX=포인터 역활)

    ; EBX = 일반적인 반복문에서 사용되는 i 역활도 함 (첨자변수)

 

cmp ebx, username_len                ; EBX 레지스터 값과 사용자 입력 이름 길이와 비교

jnz short loop_username                ; EBX != username_len 이라면 loop로 분기

 

위 어셈블리 코드를 대상으로 간단한 의사 코드를 생성할 수 있다. 일단 a[] = "amit" <- 사용자 입력 문자열, b[] = 목적지 변수라고 가정한다.

 

c = 10        ; ECX

i = 0          ; EBX

 

 

loop:

b[i] = a[i] % c;

b[i] = b[i] ^ i;

b[i] = b[i] + 2;

 

if (b[i] > 10)

  b[i] = b[i] - 10;

  i++;

 

  if (i != strlen(a))

    Goto loop;

 

따라서 위 로직이 사용자 입력 이름의 계산 알고리즘이 된다. 계산된 결과는 b[] 배열에 저장된다. 다음으로는 시리얼 생성 알고리즘을 살펴볼 것이다.

 

xor ecx, ecx    ; ECX 초기화

xor ebx, ebx

xor edx, edx

lea esi, serial_store        ; 사용자 입력 시리얼을 esi 레지스터에 저장

lea edi, serial_gene        ; 시리얼 생성 알고리즘 값을 edi 레지스터에 저장

mov ecx, 0Ah                 ; ECX = 10d = 0Ah

 

loc_401669:

movsx eax, byte ptr [esi+ebx]     ; eax  레지스터에 시리얼 저장 (한 글자씩)

cdq

idiv ecx    ; EAX / 10

mov [edi+ebx], dl            ; 잔여 값들을 edi 레지스터에 저장

inc ebx                          ; EBX ++

cmp ebx, serial_len        ; 시리얼의 길이와 ebx 레지스터 비교

jnz short loc_401669

 

 

위 로직에 의해 계산된 값은 또 다른 배열(c[])에 저장된다. 그리고 IDA를 보면 계산 로직 다음에 어플리케이션 내의 반복문에서 c[] 값과 b[] 배열 값을 비교하는 명령어를 볼 수 있다. 참고로 b[] 배열에는 사용자 이름, c[] 배열에는 이름에 알맞는 시리얼이 존재한다.

 

 

 

위와 같은 과정을 통해 어플리케이션은 사용자가 입력한 시리얼 값을 10으로 나눈 결과를 (사용자가 입력한 이름 => 계산 로직에 의해 생성 된 값)과 비교한다. 따라서 x 변수가 계산된 이름 값이라면 x * 11 값이 시리얼이 된다.

 

 

 

* 원본 페이지에서 제공하는 키젠 코드는 다음과 같다. ( C  )

/* Author: DouBle_Zer0  */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
	char a[10];
	char b[10];
	int c;
	int i;
	int d;

	printf("[-]Coded By DouBle_Zer0\n");
	printf("Plz enter your name: ");
	scanf("%s", a);

	d = strlen(a) - 1;

	for (i = 0; i <= d; i++)
	{
		c = a[i] % 10;
		printf("%d %d\n", a[i], c);

		c = c ^ i;
		c = c + 2;
		if (c > 10)
		{
			c = c - 10;
		}
		b[i] = c * 11;
	}

	b[i] = 0;

	printf("Corresponding password is: %s\n", b);
	system("pause");

	exit(0);

}

 

 

 

 

* 본인이 제작한 키젠 코드는 다음과 같다. ( Python )

# author : Wraith
# coding:utf-8


print "[+] Coded by Wriath"
u_name = raw_input("Name : ")

nl = len(u_name)
serial = ""

for i in range(0,nl):
	c_ascii = ord(u_name[i])
	tmp = c_ascii % 10;
	tmp ^= i
	tmp += 2

	if tmp > 10:
		tmp -= 10

	serial += str(chr(tmp * 11));


print "Corresponding Serial :", serial

 

Comments