시스템 해킹 01주차

준필·2022년 1월 12일
0

시스템 해킹

목록 보기
1/4
post-thumbnail

메모리 영역

프로그램이 실행되면 보조기억장치(HDD, SSD)에 저장되어 있던 프로그램이 주기억장치(RAM)에 로드된다. 또한 해당 프로그램이 실행되기 위한 데이터들을 저장하기 위해 OS는 해당 프로그램에 메모리 영역을 할당한다. 이렇게 할당된 메모리 영역은 크게 4가지 영역으로 구분된다.

코드 영역(Code)

  • 실행 파일에 의해 초기화된다.
  • 실행할 프로그램의 함수, 제어문, 상수 등이 할당된다.

데이터 영역(Data)

  • 실행 파일에 의해 초기화된다.
  • global variable, static variable이 할당된다.

힙 영역(Heap)

  • 동적 할당(Dynamic Memory Allocation)을 하게 될 경우 할당되는 영역이다.
  • 메모리 내의 낮은 주소에서부터 팽창한다.

스택 영역(Stack)

  • 함수 호출(function call)을 위해 동적으로 팽창하고 수축한다.
  • push & pop으로 동작해 메모리 내의 높은 주소에서부터 팽창한다.

    추후에 나오게 될 BOF, 어셈블리어의 동작 방식과 관련이 많다.


레지스터(Register)

  • 프로세서가 데이터를 처리할 때 RAM으로부터 주소를 가져오기에는 프로세서의 속도에 비해 RAM의 속도가 너무 느려 성능 저하가 발생할 수 있다. 레지스터는 이러한 성능 저하를 없애는 프로세서 내부의 매우 빠른 저장 장치로서 여러가지 연산에 필요한 정보를 임시로 저장하는 역할을 한다.

레지스터는 아키텍쳐마다 상이하다. 이 글에서는 x86-64 아키텍쳐의 레지스터에 대해 다룬다.

64bit 레지스터는 OS의 최적화에 의해 32bit(E_X), 16bit(_X) 등으로 나누어져 사용될 때도 있다.

범용 레지스터(General Purpose Register)

  • 말 그대로 '범용' 레지스터이다. 아래 나오는 설명들은 가장 많이 사용되는 예시일 뿐 해당 레지스터들은 설명 이외의 목적으로 사용되기도 한다.

어셈블리어를 분석할 때 레지스터에 왜 이런 값이 저장되는지 모르겠을 경우 구글링을 해보자. 호출 규약(calling convetion)과 관련이 있을 가능성이 높다.

Data Registers

  • 산술,논리 및 여러가지 연산에 사용되는 레지스터이다.
    • EAX(Accumulator) : 산술 연산 및 논리 연산에 사용, 함수 호출 시 변수 인자의 개수 정보를 저장, 첫 번째 return register이다.
    • EBX(Base) : 메모리의 주소를 저장한다.
    • ECX(Counter) : 반복문 사용시 카운터로 사용한다.
    • EDX(Data) : EAX레지스터와 유사하게 사용, 큰 수의 곱셈 & 나눗셈을 할 때는 EAX레지스터와 함께 사용한다.

Index Register

  • 주로 Index addressing(인덱스 주소 지정)시 사용되며 덧셈과 뺄셈 연산에도 사용된다.

    • EDI(Destination Index) : string 연산의 Dest 주소 저장한다.
    • ESI(Source Index) : string 연산의 Src 주소 저장한다.

윈도우에서의 레지스터 인자 전달 순서 :
rcx - rdx - r8 - r9 - rest on stack(스택을 이용해 인자를 전달한다는 뜻이다.)
이외의 운영 체제에서의 레지스터 인자 전달 순서 :
rdi - rsi - rdx - rcx - r8 - r9 - rest on stack
이러한 인자 호출 순서를 calling convetion이라 한다.

Pointer Register

이 부분에서 등장하는 Stack은 앞서 언급되었던 메모리 구조의 Stack을 의미한다.

  • 메모리의 주소값을 저장한다.

    • ESP(Stack Pointer) : Stack frame의 끝 지점 주소(가장 낮은 메모리 주소)를 저장한다.

    • EBP(Base Pointer) : Stack의 시작 지점 주소(가장 높은 메모리 주소)를 저장한다.

    • EIP(Instruction Pointer) : 다음에 실행해야 할 명령어의 주소를 저장한다.

