System Call & SSDT Hooking
System CallWindows 운영체제는 사용자 모드와 커널 모드라는 두 가지 형태의 권한이 존재하고 있다. 굳이 하나가 아닌 두 가지로 분류되는 것은 모든 프로세스가 하나의 권한으로만 동작할 경우, 각 프로세스는 하드웨어나 프로세스에 직접 접근할 수 있게 된다. 이는 어떠한 프로세스라도 운영체제의 핵심 기능을 조작할 수 있게 되는 것이므로 보안에 있어 매우 취약하게 된다. 이러한 요소를 방지하기 위해 두 개의 영역으로 분류되었고, 당연히 사용자 모드에 존재하고 있는 프로세스는 커널 영역에 접근할 수가 없다. 하지만 커널 영역에 접근할 수 없다는 것은 해당 프로세스가 디스크의 내용을 읽을 수가 없게 되고, 그 외에도 많은 작업들에 제한이 생긴다. 따라서 이러한 불편함을 보완하기 위해 "사용자 모드의 ..
2016.04.10
[Malware] PETYA.exe 분석
2016.04.02
BOF에 취약한 함수
취약한 함수란취약한 함수란 컴파일되기 이전에 프로그래머로부터 작성된 코드 중 버퍼 오버 플로우나 포맷 스트링 공격 등에 노출될 수 있는 함수를 뜻한다. 이러한 함수의 사용은 오류를 발생시키거나 심할 경우 상위 권한까지 탈취될 수 있기에 주의하여야 한다. 따라서 이러한 함수의 어떠한 부분이 취약한지 등을 알고 제작할 때 해당 함수들의 사용을 자제하므로 추가적인 피해를 방지할 수 있다. 취약한 함수에는 대표적으로 gets, scanf 등과 같은 함수로 입력받는 문자열의 크기와 주어진 변수의 크기를 고려하지 않는다는 점이다. 이렇게 변수의 크기를 고려하지 않는 함수들은 입력받은 문자열 등이 변수 공간보다 클 경우, 결국 스택의 다른 곳까지 침범하게 된다. 스택의 다른 요소들이 침범될 경우 BOF 공격 등에 쉽..
2016.03.30
CPU 레지스터
CPU 레지스터메모리는 아래의 그림과 같은 계층 구조를 갖고 있다. 이러한 계층 구조는 메모리를 필요에 따라 여러 가지 종류로 나누어 놓는 것으로, 이렇게 나누어 놓은 것은 대부분 CPU가 메모리에 더 빨리 접근하도록 하기 위함이다. 하드 디스크는 직접 CPU에 접근할 방법이 존재하지 않고, 메모리는 CPU 외부에 존재하고 있기 때문에 캐시와 레지스터보다 더욱 느리게 접근된다. 이와 같이 레지스터는 CPU가 메모리에 더 빨리 접근하기 위해 존재한다.레지스터는 CPU의 작은 저장 공간으로 CPU가 데이터에 접근하는 가장 빠른 방법을 제공한다 하였다. IA-32에서 CPU는 8개의 범용 레지스터와 명령 포인터, 5개의 세그먼트 레지스터, 그리고 부동 소수점 데이터 레지스터가 존재하고 있다. 이에 대하여 각각..
2016.03.26
[Malware] DarkSeoul 분석 (3.20 전산마비)
2016.03.23
C기본 문법 어셈블리 변환
개요보안 공부를 하면서 가장 많이 하는 말은 "어떤 공부부터 시작해야 하나요?"라는 질문이며, 이에 대한 답으로 흔히 "C언어부터 공부하세요."라고 한다. 이처럼 C언어는 프로그래밍의 기본을 이해할 수 있게 해주며 이에 대한 이해는 이후 다른 프로그래밍 언어나 리버싱에도 영향을 미치게 된다. 그렇기에 C언어에 대해 다시 학습을 하면서 리버싱까지 겸하여 공부하기 위해 이번 문서를 준비하였다. 이번 문서에서는 C언어에 대한 입문적인 단계를 다루는 것이 아니다. C언어와 같은 프로그래밍 언어들 컴파일되어 사람이 읽을 수 없는 기계어의 형태로 나타나게 되며 이러한 기계어를 사람이 읽을 수 있는 형태로 변화하는 것이 바로 디스 어셈블링이다. 따라서 바로 이러한 디스 어셈블링 된 C언어의 기본 문법을 살펴보고자 한..
2016.03.20
Visual Studio 메인함수 찾기
개요리버싱을 공부하면서 자신이 만든 프로그램을 직접 디버깅해보고 싶단 의욕이 생기는 일은 당연하다. 하지만 처음 리버싱을 공부하는 입장이 아니라면 보통 Abex's CrackMe부터 다양한 문제를 통해 먼저 접해본 경험이 많을 것이다. 이러한 프로그램들은 대부분 쉽게 리버싱을 학습하도록 메인 함수 부분이 바로 나타나거나 찾기 쉽도록 되어 있다. 따라서 직접 간단한 파일을 컴파일하여 디버거로 열어본다면 복잡해 보이는 엔진 코드로 인해 당황하게 된다. 필자 또한 리버싱을 접한지 얼마 안 되었을 때, 매우 당황했던 것을 기억한다. 우선 필자의 컴파일 환경은 WIndows10 x64, Visual Studio 2013이다. 실습을 위해 C언어로 대부분의 사람들은 모두 접해보았을 "Hello World!"를 출력..
2016.03.16
no image
Yara 규칙 제작 & Python
개요Yara를 통해 특정 패턴을 기준으로 파일을 매칭 할 수가 있음을 저번 문서를 통해 알 수 있었다. 그렇다면 이러한 룰은 어떻게 생성되어야 할까? 우선 고려해야 할 사항으로 다른 정상적인 파일에 포함되어 있는 패턴은 배제시켜야 한다. 따라서 이번 문서는 다른 파일과 겹치지 않는 문자열을 걸러내도록 할 것이다. 이를 위해 구상한 내용은 다음과 같다. 우선 정상적인 파일로부터 문자열을 추출한 다음, 이를 DB로 만들어 Yara 규칙을 제작해야 하는 대상 악성코드의 문자열을 DB와 비교한다. 만약 DB에 이미 해당 문자열이 존재할 경우, 해당 문자열은 정상적인 파일에 포함되어 있을 수 있으므로 제외한다. 이러한 작업을 반복하여 결국 DB에 포함되지 않은 악성코드 특유의 문자열만 남게 된다. 그리고 이러한 ..
2016.03.07
System Call

Windows 운영체제는 사용자 모드와 커널 모드라는 두 가지 형태의 권한이 존재하고 있다. 굳이 하나가 아닌 두 가지로 분류되는 것은 모든 프로세스가 하나의 권한으로만 동작할 경우, 각 프로세스는 하드웨어나 프로세스에 직접 접근할 수 있게 된다. 이는 어떠한 프로세스라도 운영체제의 핵심 기능을 조작할 수 있게 되는 것이므로 보안에 있어 매우 취약하게 된다. 이러한 요소를 방지하기 위해 두 개의 영역으로 분류되었고, 당연히 사용자 모드에 존재하고 있는 프로세스는 커널 영역에 접근할 수가 없다.


하지만 커널 영역에 접근할 수 없다는 것은 해당 프로세스가 디스크의 내용을 읽을 수가 없게 되고, 그 외에도 많은 작업들에 제한이 생긴다. 따라서 이러한 불편함을 보완하기 위해 "사용자 모드의 프로세스가 커널 영역에 접근할 수 있는 방법을 만들자."라는 의도에 부합되는 것이 바로 시스템 호출(System Call)이다. 즉, Windows는 커널에 대한 직접적인 접근을 제한하는 대신 사용자 영역에서 API를 호출하면 NTDLL.DLL을 거쳐 커널 모드로 진입하게 된다.

시스템 콜은 크게 2가지 방법으로 진행될 수 있는데, 바로 "INT 0x2E"와 "SYSENTER"이다. INT 0x2E의 경우 소프트웨어 인터럽트로 Windows XP 이전에는 이를 통해 시스템 콜을 진행하였다면, XP부터는 SYSENTER를 통해 시스템 콜을 진행하였다. 이 둘의 차이점이 몇 가지 존재하고 있지만 가장 큰 차이점은 바로, SYSENTER는 수행 시간에 따른 부하가 적다는 것이다. 또한 각 명령어가 호출된 후 과정에 차이가 있는데 이는 아래의 그림에서와 같이 SYSENTER 명령어가 진행될 경우 KiFastCallEntry()를 호출한 후, 이를 통해 다시 KiSystemService()를 호출한다. 이에 반해 INT 0x2E의 경우 KiFastCallEntry()를 거치지 않고 바로 호출된다는 것이다.


INT 0x2E나 SYSENTER 명령어의 경우 우리가 직접 확인할 수도 있다. 흔히 자주 접할 수 있는 OllyDBG와 같은 유저 모드 디버거를 통해 특정한 함수를 호출하는 부분을 계속 따라가다 보면 ntdll.dll의 함수를 볼 수가 있다. 그리고 이러한 함수를 계속 트레이싱하다 보면 INT 0x2E나 SYSENTER 명령어를 확인할 수 있다. 단, 유저 모드 디버거이기 때문에 해당 명령어가 수행되는 자세한 과정은 확인할 수가 없으므로 이를 확인하기 위해선 WinDBG와 같은 커널 디버거를 이용해야 한다. 진입하는 과정은 아래의 그림과 같다.

