[EVI$ION 7기] 4주차 reversing

김예원·2024년 11월 7일

리버스 엔지니어링

이미 만들어진 시스템이나 장치에 대한 해체나 분석을 거쳐 그 대상 물체의 구조와 기능, 디자인 등을 알아내는 일련의 과정. 완성품의 설계도 없이 구조와 동작 과정을 알아내는 모든 단계

소프트웨어 리버스 엔지니어링 ( Software Reverse Engineering )
: 소스 코드가 없는 상태에서 컴파일된 대상 소프트웨어의 구조 를 여러 가지 방법으로 분석. 메모리 덤프를 비롯한 바이너리 분석 결과를 토대로 동작 원리와 내부 구조를 파악
-> 위 과정을 바탕으로 원래의 소스 코드가 어떻게 작성된 것인지 알아냄

프로그램?

연산 장치가 수행해야 하는 동작을 정의한 일종의 문서
binary라고 하기도 함

고급 언어 -> 기계어 변환 과정

전처리 -> 컴파일 -> 어셈블 -> 링크 -> EXE 파일

(1) 전처리(Preprocess)

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

  • 주석 제거, 매크로 치환, 파일 병합
    ex) 소스 코드에 #define HI 3"과 같이 선언된 매크로를 해당 값으로 치환

(2) 컴파일(Compile)

소스 코드를 컴파일러가 어셈블리어로 번역하는 과정

  • 어셈블리어: 0과 1의 조합을 상징적인 코드로 변환. 기계어와 1대 1 매핑됨. machine dependent함
  • 이 과정에서 문법적 오류가 발생하면 에러를 출력함
  • 옵션을 통해 최적 기술을 적용할 수 있음
    ex) gcc -o opt opt.c -o2 -> 최적화 옵션 적용
  • 어셈블리 프로그램은 시스템에 따라 다름
    ex) MIPS

(3) 어셈블(Assemble)

어셈블리어 코드를 어셈블러가 ELF 형식의 Object file로 변환하는 과정

  • 기계어: 컴퓨터가 직접 이해 가능한 언어. 0과 1의 조합. machine dependent함
  • Object file로 변환이 되면 어셈블리어 코드가 기계어로 번역
  • gcc에서 -c 옵션을 통해 add.S를 Object file로 변환할 수 있음
    ex) gcc -c add.S -o add.o

링커가 여러 Object file과 library file들을 연결하여 실행 가능한 바이너리로 만드는 과정

  • 이 과정을 통해 실행할 수 있는 exe 파일(=프로그램)이 만들어짐

*상용 컴파일러 소프트웨어(gcc, visual c)가 컴파일러, 어셈블러, 링커 등을
내포하고 있어 소스 파일에서 실행 파일 변환이 한번에 이루어짐

바이너리 분석

바이너리를 분석하기 위해서는 역분석 과정이 필요함
EXE 파일 -> 디스어셈블 -> 어셈블 -> 디컴파일 -> 전처리

(1) 정적 분석

프로그램을 실행하지 않고 진행

(2) 동적 분석

프로그램을 실행하면서 진행

컴퓨터 구조(Computer Architecture)

컴퓨터가 효율적으로 작동할 수 있도록 하드웨어 및 소프트웨어의 기능을 고안하고 구성하는 방법
CPU에 대한 설계: 명령어 집합 구조
ex) ARM, x86, x86-64, x64 등

  • x86-64 및 x64의 아키텍처: 레지스터
  • 종류: 범용 레지스터(eax, ebx, ecx, edx), 세그먼트 레지스터, 명령어 포인터 레지스터, 플래그 레지스터

(1) 범용 레지스터

eax: 곱셈과 나눗셈 명령에서 자동으로 사용. 함수의 리턴 값 저장
ebx: ESI 또는 EDI와 결합하여 인덱스에 사용
ecx: 반복 명령어 사용 시 카운터로 사용
edx: eax와 같이 사용
esp: stack point. 스택으로만 모든 것들을 구별
esi: 데이터를 조작 또는 복사 시, 소스 데이터의 주소 저장
edi: 복사 시, 목적지의 주소 저장

  • 모든 변수들은 같은 메모리 공간을 사용

