본문 바로가기
Security/Reversing

[Dreamhack Reverse Engineering] STAGE 3

by 단월໒꒱ 2022. 4. 28.

[Computer Architecture]

1. 컴퓨터 구조

 1) 컴퓨터 구조

   - 컴퓨터가 효율적으로 작동할 수 있도록 하드웨어 및 소프트웨어의 기능을 고안하고 이들을 구성하는 방법

   - 컴퓨터 기능 구조에 대한 설계, 명령어 집합구조, 마이크로 아키텍처, 기타 하드웨어 및 컴퓨팅 방법에 대한 설계 포함

 

 2) 컴퓨터 기능 구조에 대한 설계

   - 효율적인 연산을 위해 컴퓨터에 필요한 기능들을 고민, 설계하는 분야

   - ex. 폰 노이만 구조, 하버드 구조, 수정된 하버드 구조

 

 3) 명령어 집합구조(CPU 명령어에 대한 설계)

   - CPU가 처리해야하는 명령어 설계하는 분야

   - ex. ARM, MIPS, AVR, 인텔의 x86 및 x86-64 등

 

2. 폰 노이만 구조

 

 

 

 1) 컴퓨터의 핵심 기능

   - 연산, 제어, 저장

   - 연산과 제어 -> 중앙처리장치(CPU) 사용

   - 저장 -> 기억장치 사용

   - 장치 간에 데이터나 제어 신호 교환 -> 버스(전자 통로) 사용

 

 2) 중앙처리장치(CPU)

   - 프로그램의 연산을 처리하고 시스템을 관리하는 컴퓨터의 두뇌

   - 프로세스의 코드를 불러오고, 실행하고, 결과를 저장하는 기능을 함

   - 산술논리장치 / 제어장치 / 레지스터로 구성

    ① 산술논리장치 : 산술/논리연산 처리

    ② 제어장치 : CPU 제어

    ③ 레지스터 : CPU에 필요한 데이터 저장

 

 3) 기억장치

   - 컴퓨터가 동하는데 필요한 데이터 저장

   - 주기억장치 / 보조기억장치로 분류

    ① 주기억장치 : 프로그램 실행과정에서 필요한 데이터를 임시 저장 ex. 램(RAM)

    ② 보조기억장치 : 운영체제, 프로그램 등과 같은 데이터를 장기간 보관 ex. 하드드라이브(HDD), SSD

 

 4) 버스

   - 컴퓨터 부품 간에, 또는 컴퓨터 간에 신호를 전송하는 통로

   - 데이터 버스 / 주소 버스 / 제어 버스

    ① 데이터 버스 : 데이터 이동

    ② 주소 버스 : 주소 지정

    ③ 제어 버스 : 읽기/쓰기 제어

   - 랜선, 데이터 전송 소프트웨어, 프로토콜 등도 버스라 불림

 

 5) 기억장치가 있는데 CPU 안에 레지스터가 필요한 이유

   - CPU가 빠른 속도로 연산을 처리하기 위해 데이터의 빠른 교환이 필요

   - CPU의 연산 속도가 기억장치와 데이터 교환속도보다 압도적으로 빨라, 기억장치만 사용하면 병목현상 발생

   - 따라서 CPU는 교환 속도를 단축하기 위해 레지스터와 캐시라는 저장장치를 내부에 갖고 있음

 

3. 명령어 집합 구조

 1) 명령어 집합 구조(Instruction Set Architecture, ISA)

   - CPU가 해석하는 명령어의 집합

   - IA-32, x86-64(x64), MIPS, AVR 등 존재

 

 2) 인텔의 x86-64

   - 고성능 프로세서 설계를 위해 사용

   - 이를 기반으로 한 CPU는 전력소모 크고 발열도 상대적으로 심함

   - 데스크탑, 랩탑에 적합

 

 cf) 배터리 사용하는 드론, 공유기, 인공지능 스피커처럼 작은 임베디드 기기들은 인텔의 고성능 프로세서를 장착하기 부적합

    -> 많은 임베디드 장비들은 전력 소모와 발열이 적은 ARM, MIPS, AVR의 프로세서 주로 사용

 

4. x86-64 아키텍처

 1) x86-64 아키텍처

   - x64 아키텍처 : 인텔의 64비트 CPU 아키텍처

                              인텔의 32비트 CPU 아키텍처인 IA-32를 64비트 환경에서 사용할 수 있도록 확장한 것

 

 2) n 비트 아키텍처

   - n은 CPU가 한번에 처리할 수 있는 데이터의 크기

   - WORD : CPU가 이해할 수 있는 데이터의 단위

   - WORD의 크기는 CPU가 어떻게 설계됐느냐에 따라 달라짐

     ex. 일반적인 32비트 아키텍처

           -> ALU는 32비트까지 계산 가능, 레지스터의 요량 및 각종 버스들의 대역폭이 32비트

                이들로 구성된 CPU는 설계 상 32비트의 데이터까지만 처리 가능

 

 3) WORD가 크면 유리한 점

   - 작으면 CPU가 제공할 수 있는 가상메모리의 크기가 작음

      -> 많은 메모리 자원을 소모하는 전문 소프트웨어나 고사양의 게임을 실행할 때 부족할 수 있음

   - 크면 가용한 메모리 자원이 부족해서 소프트웨어의 최고 성능을 낼 수 없다거나 실행 불가능한 상황이 거의 발생하지 x

 