CALL EDX를 통해 지정된 주소를 호출하게 되는데, 해당 부분은 아래의 그림과 같다. 여기서 FS:[0x30]은 TEB의 0x30번째에 있는 값을 나타낸다. 이는 PEB를 나타내는 것으로 이러한 방법을 통해 PEB에 접근할 수가 있다. 그리고 PEB의 0x464번째 바이트 값을 통해 해당 값이 2인지 비교한 다음 만약 2라면 INT 0x2E를 통해 커널 모드에 진입하게 된다.

만약 해당 값이 2가 아니라면 JMP 명령어를 통해서 KiFastSystemCall 부분으로 넘어오게 되는데, 해당 함수의 부분은 SYSENTER 명령어를 통해 KiFastCallEntry로 이동하게 된다.

이렇게 커널 모드에 접근하는 과정을 확인할 수가 있었다. 그렇다면 어떻게 다시 사용자 영역으로 복귀할까? 이 역시 두 명령어가 차이를 보인다. SYSENTER 명령어의 경우 커널 영역에서 "SYSEXIT" 명령어를 통해 사용자 영역으로 돌아오게 되며, INT 0x2E의 경우 "IRET" 명령어를 통해 원래의 코드로 복귀가 가능하다.


IDT Hooking

IDT는 256개의 Enyrt로 이루어진 배열이며 엔트리 하나당 하나의 인터럽트에 대응되며 이러한 각 인터럽트는 IDT로부터 처리할 함수의 주소(ISR)를 전달받는다. 다시 말해, IDT에서는 각 인터럽트를 처리할 함수의 주소를 갖고 있다. 인터럽트 테이블의 메모리 주소를 얻으려면 IDTR 레지스터 값을 읽어야 하는데 이는 "sidt" 명령을 통해 알 수가 있으며, 반대로 'lidt' 명령을 통해 IDTR 레지스터의 값을 변경할 수 있다. sidt 명령에 의해 반환되는 idt 데이터 구조는 아래와 같다.

typedef struct
{
    unsigned short IDTLimit;
    unsigned short LowIDTbase;
    unsigned short HiIDTbase;
}

IDT의 주소는 위 구조체에서 확인할 수 있듯이, 하위 주소와 상위 주소가 나누어져 있다. 이를 통해 IDT의 주소를 확인할 수가 있는데, 그렇다면 IDT에는 어떠한 내용이 포함되어 있는지 아래의 그림을 보자. 크게 중요한 내용은 바로 위 시스템 콜에서 볼 수 있었던 0x2E와 키보드 인터럽트인 0x93이 존재하고 있다. 0x93의 경우 키보드 메시지 후킹과 관련된 내용이므로 자세히 다루지 않고 0x2E를 위주로 살펴보자.


INT 0x2E


응용 프로그램이 운영체제가 제공하는 API를 호출하면 NTDLL.DLL은 EAX 레지스터에 해당 시스템 함수의 번호를 로드하고 EDX 레지스터에는 그 시스템 함수로 전달되는 인자가 저장된 사용자 영역의 스택 주소를 로드한다. 그리고 INT 0x2E 명령을 수행하여 인터럽트를 발생시킨다. 이 인터럽트에 의해 유저 모드에서 커널 모드로 전환된다. 따라서 INT 0x2E 명령을 수행하여 인터럽트가 발생한 다음, IDT로부터 ISR의 주소를 얻어 실행하고자 할 때 바로 ISR의 주소를 바꾸어 IDT를 후킹 할 수가 있는 것이다. 이를 표현하면 아래와 같다.

IDT를 후킹 하는 데 있어 동작하고 있는 중 그 값이 변경되면 오류를 일으킬 수 있기 때문에 'cli' 명령을 통해 인터럽트 처리를 정지시킨 후, 해당 값이나 주소를 변경해야 한다. 모든 수정이 완료된 이후에는 다시 인터럽트 처리를 활성화시켜야 하기 때문에 'sti' 명령을 사용하여야만 한다. 이를 위한 코드는 아래와 같다.

_asm {
     sidt idt_info;
}

idt_entries = (*IDTENTRY) MAKELONG (idt_info.LowIDTbase, idt_info.HiIDTbase);
int2e_entry = &(idt_entries[0x2E])    //IDT에서 0x2E번째 엔트리 주소

_asm {
     cli;                                                                // 인터럽트 Disable
     lea eax, HookKiSystemService;             // EAX에 후킹 함수의 주소를 저장
     mov ebx, int2e_entry;                             // IDT에서 0x2E번째 엔트리의 주소 전달
     mov [ebx], ax;                                            // 후킹 함수 하위 16비트 주소를 IDT엔트리에 기록
     shr eax, 16;                                                 
     mov [ebx+6], ax;                                        // 후킹 함수 상위 16비트 주소 또한 기록
     sti;                                                                // 인터럽트 Enable
}


MSR Hooking

그렇다면 SYSENTER의 경우는 어떻게 될까? 윈도우 XP 이상에서는 INT 0x2E가 아닌 SYSENTER를 사용하는데, 시스템 콜이 요청되면 NTDLL은 EAX에 해당 시스템 콜의 번호를 로드하고 EDX 레지스터에서는 포인터 레지스터인 ESP를 로드한다. 그다음 NTDLL은 SYSENTER 명령을 실행한다. SYSENTER 명령이 실행되면 커널 영역에 들어가게 된다. 이때 그냥 커널로 들어가는 것이 아니라 실행되어질 커널의 주소인 MSR 0x176 레지스터(KiFastCallEntry) 내부에 있는 값으로 이동하게 된다. 그 후 KiFastCallEntry는 EAX에 저장되어 있는 시스템 콜 번호를 가지고 SSDT에서 Nt함수 주소로 가져오고 해당 함수를 호출한다. SSDT에 대해선 뒤에서 더 자세히 알아볼 것이다.

위 그림과 같이 나타낼 수 있으며, 바로 MSR 0x176의 값을 변경하므로 이를 수정하는 것이다. 이를 변경하기 위해서는 rdmsr 명령어와 wrmsr 명령어를 사용하여 msr의 값을 읽고 조작하여야 한다. 이를 위한 코드는 아래와 같다.

_asm {
    mov ecx, 0x176            // MSR 0x176 지정
    rdmsr                             // 어떠한 주소가 있지 읽음
    mov  OrigFunc, eax    // 기존에 있던 값을 저장
    mov eax, HookFunc  
    wrmsr                            // MSR 0x176에 HookFunc의 주소를 기록함
}

이렇게 후킹 작업을 완료한 다음, SYSENTER 명령으로 인해 MSR 0x176에 존재하고 있는 KiFastCallEntry의 주소가 아닌 훅 함수의 주소가 호출되므로 인해 KiFastCallEntry가 아닌 HookFunc()이 먼저 호출된 다음 KiFastCallEntry가 호출된다. 실제로 이러한 후킹 작업이 성공적으로 이루어졌다면 훅 함수에서는 EAX 레지스터에 있는 값을 통해 어떠한 시스템 콜을 요청했는지 확인할 수가 있다. 이는 다시 말해 특정한 시스템 콜을 지정하여 해당 시스템 콜은 거부되도록 할 수 있다.


단, 시스템 콜은 빈번하게 이루어지는 작업이기 때문에 과도한 조건문을 걸어놓는 것은 시스템의 성능을 크게 하락시키므로 사용자가 속도의 변화를 체감할 수도 있다. 따라서 빈번하게 호출되는 만큼 최적화된 내용만을 후킹 함수에 넣어야 한다.


SSDT Hooking

시스템 서비스 디스패치 테이블은 시스템 콜을 처리하기 위한 함수를 찾을 때 사용된다. 위에서 프로그램이 시스템 콜을 호출하는 두 가지 방법(INT 0x2E, SYSENTER)에 대하여 알아보았다. 이 두 명령어를 통해 결국 KiSystemService 함수(시스템 서비스 디스패처)를 호출하게 되는데, 이 함수는 EAX 레지스터에서 시스템 콜 번호를 읽어 SSDT에서 해당 시스템 콜의 루틴을 찾는다. 


또한 KiSystemService는 시스템 콜의 인자를 유저 모드 스택에서 커널 모드 스택으로 복사하는데, 이때 EDX 레지스터가 시스템 콜에 사용할 인자를 가리키고 있다(몇몇 루트킷은 이런 시스템 콜 처리 과정에서 시스템 콜의 인자를 변경하거나 시스템 콜의 수행 루틴을 변경시키기도 한다). 시스템 콜 번호에 맞게 KeServiceDescriptorTable을 참조하여 Native API를 호출한다. 그 후 시스템 콜을 종료하고 각각 IRET나 SYSEXIT 명령어를 사용하여 유저 모드로 복귀한다. 아래의 그림은 이를 종합적으로 표현한 내용이다.

결국 SSDT에서 서비스 호출 번호에 맞는 주소를 얻은 다음 이를 호출하는 형태로 진행되는 것이다. 그렇다면 SSDT Hooking은 어느 부분에서 후킹 해야 하는 것일까? 바로 GetFuncAddress 과정에서 인덱스 번호에 맞는 함수의 주소를 SSDT에서 가지고 올 때이다. 예를 들어, 0xAD 서비스 함수(EAX=0xAD) 시스템 콜이 발생하면 SSDT에서 0xAD번째에 위치한 함수의 주소를 가지고 오게 된다. 그리고 해당 함수를 호출하므로 진행되는 방식인데, 만약 SSDT를 변조하므로 0xAD번째에 위치한 함수의 주소를 HookFunc의 주소로 대체하면 해당 시스템 콜이 발생할 때마다 HookFunc가 호출된다.

