리버스 엔지니어링(Reverse Engineering) 이란?
- 리버싱(Reversing) 이라고도 부름
- = 역공학
- 완성되어 있는 장치 or 프로그램을 역으로 분석
- 다양한 개발 언어적 지식이 필요
- 분석 방법
- 정적 분석
- 소프트웨어 실행하지 않고 분석하는 방법
- 헤더 정보, 크기, 내부 코드 등
- 동적 분석
- 소프트웨어 실행하고 분석하는 방법
- 메모리 상태, 레지스트리, 네트워크 상태 등
- 정적 분석
- 언어별 리버스 엔지니어링 방법
- 컴파일 언어
- 컴파일러가 원본 소스 코드를 기계어로 전체 변환 후 실행
- 원본 소스 코드 복원 불가 => 어셈블리어로 분석
- disassembler 도구 사용해야 함
- 인터프리터 언어
- 인터프리터는 소스 코드를 한 줄씩 읽어 실행
- 실행 파일 자체가 원본 소스 코드 => 소스 코드 분석
- JIT 언어
- JIT 컴파일러가 각 언어마다 자체 특정 코드(바이트코드)로 변환
- 각 언어별 바이트 코드의 패턴을 원본 소스 코드로 복원 가능 => 소스 코드 분석
- 컴파일 언어
리버스 엔지니어링 순기능
- 소프트웨어 호환성 및 성능 향상
- 소프트웨어 보안성 테스트
- 학술/학문적 추구
- 악성코드 분석
리버스 엔지니어링 역기능
- 소프트웨어 불법 복제
- 불법 정품 인증
- 소프트웨어 키젠 및 크랙 생성
리버스 엔지니어링과 법
컴퓨터 구조 개요
- 하드웨어
- 특정한 외형 가진 물리적 자원
- 입력, 출력, 기억, 연산, 제어(CPU) 장치
- 소프트웨어
- 특정한 외형 없이 컴퓨터의 작동과 기능
- 시스템 소프트웨어
- 하드웨어와 소프트웨어의 기능을 효율적으로 관리 및 제어하는 프로그램
- OS, 펌웨어 등
- 응용 소프트웨어
- 사용자가 원하는 기능을 실행하는 프로그램
- Microsoft Offcie 프로그램, 게임 등
- 메인 메모리(Main Memory)
- 램(RAM)
- 프로그램 코드가 올라가서 실행 되는 영역
- 입출력 버스(Input/Output Bus)
- 데이터를 이동하는 전송 경로
- 주고받는 데이터의 종류에 따라 3가지 요소로 구성
- 데이터 버스
- 어드레스 버스
- 컨트롤 버스
폰 노이만 구조(Von Neumann architecture)
- 내장 메모리 순차 처리 방식으로 데이터 메모리와 프로그램 메모리가 구분되어 있지 않고 하나의 버스를 가지고 있는 구조
- 최근엔 확장된 하버드 구조를 사용
CPU 구조 및 기능
- ALU(Arithmetic Logic Unit)
- CPU 내부에 실제 연산을 담당하는 부분으로 산술 연산과 논리 연산 수행
- CPU로 입력 되었을 때 명령어의 내용대로 연산하는 주 요소
- 컨트롤 유닛(Control Unit)
- CPU 내부로 들어온 명령어를 해석해서 ALU에게 전달
- 명령어 분석해서 해야 할 일 결정하는 요소
- 레지스터(Register Set)
- CPU 내부에 임시적으로 데이터를 저장하기 위한 메모리 공간
- 버스 인터페이스(Bus Interface)
- CPU 내에 I/O 버스의 통신 프로토콜(Protocol)을 이해하고 있는 장치
- 버스의 통신 방식에 맞게 데이터 입, 출력을 돕는 인터페이스 장치는 컨트롤러 또는 어댑터
- CPU 내에 I/O 버스의 통신 프로토콜(Protocol)을 이해하고 있는 장치
프로그램 실행
1. 인출(Fetch)
- 메모리 상에 존재하는 명령어를 CPU로 가져오는 작업
- 레지스터에 저장
2. 해독(Decode)
- 가져온 명령어를 CPU가 해석
- 무슨 일 하라는 명령어인지 분석
- 컨트롤 유닛에서 실행
3. 실행(Execute)
- 해석된 명령대로 CPU가 실행
- ALU에서 실행
- 대부분 인텔 CPU 기반으로 시스템 운영
- x86, x64 CPU가 존재
- 각각 IA-32, IA-64로 표현
- 32비트 인텔 CPU는 8086부터 시작 - 8086은 80x86으로 불림
- 그 후 2세대는 286, 3세대는 386, 4세대는 486 등 불리며 발전
- IA-64는 IA-32와 호환성을 가지지 않음
- 호환성 가지는 모델 개념은 AMD64로 시작
- EM64T, IA-32e 등으로 불리다가 현재 인텔에서는 Intel 64로 부름
IA-32구조
- 주소 공간(Address Space)
- 프로그램당 최대 4GB(2^32 바이트) 선형 주소 공간(Linear Address Space)
- 최대 64GB(2^36 바이트) 물리 주소 공간(Physical Address Space)을 쓸 수 있음
- 기본 프로그램 실행 레지스터(Basic Program Execution Registers)
- 8개 범용 레지스터, 6개 세그먼트 레지스터, EFLAGS 레지스터, 명령 레지스터(EIP)로 구성
- Byte, Word, Double Word 크기의 기본 산술연산 지원
- 64bit에는 Quater Word도 있음
- 프로그램 흐름 처리, 비트와 바이트 문자열 처리, 메모리 주소 지정 등 수행
선형 주소 공간 - 분할된 메모리 모델의 핵심
- 가상 메모리라고 불리며, 디버깅 할 때 볼 수 있는 주소
- 최대 4GB로 구성
- 32비트 CPU에서만 선형 주소 공간을 쓰기 때문
- 낮은 주소부터 높은 주소로 표현
- 하나의 프로그램의 커널, 힙, 스택, 텍스트 등 영역들로 분할
- 커널과 사용자 영역의 크기는 일정한 규칙을 가짐
- (일반모드) 커널:사용자 = 2GB:2GB
- (4GT 모드) 커널:사용자 = 1GB:3GB
메모리 구조
- 텍스트(Text) 영역
- 실행할 프로그램 코드가 저장되는 영역
- 데이터(Data) 영역
- 전역 변수, 정적(static) 변수 등이 저장되는 영역
- 초기화 된 데이터 영역과 초기화 되지 않은 데이터 영역 구분(BSS)
- 힙(Heap) 영역
- 메모리 동적 할당 시 데이터가 저장되는 영역
- 높은 주소 방향으로 데이터 증가
- 스택(Stack) 영역
- 지역 변수, 매개변수 등의 임시 데이터가 저장되는 영역
- 후입선출(LIFO, Last In First Out)
- 낮은 주소 방향으로 데이터 증가
범용 레지스터
- eax의 하위 16비트는 ax
- ax의 상위 8비트는 ah
- ax의 하위 8비트는 al
- eax, ax, ah, al이 각각 다른 공간을 의미하지는 않음
- eax에다 0000 0000 ... 0000 0001 을 집어넣으면 ax도 al도 1이 됨
- EAX, EBX, ECX, EDX, ESI, EDI 레지스터는 우리 마음대로 쓸 수 있는 레지스터
- EAX : 계산을 위해 사용되며, 함수 호출의 리턴 값 저장하고 더하기, 빼기, 비교 등과 같은 기본적인 연산
- EBX : 범용성은 없으며, 데이터 저장하는데 사용됨
- ECX : 반복을 위해 사용되며, 아래쪽으로 카운트(루프 카운터로 쓰임)
- EDX : EAX 레지스터의 확장이며, 더 복잡한 계산(곱하기, 나누기)을 가능하게 함
- ESI와 EDI : 고속 메모리 복사에 쓰이며, 다량의 메모리를 옮기거나 비교할 때의 주소 값을 가짐
- S는 Source, D는 Destination의 약자
- ESI(Source Index) : 입력 데이터의 위치를 가지고 있음
- EDI(Destination Index) : 데이터 연산 결과가 저장되는 위치를 가리킴
범용 레지스터
- EBP
- Stack Base 포인터로 쓰임
- 고급 언어에서 사용하는 함수 파라미터와 지역변수를 가리키는데 쓰임
- ESP
- Stack Pointer의 약자
- push, pop, ret, call 등에 사용됨
- 스택의 탑을 가리키고 있음
세그먼트 레지스터
- CPU가 사용하는 논리주소를 4GB 선형주소로 변환하는데 사용
- 64비트 CPU부터 세그먼트 레지스터를 사용하지 않음
- 종류
- 코드 세그먼트(CS, Code Segment)
- 메모리 코드 영역의 시작주소 가지며, 실행할 명령을 가리키는 레지스터인 EIP가 이 세그먼트를 참조
- 인출(fetch)하는 모든 명령
- 데이터 세그먼트(DS, Data Segment)
- 데이터 집합의 시작주소 가지며, 데이터와 관련있는 A, C, D, SI, DI가 이 세그먼트 참조
- 스택 세그먼트(SS, Stack Segment)
- 스택의 시작주소 가지며, 스택과 관련있는 SP와 BP가 이 세그먼트 참조
- push와 pop하는 모든 스택
- 코드 세그먼트(CS, Code Segment)
FLAGS 레지스터
- 프로세서의 현재 상태를 표현함
- 상태 레지스터라고도 부름
- 플래그 레지스터는 16비트이며, E를 접두어로 사용하는 EFLAGS는 32비트, R을 접두어로 사용하는 RFLAGS는 64비트
- 32비트로 충분히 사용 가능하며, 확장될 경우 사용하지 않는 비트는 0으로 설정
- 1, 3, 5 비트는 고정 값을 사용
FLAGS 레지스터 종류
- 상태 레지스터
- CF(Carry) : 연산 결과가 저장 공간의 범위를 벗어날 때 1로 설정되는 플래그
- PF(Parity) : 연산 결과의 최하위 바이트(8비트)에서 1로 설정된 비트 개수가 홀수면 0 짝수면 1로 설정. 검사를 위해 사용
- 26(10) = 00011010(2) > PF = 0
- 10(10) = 00001010(2) > PF = 1
- AF(Adjust or Auxiliary) : 특수 플래그로 최하위 4비트를 의미하는 니블(Nibble)에서 자리 올림이나 자리 내림이 발생할 때 1로 설정, 주로 이진화 십진법(BCD, Binary-Coded Decimal) 산술을 지원하는데 사용
- 이진화 십진법은 니블로 10진수를 표현하는 방법, 1001(2) 이상 넘어가지 않음
- ZF(Zero) : 산술 결과가 0이면 1로 설정, 그렇지 않으면 0으로 설정
- 분기와 같은 조건에서 많이 사용하기에 중요한 플래그
- SF(Sign) : 부호 플래그로 값이 양수일 경우 0, 음수일 경우 1로 설정. 최상위 비트가 부호비트로 사용될 경우 동일한 값을 가짐.
- OF(Overflow) : 연산에 사용되는 비트 수(ALU에서 받아서 처리하는 폭)을 넘어섰을 때 1로 설정
- word 연산에서 127(10) + 127(10) 연산 하면
- 0111 1111(2) + 0111 1111(2) = 1111 1110(2) = -2(10)
- 0000 0010(2) 의 1의 보수는 1111 1101(2)
- 1111 1101(2) 의 2의 보수는 1111 1101(2) + 1 = 1111 1110(2) = -2
- 다시 말해 254가 아닌 -2가 생성되므로 오버플로우 플래그를 설정함으로써 254와 -2를 구분(word 기준)
- 제어
- TF(Trap) : 디버깅에 사용되는 플래그로 1로 설정되면 실행 명령 하나하나 살펴볼 수 있음
- IF(Interrupt) : 마스크 가능한 인터럽트의 처리 여부 결정하는 플래그
- DF(Direction) : 저장된 문자열을 왼쪽에서 처리할지 오른쪽에 처리할지 결정하는 플래그로 0으로 설정되면 낮은 주소부터 높은 주소 방향으로, 1로 설정되면 높은 주소에서 낮은 주소 방향으로 처리
- 명령 포인터 레지스터
- IP 앞에 32비트로 확장한 E가 붙어 EIP로 불림
- CPU는 EIP 레지스터에 정의된 메모리 섹션의 명령을 처리
- EIP는 JMP나 CALL 명령으로 수정할 수 있음
- 공격자가 원하는 메모리 위치를 가리키도록 EIP를 제어할 수 있으며, 메모리에 공격자의 명령을 작성할 수 있다면 시스템을 장악할 수 있음
- 버퍼 오버플로우 공격으로 EIP를 덮어쓸 수 있음
- 덮어쓴 EIP 부분을 찾아 원하는 위치로 이동
- 해당 위치에 공격자의 공격코드 삽입
- 버퍼 오버플로우 공격으로 EIP를 덮어쓸 수 있음
- EIP 레지스터는 특별한 구조 없이 다음에 실행시킬 명령의 주소를 가짐
어셈블리어 개요
- 기계어를 인간이 알 수 있는 최소한의 언어로 변환한 언어
- 명령 실행 속도가 매우 빠름
- 어셈블리어와 기계어는 1:1로 매칭되어 있음
- CPU마다 서로 다른 어셈블리어 존재
어셈블리어 종류 - 스택 조작
- PUSH oper1
- oper1을 스택에 저장
- push 1 -> 값 1을 스택에 저장
- push 00DA1086 -> 00DA1086을 스택에 저장
- push EAX -> EAX 레지스터에 저장된 값을 스택에 저장
- oper1을 스택에 저장
- POP oper1
- 스택의 제일 위에 있는 값을 빼서 oper1에 저장
- MOV oper1, oper2
- oper2의 값을 oper1에 저장
- LEA oper1, oper2
- oper2의 주소를 oper1에 저장
- CALL oper1
- oper1 주소 호출
- RET/RETN oper1
- 이전에 호출된 함수로 되돌아감
- ADD oper1, oper2
- oper1에 oper2를 더한 뒤 결과 값을 oper1에 저장
- SUB oper1, oper2
- oper1에서 oper2를 빼고 결과 값을 oper1에 저장
- MUL/IMUL
- MUL은 부호 없는 곱셈 명령, IMUL은 부호 있는 곱셈 명령 수행
- MUL은 오퍼랜드가 1개, IMUL은 오퍼랜드가 1~3개
- mul / imul oper1 -> eax(ax) = eax(ax) * oper1
- mov eax, 90000; mul eax; -> eax는 1E2CC310h -> eax=E2CC3100h, edx = 1h
- imul oper1, oper2 -> oper1 = oper1 * oper2
- imul oper1, oper2, oper3 -> oper1 = oper2 * oper3
- 32비트 연산은 EDX:EAX에 저장
- 곱셈 했을 떄 두 배의 크기의 공간에 저장되는 이유
- 곱셈 결과가 2배가 되어 자리수를 넘어가기 때문 -> 곱셈 후 확장함
- 자리수 넘어가면 캐리 또는 오버플로우가 발생함
- DIV/IDIV
- MUL은 부호 없는 곱셈 명령, IMUL은 부호 있는 곱셈 명령 수행
- div / idiv oper1
- oper가 byte 단위인 경우 : div oper1 나눈 후 몫 al, 나머지는 ah에 저장
- word 이상인 경우 : div oper1 나눈 후 몫은 eax, 나머지는 edx에 저장
- div / idiv oper1
- IDIV 나눗셈 시 CBW, CWD, CDQ 등 명령어를 통해 부호 확장 이유
- 부호를 처리해야 하기 때문 -> 확장시 나누어지는 수가 양수인 경우 0, 음수인 경우 F로 채움
- 자리수 넘어가면 캐리 또는 오버플로우 발생
- MUL은 부호 없는 곱셈 명령, IMUL은 부호 있는 곱셈 명령 수행
- INC oper1
- oper1의 값이 1만큼 증가
- DEC oper1
- oper1의 값이 1만큼 감소
- AND oper1, oper2
- oper1과 oper2의 비트 AND 연산 후 결과 값을 oper1에 저장
- OR oper1, oper2
- XOR oper1, oper2
'정보보호과정' 카테고리의 다른 글
6주차(1) - 2022/03/07 네트워크 기초 개요 (0) | 2022.03.14 |
---|---|
4주차(2) - 2022/03/02 프로그래밍 개론 (0) | 2022.03.02 |
4주차(1) - 2022/02/28 프로그래밍 개론 (0) | 2022.02.28 |
3주차(4) - 2022/02/24 리눅스 기초 (0) | 2022.02.24 |
3주차(3) - 2022/02/23 리눅스 기초 (0) | 2022.02.23 |