세그먼트 레지스터(Segment Register)

  • 각 영역의 시작 주소를 저장해 이를 기준삼아 메모리 상의 위치를 구할 수 있게 해준다.

    • CS(Code Segment) : 메모리 구조의 코드 영역(code segment)의 시작 주소를 저장한다.

    • DS(Data Segment) : 메모리 구조의 데이터 영역(data segment)의 시작 주소를 저장한다.

    • SS(Stack Segment) : 메모리 구조의 스택 영역(stack segment)의 시작 주소를 저장한다.

    • ES(Extra Segment), FS, GS : 부수적으로 데이터를 저장하기 위한 Segment이다.

컨트롤 레지스터(Control Register)

  • 프로세서의 행동에 영향을 미치는 레지스터이다.

Flags Register

  • 다양한 연산들의 결과 상태를 저장한다. 조건문의 실행 분기를 결정할 때 주로 사용된다.

    • OF(Overflow Flag) : singed(부호가 있는) 산술 연산 이후의 overflow 여부를 나타낸다.
    • DF(Direction Flag) : 문자열 연산에서 0이면 왼쪽에서 오른쪽으로, 1이면 오른쪽에서 왼쪽으로 연산을 수행한다.
    • IF(Interrupt Flag) : 외부 interupt(키보드 입력 등)의 영향을 받을 것인지 정한다. 값이 0이면 외부 interupt의 영향을 받지 않고, 1이면 외부 interupt의 영향을 받는다.
    • TF(Trap Flag) : 명령어를 한 단계씩 실행할 수 있도록 해준다. 디버깅을 할 때 1의 값을 가진다.
    • SF(Sign Flag) : 산술 연산의 결과값의 부호를 나타낸다. 음수이면 1, 양수이면 0의 값을 가진다.
    • ZF(Zero Flag) : 산술 연산의 또는 비교 연산의 결과를 나타낸다. 연산의 결과가 0이라면 1의 값을 가지고 이외의 경우에는 0의 값을 가진다.
    • AF(Auxiliary Carry Flag) : 산술 연산의 결과 carry(올림) 또는 borrow(내림)가 3bit 이상 발생할 경우 1의 값을 가진다.
    • PF(Parity Flag) : 산술 연산의 결과값에서 1인 비트의 개수가 짝수개이면 1, 홀수개이면 0의 값을 가진다.
    • CF(Carry Flag) : 산술 연산의 결과 carry(올림) 또는 borrow(내림)가 발생하면 1의 값을 가진다.

Endian

  • 메모리에 자료를 연속적으로 배열하는 방법이며 현재는 대다수의 시스템에서 Little Endiand을 사용하고 있다. 시스템에 메모리 단위로 코드를 작성할 경우 이에 대한 이해가 필요하다.

Big Endian

  • 메모리의 낮은 주소에 데이터의 높은 바이트부터 저장하는 방법이며 저장된 순서 그대로 읽을 수 있고 이해하기 쉽다.

Little Endian

  • 메모리의 낮은 주소에 데이터의 낮은 바이트부터 저장하는 방법이며 평소에 숫자를 읽는 방법과 반대로 읽어야 한다.

각 Endian의 활용 분야

  • Little Endian은 산술 연산, 자료형 읽기와 형 변환을 하는 속도가 빠르다.
  • Big Endian은 비교 연산, 디버깅을 할 때 편하다.
    TCP, UDP, IPv4, IPv6 등의 많은 프로토콜에서 사용된다.

어셈블리 언어

  • 기계어(ex 01010100...)와 일대일대응되는 언어로 고급 언어로 작성된 소스 코드를 컴파일하는 과정에서 생성된다.

아키텍쳐에 따라 어셈블리어가 다르다. 이 글에서는 x86-64 아키텍쳐의 어셈블리어를 기준으로 설명한다.

AT&T vs Intel

  • 두 가지 모두 어셈블리 언어의 문법으로 읽는 순서가 서로 반대이다.

어떤 문법이 어떤 구조를 가지는지 외울 필요는 없고 그냥 이런게 있다는 걸 알고 넘어가면 된다. 필요할 때 찾아보도록 하자.

접두사

AT&T : 레지스터는 '%' 접두사를 가지며 값들에는 '$' 접두사를 가진다.
Intel : 문법이 대체로 간단하고 접두사가 없다. 16진수와 2진수 데이터는 'h', 'b'의 접두사를 가진다.