위 예를 좀 더 구체적으로 제시하자면 0xAD 시스템 콜의 루틴이 SSDT에서 0xCCCCCCCC로 존재하고 있는 상황에 이 주소를 Hook함수의 주소인 0xDDDDDDDD로 대체하게 되면 0xAD 시스템 콜이 발생할 때마다 0xDDDDDDDD의 함수가 호출되게 된다. 대개 이러한 후킹 함수는 정상적인 루틴 0xCCCCCCCC를 조작하는데, 어떠한 인자가 오느냐에 따라 이를 진행하지 않거나 원래 함수의 결과가 특정 값일 경우 이를 변조하여 반환하는 등의 조작을 가할 수가 있다.


하지만 SSDT는 Read Only로 설정되어 있기 때문에 SSDT Hooking을 진행하기 위해서는 쓰기 권한이 필요하다. 이를 우회하기 위한 방법 중 하나인 CR0 Register에 대하여 알아보자. CR0 레지스터는 WP(Write Protect) Bit를 포함하고 있는데 이 값이 0 이면 메모리 보호 기능이 해제되고 1 이면 메모리 보호가 활성화된다. 따라서 메모리 보호 기능을 비활성화하므로 쓰기 권한을 얻을 수가 있다. 해당 코드는 다음과 같다.

__asm {    // 메모리 보호 기능 비활성화
    push eax
    mov eax, CR0
    and eax, 0xFFFEFFFF
    mov CR0, eax
    pop eax}

__asm {    // 메모리 보호 기능 활성화
    push eax
    mov eax, CR0
    or eax, NOT 0xFFFEFFFF
    mov CR0, eax
    pop eax}

이러한 후킹은 목적은 대개 흔적을 남기지 않는 것이 중요하므로, SSDT를 후킹 한 다음에는 다시 CR0를 조작하여 메모리 보호 기능을 활성화해주어야 한다. 이는 비활성화와는 반대로 or eax, NOT 0xFFFEFFFF 연산을 해주어야 한다. 이제 SSDT에 값을 기록할 수 있으므로 어떻게 변조할 것인가에 대하여 알아보자. SSDT Hooking에는 유용하게 사용되는 몇 가지 매크로가 존재한다. 


"SYSTEMSERVICE" 매크로는 ntoskrnl.exe에서 제공하는 Zw* 함수의 주소를 입력받아 그에 상응하는 Nt* 함수의 주소를 SSDT에서 구할 때 사용한다. "SYSCALL_INDEX" 매크로는 Zw* 함수의 주소를 입력받아서 SSDT에서 해당 함수의 인덱스 번호를 구하는 데 사용할 수 있다. 이렇게 "SYSTEMSERVICE"와 "SYSCALL_INDEX" 매크로가 해당 함수의 시작 부분(_func)에 +1을 하는 이유는 opcode에 따라 [mov eax, 인덱스 값] 이므로 해당 인덱스 값은 함수의 시작점에서 mov eax의 opcode 값 다음에 오므로 +1을 더해 해당 값을 알 수 있기 때문이다. 이 두 매크로를 통해 각각 Nt* 함수의 주소와 인덱스 번호를 구할 수가 있다.

#define SYSTEMSERVICE(_func)  KeServiceDescriptorTable.ServiceTableBase[ *(PULONG)                                                                                                              ((PUCHAR)_func+1)]
#define SYSCALL_INDEX (_func) *(PULONG) ((PUCHAR) _func+1)


HOOK_SYSCALL과 UNHOOK_SYSCALL 매크로는 후킹을 수행할 Zw* 함수의 주소와 SSDT에서의 인덱스, 그리고 새로운 HookFunc 함수의 주소를 이용해 SSDT 안에서의 주소를 변경해준다. 밑의 코드 중에서 "InterlockedExchange"가 있는데 이는 두 개의 인자 값이 일치하지 않을 경우 첫 번째 인자가 지정하는 메모리의 값을 두 번째 인자로 바꾸는 함수이다.

#define HOOK_SYSCALL (_func, _Hook, _Orig)\
               _Orig=(PVOID) InterlockedExchange((PLONG)\                &MappedSystemCallTable[SYSCALL_INDEX(_func)],(LONG) _Hook)
#define UNHOOK_SYSCALL(_func, _Hook, _Orig)\
               InterlockedExchange((PLONG)\
               &MappedSystemCallTable[SYSCALL_INDEX(_func)], (LONG) _Orig)

이러한 과정을 통해 SSDT에 존재하고 있는 서비스 루틴을 훅 함수로 대체할 수가 있고, 다시 원래대로 되돌릴 수가 있다. 


Reference


http://luckyyowu.tistory.com/133

http://hackerspace.tistory.com/entry/시스템-호출-처리순서

http://bbolmin.tistory.com/158

http://luckey.tistory.com/86

http://securityfactory.tistory.com/158

https://en.wikipedia.org/wiki/Interrupt_descriptor_table

http://blog.naver.com/wwwkasa/80167132015

http://ezbeat.tistory.com/279

SSDT(System Service Descriptor Table) Hooking.pdf - Written by 백구

취약한 함수란

취약한 함수란 컴파일되기 이전에 프로그래머로부터 작성된 코드 중 버퍼 오버 플로우나 포맷 스트링 공격 등에 노출될 수 있는 함수를 뜻한다. 이러한 함수의 사용은 오류를 발생시키거나 심할 경우 상위 권한까지 탈취될 수 있기에 주의하여야 한다. 따라서 이러한 함수의 어떠한 부분이 취약한지 등을 알고 제작할 때 해당 함수들의 사용을 자제하므로 추가적인 피해를 방지할 수 있다. 


취약한 함수에는 대표적으로 gets, scanf 등과 같은 함수로 입력받는 문자열의 크기와 주어진 변수의 크기를 고려하지 않는다는 점이다. 이렇게 변수의 크기를 고려하지 않는 함수들은 입력받은 문자열 등이 변수 공간보다 클 경우, 결국 스택의 다른 곳까지 침범하게 된다. 스택의 다른 요소들이 침범될 경우 BOF 공격 등에 쉽게 노출될 수가 있으며 이는 공격자가 해당 프로그램을 조작하거나 심지어 상위 권한을 획득할 수 있는 여건을 줄 수가 있다. 이제 이러한 함수들에 대하여 알아보자.


Gets


사용자로부터 문자열을 입력받는 gets 함수는 가장 대표적으로 취약한 함수 중 하나이다. 해당 함수는 문자열을 입력받지만 문자열을 담을 공간의 길이와 입력받은 문자열의 길이를 확인하지 않기 때문에 버퍼오버플로우에 취약하다. 우선 아래의 코드를 확인해보자.

#include <stdio.h>
int main()
{
    char buf[10];
    gets(buf);
    printf("%s\n",buf);
}

char형 배열 buf를 10만큼 선언한 뒤 여기에 사용자로부터 입력을 받고 해당 내용을 출력한다. 이를 어셈블리로 나타내면 위 그림과 같은데, 컴파일 과정에 있어서 CheckEsp와 CheckStackVars와 같이 별도의 함수가 추가되어 있는 것을 확인할 수가 있다. 이 두개는 모두 취약한 함수가 공격당했는가를 확인하는 부분이라 생각하면 된다.


어셈블리의 형태로 보면 사용자로부터 입력을 받을 공간 [ebp+buf]를 gets의 인자로 주기 위해 스택에 넣고 gets 함수를 호출한다. 호출된 함수를 통해 [ebp+buf]에는 사용자가 입력한 문자열이 위치하게 되며, 이는 다시 printf의 인자로 출력된다. 여기서 자세히 보아야 할 부분은 바로 gets를 호출하기 바로 전 변수 buf의 주소 [ebp+buf]이다. 해당 부분을 스택에서 확인하면 주어진 공간은 10이기 때문에 그 이후의 공간은 다른 내용(여기서 다른 내용이란 스택의 변조를 체크하기 위한 \xCCCCCCCC이다.)으로 채워져 있다. =


사용자로부터 입력받을 문자열의 길이를 확인하지 않기 때문에 만약 10보다 큰 내용의 문자열이 입력된다면 이러한 내용은 변조된다. 아래의 그림의 첫 번째 표와 같이 스택에 아홉 개의 "A"와 \x00이 채워지면 그 뒤의 내용은 변화되지 않는다. 하지만 만약 10 보다 큰 내용을 입력할 경우 두 번째 표와 같이 기존에 존재하던 내용들이 덮어 써지는 걸 확인할 수 있다.

이러한 경우 스택 변화를 방지하기 위한 \xCC까지 침범하여 결국 CheckStackVars를 호출하는 과정에 있어 오류가 나타나며 프로그램이 종료된다. 만약 CheckStackVars와 같이 스택의 변화를 방지하는 요소가 없다면 "A"라는 문자열은 스택에 저장되어 있는 다른 부분까지 침범이 가능해지고 이를 통해 RETN 값을 수정하는 등을 통해 BOF 공격을 취할 수가 있다.


Scanf


scanf 또한 사용자로부터 문자열을 입력받아 변수에 저장하는 용도로 사용된다. 하지만 역시 입력받은 문자열의 길이를 체크하지 않기 때문에 스택의 값이 변조될 수가 있다. 아래의 코드를 보자.

#include <stdio.h>
int main()
{
    char buf[10];
    scanf("%s",buf);
    printf("%s",buf);
}

전체적인 내용은 위 gets와 유사하며, 변수 buf의 주소 [ebp+buf]에 scanf를 통해 입력을 받지만 그 길이의 제한이 없기 때문에 이전에 스택에 push 된 다른 값들이 변조될 수 있다. 주어진 길이보다 클 경우 스택 체크 함수로 인해 오류를 나타내며 프로그램은 종료된다.