5. 범용 레지스터

 1) 범용 레지스터

   - 주용도는 있으나 그 외의 다양한 용도로 사용될 수 있는 레지스터

   - x86-64에서 각각의 범용 레지스터는 8바이트 저장 가능, 부호없는 정수 기준 2^64-1 까지의 수 표현 가능

 

 2) x64에서 자주 쓰이는 범용 레지스터의 종류

 

 

이름 주용도
rax (accumulator register) 함수의 반환 값
rbx (base register) x64에서는 주된 용도 없음
rcx (counter register) 반복문의 반복 횟수, 각종 연산의 시행 횟수
rdx (data register) x64에서는 주된 용도 없음
rsi (source index) 데이터를 옮길 때 원본을 가리키는 포인터
rdi (destination index) 데이터를 옮길 때 목적지를 가리키는 포인터
rsp (stack pointer) 사용 중인 스택의 위치를 가리키는 포인터
rbp (stack base pointer) 스택의 바닥을 가리키는 포인터

 

 

6. 세그먼트 레지스터

 1) 세그먼트 레지스터

   - x64로 아키텍처가 확장되면서 용도에 큰 변화가 생김

   - x64에 존재하는 세그먼트 레지스터 : cs, ss, ds, es, fs, gs (각 레지스터의 크기는 16비트)

   - cs, ds, ss 레지스터 : 코드 영역과 데이터, 스택 메모리 영역을 가리킬 때 사용

   - es, fs, gs 레지스터 : 운영체제 별로 용도를 결정할 수 있도록 범용적인 용도로 제작됨

 

7. 플래그 레지스터

 1) 플래그 레지스터

   - 프로세서의 현재 상태를 저장하고 있는 레지스터

   - x64에는 RFLAGS라 불리는 64비트 크기의 플래그 레지스터 존재

   - 자신을 구성하고 있는 여러 비트들로 CPU의 현재 상태 표현 (마치 깃발을 올리고 내리는 것처럼)

   - RFLAGS는 최대 64개의 플래그를 사용 가능하지만 실제로는 아래의 20여개의 비트만 사용

 

 

 

 

 2) 자주 접하게 될 플래그

 

플래그 의미
CF (Carry Flag) 부호 없는 수의 연산 결과가 비트의 범위를 넘을 경우 설정됨
ZF (Zero Flag) 연산의 결과가 0일 경우 설정됨
SF (Sign Flag) 연산의 결과가 음수일 경우 설정됨
OF (Overflow Flag) 부호 있는 수의 연산 결과가 비트의 범위를 넘을 경우 설정됨

 

 

8. 명령어 포인터 레지스터

 1) 명령어 포인터 레지스터

   - 프로그램을 이룬 기계어 코드들 중에서 CPU가 어느 부분의 코드를 실행할 지 가리킴

   - x64 아키텍처의 명령어 레지스터는 rip이며 크기는 8byte

 

9. 레지스터 호환

 - IA-32에서 CPU의 레지스터들은 32비트 크기를 가짐

   -> 명칭은 각각 eax, ebx, ecx, edx, esi, edi, esp, ebp

 - 호환성을 위해 이 레지스터들은 x86-64에서도 그대로 사용 가능

 - rax, rbx, rcx, rdx, rsi, rdi, rsp, rbp가 이들의 확장된 형태, eax, ebx 등은 확장된 레지스터의 하위 32비트를 가리킴

 - 마찬가지로 과거 16비트 아키텍처인 IA-16과의 호환을 위해 ax, bx, cx, dx, si, di, sp, bp는 eax, ebx, ecx, edx, esi, edi, esp ,ebp의 하위 16비트를 가리킴

 - 이들 중 몇몇은 다시 상위 8비트, 하위 8비트로 나뉨

 

 

 

 

[Windows Memory Layout]

1. 메모리 레이아웃 (Memory Layout)

 1) 메모리 레이아웃

   - 프로세스 가상 메모리의 구성

   - 가상 메모리 : 프로그램을 실행할 때 운영체제가 프로세스에 할당해주는 사용 가능한 메모리 공간

   - 리버싱의 핵심은 바이너리를 분석하여 바이너리의 동작을 이해하는 것

   - 바이너리의 동작은 메모리와 관련이 있기 때문에, 바이너리 동작을 이해하기 위해 그와 상호작용하는 메모리에 대한 이해가 필요

 