왜 h가 뒤에 붙는데 접두사인지는 필자도 잘 모르겠다.

피연산자(Operend)의 위치

AT&T : instr src,dest
Intel : instr dest,src

메모리 피연산자

AT&T : '()'를 사용해 표현
Intel : '[]'를 사용해 표현

접미사

AT&T : 정해진 규칙에 따라 접미사를 사용한다.
Intel : 접미사를 사용하지 않는다. 문장 그 자체로 의미를 가지도록 사용한다.


Opcode & Operands

Opcode(Operation Code)

  • 연산자를 의미한다.
  • 함수 연산(ex : ADD, SUB, AND, OR etc), 자료 전달 연산(ex: Load, Store etc), 제어 연산, 입출력 연산으로 나뉘어진다.

    그럼 Instruction과 Opcode의 차이점이 무엇일까? 링크에 따르면 같은 Instruction이라도 Operands의 종류에 따라 다른 Opcode를 가질 수 있다고 한다. MOV EAX, EBX와 MOV EAX, 8 의 Opcode가 다를 수 있다는 것. 따라서 MOV, LEA 등등은 Opcode가 아닌 Instruction으로 봐야한다.

Operands

  • 피연산자를 의미한다.
  • operand의 종류에는 Reg(Register)/ Mem(Memory)/ Imm(Immediate)이 존재한다.

자주 사용되는 명령어(Instruction)

MOV(Move) : src의 데이터를 복사하여 dest에 저장한다.
LEA(Load Effective Address) : src의 주솟값을 dest에 저장한다.

PUSH : stack frame에 src 데이터를 저장한다.
POP : dest에 stack frame 데이터를 저장한다.

  • PUSH와 POP모두 ESP의 값이 자동으로 갱신된다.

ADD/SUB/MUL/DIV(Add/Subtract/Multiply/Divide) : 차례대로 덧셈, 뺄셈, 곱셈, 나눗셈 연산을 수행한다. (ex : dest = dest - src)

INC/DEC(Increase/Decrease) : 차례대로 증가 연산, 감소 연산이다.

AND/OR/XOR/NOT : 차례대로 and, or, xor, not 논리 연산이다.

CMP(Compare) : dest와 src의 값을 비교해 같다면 ZF의 값을 0으로 만든다.
JMP/JE/JNE: PC의 값을 변경해 실행 흐름을 이동(JUMP)시킨다. JE(Jump if Equal)는 ZF의 값이 0일 경우, JNE(Jump if Not Equal)는 ZF의 값이 1일 경우 점프시킨다.

JLE(Jump if Less or Equal), JGE(Jump if Greater or Equal), JG(Jump if Greater) 등의 여러 jump 계열 명령어가 존재한다.
추가적인 명령어들은 이곳을 참고하면 좋을 것 같다.

CALL : EIP의 값을 Stack의 최상단에 넣고 함수를 호출한다.
RTN : Stack 최상단의 주소를 EIP에 저장한다. 이 주소는 복귀 주소가 된다.

NOP : 공백 처리. 쉬어가는 명령어이다.


주소 지정 방식(Memory Addressing Mode)

  • 주소 지정(Memmory Adrresing)은 명령어가 필요로 하는 피연산자(Operand)를 지정하는 것을 의미한다.

예시들은 모두 Intel 문법으로 작성되었다.

Complete Memory Addressing Form

메모리에 접근하는 가장 일반적인 형태의 주소 지정 방식은 [RB+S*RI+D] 의 형태이고 Segment는 암묵적으로 Data Segment이다.

RB(Base Register) : 말 그대로 base의 역할을 한다.
S(Scale) : RI에 곱해지는 수로 몇 개의 주소를 건너 뛸 지 정한다.
RI(Index Register) : 배열의 index와 유사한 역할을 한다.
D(Displacement) : 주소 지정에서 변위의 역할을 한다.

아래 등장하는 모든 주소 지정 방식의 이름을 외울 필요는 없다.
결국 Complete Memory Addressing Form으로 전부 표현할 수 있기 때문이다.

레지스터 주소 지정(Register Addressing)

  • 레지스터의 값이 직접 피연산자로 사용된다. 레지스터 연산은 메모리에 접근하지 않으므로 데이터 처리 속도가 가장 빠르다.

ex)

