Reverse Engineering

  • Reverse Engineering

    이미 만들어진 시스템이나 장치에 대한 해체나 분석을 거쳐 그 대상 물체의 구조와 기능, 디자인 등을 알아내는 일련의 과정

  • Software Reverse Engineering

    • 소스 코드가 없는 상태에서 컴파일된 대상 소프트웨어의 구조를 여러가지 방법으로 분석

    • 메모리 덤프를 비롯한 바이너리 분석 결과를 토대로 동작 원리와 내부 구조를 파악
    • 이를 바탕으로 원래의 소스가 어떻게 작성된 것인지 알아냄
  • Disclaimer

    소스 코드를 비롯한 전체적인 작동 원리를 알아낼 수 있다는 점에서, 각종 상용 프로그램의 지적 재산권을 침해할 수도 있는 양날의 검

Static Analysis vs Dynamic Analysis

  • 정적 분석 방법 (Static Analysis)

    프로그램을 실행시키지 않고 분석

    • 실행 파일을 구성하는 모든 요소, 대상 실행 파일이 실제로 동작할 CPU 아키텍처에 해당하는 어셈블리 코드를 이해해야 함
  • 동적 분석 방법 (Dynamic Analysis)

    프로그램을 실행시켜서 입출력과 내부 동작 단계를 살피며 분석

    • 실행 단계별로 자세한 동작 과정을 살펴봐야 하므로, 환경에 맞는 디버거를 이용해 단계별로 분석하는 기술 익히기

Source Code → Binary Code

  • 어셈블(Assemble): 어셈블리 코드가 기계 코드로 번역되는 과정

    1. 소스 코드: 각종 주석이나 매크로, 참조할 헤더 파일 등을 포함

    2. 중간 언어: 컴파일러로 소스 코드를 변환한 것

    3. 어셈블리 코드: 컴퓨터가 이해할 수 있는 기계 코드를 사람이 알아보기 쉽게 명령어(Instruction) 단위로 표현

      ※어셈블리 코드와 기계 코드는 1:1 대응이 가능

image

  • Disassemble(디스어셈블): 바이너리 코드를 어셈블리 코드로 변환하는 과정

    => 어셈블 과정의 반대

    image

x64

Instruction Cycle

  • CPU가 하는 기본적인 동작 과정:
    1. 실행할 명령어를 읽기(Fetch)
    2. 읽어온 명령어를 해석(Decode)
    3. 해석한 결과를 실행(Execute)

레지스터(Register)

  • 레지스터(Register)

    CPU의 동작에 필수적인 저장 공간의 역할을 하는 CPU의 구성 요소

범용 레지스터

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

  • x64의 범용 레지스터

    image

    • rax: 함수가 실행된 후 리턴값을 저장

      ​ (리턴값을 위해서만 쓰이는 것은 아님)

    • 함수 호출 규약(Calling Convention):

      함수가 실행될 때 필요한 인자들을 저장하는 용도로 사용하는 레지스터

      • rcx, rdx, r8, r9: Windows 64bit에서 함수를 호출할 때 필요한 인자들을 순서대로 저장

        ex) 첫번째 인자는 rcx에, 두번째 인자는 rdx에… 하는 방식으로 인자를 레지스터에 담아 함수를 호출

    • rsp: 스택 포인터(Stack Pointer)로, 스택의 가장 위쪽 주소를 가리킴

      ​ (다른 범용 레지스터들과 달리 용도가 정해져 있음)

명령어 포인터

​ : 그 용도가 엄격히 정해져 있는 레지스터

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

Data Size

  • WORD : CPU가 사용하는 값의 크기 단위

    • 16bit CPU 범용 레지스터: ax, cx, dx, bx

    • 32bit CPU 범용 레지스터: eax, ecx, edx, ebx

    • 64bit CPU 범용 레지스터 : r8~ r15

      ( x64의 레지스터들이 담을 수 있는 값의 크기: 64bit (8byte, QWORD))

      -> 꼭 8byte 단위로만 값을 저장해야 하는 것은 아님

image

FLAGS

  • FLAGS: 현재 상태나 조건을 0과 1로 나타내는 상태 레지스터

    Flag Abbreviation 설명 예시
    CF(Carry Flag) 자리 올림(carry)이 생기는 경우 CF의 값이 1이 됨
    CF의 특징: 연산에 사용된 값들에 부호가 없음(unsigned)
    image
    ZF(Zero Flag) 연산의 결과가 0일 때 ZF는 1이 됨. image
    SF(Sign Flag) 수행한 결과가 양수일 때, 즉 최상위 비트가 0이면 SF=0, 반대로 결과가 음수가 되어 최상위 비트가 1이면 SF=1이 됨 image
    OF(Overflow Flag) ‘overflow’ 라는 이름에서 알 수 있듯이 부호 있는 값들을 대상으로 산술 연산을 했을 때 자리 올림이 생겼다는 것은 표시할 수 있는 값의 범위를 넘어갔다(overflowed)는 것을 의미 image