Strcat


이번엔 strcat 함수에 대하여 알아보자. strcat은 변수 a에 변수 b의 내용을 덧붙여주는 함수로, 이 또한 변수의 길이를 체크하지 않기 때문에 BOF 공격에 취약성을 가지고 있다. 아래의 코드와 어셈블리어를 보자.

#include <stdio.h>
int main()
{
    char buf[10]="AAAAAAAAA";
    char str[10]="BBBBBBBBB";
    strcat(buf,str);
    printf("%s\n",buf);
}

우선 [ebp+buf], [ebp+buf+4], [ebp+buf+8] 등을 이용해 해당 문자열을 저장하는 것을 확인할 수가 있다. 이렇게 저장된 문자열의 시작 주소 [ebp+buf], [ebp+str]은 스택에 넣어지는데 여기서 [ebp+str]이 변수 스택에 들어가고 그다음 [ebp+buf]가 스택에 들어가게 된다.


그 후 "BBB..."를 "AAA..."에 덧붙이게 되는데, 이 과정에서 변수 buf의 공간은 초기에 10만큼만 할당되었기에 변수 str의 크기를 확인한다면 이를 덧붙일 수 없는 것이 정상적이다. 하지만 strcat 함수의 경우 이러한 길이를 고려하지 않기 때문에 결국 [ebp+buf]의 뒷부분에 [ebp+str]의 내용이 덧붙여진다. 그러므로 결국 아래의 그림과 같이 뒷부분에 존재하고 있던 내용 "\x00\xCC..."가 사라지게 되고 변수 str의 내용인 "BBBB..."가 자리 잡게 된다. 이 또한 gets와 마찬가지로 스택의 내용을 변조시키므로  CheckStackVars 함수에서 오류를 나타내게 된다. 만약 이러한 스택 변화를 확인하는 함수가 없을 경우 쉽게 스택의 내용이 변조되어 공격자에게 이용당할 수 있다.


Strcpy


strcat이 버퍼에 있는 내용을 덧붙이는 것이라면, strcpy 함수는 해당 내용을 통째로 옮기는 용도로 사용된다. 전반적인 내용은 strcat과 유사하며, 우선 아래의 코드를 확인하자.

#include <stdio.h>
int main()
{
    char buf[10] = "AAAAAAAAA";
    char str[] = "BBBBBBBBBBBB";
    strcpy(buf,str);
    printf("%s\n", buf);
}

선언된 변수가 스택에 저장되는 과정은 위와 동일하며, strcpy를 호출하기 전에 [ebp+str]을 먼저 스택에 넣은 다음 [ebp+buf]를 인자로 넣어준다. 이러한 과정을 거쳐 결국 변수 buf에 있던 내용은 str의 내용으로 바뀌게 된다. 하지만 strcpy 함수 역시 크기를 체크하지 않기 때문에 변수 buf의 크기가 10 임에도 불구하고 10보다 큰 내용이 오게 되었다. 이를 표현하면 아래의 표와 같다.


Sprintf


sprintf는 printf와 비슷하게 출력 함수로 사용되지만, 다른 점이 있다면 printf가 모니터 화면에 출력되는 것이라면 sprintf는 버퍼로 사용될 변수로 출력이 된다는 점이다. 아래의 코드를 확인해보자.

#include <stdio.h>
int main()
{
    char buf[10];
    char str[] = "BBBBBBBBBBBB";
    sprintf(buf, "%s", str);
    printf("%s\n", buf);
}

[ebp+str]부터 [ebp+str+0xC]까지 해당 문자열을 넣은 다음, 해당 문자열의 시작 주소인 [ebp+str]을 인자로 넣어준 뒤 포맷 스트링, 그리고 해당 버퍼가 저장될 변수 [ebp+buf]가 차례대로 스택에 쌓이게 된다. 아래의 표와 같이 원래 해당 buf의 내용이 존재하지 않았으며 buf 뒤에는 다른 내용의 값들이 존재하고 있다. 하지만 sprintf 계열의 함수를 사용할 경우 하단의 표와 같이 다른 부분의 값을 덮어씌울 수가 있다.


Reference


http://j3nasis.tistory.com/entry/버퍼오버플로우-취약-함수별-대책

http://ljsk139.blog.me/30129428446

http://itguru.tistory.com/66

http://www.hackerschool.org/HS_Boards/data/Lib_system/sprintf.txt



'Reversing > Theory' 카테고리의 다른 글

윈도우 후킹 원리 (1) - User Mode  (3) 2016.04.23
System Call & SSDT Hooking  (0) 2016.04.10
윈도우 메모리구조와 메모리분석 기초  (3) 2016.03.29
CPU 레지스터  (0) 2016.03.26
C기본 문법 어셈블리 변환  (5) 2016.03.20

CPU 레지스터

Kail-KM
|2016. 3. 26. 02:28
CPU 레지스터

메모리는 아래의 그림과 같은 계층 구조를 갖고 있다. 이러한 계층 구조는 메모리를 필요에 따라 여러 가지 종류로 나누어 놓는 것으로, 이렇게 나누어 놓은 것은 대부분 CPU가 메모리에 더 빨리 접근하도록 하기 위함이다. 하드 디스크는 직접 CPU에 접근할 방법이 존재하지 않고, 메모리는 CPU 외부에 존재하고 있기 때문에 캐시와 레지스터보다 더욱 느리게 접근된다. 이와 같이 레지스터는 CPU가 메모리에 더 빨리 접근하기 위해 존재한다.

레지스터는 CPU의 작은 저장 공간으로 CPU가 데이터에 접근하는 가장 빠른 방법을 제공한다 하였다. IA-32에서 CPU는 8개의 범용 레지스터와 명령 포인터, 5개의 세그먼트 레지스터, 그리고 부동 소수점 데이터 레지스터가 존재하고 있다. 이에 대하여 각각 알아보자.


범용 레지스터 & 명령 포인터


범용 레지스터에는 8개의 레지스터가 존재하고 있다. 리버싱을 한 번이라도 해본 사람이라면 OllyDBG의 우측 상단에 존재하고 있는 레지스터 창을 보았을 것이다. 이러한 범용 레지스터를 설명하기 전에 레지스터의 크기에 대하여 먼저 알아보자.


일반적으로 EAX, ECX, EDX와 같이 'Extened'가 붙어 있는 경우를 많이 볼 수 있다. 하지만 유연한 진행을 위하여 하나의 EAX에서도 일부만 사용해야 할 상황이 필요하다. 그렇기에 비트가 나누어지는 것에 대하여 알아야 하는데, EAX는 32비트(DWORD)로 우리가 흔히 알고 있는 4바이트의 크기를 같는다. AX의 경우 16비트(WORD)의 크기를 갖는데, 이는 다시 상위 8비트(BYTE) AH와 하위 8비트(BYTE) AL로 구분할 수 있다.

EAX 레지스터(Accumulator Register)

주로 산술 연산과 리턴 값을 전달하기 위해 사용되며 상대적으로 사용되어 값이 자주 변하기 때문에 값을 보존하는 용도로 사용하지는 않는다. 산술 연산으로는 곱셈이나 나눗셈, 덧셈, 뺄셈 등 대부분의 경우 EAX에 해당 값이 들어 있는 경우가 많다. EAX가 리턴 값을 사용되는 경우란, C언어를 예로 Main() 함수에서 다른 함수 Sub()를 호출하였을 때 Sub()에서 "return 0;"을 사용하였다면 어셈블리에서는 Sub()가 RETN 명령어를 동작하기 전에 EAX 레지스터에 "MOV EAX, 0"과 같은 명령어를 사용하여 저장한다. 이렇게 값을 저장하기 때문에 해당 값을 다시 Main()에서 사용될 수 있게 된다.


ECX 레지스터(Counter Register)

반복문으로 인해 나타난 루프(Loop)에서 반복의 횟수를 제어할 때 주로 사용되며, EAX와 같이 많은 연산에 사용될 수도 있다.


EDX 레지스터(Data Register)

EAX와 함께 연산 작업에 주로 사용되며, 특히 나눗셈의 경우 피제수(소수)를 EDX에 넣어서 연산하며 연산 결과 몫은 EAX에 나머지는 EDX에 입력된다.


EBX(Base Index Register)

베이스 인덱스를 지정하는 용도로 사용되지만, 다른 레지스터 또한 베이스 인덱스를 지정하는데 자주 사용된다. 따라서 EBX는 특정한 역할을 갖기보다는 주로 변하지 않는 값을 저장할 때 사용된다.


ESI(Source Index Register) & EDI(Destination Index)

ESI와 ESI의 경우 반복문 등을 통하여 ESI에 있는 값을 EDI에 복사하고자 할 때 사용된다. 따라서 복사할 데이터는 ESI에 존재하고 있으며, 복사되어 저장될 새로운 공간을 EDI가 가리키고 있다.


ESP(Stack Pointer Register) & EBP(Base Pointer Register)

하나의 스택 프레임에 있어서 ESP는 스택의 끝 위치를 나타내며 EBP의 경우 스택의 시작 위치를 나타낸다. EBP의 경우 스택 프레임의 시작 위치 값을 갖고 있기 때문에 값이 잘 변하지 않지만, ESP의 경우 스택에 PUSH와 POP 명령어 등을 사용하기 때문에 유동적으로 값이 자주 변화한다.


EIP(Instruction Pointer)

EIP에는 다음에 실행해야 할 명령어가 존재하는 메모리 주소가 저장된다. 현재 명령어를 실행 완료한 후 EIP레지스터에 저장되어 있는 주소에 위치한 명령어를 실행하게 된다. 



