개발블로그

리버스 엔지니어링(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)을 이해하고 있는 장치
      • 버스의 통신 방식에 맞게 데이터 입, 출력을 돕는 인터페이스 장치는 컨트롤러 또는 어댑터

 

프로그램 실행

 

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하는 모든 스택

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 레지스터는 특별한 구조 없이 다음에 실행시킬 명령의 주소를 가짐

 

어셈블리어 개요

  • 기계어를 인간이 알 수 있는 최소한의 언어로 변환한 언어
    • 명령 실행 속도가 매우 빠름
  • 어셈블리어와 기계어는 1:1로 매칭되어 있음
  • CPU마다 서로 다른 어셈블리어 존재

 

어셈블리어 종류 - 스택 조작

  • PUSH oper1
    • oper1을 스택에 저장
      • push 1 -> 값 1을 스택에 저장
      • push 00DA1086 -> 00DA1086을 스택에 저장
      • push EAX -> EAX 레지스터에 저장된 값을 스택에 저장
  • 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에 저장
    • IDIV 나눗셈 시 CBW, CWD, CDQ 등 명령어를 통해 부호 확장 이유
      • 부호를 처리해야 하기 때문 -> 확장시 나누어지는 수가 양수인 경우 0, 음수인 경우 F로 채움
    • 자리수 넘어가면 캐리 또는 오버플로우 발생
  • INC oper1
    • oper1의 값이 1만큼 증가
  • DEC oper1
    • oper1의 값이 1만큼 감소
  • AND oper1, oper2
    • oper1과 oper2의 비트 AND 연산 후 결과 값을 oper1에 저장
  • OR oper1, oper2
  • XOR oper1, oper2

 

profile

개발블로그

@ORIONPOINT

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

검색 태그