MOV EDX, COUNT	; 첫 번재 피연산자가 레지스터이다.
MOV COUNT, ECX	; 두 번째 피연산자가 레지스터이다.
MOV EAX, EBX	; 두 피연산자가 모두 레지스터이다.

직접 메모리 주소 지정(Direct Memory Addressing)

  • 메모리에 접근하므로 데이터 처리 속도가 느리다. 피연산자가 각각 레지스터, 메모리를 참조한다.

ex)

BYTE_VALUE DB 150	; BYTE_VALUE가 정의된다. DB는 데이터의 크기를 나타낸다.
ADD BYTE_VALUE, DL	; BYTE_VALUE의 메모리 위치에 DL의 값을 더한다.

MOV AL, DS:[8088h]	; AL에 Data Segment의 offset 위치의 값을 저장한다.

즉시 주소 지정(Immediate Addressing)

  • 상수나 표현식의 형태로 Operand를 지정한다.첫 번째 피연산자는 데이터의 길이를 정한다.

ex)

BYTE_VALUE DB 150	; BYTE_VALUE가 정의된다.
ADD BYTE_VALUE, 35	; BYTE_VALUE의 값에 35를 더한다.

레지스터 간접 주소 지정(Register Indirect Addressing)

  • Segment:Offset 주소 지정 방식을 사용한다. 일반적으로 배열과 같이 여러개의 원소를 포함하는 변수에 사용한다.
  • [RB]의 형태이다.

ex)

MOV AL, [BX]	; AL에 DS:[BX]의 값을 저장한다.

인덱스 주소 지정(Indexed Addressing)

  • 배열을 인덱싱(?)할 때 많이 사용된다.
  • [RB+D]의 형태이다.

ex)

MOV AL, [BX+20h]	; AL에 DS:[BX+20h]의 값을 저장한다.

인덱스 주소 지정 방식은 어셈블리 코드를 분석할 때 등장하게 되는 RIP Relative Addressing과 관련 있다.

베이스 레지스터 주소 지정(Base Register Addressing)

  • 2차원 배열을 인덱싱할 때 많이 사용된다.
  • [RB+RI]의 형태이다.

ex)

MOV AL, [BX+SI]	; AL에 DS:[BX+SI]의 값을 저장한다.

변위(displacement)를 갖는 인덱스 주소 지정

  • [RB+RI+D]의 형태이다.

ex)

MOV AL, [BX+SI+30h]	; AL에 DS:[BX+SI+30h]의 값을 저장한다.

시스템 콜(System Call)

  • 커널에 접근할 수 있는 인터페이스이다. 프로세스 제어, 파일 조작, 장치 관리, 정보 유지, 통신 등이 가능하다.

라이브러리 콜과 구분되는 개념이며 해당 내용은 시스템 프로그래밍에서 배울 수 있다.


함수 프롤로그/에필로그(Function Prolouge/Epilogue)

Stack Frame이란 메모리 구조의 Stack에 차례대로 저장되고 소멸되는 각 함수의 호출 정보(매개 변수, 반환 주솟값, 변수 etc)를 저장하고 있는 부분이다.

함수 프롤로그

  • 함수가 호출될 때 Stack frame을 구성해주는 과정이다.
  1. 현재 BP의 값을 Stack에 push한다.
  2. BP의 값을 SP(Stack Pointer)의 값으로 변경한다. 이로 인해 BP는 특정 Stack Frame의 가장 높은 주소를 가르키게 된다.
  3. SP의 값을 감소(또는 증가)시켜 Stack Frame 공간을 확보한다.
#function prolouge explain
push rbp # push, pop 연산은 모두 rsp를 기준으로 일어난다.
mov rbp, rsp
sub rsp, 0x30 # 임의의 상수(예시에서는 48)만큼 스택 프레임을 할당한다.

함수 에필로그

  • 함수가 종료될 때 Stack frame을 없애는 과정이며 함수 프롤로그의 역과정이다.
  1. SP의 값을 BP의 값으로 변경한다. 이로 인해 함수 프롤로그 단계에서 할당했던 Stack Frame 공간을 free 시킨다.
  2. 함수 프롤로그 단계에서 Stack에 push 시켰던 BP값을 pop해 이전의 BP값을 얻는다.
  3. 함수가 종료되며 해당 함수를 호출한 함수로 돌아간다.
#function epilouge explain
mov rsp, rbp
pop rbp
pop rip
profile
공부할 게 너무 많잖아?

0개의 댓글