ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리버싱 핵심원리 study] 37장 x64 프로세서 이야기
    Reverse Engineering 2021. 2. 1. 19:11

    1. x64에서 추가/변경된 사항

    (1) 64비트

    메모리 주소가 64bit(8byte)로 표현된다. 마찬가지로 레지스터와 스택의 기본 단위도 64bit로 증가하였다.

     

    (2) 메모리

    가상 메모리의 크기가 2^64 = 16TB로 증가하였으며, User 영역 및 kernel 영역이 각각 8TB의 크기를 갖는다. (이론상 그러하며 실제로 사용되는 영역은 이보다 적다.)

     

    (3) 범용 레지스터

    레지스터들이 64bit로 확장되었으며, R8 ~ R15 레지스터가 추가되었고, R로 시작한다. (ex. EAX->RAX로 변경, RAX의 하위 32bit가 EAX)

     

    (4) CALL/JMP Instruction

    똑같은 기능을 하는 다음 두 명령어를 살펴보자.

     

    - x86

    => Addresss : 00401000     FF1500504000     CALL DWORD PTR DS:[00405000]

    00405000 주소가 가리키는 값으로 점프하기 위해 FF15 다음에 00405000 값이 나타나며, 이 값은 절대 주소로 취급된다.

     

    - x64

    => Address : 00000001`00401000     FF15FA3F0000     CALL QWORD PTR DS:[00000001`00405000]

    00405000 주소가 가리키는 곳으로 점프하지만, FF15 다음에 나타나는 값의 의미는 x86과는 다르다. x64에서는 상대 주소로 취급되므로 target address  = 00000001`00401000 + 00003FFA(op 다음 값) + 6(CALL 명령어 길이) = 00000001`00405000이 된다.

     

    (5) 함수 호출 규약

    함수 호출 규약에는 cdecl, stdcall, fastcall 등이 존재하는데, 64bit에서는 fastcall로 통일되었다. fastcall은 4개 이하의 argument에 대해서는 레지스터를 이용하는 특징이 있다.

    Argument 정수형 실수형
    1st RCX XMM0
    2nd RDX XMM1
    3rd R8 XMM2
    4th R9 XMM3

    argument가 4개를 넘어가는 경우 스택을 이용하며, 스택에 대한 정리는 Caller에서 담당한다. 또한 첫 4개의 argument는 레지스터로 전달되지만, 스택에 이 argument를 위한 공간 32byte를 예약해 놓는다는 특징이 있다.

     

    (6) 스택 & 스택 프레임

    (5)에서 설명한 내용 이외에도, 64bit 환경에서 스택 프레임을 구성할 때는 RBP를 이용하지 않고 RSP 레지스터를 직접 이용한다. 이러한 방식을 이용하면 스택 포인터를 정리할 필요가 없어 실행 속도를 향상 시킬 수 있다느 장점이 있다.

     

    2. 실습 Stack32.exe & Stack64.exe

    책에서는 다음과 같은 동일한 소스코드를 각각 x86, x64 환경에서 컴파일 한후, 각각 어떤 방식으로 코드가 전개되는지 디버깅한다.

     

    -Stack.cpp

    #include "stdio.h"
    #include "windows.h"
    
    void main()
    {
        HANDLE hFile = INVALID_HANDLE_VALUE;
        
        hFile = CreateFileA("c:\\work\\ReverseCore.txt",    // 1st - (string)
                            GENERIC_READ,                   // 2nd - 0x80000000
                            FILE_SHARE_READ,                // 3rd - 0x00000001
                            NULL,                           // 4th - 0000000000
                            OPEN_EXISTING,                  // 5th - 0x00000003
                            FILE_ATTRIBUTE_NORMAL,          // 6th - 0x00000080
                            NULL);                          // 7th - 0x00000000
    
        if( hFile != INVALID_HANDLE_VALUE )
            CloseHandle(hFile);
    }

    코드 내용 자체는 간단히 CreateFile() API를 호출하는 코드다.

     

    (1) Stack32.exe (x86)

    x32dbg를 이용해 Stack32.exe 파일을 열어보자.

    Stack32.exe main

    컴파일러 최적화 옵션에 의해 스택 프래임이 생략되었고, CreateFile() API, CloseHandle() API를 호출할 때 push 명령어로 stack을 통해 argument가 전달되는 것을 확인할 수 있다. 또한 Win32 API는 stdcall 방식을 사용하므로 스택 정리를 callee에서 하기 때문에 main 함수에서 스택 정리하는 부분을 찾아볼 수 없다. CreateFileA() API까지 F8을 눌러 Step over 한 후, F7(Step in)을 이용해 API 내부로 들어가 보자.

     

    CreateFile() API 내부

    우선 API 내부 앞부분에서 1. push ebp / mov ebp, esp를 통해 스택 프레임을 생성하는 것을 확인할 수 있다. 2.에서는 내부적으로 CreateFileW() API를 호출하기 위해 스택을 통해 argument를 전달하는 모습을 확인할 수 있으며, 3.에서 leave 명령어(=mov esp, ebp / pop ebp)를 통해 스택을 정리한다.

     

    (2) Stack64.exe (x64)

    x64dbg를 이용해 Stack64.exe를 열어보자.

    stack64.exe main

    우선 스택 프레임 부분을 보면 코드 시작 전에 sub rsp, 48을 통해 스택을 늘리고, ret 전에 add rsp,48을 통해 스택을 해제하는 것을 볼 수 있다. 또한 argument를 넘길 때 레지스터를 이용하고 앞에서 4개를 넘어서는 경우 mov 연산을 통해 늘어난 스택에 넘겨주는 것을 확인할 수 있다. 이때 넘어가는 5번째 argument는 [rsp]가 아닌 [rsp+20]에 저장되는 것을 확인할 수 있는데, 앞의 4개의 argument를 위한 공간을 스택에 예약하는 특징이 있기 때문이다. CreateFileA() API 내부로 들어가도 마찬가지의 형태를 보인다.

     

    3. Comment

    리버싱 핵심원리 책 초반부에 stack frame에 대한 내용을 배우고 문제들을 풀면서 디버깅을 진행할 때 함수를 찾으려 push ebp / mov ebp, esp 코드를 찾으려 했지만 찾아지지 않았던 기억이 있다. 오늘 실습을 진행하면서 x64와 x32의 차이점을 배움으로써 이전의 궁금증을 해소할 수 있었다.

     

     

    반응형

    댓글

Designed by Tistory.