2. 프로세스 메모리 구조

 1) 섹션

   - 윈도우의 PE 파일 구성 = PE 헤더 + 1개 이상의 섹션

   - 섹션 : 유사한 용도로 사용되는 데이터가 모여있는 영역

   - ex. .text : PE의 코드

            .data : PE가 실행 중에 참조하는 데이터

   - 섹션에 대한 정보는 PE 헤더에 적혀있음

   - PE 헤더에 저장되는 섹션과 관련된 데이터 중, 중요한 것은 다음과 같다.

      ① 섹션의 이름

      ② 섹션의 크기

      ③ 섹션이 로드될 주소의 오프셋

      ④ 섹션의 속성과 권한

    - 윈도우는 PE를 실행할 때, 위의 정보를 참조하여 PE의 각 섹션들을 가상 메모리의 적절한 세그먼트에 매핑함

 

 2) 일반적으로 사용되는 섹션

      ① .text

          - 실행 가능한 기계 코드가 위치함

          - 프로그램이 동작하려면 코드를 실행할 수 있어야 하므로, r/e 권한이 부여되지만, w 권한은 대부분 제거 (공격 위험 때문)

 

 

int main() { return 31337; }

 

 

          - 정수 31337을 반환하는 main 함수가 컴파일 되면 554889e5b8697a00005dc3이라는 기계코드로 변환되는데,

             이 기계 코드가 코드 세그먼트에 위치하게 됨

 

      ② .data

          - 컴파일 시점에 값이 정해진 전역 변수들이 위치함

          - CPU가 이 섹션의 데이터를 읽고 쓸 수 있어야 하므로, r/w 권한이 부여

 

 

//.data 섹션에 포함되는 여러 데이터의 유형

int data_num = 31337;
char data_rwstr[] = "writable_data";        // data

int main() { ... }

 

 

 

      ③ .rdata

          - 컴파일 시점에 값이 정해진 전역 상수와 참조할 DLL 및 외부 함수들의 정보가 저장됨

          - CPU가 이 섹션의 데이터를 읽을 수 있어야 하므로, r 권한이 부여되지만, w는 불가능

 

 

//.rdata 섹션에 포함되는 여러 데이터의 유형

const char data_rostr[] = "readonly_data";
char *str_ptr = "readonly";  // str_ptr은 .data, 문자열은 .rdata

int main() { ... }

 

 

          - str_ptr은 "readonly"라는 문자열을 가리키고 있는데, str_ptr은 전역 변수로서 .data에 위치하지만,

            "readonly"는 상수 문자열로 취급되어 .rdata에 위치함

          - 과거에는 참조할 DLL과 외부 함수들의 정보를 .idata 섹션에 저장하였으나, 최근에는 대부분 .rdata에 저장함

 

 

 

 2) 스택

   - 윈도우즈 프로세스의 각 쓰레드는 자신만의 스택 공간을 가지고 있음

   - 보통 지역 변수나 함수의 리턴 주소가 저장됨

   - 자유롭게 읽고 쓸 수 있어야 하므로, r/w 권한이 부여

   - 스택에 대해 '아래로 자란다'라는 표현을 종종 사용하는데, 이는 스택이 확장될 때, 기존 주소보다 낮은 주소로 확장되기 때문

 

 

void func() {
  int choice = 0;
  scanf("%d", &choice);
  if (choice)
    call_true();
  else
    call_false();
  return 0;
}

 

   - 지역변수 choice가 스택에 저장됨

 

 

 

 3) 힙

   - 힙 : 프로그램이 여러 용도로 사용하기 위해 할당받는 공간

   - 모든 종류의 데이터가 저장될 수 있음

   - 스택과 다른 점

      ① 비교적 스택보다 큰 데이터 저장 가능

      ② 전역적으로 접근 가능하도록 설계됨

      ③ 실행 중 동적으로 할당 받음

   - 보통 데이터를 읽고 쓰기만 하기 때문에 r/w 권한이 부여되지만, 상황에 따라 e 권한을 가지기도 함

 

 

int main() {
  int *heap_data_ptr =
      malloc(sizeof(*heap_data_ptr));  // 동적 할당한 힙 영역의 주소를 가리킴
  *heap_data_ptr = 31337;              // 힙 영역에 값을 씀
  printf("%d\n", *heap_data_ptr);  // 힙 영역의 값을 사용함
  return 0;
}

 

 

   - heap_data_ptr에 malloc()으로 동적 할당한 영역의 주소를 대입하고, 이 영역에 값을 씀

   - heap_data_ptr은 지역변수이므로 스택에 위치하며, malloc으로 할당받은 힙 세그먼트의 주소를 가리킴

 

 

 

 

댓글