세그먼트 레지스터


세그먼트는 프로그램에 정의된 메모리 상의 특정 영역으로 코드, 데이터, 스택 등을 포함하며 메모리의 대부분에 위치할 수 있다. 각 세그먼트 레지스터는 자신에게 지정된 주소를 가리키고 있으며, 기본적으로 CS, DS, SS 3개의 레지스터가 사용되며 이외에 ES, FS, GS 레지스터가 필요에 따라 사용될 수 있다.


CS (Code Register)

CS 레지스터는 코드 세그먼트의 시작 주소를 가리키며, 해당 세그먼트 주소에 EIP 레지스터의 오프셋 값을 더하면, 실행하기 위해 메모리로부터 가져와야 할 명령어의 주소가 된다. 일반적인 프로그래밍에서는 이 레지스터를 직접 참조할 필요가 없다.


DS (Data Register)

DS 레지스터는 프로그램의 데이터 세그먼트의 시작 주소를 포함한다. 명령어는 이 주소를 사용하여 데이터의 위치를 알아내며, 이 주소에 EIP 값을 더하면 데이터 세그먼트에 속한 특정 바이트 위치에 대한 참조가 생성된다.


SS (Stack Register)

SS 레지스터는 메모리 상에 스택의 구현을 가능하게 한다. 프로그램은 주소와 데이터의 임시 저장 목적으로 스택을 사용한다. 시스템은 프로그램의 스택 세그먼트의 시작 주소를 SS레지스터에 저장하며, 이 세그먼트 주소에 ESP 레지스터의 오프셋 값을 더하면 참조되고 있는 스택의 현재 워드를 가리킨다.


ES & FS & GS

이 3개의 레지스터는 Extra Segment로 여분의 데이터 세그먼트이다. ES의 경우 주로 문자열과 관련된 명령어를 위해 사용되는 세그먼트며, FS 세그먼트의 경우 TIB를 가리키는 세그먼트로 주로 안티 디버깅에 의해 참조될 수 있다. 특히 FS:[0x30]의 경우 PEB(Process Environment Block)를 가리키고 있기 때문에 디버깅 여부를 확인할 수가 있다. 이와 같이 FS의 경우 특정한 용도로 사용된다. 마지막으로 GS의 경우도 여분의 데이터 세그먼트로 주로 스택 스매싱이 일어났는지 확인할 때 사용할 수 있다.



플래그 레지스터


32비트 플래그는 다양한 컴퓨터 행동의 상태를 나타내는 비트를 포함하고 있다. 많은 플래그가 존재하고 있지만, 여기서는 OllyDBG와 같이 디버거에서 자주 볼 수 있는 플래그들에 대해서만 알아보자.


CF (Carry flag), 연산을 수행하면서 carry 혹은 borrow가 발생하면 1이 된다. Carry와 Borrow는 덧셈 연산 시 bit bound를 넘어가거나 뺄셈을 하는데 빌려오는 경우를 말한다.


PF (Parity flag), 연산 결과 최하위 바이트의 값이 짝수일 경우에 1이 되며 홀수일 경우 0이 된다. 이는 패리티 체크를 하기 위해 사용된다.


AF (Adjust flag), 8(16) 비트 연산에서, 하위 4(8) 비트로부터 상위 4(8) 비트로 자리올림이나 내림이 발생한 경우에 1로 셋 되고 그 외의 경우 0으로 셋 된다. 


ZF (Zero flag), 산술 및 논리 연산의 결과가 0 일 때 설정된다. 만약 연산의 결과가 0이 아닌 경우 해당 플래그는 0으로 나타나며, 연산 결과가 0일 경우 플래그가 셋 되어 1이 된다.


SF (Sign flag), 부호 비트가 1인 경우에는 1로 설정되고, 0인 경우 0으로 설정된다. 이는 다시 말해 음수인 경우에 1이 되는 것이며, 양수인 경우 0 임을 뜻한다.


TF (Trap flag), 디버깅에 사용되는 플래그로 설정된 경우 CPU는 명령 하나를 실행할 때마다 자동적으로 내부 인터럽트 1(INT1)이 발생한다. 해당 플래그가 설정된 경우 디버깅 시 Sing-Step이 가능해진다.


DF (Direction flag), 문자열을 처리할 때 해당 플래그가 0일 경우 문자열의 번지를 나타내는 레지스터 값이 자동으로 증가하고, 해당 플래그가 1일 경우 이러한 번지를 나타내는 값이 자동으로 감소한다.


OF (Overflow flag), 정수형 결과 값이 너무 큰 양수이거나 너무 작은 음수여서 피연산자의 데이터 타입에 모두 들어가지 않을 경우 1이 된다.



Reference


http://mafa.tistory.com/entry/3장-CPU-레지스터

http://carpedm20.blogspot.kr/2012/08/blog-post_13.html

https://en.wikipedia.org/wiki/Win32_Thread_Information_Block#Accessing_the_TIB

https://en.wikipedia.org/wiki/Process_Environment_Block

http://egloos.zum.com/voals/v/1669866

http://karfn84.tistory.com/entry/어셈블리-레지스터의-기능


'Reversing > Theory' 카테고리의 다른 글

BOF에 취약한 함수  (1) 2016.03.30
윈도우 메모리구조와 메모리분석 기초  (3) 2016.03.29
C기본 문법 어셈블리 변환  (5) 2016.03.20
Visual Studio 메인함수 찾기  (1) 2016.03.16
ClamAV & PEiD to Yara Rules  (1) 2016.03.11

'Reversing > Malware Analysis' 카테고리의 다른 글

공인인증서 탈취 악성코드  (0) 2016.08.06
[Malware] PETYA.exe 분석  (2) 2016.04.02
[Ransomware] .micro 랜섬웨어 분석 보고서  (1) 2016.02.22
ixaobny.exe 분석 보고서  (0) 2016.02.16
90u7f65d.exe Malware Analysis  (0) 2015.12.28
개요

보안 공부를 하면서 가장 많이 하는 말은 "어떤 공부부터 시작해야 하나요?"라는 질문이며, 이에 대한 답으로 흔히 "C언어부터 공부하세요."라고 한다. 이처럼 C언어는 프로그래밍의 기본을 이해할 수 있게 해주며 이에 대한 이해는 이후 다른 프로그래밍 언어나 리버싱에도 영향을 미치게 된다. 그렇기에 C언어에 대해 다시 학습을 하면서 리버싱까지 겸하여 공부하기 위해 이번 문서를 준비하였다.


이번 문서에서는 C언어에 대한 입문적인 단계를 다루는 것이 아니다. C언어와 같은 프로그래밍 언어들 컴파일되어 사람이 읽을 수 없는 기계어의 형태로 나타나게 되며 이러한 기계어를 사람이 읽을 수 있는 형태로 변화하는 것이 바로 디스 어셈블링이다. 따라서 바로 이러한 디스 어셈블링 된 C언어의 기본 문법을 살펴보고자 한다. 이러한 이해는 이후 악성코드를 분석하거나 리버싱을 할 때, 해당 명령어가 왜 존재하는지 이해하는데 도움을 줄 것이다.


Return 호출

C언어에 있어서 가장 자주 사용되는 예제는 바로 Hello World를 출력하는 코드일 것이다. 하지만 printf와 같이 출력 함수에 대해서는 이후에 다룰 것이며, 여기서는 return 0;에 대해서만 알아보자. 아래의 코드는 아무런 기능이 없는 메인 함수로 호출된 후 바로 0을 반환한다. 이에 대하여 아래의 코드와 그림을 보자.

#include <stdio.h>
int main()
{
     return 0;
}

*main+8에 보면 mov eax, 0x0라는 명령어를 볼 수 있다. 해당 명령어가 바로 return 0;을 나타내는 부분으로 eax에 값 0을 넣는 것이다. 여기서 프로세스의 구조에 대해 잘 모르는 사람은 저것이 왜 필요한지 모를 수가 있다. 이에 대해 같이 설명을 하자면 하나의 프로세스는 메인 함수로만 구성되어 있는 것이 아니라, 프로그램이 동작하기 위한 다른 함수 및 명령어들과 같이 이루어져 있다. 아래의 그림을 보자.

그림은 하나의 프로세스를 나타내는 것으로 "Main() == Process"이 아니라 "Main() in Process"와 같은 형태이다. 따라서 메인 함수에서 0을 반환하는 것은 일반적으로 우리가 제작한 부분이 아닌 곳에 반환에 되는 것이다. 만약 메인 함수 외에 다른 함수를 만들어 return 0을 할 경우 이는 메인 함수로 반환되는 것과 같다. 메인 함수도 프로세스의 일부에 불과하기 때문에 반환 값이 존재할 수 있는 것이다.


만약 위 코드에서 return 0을 없앨 경우 어셈블리에서는 *main+3 한 줄만 사라지고 나머지는 똑같다. 보통 반환 값은 EAX에 넣는 경우가 일반적이며, 바로 위 코드에서 그러한 역할을 하고 있는 것을 알 수가 있다. 만약 0이 아닌 값을 반환할 경우, 가령 return 1, mov eax, 0x1이라는 어셈블리의 형태로 나타나게 된다.


int 선언

C언어에서는 변수를 사용하기 전에 먼저 선언을 해놓아야 한다. 이러한 변수가 선언되어 값이 주어질 때, 어셈블리에서는 어떻게 나타날까? 이에 대하여 알아보자. 우선 비교를 위하여 두 개의 코드를 비교할 것이다. 우선 아래의 코드와 그림을 보자.