Instruction Format

Opcode (Operation Code)

  • 명령 코드(Opcode, Operation Code) :

    ​ 명령어에서 실제로 어떤 동작을 할지를 나타내는 부분

  • 어셈블리 코드 (Assembly Code):

    • 명령 코드를 알아보기 쉽도록 문자로 치환한 것
    • 명령 코드와 1:1로 대응
    • 명령 코드가 연산할때 사용할 피연산자도 알아보기 쉬움
    • 명령 코드와 피연산자를 묶어 하나의 명령어(Instruction)로

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

    but, 실제 소스코드와 달리 고차원적인 전체 흐름을 파악하기는 어렵

    55                   push  rbp
    48 89 e5             mov   rbp,rsp
    48 8d 3d 9f 00 00 00 lea   rdi,[rip+0x9f]
    e8 c6 fe ff ff       call  510 <puts@plt>
    b8 00 00 00 00       mov   eax,0x0
    5d                   pop   rbp
    c3                   ret
    
  • Operand:

    • 명령 코드가 연산할 대상 (함수에 들어가는 인자)
    • Intel 방식의 어셈블리를 읽을 때에는 명령 코드에 따라 연산한 결과를 왼쪽 피연산자에 저장됨
  • Operand Types:

    레지스터에 들어있는 값은 메모리 주소로, 실제로는 해당 메모리 주소를 참조한 값이 피연산자로 사용

    • Addressing Modes:

      • [reg] :

        메모리 저장

      • [reg+d] :

        레지스터에 들어있는 값을 주소의 기준으로 하여 d 만큼 떨어진 오프셋을 실제로 참조한 다음 피연산자로 씀

      • [reg1+reg2] :

        한 레지스터에 들어있는 값과 다른 레지스터에 들어있는 값을 더한 결과를 참조할 메모리 주소로 사용하는 경우

      • [reg1+reg2*i+d] :

        ​ 구조체가 사용된 경우 등에서 자주 보이는 방식

      mov   [rcx],rax                   ; *rcx = rax
      mov   byte ptr [rcx],al           ; *rcx = al
      mov   dword ptr [rbp-1Ch],eax      
      mov   byte ptr [rdi+rcx*4+3],0FFh 
      

Hello World

  • hello world 출력

    #include <stdio.h>
    int main(){
        puts("hello world!\n");
        return 0;
    }
    
  • 어셈블리 코드

    7FF6ED801000 | 48:83EC 28       | sub rsp,28                          |
    7FF6ED801004 | 48:8D0D 15120000 | lea rcx,qword ptr ds:[7FF6ED802220] | 00007FF6ED802220:"hello world!\n"
    7FF6ED80100B | FF15 5F110000    | call qword ptr ds:[<&puts>]         |
    7FF6ED801011 | 33C0             | xor eax,eax                         |
    7FF6ED801013 | 48:83C4 28       | add rsp,28                          |
    7FF6ED801017 | C3               | ret                                 |
    
    1. 주소

      해당 어셈블리 코드의 시작 주소 표시.

    2. 기계 코드

      : 앞에 있는 값은 prefix 이고 띄어쓰기 다음에 있는 부분은 어셈블리 코드의 2번째 인자 부분

    3. 어셈블리어

      주소값의 경우 x64dbg가 적절한 형태로 바꾸어서 보여주기도 함

    4. 코멘트

    5. : x64dbg가 프로그램을 분석하여 알게 된 추가적인 정보가 여기에 표시

hello world - 디스어셈블리 결과 살펴보기

7FF6ED801000 | 48:83EC 28       | sub rsp,28                          |
7FF6ED801004 | 48:8D0D 15120000 | lea rcx,qword ptr ds:[7FF6ED802220] | 00007FF6ED802220:"hello world!\n"
7FF6ED80100B | FF15 5F110000    | call qword ptr ds:[<&puts>]         |
7FF6ED801011 | 33C0             | xor eax,eax                         |
7FF6ED801013 | 48:83C4 28       | add rsp,28                          |
7FF6ED801017 | C3               | ret                                 |
  • sub rsp, 28

    rsp에서 0x28만큼 빼 함수 내부에서 사용할 스택의 용량을 확보하는 명령어

  • lea rcx,qword ptr ds:[7FF6ED802220]

    rcx에 0x7FF6ED802220 값을 저장

  • call qword ptr ds:[<&puts>]

    puts를 호출하는 명령어

  • xor eax,eax

    eax를 0으로 만들어주는 명령어

  • add rsp,28

    함수 시작시 확보해두었던 스택을 정리하는 명령어