본문 바로가기

Reversing/개념

[DreamHack] 2강 : x64 기초

어제에 이어 오늘은 2강

오늘은 가장 널리 쓰이는 Intel 구조의 64bit 버전 명령어 집합에서 쓰이는 x64 명령어 집합(x86-68 Instruction Set)에 대한 주제이다. 

내용이 워낙 많아서 2강에만 꼬박 하루가 걸렸다,,

 

 

<2강> : x64 기초

< 들어가기에 앞서 >

    1.  CPU의 기본 동작

       : < 실행할 명령어 읽어오기(Fetch) → 읽어온 명령어 해석하기(Decode) → 해석한 결과 실행하기(Execute) >

       : 위와 같이 기계 코드가 실행되는 한 번의 과정을 Instruction Cycle이라고 한다.

    2.  레지스터(Register)와 명령어(Instruction)

       - CPU는 Instrcution Cycle을 수행하기 위해 기계 코드에 해당하는 각종 명령어를 해석하기 위한 구성 요소 외에도

          읽어온 명령어가 저장된 공간을 임시로 기억해 둘 구성 요소나, 명령어를 실행한 결과를 저장해 둘 구성 요소

          필요

       - 레지스터(Register) : CPU의 동작에 필수적인 저장 공간의 역할을 하는 CPU의 구성 요소

       - 명령어(Instruction) : CPU가 실행 / 수행하는 동작에 따라 형태가 조금씩 다름

 

< 레지스터(Register) >

    : CPU가 사용하는 저장 공간

    : 대개의 경우 각각의 레지스터들은 특별히 쓰임새가 정해져 있지 않지만, 관행적으로 용도를 정해놓고

      사용하는 레지스터도 있고, 엄격히 정해진 용도로만 쓰이는 레지스터도 있다.

    1.  범용 레지스터 (General-Purpose Registers, GPR)

         :용도를 특별히 정해두지 않고 다양하게 쓸 수 있는 레지스터

         : CPU에게 연습장과 같은 레지스터로, x64의 범용 레지스터는 총 16개이다.

           위 사진의 빨간 글씨로 표시된 레지스터들이 이에 해당한다.

            (범용 레지스터는 원칙적으로 용도가 정해져 있진 않지만, 관행적으로 쓰임새가 정해져 있는 경우도 존재함)

< 범용 레지스터의 종류 >
1.  rax 
     :  함수가 실행된 후 리턴값을 저장하기 위해 사용
        (어떤 함수의 실행이 종료되고 난 후, 해당 함수의 결과값이 반환될 때, rax 레지스터에 담겨 반환됨.)
     :  단, 리턴값을 위해서만 사용되지는 않고, 함수 반환전까지는 범용 레지스터로 자유롭게 사용되다가,
         종료 후 리턴값을 반환하기 위한 레지스터로는 rax만이 사용됨
2.  rcx, rdx, r8, r9 ~ r15
     :  Windows 64bit에서 함수를 호출할 때 필요한 인자들을 순서대로 저장
        (첫번째 인자는 rcx, 두번째 인자는 rdx에, ····)
     : 함수 호출 규약(Calling Convention) : 함수가 실행될 때 필요한 인자들을 저장하는 용도로 사용