int형 변수 a를 선언하였고 a에 1이라는 값을 넣어주었다. 그렇다면 이는 어떠한 형태의 어셈블리어로 나타날까? *main+3의 sub esp, 0x10으로 스택에 0x10만큼의 공간을 할당한 뒤, mov 명령어를 통해 스택의 한 공간[ebp-0x4]에 1의 값을 넣어주고 있다. 바로 이렇게 우리가 선언한 변수는 스택의 한 "공간"으로 자리 잡게 되는 것이다. 

#inclue <stdio.h>
int main()
{
    int a=1;
    return 0;
}

그렇다면 여러 개의 int형 변수를 선언해주면 어떻게 될까? 이번에는 int형 변수를 5개 선언하였으며 각 변수에 값을 넣었다. *main+6부터 할당된 공간 중 하나씩 각 변수의 값이 주어져 들어가게 된다. [ebp-0x14]는 int a를 나타내며 [ebp-0x10]은 int b를 나타내며 이렇게 총 5개의 공간에 값이 채워진다. 

#include <stdio.h>
int main()
{
    int a=1;
    int b=2;
    int c=3;
    int d=4;
    int e=5;
    return 0;
}

하지만 한 가지 더 보아야 할 요소가 있다. 바로 첫 번째와 두 번째 코드의 *main+3 부분을 보면 sub 명령어를 통해 스택에 공간을 할당한다. 첫 번째 예제에서는 분명 0x10만큼 할당했지만 두 번째 예제에서는 0x20만큼의 공간을 할당하였다.


이는 자료형의 크기에 대해 먼저 알아야 한다. 하나의 int형 변수는 4바이트의 크기를 갖기 때문에, 첫 예제에서는 4바이트의 변수가 하나 존재하였기 때문에 0x10만큼의 공간만 할당했어도 충분하였다. 이러한 공간은 int형 변수가 4개(16 바이트)까지 선언되어도 모두 담을 수가 있다. 하지만 두 번째 예제에서는 int형 변수가 5개 선언되었기 때문에 최소 20바이트가 필요하다. 그렇기에 0x10만큼을 더 할당하므로 32(0x20)만큼의 공간을 할당한 것이다. 만약 변수의 수가 늘어나면 또다시 스택에 할당되는 크기는 증가할 것이다.


printf 함수

Hello World를 출력할 때 가장 많이 사용하는 함수가 바로 printf로, 이는 아마 C언어를 배우는 사람이 가장 처음 배우는 함수일 것이다. 이러한 printf가 어떻게 사용되는지 확인해보자. 우선 가장 기본적인 형태로 간단한 문자열을 출력하는 코드를 보자. printf를 제외하고 다른 내용은 아무것도 존재하지 않는다. 디스 어셈블링 된 코드를 보면 call 명령어와 함께 printf를 호출한다는 것을 확인할 수 있다.


하지만 여기서 중요한 것은 바로 call 명령어의 바로 위에 위치한 mov 명령어이다. ESP는 현재 스택의 최상단(제일 낮은 값)을 가리키고 있는데, 바로 이 부분에 0x80484d0을 넣어주는는데 바로 이 주소에는 printf 함수에 사용될 문자열인 "Hello"가 존재하고 있다. 이와 같이 MOV를 통해 스택에 바로 값을 넣을 수가 있으며, 이와는 다르게 push 명령어를 통해 해당 값을 스택에 넣을 수도 있다.

#include <stdio.h>
int main()
{
    printf("Hello");
}

위의 경우 바로 문자열을 넣어주었다. 그렇다면 이번에는 변수를 하나 선언하여 값을 저장한 다음 이를 출력해보자. 아래의 코드와 같이 int형 변수 a를 선언한 뒤 10이라는 값을 넣었다. 그 후 printf를 통해 "%d\n", 그리고 a를 인자로 주었는데 이에 대해 변환한 코드를 보면 역시 call 명령어를 통해 printf를 호출하고 있다.


하지만 위와는 다르게 int a에 10(0xa)이라는 값을 주었기에 *main+9에 mov 명령어를 통해 주어진 스택의 공간에 0xa라는 값을 넣는 것을 확인할 수 있다. 그다음 해당 값을 eax에 저장한 다음 이를 스택에 넣는 것을 확인할 수 있다. 그다음 스택의 최상단 ESP에 0x80484e0의 값을 넣는다. 이는 아래에서 확인한 바와 같이 "%d\n"라는 문자열을 나타내고 있다.

#includ <stdio.h>
int main()
{
    int a=10;
    printf("%d\n",a);
}

어떠한 함수를 호출하는 데 있어 인자가 스택에 역순으로 놓이게 된다. 스택의 특성상 최상단(가장 낮은 값=ESP)에 있는 값부터 빼내기 때문에 스택에 "% d\n"이 a보다 상단에(낮은 주소)에 위치해있어야 한다. 


* 참고 : *main+3의 and 명령어는 스택의 주소를 16 단위에 맞추기 위해 사용되며 이로 인해 스택에 할당되는 공간이 넓어지는 효과가 있다. 하지만 이번 학습에서는 중요하지 않은 내용이기에 자세히 다루진 않는다.



scanf 함수

Scanf 함수의 경우 사용자가 입력한 내용의 문자열을 입력받아 지정된 변수에 해당 내용을 저장한다. 여기서 한 가지 알아야 할 것은, prinf 함수에서는 "%d", a 의 형태로 인자를 주었지만, scanf 함수에서는 a의 앞에 &을 붙여야 한다. 이는 변수 a의 주소를 넘겨주는 것으로 이렇게 주소를 넘겨주는 이유는 다음과 같다. 함수가 다른 함수를 호출할 때 인자를 넣어주는데, 이러한 인자는 보통 값의 "복사"를 통해서 이루어진다. 그렇기에 A함수에서 B함수로 어떠한 인자를 넣어준 다음, B에서 해당 값을 변경하더라도 A에는 미치는 영향이 없다. 따라서, scanf함수에서는 &a와 같이 변수 a의 주소를 넘겨주어야 그곳에 올바르게 값을 저장할 수가 있다.

#include <stdio.h>
int main()
{
    int a;
    scanf("%d", &a);
    return 0;
}

*main+9~13에서 lea 명령어를 통해 변수 a에 할당된 주소를 스택에 넣어주는 것을 확인할 수가 있다. 그리고 *main+17에서 "%d"를 인자로 넣어주므로 scanf("%d",&a);가 완성이 된다.  단, 여기서 만약 int형이나 char형이 아닌 배열이나 포인터가 올 경우 그 자체가 포인터를 지칭하고 있으므로 &를 넣어줄 필요가 없다.


두 번째 예제는 세 개의 연속된 인자를 넣어주었다. 위 예제와 마찬가지로 lea 명령어를 통해 스택에서 변수를 위한 공간을 각 각 할당받으며, 할당과 동시에 해당 주소를 스택에 넣어주는 것을 확인할 수 있다. 여기서 자세히 보아야 할 것은 printf에서는 바로 스택에 그 값을 넣어주었지만, scanf에서는 주소를 먼저 할당한 뒤, 그 주소를 스택에 넣었다는 것이다.

#include <stdio.h>
int main()
{
    int a,b,c;
    scanf("%d %d %d", &a, &b, &c);
    return 0;
}

While & For 

이번에는 C언어에서 반복문에 주로 사용되는 두 가지 문법 While과 For에 대하여 알아보자. 우선 두 가지 문법에 있어서 어떠한 것이 편한지는 상황에 따라서 다르다. 필자 개인적으로는 while 문의 경우 while(1)과 같이 제작할 때 편하게 사용할 수가 있으며, for문의 경우 어떠한 조건이 따라올 경우 사용하기 편하다. 하지만 이에 대해선 제작자에 의해 차이가 있으므로 자신의 맞게 사용하면 된다.


우선 While 문에 대하여 알아보자. a라는 int형 변수를 선언한 다음, while 문을 통해 a가 0부터 9까지 출력되도록 하였다. 코드 자체는 쉬우므로 추가적인 설명을 하지 않고 바로 어셈블리어를 확인하자. 우선 스택 프레임을 구성하고, 메인 함수를 위한 스택을 0x20만큼 할당한다. 그 후 [esp+0x1c]에 변수 a의 값 0을 넣어준 뒤 바로 main+44로 점프하는 것을 확인할 수 있다. main+44와 main+49에서는 변수 a의 값이 존재하고 있는 [esp+0x1c]의 값을 0x9와 비교한 다음, 만약 9와 같거나 이보다 작은 경우 main+19 지점으로 점프한다.


이렇게 점프한 다음 해당 a의 값을 EAX에 넣은 뒤, 이를 [esp+0x4]에 printf의 인자로 넣어준다. 그 후 printf의 0x80484f0에 존재하는 "%d"를 [esp]에 넣어주고 printf를 호출한다. printf를 통해 값이 출력되고 [esp+0x1c] 변수 a에 1을 더하는 것을 확인할 수 있다. 이렇게 1을 더해진 a는 다시 cmp를  통해 9보다 작거나 같은지 확인하는 작업을 반복한다. a 값이 하나씩 증가하여 a가 9가 된 경우 printf를 통해 9를 출력한 다음, 1이 더해져 10이 되고 cmp 명령어와 jle 명령어를 통해 main+51로 넘어가는 것을 확인할 수 있다.

#include <stdio.h>
int main()
{
    int a=0;
    while(a<10)
    {
        printf("%d",a);
        a++;
    }
    return 0;
}

for 문의 경우 while문과 비슷하게 사용된다는 것은 위에서 설명하였다. 이 역시 문법적으로는 비슷하므로 설명하지 않고 어셈블리어를 확인해보자. for문을 통해 역시 a가 0부터 9까지 출력되도록 하였다. GDB를 통해 열어서 확인한 결과 신기할 정도로 위의 while문과 동일하게 나타난다. 


