보안/리버싱 스터디 라이트업

0917 드림핵 리버싱 (stage 1, 2, 3, 4)

고고예준 2024. 9. 22. 21:39

Introduction: Reverse Engineering (stage1)

리버스 엔지니어링

:완성된 제품을 해체하고 분석하여 구조와 기능, 디자인을 파악

- 제작사가 이미 개발을 중단한 프로그램에 대한 패치가 필요할 때, 프로그램의 보안성을 평가하거나 악성코드를 분석할 때

- 키젠 프로그램이나 시리얼 넘버 생성기, 크랙 등의 불법 프로그램 생성, 상용 프로그램의 지적 재산권을 침해 위험성

 

Binary

프로그램

- 연산 장치가 수행해야 하는 동작을 정의한 일종의 문서

-역사: 천공카드 사용하던 에니악 -> Stored-Program Computer

-바이너리(binary)라고 부름 - 프로그램이 저장 장치에 이진 형태로 저장되기 때문

 

프로그래밍 언어: 프로그램을 개발하기 위해 사용하는 언어(고급언어와 저급언어)

소스 코드(Source Code): CPU가 수행해야 할 명령들을 프로그래밍 언어로 작성한 것

컴파일(Compile): 이를 컴퓨터가 이해할 수 있는 기계어의 형식으로 번역하는 것

컴파일러(Compiler): 컴파일을 해주는 소프트웨어

* 한번 컴파일되면 결과물이 프로그램으로 남기 때문에 언제든지 이를 실행하여 같은 명령을 처리하게 할 수 있음

결과물이 남아서 언제든 다시 읽어볼 수 있지만 한 번 번역하는데 시간이 많이 필요

 

 

컴파일 과정: 전처리(Preprocess), 컴파일(Compile), 어셈블(Assemble), 링크(Link)의 과정을 거쳐 바이너리로 번역

 

전처리(Preprocessing): 파일러가 소스 코드를 어셈블리어로 컴파일하기 전에, 필요한 형식으로 가공하는 과정

- 1. 주석 제거

- 2. 매크로 치환

  #define으로 정의한 매크로: 자주 쓰이는 코드나 상숫값을 단어로 정의 -> 매크로의 이름을 값으로 치환

- 3. 파일 병합

 

컴파일(Compile): C로 작성된 소스 코드를 어셈블리어로 번역하는 것

소스 코드의 문법을 검사-> 오류가 있다면 컴파일을 멈추고 에러를 출력

 

어셈블(Assemble): 컴파일로 생성된 어셈블리어 코드를 ELF형식의 목적 파일(Object file)로 변환하는 과정

 

링크(Link): 여러 목적 파일들을 연결하여 실행 가능한 바이너리로 만드는 과정

 

 

인터프리팅(Interpreting): 사용자의 입력, 또는 사용자가 작성한 스크립트를 그때 그때 번역하여 CPU에 전달(컴파일 필요없음)

인터프리터(Interpreter): 인터프리팅 처리하는 프로그램

빠르게 의사소통할 수 있지만, 같은 이야기를 하더라도 매번 번역해야 함

 

 

디스어셈블(Disassemble)

- 파일된 프로그램의 코드는 기계어로 작성-> 이해 어렵기 때문에 어셈블리어로 재번역

디컴파일

-규모 큰 바이너리 동작을 어셈블리 코드만으로 이해하기 어려움 -> 어셈블리어보다 고급언어로 바이너리 번역

- 디컴파일러는 일반적으로 바이너리의 소스 코드와 동일한 코드를 생성하지는 못함

-그러나 바이너리의 동작을 왜곡하지 않고, 압도적으로 분석 효율 높기 때문에 반드시 디컴파일러 사용


Static Analysis vs. Dynamic Analysis

 

정적 분석(Static Analysis): 프로그램을 실행시키지 않고 분석하는 방법

장점: 전체 구조 파악 쉬움

        분석 환경의 제약에서도 비교적 자유로움

        바이러스와 같은 악성 프로그램의 위협으로부터 안전

단점: 프로그램에 난독화(Obfuscation)가 적용되면 분석이 매우 어려워짐

         다양한 동적 요소를 고려하기 어려움

 

동적 분석(Dynamic Analysis): 프로그램을 실행시키면서 분석하는 방법

장점: 코드를 자세히 분석해보지 않고도 프로그램의 개략적인 동작을 파악

단점: 분석 환경을 구축하기 어려울 수 있다, 안티 디버깅


Computer Architecture (stage2)

컴퓨터 구조(Computer Architecture)

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

-컴퓨터에 대한 기본 설계-> 서로 다른 부품들이 모여서 ‘컴퓨터’라는 하나의 기계로서 작동

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

 

- 컴퓨터의 기능 구조에 대한 설계: 컴퓨터가 연산을 효율적으로 하기 위해 어떤 기능들이 컴퓨터에 필요한지 고민하고, 설계하는 분야

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

: 전체적인 컴퓨터 구조 중에서 CPU가 사용하는 명령어와 관련된 설계

가장 널리 사용되는 ISA 중 하나가 x86-64 아키텍처

- 마이크로 아키텍처: 정의된 명령어 집합을 효율적으로 처리할 수 있도록, CPU의 회로를 설계하는 분야

 