3.  rsp
     : 다른 범용 레지스터들과 달리 용도가 정해져 있음
     : 스팩 포인터(Stack Pointer)로, 스택의 가장 위쪽 주소를 가리킴
        (스택 : 함수가 사용할 지역 변수들을 저장하기 위해 준비해놓는 공간)

    2.  명령어 포인터 (Instruction Pointer)

       : 범용 레지스터와 달리 그 용도가 엄격히 정해져 있는 레지스터

       : rip (다음에 실행될 명령어가 위치한 주소를 가리킴

    3.  Data Size 

        : WORD : CPU가 사용하는 값의 크기 단위 / "16bit = 2byte = WORD"

     4.  FLAGS

        : 상태 레지스터 (현재 상태나 조건을 0과 1로 나타냄)

        : FLAGS 레지스터를 구성하는 64개의 비트들 각각은 서로 다른 의미를 지님

 

< 중요 플래그들 >
1.  CF (Carry Flag)
     :  +,- 등의 산술연산 혹은 bit shift/rotate 등의 연산이 일어났을 때, 자리 올림(carry)이 생기는 경우 CF의
       값은 1이 됨
     :  특징 : 연산에 사용된 값들에 부호가 없음(Unsigned)
2.  ZF (Zero Flag)
     :  연산의 결과가 0일때 ZF는 1이 됨
     :  두 값이 같은 경우, ZF가 1이 되므로 비교한 두 대상의 값이 같은지를 확인할 수 있음
3.  SF (Sign Flag)
     :  부호가 있는(Signed) 값의 연산에서 사용되어 결과가 음수/양수인지를 가리킴
     :  최상위 비트가 0(결과가 양수)이면 SF=0, 최상위 비트가 1(결과가 음수)이면 SF=1
4.  OF (Overflow Flag)
     :  부호가 있는(Signed) 값의 연산에서 CF의 역할을 함 (자리 올림이 생김)
     :  "표시할 수 있는 값의 범위를 넘어갔다는 것 (Overflowed)"

 

< Instruction Format >

    1.  Opcode (Operation Code, 명령 코드)

       : 명령어에서 실제로 어떤 동작을 할지를 나타내는 부분 (자료 이동, 산술 연산, 자료 제어 등)

 

명령 코드(좌)와 어셈블리 코드(우)

         - 기계 코드 (Machine Code) or 명령 코드(Opcode)

            : CPU가 실제로 수행할 작업을 나타내는 숫자 (CPU의 종류별로 다른 값일 수 있음)

         - 어셈블리 코드 (Assembly Code)

            : 숫자로 이루어진 명령코드를 문자로 작성한(Mnemonic) 코드 (명령 코드와 1:1 대응)

            : 명령 코드와 피연산자를 묶어 하나의 명령어(Instruction)이 됨

            : CPU의 동작을 그대로 옮겨놓은 것에 가깝기 때문에 메우 직관적이고 단순함

    2.  Operand (피연산자)

       : 명령 코드가 연산할 대상

       : Intel 방식의 어셈블리를 읽을 때에는 명령 코드에 따라 연산한 결과가 왼쪽 피연산자에 저장된다고

         이해하는 것이 일반적

       : 피연산자의 지정은 그 값을 사용할 방식에 따라 다양함

          (상수, 레지스터 값, 레지스터가 가리키고 있는 메모리의 주소)

< Operand Type > 
1. 상수값 (Immediate)
2. 레지스터 (Register)
   : 레지스터에 들어있는 값이 피연산자로 사용됨
3. 레지스터에 저장된 메모리 주소를 참조한 값 (Addressing Modes)
   : 메모리 주소를 참조한 값이 피연산자로 사용

 

< Instrcutions >

    1.  Data Movement

        : 값을 레지스터나 메모리 주소에 옮기는 명령어

          - mov : mov  a, b (b의 값을 a로 옮긴다.)

          - lea : lea  c, addr (c에 주소를 저장한다.)

    2.   Arithmetic Operations (산술 연산)

        - Unary Instructions

            - inc, dec : 값을 1 증가시키거나 감소

            - neg : 값의 부호를 바꿈 (2의 보수)

            - not : 값의 비트를 반전

        - Binary Instrcutions (명령어  a, b)

            - add : a에 b를 더한다

            - sub : a에서 b를 뺀다

            - imul : a에 b를 곱한다

            - and : a의 값과 b를 AND 논리연산 한 결과를 a에 저장해라

            - or : a의 값과 b를 OR 논리연산 한 결과를 a에 저장해라

            - xor : a의 값과 b를 XOR 논리연산 한 결과를 a에 저장해라

        - Shift Instructions (명령어  a, k)

            - shl, shr : a의값을 k만큼 오른쪽이나 왼쪽으로 shift

                          (shr의 경우 오른쪽으로 shift할 때 빈 bit 자리에는 0이 채워짐)

            - sal, sar : a의 값을 k만큼 왼쪽으로 shift하지만, 부호가 보전됨

                          (따라서 sar의 경우 최상위비트가 shift 이후에도 보전됨)

    3.  Conditional Operations (코드의 실행 흐름 제어)

         - test : a와 b를 AND 논리연산 한 결과

                   음수이면(최상위비트가 1) SF=1,

                   0이면 ZF=1

         - cmp : a에서 b를 뺀 결과

                    a=b ZF=1,CF=0,

                    a<b : ZF=0, CF=1

                    a>b : ZF=0, CF=0

         - jmp, jcc (조건부 jmp를 묶어서 부르는 이름)

                + 추가

                   - jae : >=

조건부 jmp 모음

   4.  Stack Operation

        : 지역 변수들은 스택에 저장되며, 스택은 레지스터가 아닌 메모리에 준비된다.

        : rsp 레지스터와 밀접한 관련이 있음

        : Intel x86-64 아키텍처에서 스택은 낮은 주소(더 작은 숫자)를 향해 자라기 때문에, 스택이 자랄수록 rsp

          저장된 메모리 주소는 점점 낮아진다.

         - push, pop : 스택에 새로운 데이터를 추가하거나 뺄 때 사용

    5.  Procedure Call Instructions (함수 호출, 함수 종료)

        - call : 함수 실행 (call은 피연산자로 실행할 함수의 주소를 받는다)

        - ret : 함수를 종료한 뒤 Return Address로 돌아가는 역할

 

 

 

<추가>

2강을 듣다보니 16진수 표기법이 부족한 것 같아서, 아스키 코드표도 첨부한다.

리버싱을 제대로 하기 위해서는 마스터해야 할 것 같다.