어셈블리의 면에서는 똑같으므로 결국 for문과 while문의 차이는 C언어를 통해 코딩을 하는 사람의 입장을 편하게 하기 위함이며, 어셈블리어나 기계어의 경우 이를 똑같이 인식한다는 것을 알 수 있다.

#include <stdio.h>
int main()
{
    int a;
    for(a=0;a<10;a++)
    {
        printf("%d",a);
    }
    return 0;
}


If & Switch

프로그래밍을 하면서 다양한 조건을 사용해야하는 경우가 있다. 이러한 경우에 사용할 수 있는 것이 바로 if와 switch로, 지정한 조건에 부합될 경우 이에 대하애 지정된 행동을 수행하도록 한다. 그렇다면 if와 switch에는 어떠한 차이가 있을까? if의 경우 else와 함께 사용하여 다양한 조건을 걸 수 있으며, switch의 경우 case와 default를 통해 조건을 지정할 수 있다.


if의 경우를 먼저 살펴보자. scanf 함수를 통해 숫자를 입력받고 각 숫자에 따라 어떠한 곳으로 지정된 행동을 수행하게 된다. a가 2 이하라면 각 숫자를 출력하고, 그 외의 경우 "a > 2"를 출력하게 되어있다. 

#include <stdio.h>
int main()
{
    int a;
    scanf("%d",&a);
    if(a==0)
        printf("a : 0");
    else if(a==1)
        printf("a : 1");
    else if(a==2)
        printf("a : 2");
    else
        printf("a  > 2");
    return 0;
}

어셈블리에서는  어떻게 나타날까? 우선 main+9를 보면  [esp+0x1c]에 변수 a의 주소를 인자로 가져간 다음 call _scanf_를 확인할 수 있다. 이렇게 사용자로부터 입력된 값을 main+29에서 eax에 넣는다. eax에 존재하는 a의 값은 바로 test eax, eax 명령어를 통해 0인지 아닌지 확인하게 된다. test eax, eax는 eax의 값이 0일 경우 점프 플래그를 설정하게 된다. 값이 0일 경우 0x8048583에 있는 문자열 "a : 0"을 인자로 주고 printf 함수를 호출한 다음 종료한다.


하지만 만약 main+33에서 0이 아닌 값이 존재할 경우 main+51로 점프하게 된다. 다시 main+51에서 [esp+0x1c]의 값을 가져와 1과 비교한다. 만약 1이 아닐 경우 main+74로 넘어가게 되고, 해당 부분에선 다시 2와 비교한다. 만약 2 또한 아닐 경우 main+97로 넘어가 "a > 2"를 인자로 주어 출력한다.

Switch문의 경우 if 문과 유사한 형태를 갖는다. 이전과 마찬가지로 사용자에 따라 switch를 사용할 수도 있고 if 문을 사용할 수도 있다. switch 문의 경우 case를 통해 값을 지정할 수 있으며, if문의 else는 default를 통해 나타낸다. 아래의 코드를 보면 위와 마찬가지로 scanf를 통해 값을 입력 받고 어떠한 조건에 해당하는지 확인한 후 그에 맞는 문자열을 출력한다.

#include <stdio.h>
int main()
{
    int a;
    scanf("%d", &a);
    switch(a)
    {
        case 0:
            printf("a : 0");
            break;
        case 1:
            printf("a : 1");
            break;
        case 2:
            printf("a : 2");
            break;
        default:
            printf("a > 2");
    }
}


scanf 함수까지는 이전과 동일하므로 생략하겠다. main+29부터 a의 값을 eax에 넣은 후 cmp 명령어를 통해 1과 비교한다. 만약 1이 맞다면 바로 main+61로 점프를 하게되고, 아닐 경우 해당 값을 바로 2와 비교한다. 그리고 test 명령어를 통해 0인지 비교하며, 만약 0이 아닌 경우 main+89로 점프하게 되고 0일 경우 main+47을 인자로 printf 함수를 호출한다.

두 코드의 차이점에 대하여 알아보자. if-else 문의 경우 하나의 비교 명령어를 지나면 다시 변수 a의 값을 가져온 후 다시 비교를 하는 형태로 진행되었다. 이에 반해 switch문의 경우 main+29에서 eax 레지스터에 단 한번 넣은 상태로 지정된 값들과 비교하는 형태로 진행된다. 


Array & Pointer

C언어서 배열과 포인터는 밀접한 관련이 있다. 그렇기에 같은 문자열을 하나는 배열의 형태로, 다른 하나는 포인터의 형태로 선언한 다음 이를 출력하는 내용의 코드를 분석해보자. char형 배열 arr을 선언하여 "Hello World!\n"라는 문자열을 넣어주었다. 그 후 printf 함수를 통해 arr을 출력하는 코드이다. 어셈블리를 확인하기 이전에 코드가 복잡해 보일 수 있는데, 버퍼오버플로우 등을 확인하기 위한 코드이므로 현재는 이에 대하여 자세히 알 필요는 없다. 따라서 우리가 확인해야할 부분은 main+21부터 main+64까지이다.


main+21과 +29 +37 + 45를 보면 [esp+0x??]에 어떠한 값들을 넣는 것을 확인할 수 있다. 이 값들은 바로 "Hello World!\n"에 대한 문자열로 [esp+0x1e]부터 [esp+0x2a]까지 넣는 것임을 알 수 있다. 그리고 main+52에서 문자열이 시작되는 주소 [esp+0x1e]의 주소를 eax에 넘기고 이를 printf 함수(또는 puts)의 인자로 넣는다.

#include <stdio.h>
int main()
{
    char arr[] = "Hello World!\n";
    printf("%s\n",arr);
    return 0;
}

포인터를 통해 선언한 경우 main+9를 확인하면 [esp+0x1c]에 0x80484e0를 넣어준다. 이렇게 넣어진 값은 "Hello World!\n"를 포함하고 있는 주소이며, 해당 주소는 printf(또는 puts) 함수의 인자로 넘어가 결과를 출력하게 된다.

#include <stdio.h>
int main()
{
    char *p = "Hello World!\n";
    printf("%s\n",p);
    return 0;
}

'Reversing > Theory' 카테고리의 다른 글

윈도우 메모리구조와 메모리분석 기초  (3) 2016.03.29
CPU 레지스터  (0) 2016.03.26
Visual Studio 메인함수 찾기  (1) 2016.03.16
ClamAV & PEiD to Yara Rules  (1) 2016.03.11
Yara 규칙 제작 & Python  (1) 2016.03.07
개요

리버싱을 공부하면서 자신이 만든 프로그램을 직접 디버깅해보고 싶단 의욕이 생기는 일은 당연하다. 하지만 처음 리버싱을 공부하는 입장이 아니라면 보통 Abex's CrackMe부터 다양한 문제를 통해 먼저 접해본 경험이 많을 것이다. 이러한 프로그램들은 대부분 쉽게 리버싱을 학습하도록 메인 함수 부분이 바로 나타나거나 찾기 쉽도록 되어 있다. 따라서 직접 간단한 파일을 컴파일하여 디버거로 열어본다면 복잡해 보이는 엔진 코드로 인해 당황하게 된다. 필자 또한 리버싱을 접한지 얼마 안 되었을 때, 매우 당황했던 것을 기억한다.


우선 필자의 컴파일 환경은 WIndows10 x64, Visual Studio 2013이다. 실습을 위해 C언어로 대부분의 사람들은 모두 접해보았을 "Hello World!"를 출력하는 문구를 만들었다. 사용된 코드는 다음과 같다.

#include <stdio.h>

int main()
{
    printf("Hello World!\n");
}

이후 빌드를 한 후 exe 파일의 형태로 존재하는 것을 확인할 수 있다. 바로 이 EXE 파일이 이번 실습의 대상이다. 이제 OllyDBG를 통해 해당 파일을 열어보자.


디버깅

열자마자 너무나 많은 JMP 명령어가 존재하고 있다. 하지만 겁먹지 말자, mainCRTStartup이라 친절하게 나와있듯이 우리는 바로 첫 줄에 해당하는 부분으로 이동해 확인해주면 된다. OllyDBG를 통해 JMP 명령어를 실행해보자.

해당 부분으로 이동하면 PUSH EBP로 시작하여 RETN으로 끝나는 지점을 확인할 수 있다. 우선 맨 위의 두 줄은 흔히 스택 프레임을 구성하는 부분으로, 스택 프레임이란 해당 함수가 가지는 공간이라고 볼 수 있다. 스택에 현재 EBP(Base Pointer)를 넣어주므로 현재 위치로 다시 되돌아 올 수 있도록 한다. EBP에는 현재 스택의 최하점을 가리키고 있는 것으로 이를 기억하기 위해 PUSH EBP 명령을 해주는 것이다. 다음으로 MOV 명령은 방금 넣은 EBP에 이제 새로운 주소 공간을 위한 값(ESP)을 넣어주는 것이다. 이렇게 ESP는 보통 수시로 값이 변하기 때문에 EBP가 기억 지점 같은 역할을 하는 것이다. 

이렇게 스택 프레임을 구성한 후 CALL 명령을 통해 두 번의 호출이 이루어지는 것을 볼 수 있다. 첫 번째 호출 부분의 경우 Step Into를 통해 자세히 확인해보면 다른 함수를 호출하는 부분은 존재하지 않다. 따라서 우리는 해당 부분은 넘어가 주면 된다. 이제 OllyDBG에서도 친절히 _tmainCRTStartup이라 되어 있는 부분으로 들어가 보자.