폰 노이만 구조

- 컴퓨터에 연산, 제어, 저장의 세 가지 핵심 기능

- 연산과 제어를 위해 중앙처리장치(Central Processing Unit, CPU)를, 저장을 위해 기억장치(memory), 장치간에 데이터나 제어 신호를 교환할 수 있도록 버스(bus)라는 전자 통로

 

- 중앙처리장치

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

술/논리 연산을 처리하는 산술논리장치(Arithmetic Logic Unit, ALU)와 CPU를 제어하는 제어장치(Control Unit), CPU에 필요한 데이터를 저장하는 레지스터(Register)로 구성

- 기억장치

용도에 따라 주기억장치와 보조기억장치로 분류

주기억장치는 프로그램 실행과정에서 필요한 데이터들을 임시로 저장하기 위해 사용, 램

보조기억장치는 운영 체제, 프로그램 등과 같은 데이터를 장기간 보관하고자 할 때 사용, HDD, SSD

- 버스

컴퓨터 부품과 부품 사이 또는 컴퓨터와 컴퓨터 사이에 신호를 전송하는 통로

데이터 버스, 주소 버스, 제어 버스

 

 

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

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

x86-64아키텍처

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

- 많은 전력을 소모하며, 발열도 상대적으로 심함

- 안정적으로 전력을 공급, 냉각 장치를 구비하는데 공간상의 부담이 크지 않은 데스크톱 또는 랩톱에 적합

n 비트 아키텍처

- CPU가 이해할 수 있는 데이터의 단위라는 의미에서 WORD( CPU가 어떻게 설계됐느냐에 따라 달라짐)

 

x86-64 아키텍처: 레지스터

- CPU가 빠르게 접근

- 범용 레지스터(General Register), 세그먼트 레지스터(Segment Register), 명령어 포인터 레지스터(Instruction Pointer Register, IP), 플래그 레지스터(Flag Register)

 

-범용 레지스터

8바이트를 저장할 수 있으며, 부호 없는 정수를 기준으로 2^64 - 1까지

 

-세그먼트 레지스터

x64 아키텍처에는 cs, ss, ds, es, fs, gs 총 6가지 세그먼트 레지스터가 존재, 16비트

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

나머지 레지스터는 운영체제 별로 용도를 결정할 수 있도록 범용적인 용도로 제작

 

-명령어 포인터 레지스터

CPU가 어느 부분의 코드를 실행할지 가리킴

x64 아키텍처의 명령어 레지스터는 rip, 8바이트

 

-플래그 레지스터

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

x64 아키텍처에서는 RFLAGS, 64비트

 


Windows Memory Layout (stage3)

메모리 레이아웃(Memory Layout): 프로세스 가상 메모리(Virtual Memory)의 구성

가상 메모리: 운영체제가 프로세스에게 할당하는 사용 가능한 메모리 공간

 

프로세스 메모리 구조

섹션

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

- PE 헤더에 정보 적혀있음

-섹션의 이름, 크기, 속성과 권한, 섹션이 로드될 주소의 오프셋

-윈도우는 PE의 각 섹션들을 가상 메모리의 적절한 세그먼트에 매핑

 

- .text 섹션실행 가능한 기계 코드가 위치하는 영역

- .data 섹션에는 컴파일 시점에 값이 정해진 전역 변수들이 위치. 읽기쓰기 권한 부여

- .rdata 섹션에는 컴파일 시점에 값이 정해진 전역 상수와 참조할 DLL 및 외부 함수들의 정보가 저장. 읽기 권한만 부여

 

섹션이 아닌 메모리

-스택

지역 변수나 함수의 리턴 주소가 저장

자유롭게 읽고쓰기 (읽기/쓰기 권한 부여)

-힙

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

비교적 스택보다 큰 데이터도 저장할 수 있고 전역적으로 접근이 가능하도록 설계, 실행중 동적으로 할당받음(스택과 다른점)

읽기/쓰기 권한 + 상황에 따라 실행 권한

 


x86 Assembly (stage 4)

x64 어셈블리 언어

기본 구조

- 동사에 해당하는 명령어(Operation Code, Opcode)와 목적어에 해당하는 피연산자(Operand)로 구성

-명령어

비교: 두 피연산자 값 비교, 플래그 설정

분기: rip을 이동시켜 실행 흐름 바꿈

 

 

-피연산자 (상수, 레지스터, 메모리)

 

Opcode: 스택

- push val : val을 스택 최상단에 쌓음

Opcode: 프로시저

- 프로시저(Procedure)는 특정 기능을 수행하는 코드 조각

 반복되는 연산을 프로시저 호출로 대체-> 전체 코드 크기 줄임 + 기능별로 코드 조각에 이름붙임-> 코드 가독성 업

 프로시저를 부르는 행위를 호출(Call)이라고 부르며, 프로시저에서 돌아오는 것을 반환(Return)

프로시저를 호출할 때는 프로시저를 실행하고 나서 원래의 실행 흐름으로 돌아와야 하므로, call 다음의 명령어 주소(Return Address, 반환 주소)를 스택에 저장하고 프로시저로 rip를 이동

- call addr : addr에 위치한 프로시져 호출

- leave: 스택프레임 정리

- ret : return address로 반환