(2) 세그먼트 레지스터

  • 6가지 종류: CS, DS, ES, FS, GS..
  • 각 레지스터의 크기: 16 bit
  • x64로 아키텍처가 확장되면서 용도에 큰 변화가 생김

(3) 명령어 포인터 레지스터

기계어로 작성된 프로그램의 코드에서 CPU가 어느 부분의 코드를 실행할지 가리키는 레지스터

  • x64에서는 rip

(4) 플래그 레지스터

프로세서의 현재 상태를 저장하는 레지스터
CF(carry flag): 부호 없는 수의 연산 결과가 비트의 범위를 넘을 경우 설정
ZF(zero flag): 연산의 결과가 0일 경우 설정
SF(sign flag): 연산의 결과가 음수일 경우 설정
OF(overflow flag): 부호 있는 수의 연산 결과가 비트의 범위를 넘을 경우 설정

어셈블리어의 구조

명령어 피연산자1, 피연산자2
ex) mov eax, 3
: eax에 3을 대입해라
피연산자: 상수, 레지스터, 메모리(BYTE(1byte), WORD(2byte), DWORD(3byte), QWORD(8byte))
ex) QWORD PTR[0x8048000]
: 0x8048000의 데이터를 8바이트만큼 참조해라

자주 사용하는 명령어

(1) 데이터 이동

MOV: 피연산자2를 피연산자1로 복사
MOV Dest, Src
MOV reg, reg
MOV reg, imm
MOV mem, reg
MOV mem, imm
MOV reg, mem

(2) 산술 연산

ADD: 피연산자 1 = 피연산자1 + 피연산자2
SUB: 피연산자1 = 피연산자1 - 피연산자2
MUL: AX와 피연산자를 곱함
DIV: AX와 피연산자를 나눔
INC: 피연산자에 1을 더함
DEC: 피연산자에 1을 뺌

Dest에 Src 값을 더해서 Dest에 저장, 연산 결과에 따라 ZF, OF, CF가 설정됨
ADD Dest, Src
ADD reg, reg
ADD reg, imm
ADD mem, reg
ADD mem, imm
ADD reg, mem
Dest에 Src 값을 빼서 Dest에 저장, 연산 결과에 따라 ZF, CF가 설정됨
SUB Dest, Src
SUB reg, reg
SUB reg, imm
SUB mem, reg
SUB mem, imm
SUB reg, mem
부호 없는 AL, AX, EAX의 값을 피연산자와 곱함, 연산 결과에 따라 OF, ZF가 설정됨
MUL reg
MUL mem
부호 없는 AL, AX, EAX의 값을 피연산자와 나눔, 연산 결과에 따라 CF, OF, ZF가 설정됨. 나머지: DX, 몫: AX
DIV reg
DIV mem
INC reg
INC mem
DEC reg
DEC mem

(3) 비교 연산

CMP: 피연산자1과 피연산자2를 비교

두 피연산자의 값을 Dest - Src해서 비교하고, 플래그 설정
CMP Dest, Src
CMP reg, reg
CMP reg, imm
CMP mem, reg
CMP mem, imm
CMP reg, mem

Dest = Src -> ZF = 1
Dest != Src -> ZF = 0
*ZF의 초기값은 0

(4) 조건 분기

Jxx: 플래그를 보고 해당 조건이 맞을 경우 분기함

rip를 이동시켜서 실행 흐름을 바꿈
JMP addr: addr로 rip 이동
JE addr: 직전에 비교한 두 피연산자가 같으면 점프
JNE addr: 직전에 비교한 두 피연산자가 다르면 점프

(5) 함수 호출

CALL: 함수 호출 시 사용됨

JMP와 같이 프로그램 실행 흐름이 변경됨
차이점: return 주소를 스택에 저장

ex)
AX에 담긴 값이 2로 피연산자와 같으므로 ZF가 1로 설정됨

0개의 댓글