위 사진으로 나타난 부분이 다가 아니며 많은 부분이 CALL을 통해 새로운 함수들을 호출한다. 이러한 호출은 컴파일된 프로그램이 기본적인 구성을 확인하기 위한 작업으로 이상이 있다면 프로그램이 정상적으로 실행되지 않고 종료된다. 메인 함수까지 약 7개의 함수 호출이 더 존재하기 때문에 이는 아래의 그림과 같이 시각화하였다.

최초 Jmp를 통해 mainCRPStartup에 진입한 뒤, 두 개의 CALL 명령어 중 _tmainCRTStartup을 호출한다. 그리고 바로 이 _tmainCRTStartup에는 NTCurretnTab, _amsg_exit 등의 함수들을 호출하며 CrtSetCheckCount를 호출한 뒤 우리가 찾던 Main() 함수가 존재하였다. 해당 부분을 확인하면 다음과 그림과 같다.

최초 C언어에 비해 매우 복잡한 모습을 하고 있다. 이에 대해 간략히 말하자면 보안에 대하여 위협이 지금처럼 심하지 않을 당시엔 매우 간단하게 컴파일이 되어 가시적이었다. 하지만 지금은 수많은 공격 기법이 존재하므로 이러한 공격 기법을 방지하고자 이에 대한 요소까지 같이 포함되기 때문에 ESP를 체크하는 등이 추가된 것이다.


이렇게 우리는 메인 함수로 올 수 있었다. 버전이나 컴파일러에 따라 상이하므로, 최대한 많이 찾아보며 어느 부분에서 메인 함수를 찾아야 할지 익숙해지는 것이 좋다.



'Reversing > Theory' 카테고리의 다른 글

CPU 레지스터  (0) 2016.03.26
C기본 문법 어셈블리 변환  (5) 2016.03.20
ClamAV & PEiD to Yara Rules  (1) 2016.03.11
Yara 규칙 제작 & Python  (1) 2016.03.07
Yara를 사용해보자  (0) 2016.03.06
개요
Yara를 통해 특정 패턴을 기준으로 파일을 매칭 할 수가 있음을 저번 문서를 통해 알 수 있었다. 그렇다면 이러한 룰은 어떻게 생성되어야 할까? 우선 고려해야 할 사항으로 다른 정상적인 파일에 포함되어 있는 패턴은 배제시켜야 한다. 따라서 이번 문서는 다른 파일과 겹치지 않는 문자열을 걸러내도록 할 것이다. 

이를 위해 구상한 내용은 다음과 같다. 우선 정상적인 파일로부터 문자열을 추출한 다음, 이를 DB로 만들어 Yara 규칙을 제작해야 하는 대상 악성코드의 문자열을 DB와 비교한다. 만약 DB에 이미 해당 문자열이 존재할 경우, 해당 문자열은 정상적인 파일에 포함되어 있을 수 있으므로 제외한다. 이러한 작업을 반복하여 결국 DB에 포함되지 않은 악성코드 특유의 문자열만 남게 된다. 그리고 이러한 문자열을 통해 규칙을 생성할 것이다.
DB 생성

DB를 생성하기 위하여 두 가지 준비가 필요하다. 우선 Python을 통해 DB를 생성할 것이므로 Python을 설치해야 하며, 다른 준비물로는 Sysinternals의 Strgins가 필요하다. 아래의 링크를 통해 각 도구를 다운로드 하자.

[+] Python : https://www.python.org/downloads/

[+] Strigns : https://technet.microsoft.com/en-us/sysinternals/strings.aspx

프로그래밍을 해야 하지만, strings를 통해 어렵지 않을 것이다. 우선 여러 파일의 문자열을 넣어야 하므로 파일의 목록을 가지고 올 수 있는 부분을 먼저 제작해보자. 특정 폴더만을 지정할 경우 크게 두 가지 방법이 있는데, 하나는 os.listdir()을 통해 얻을 수가 있으며 다른 하나는 아래의 코드와 같이 지정된 디렉터리에 존재하는 하위 디렉터리까지 파일의 목록을 얻을 수가 있다.

위와 같이 파일의 목록을 얻은 다음 이를 아래의 Filelist 변수에 넣어주었다. 그리고 os.system 명령어를 통해 strings를 사용하여 하나의 파일씩 DB에 추가한다. 이렇게 지정된 모든 파일의 문자열이 DB에 존재하게 된다. 하지만 여기서 문제가 생긴다. 수많은 PE 파일들이 존재하기 때문에 공통적으로 포함되어 있는 문자열인 'MZ', 'PE', '. code', '. data' 등은 수 없이 중복되어 용량이 너무 커지게 된다. 따라서 모든 파일의 문자열을 추가한 다음, 이러한 중복 문자열을 제거하기 위한 코드를 제작하여야 한다. self.DB_Update()가 바로 그 내용으로 다음 그림에서 확인하자.

아래의 그림과 같이 'GenDB.tmp'라는 새로운 임시파일을 생성하여 기존의 'GenDB.db'의 내용을 0x2000000 씩 읽는다. 이렇게 읽은 문자열들을 배열로 놓기 위하여 split()를 사용하였으며, 이를 통해 arr이라는 배열(리스트)에 존재하게 된다. 파이썬에서 배열의 경우 자체적으로 아이템의 중복을 제거하기 위한 함수가 존재하는데 바로 set(arr)이다. 따라서 아래와 같이 arr=list(set(arr))을 통해 아이템의 중복을 제거한다. 중복이 제거된 문자열은 GenDB.tmp에 기록되고, 모든 바이트를 읽은 다음 GenDB.tmp를 GenDB.db로 이름을 변경한다.

위에서 0x2000000씩 읽는 이유는, buf = f.read()와 같이 DB의 내용을 한 번에 변수에 담으면 좋겠지만, DB는 말 그대로 데이터 베이스이기 때문에 수많은 파일의 문자열이 들어가게 될 경우 용량이 거대해진다. 따라서 이렇게 거대한 용량을 한 번에 읽으려 하면 파이썬은 "MemoryError"를 반환하게 되어 프로그램이 해당 데이터를 제대로 읽을 수 없게 된다. 그렇기에 이를 방지하기 위하여 지정된 바이트씩 읽는다. 

하지만 이 방법 또한 단점은 존재한다. 0x2000000씩 읽기 때문에, 0x2000000 만큼의 문자열에서만 중복되는 문자열이 제거된다. 즉, 0x000000에 'AAA"라는 문자열이 있고 0x2000010에도 'AAA'라는 문자열이 있을 경우 제거되지 않는다. 이러한 단점이 존재하지만 DB의 크기를 줄이는 것이 목적이기 때문에 몇 개는 남더라도 지우는 것이 훨씬 효과적이라 생각한다.

실습을 위하여 %SystemRoot%\System32\에 존재하는 3903개의 파일을 복사하였다. 이를 통해 DB를 생성하였으며, 최초 중복을 제거하기 전 파일의 크기는 약 400MB지만 중복을 제거한 뒤 DB의 크기는 약 135MB로 감소하였다. 이를 통해 중복제거가 완벽하지는 않지만 충분히 효과가 있다는 점을 알 수 있다.

DB 비교 & 패턴 생성

이제 약 3903개의 파일에 대하여 DB가 생성되었으니 한 번 성능을 테스트해보자. 대상으로 사용할 파일은 "Abex's Crackme01"을 UPX로 실행 압축한 파일이다. 따라서 DB에 존재하지 않다면 "UPX0", "UPX1"과 같은 내용들이 결과로 출력될 것이며 만약 DB에 UPX 관련 파일이 있을 경우 출력되지 않을 것이다. 우선 비교를 위한 코드는 다음과 같다.

이번에도 Strings.exe를 통해 문자열을 추출한 다음, 중복 문자열을 제거한다. 중복이 제거된 문자열을 배열의 형태를 통해 DB의 문자열 하나씩 비교한다. 이를 통해 DB에 일치하는 내용이 없는 문자열만 결과로 출력한다. 이제 이렇게 완성된 코드를 통해 파일을 비교해보자. 해당 결과는 다음과 같다.

abex's crackme01에서 볼 수 있는 몇 가지 문자열들이 결과로 출력되는 것을 확인할 수 있다. 대신 위에서 예상하였던 "UPX'와 관련된 문자열은 이미 DB에 존재하고 있기에 나타나지 않았다. 출력된 결과 중 몇 가지 문자열을 통해 Yara 규칙을 작성해보자.

아주 기초적으로 문자열 기반 규칙을 작성하였다. 그리고 해당 규칙을 적용하였을 때, 제대로 파일을 인식하는 것을 확인할 수 있었으며, 다른 3903개의 정상 파일에는 해당 시그니처가 부합되지 않다는 것 또한 확인할 수 있다.

직접 만들면서 부족한 프로그래밍임을 알 수 있었지만, 그래도 만들어보고 싶은 걸 초보적이 게나마 만들 수 있기에 공부하면서 재미있었다. 구글에 다른 훨씬 좋은 제너레이터가 존재하고 있으니, 그 코드들을 보며 다른 사람들은 어떻게 생각하면서 제작했는지 보는 것 또한 나쁘지 않을 것 같다. 아래는 부족하지만 내가 만든 위 프로그램의 코드이다.




'Reversing > Theory' 카테고리의 다른 글

Visual Studio 메인함수 찾기  (1) 2016.03.16
ClamAV & PEiD to Yara Rules  (1) 2016.03.11
Yara를 사용해보자  (0) 2016.03.06
악성코드 분류  (0) 2016.03.03
악성코드 분석 방법  (0) 2016.02.26