Layer7 과제/리버싱

[리버싱] 2차시 과제

kms0204 2022. 7. 25. 21:42

<어셈블리(x64)>

프로그램이 실행되는 동안에 디버거로는 기계어만 볼 수 있다.

하지만 어셈과 기계어는 1대1 대응되므로어셈을 볼 수 있다는 뜻이 된다.

그렇다면 어셈을 읽을 수 있다면,디버거의 동작과 흐름을 이해할 수 있다.


x64란 ISA의 종류에서 다룬다.
명령어 처리 구조라는 의미로 cpu마다 아키텍처가 다르고 따라서 명령어가 다르다. 

그중에서 x64 고유의 명령어 구조를 배웠다.

x64를 공부하는 이유는 현재 cpu 점유율의 대부분은 x64와 x86(64bit 32bit) 이기 때문이다.

참고로 x64는 x86을 완벽히 지원한다.(역은 불가, 이것을 상위호환이라 한다.)
그리고 x64, x86-64, AMD64, Intel 64 ... 다 똑같은 x64를 말한다.

그 이유는 예전에 기업간의 교류 때문에 이름이 여러개 생긴 것이다. 

물론 각각 사소한 차이가 있긴 하지만, 무시해도 된다.

 

x64와 x86 아키텍처에서 숫자는 ALU(연산을 담당하는 디지털 회로) 한 번에 처리할 수 있는 데이터의 크기를 의미한다.

WORD라고 부르는데, 즉 WORD의 크기는 cpu에 설계 방식에 따라 달라지는 것이다.

 

WORD가 커서 좋은 점 중에 대표적인 것은 가상메모리의 크기가 커지기 때문이다.

(가상 메모리란 메모리, 즉 가상의 RAM을 사용하는 기술을 말한다. HDD를 RAM처럼 사용하는 것이다.)

 

x86은 4기가 바이트가 최대로 제공할 수 있는 가상 메모리 크기이지만, x64는 무려 16엑사 바이트이다. (이론상)

16엑사 바이트는 일반적으로는 다 사용할 수 없을정도로 엄청 큰 크기이다.

 

 


 

어셈블리어 명령어를 배우기 전에, 여러 사전지식이 필요하다.

대표적으로 레지스터가 있다.


레지스터란?

참고. <메모리 계층 구조>

 


cpu에 내장되어 있는, 용량은 엄청 작고 속도는 엄청 빠른 기억소자이다.
이것으로 프로그램을 실행시킨다고 할 정도로 핵심이 되는 부품이다.


우리는 cpu를 구동하는데 정말 꼭 필요하고 정말 빠른 연산을 요구하는 값들을 레지스터에 저장한다.

예를 들어 명령어의 주소(여기 실행하고 있다 라고 가리키는 주소), 연산할 떄 사용하는 값, 

*스택 프레임(뒤에 나옴)의 위치를 알려주는 포인터 등의 값이 있다.

그리고 레지스터에도 이러한 연산할 데이터를 담을 수 있는 크기가 있다.

일반적으로 상상하는 것보다 훨씬 작은 크기인데, x64에서는 8비트가 고작이다.

(단 처리 속도는 매우 빠르다.)

 




레지스터마다 역할이 있는데 권고하는 느낌이다.
다른 용도로 써도 상관없고 참고 정도만 하면 된다.

 

범용 레지스터, 인덱스 레지스터, 포인터 레지스터, 플래그 레지스터가 있다.

 


 

범용 레지스터: 작은 데이터를 임시로 저장하는 공간.연산 처리 및 데이터 주소를 지정하는 역할을 한다.

R로 시작하는건 x64, 8바이트이고 E로 시작하는건 x86, 4바이트이다.

x64 범용 레지스터 - dream hack 참고

RAX: 연산에 사용되고 함수의 리턴값을 저장한다.

RBX: 메모리 주소를 저장하기 위해 사용한다.

RCX: 반복문에서 카운터 변수로 사용된다.

RDX: 여분의 레지스터.큰 수의 곱셈, 나눗셈에서 RAX와 같이 사용된다.

R8 ~ R15: 다용도 레지스터이자 여분의 레지스터. x86에는 존재하지 않는다.

 

x86 범용 레지스터


인덱스 레지스터:  메모리 주소와 관련된 레지스터이다.

RSI RDI가 있다.

 

RSI: 데이터를 복사할 때, 복사할 데이터의 주소가 저장된다.

RDI: 데이터를 복사할 때, 목적지의 주소가 저장된다. (즉 복사한 값과 같다)



포인터 레지스터: 메모리의 특정한 위치를 가리키는 레지스터이다.

RSP RBP RIP가 있다.

RSP: 현재 사용 중인 스택의 맨 위를 가리킨다.

RBP: 현재 사용 중인 스택의 맨 아래를 가리킨다. (즉 시작 지점을 가리킨다)

RIP: 다음에 실행해야 할 명령어의 메모리 주소를 가리킨다.

 

"프로그램 실행에서 특정한 역할을 하기 때문에"


함수의 매개변수로서의 레지스터:

함수를 호출할 때 인자를 넘겨줄 수 있다.

이때 함수에 인자 전달을 레지스터로 한다.

[tip]
매개변수: 함수를 정의할때 (함수의 형식)
인자: 함수를 호출할때 (내가 형식에 맞게 변화시킴)

(cdecl, stdcall, fastcall 등의 호출 규약이 있다)

 

// x64 방식 //
int function(int a, int b, int c, int d, int e, int f, int g, int h)

int a : rdi
int b : rsi
int c : rdx
int d : rcx
int e : r8 
int f : r9
int g, int h : STACK

함수에 인자를 넘겨줄 때 인자의 값이 순서대로 지정된 레지스터에 들어간다.
6번 째 인자(즉 r9)까지 꽉 차면 나머지는 모두 스택에 쌓아버린다.

 

// x86 방식 //
int function(int a, int b, int c, int d, int e, int f, int g, int h)

전달된 인자를 레지스터에 저장하지 않고 모두 스택에 쌓아버린다.
(위로 자라나는 구조, 즉 인자의 오른쪽에서 왼쪽순으로 갈수록 스택의 위쪽에 쌓인다)

 


 

 

플래그 레지스터: 참(1) 거짓(0)의 상태를 가져서 특정한 상태를 표현한다.

CF ZF SF OF가 있다.

 

CF: 부호 없는 unsigned 수끼리의 연산에서 자리올림이 발생할 경우 1.

ZF: 수끼리의 연산 결과가 0이 될 경우 1.

SF: 수끼리의 연산 결과가 양수이면 0, 음수이면 1. (음수가 맞냐? 참 = 1, 거짓 = 0)

OF: 부호 있는 signed 수끼리의 연산 결과가 허용범위를 벗어날 경우 1.

*오버플로우를 이용해서 부호가 있는 숫자의 부호를 건들 수 있고, 이로 인해 부호가 바뀔 수 있다.

*모든 연산은 할때마다 그 결과에 따라서 flag resister값이 설정된다.


 

<참고 자료>

범용 레지스터 (전체) - 찬우 선배님 ppt 참고





CF: 최상위 1비트는 부호비트인데, 부호비트까지 숫자의 일부로 생각하고 연산했을때 자리올림
ZF: 
SF:
OF: 오버플로우가 발생했는지 따짐(허용범위벗어남)
-허용 범위란? ex 8bit 숫자끼리 더했는데 8bit를 벗어난 경우! 즉 오버플로우의 원리
****숫자를 표현할때 부호비트의 개념!!!
-부호가 있는 숫자를 잘못건들이면 오버플로우의 원리로 인해 부호비트가 바뀌기 때문에 조심해야됨
**자리올림과 오버플로우는 다르다!!
+(허용범위의 초과의 개념으로 바라봐야된다.)


[뒤에 나올 내용에 대한 사전 지식]

주요 메모리 구역은 4가지로 분류된다.

 

  1. 글로벌 or 데이터 영역
  2. 코드 영역
  3. 힙 영역
  4. 스택 영역

 

1. 글로벌 or '데이터 영역'

프로그램의 전역 변수와 정적 변수가 저장되는 영역이다.

프로그램의 시작과 함께 할당되고 프로그램이 종료되면 소멸한다.

 

2. 코드 영역

실행할 프로그램의 코드가 저장되는 영역이다.

텍스트 영역이라고도 불린다.

cpu가 코드 영역에 저장된 명령어를 하나씩 가져가서 처리한다.

코드는 기계어 형태이다.


3. 힙 영역

사용자가 직접 관리할 수 있는 영역이고, 관리를 해야만 하는 영역이다.

사용자에 의해 메모리 공간이 동적으로 할당되고 해제되는 영역이다.

메모리의 낮은 주소에서 높은 주소의 방향으로 할당된다. (자라나는 방향이 아래에서 위라고 한다)

 

4. 스택 영역

힙과는 다르게 크기 제한이 있으며 cpu에 의해 효율적으로 관리된다. (운영체제마다 다르다)

함수의 호출과 관계는 지역 변수와 매개 변수가 저장되는 영역이다.

함수의 호출과 함께 할당되며, 함수의 호출이 완료되면 소멸한다.

스택 영역에 저장되는 함수의 호출 정보를 "스택 프레임" 이라고 한다.

-함수의 매개 변수, 호출이 끝난 뒤에 돌아갈 반환 주소값, 함수에서 선언된 지역 변수 등이 저장된다.

-스택 프레임 덕분에 함수의 호출이 모두 끝난 뒤에 호출되기 이전의 상태로 돌아갈 수 있다.

-즉, 함수마다 스택 프레임이 생성되는 것이다.

-http://www.tcpschool.com/c/c_memory_stackframe

메모리의 높은 주소에서 낮은 주소의 방향으로 할당된다. (자라나는 방향이 위에서 아래라고 한다)

 

 

*스택과 힙의 자라나는 방향은 서로 반대 방향이다.



함수를 완전 low레벨-컴퓨터에 가까운 입장-에서 보면, start라는 함수가 먼저 실행되고 그 다음에 또 실행되는 것이다.

예를 들어서 main이라는 함수를 실행시키고 그 안에서 func이라는 함수를 실행했다면,

실제로는 start -> main -> func 순서로 실행된다는 것이다.

(함수의 실행은 이런식으로 반복되며, start라는 이름은 사실 줄임말이다)



main에서는 어디서부터 어디까지 각각의 스택프레임인지 구분하자지 않는다.
****따라서 스택프레임을 구분하기 위해서 rsp와 rbp를 사용한다.

<함수의 프롤로그>

함수의 프롤로그 - 찬우 선배님 ppt 참고


(0) 처음에 start만 실행되었을 때도 rsp와 rbp가 스택 프레임을 구현하고 있다.
(1) 이때 main을 호출하였고,  main의 스택 프레임을 만들기 위해 rbp를 rsp까지 올리고 rsp를 하나 올린다.

이렇게 main 스택 프레임이 현재 프레임이 된다.

(2) func 함수가 호출되었을 때 (1)과 같은 과정을 수행한다. 

 


<함수의 에필로그>

함수의 에필로그 - 찬우 선배님 ppt 참고


(1) func 함수 실행이 끝났다.

(2) func 함수의 프레임이 더 이상 필요 없어서 정리하기 위해 rsp가 하나 내려온다.

(3) rbp는 원래 main의 맨 아래로 돌아가서 다시 main의 스택 프레임의 경계를 가리킨다.


(3) 과정에서, 함수의 프롤로그와 다르게 rsp가 rbp로 내려오고 rbp는 어디까지 내려야할지 모른다. 

부연 설명을 하자면, "논리적인 해결방법이 없다." - 프롤로그와 다르게.

 

그래서 프롤로그때 값을 저장해 두었다가, 해당 이동하는 방식으로 처리한다.

 

일반적으로 특정 함수에 대한 스택 프레임에서 맨 아래에 ret, 그 위에 SFP가 쌓인다.

ret이란 return의 약자로, 현재 함수가 종료되면 이전에 실행 중이던 함수의 어느 부분으로 돌아가야할지,

즉 "반환 주소" 담당하고, sfp는 현재 함수가 끝났을 때 RBP가 가리켜야 하는 위치를 담당한다.

 

(함수를 호출했을 때, 호출하지 않았다면 실행했을 다음 명령어의 주소 - 즉 RIP - 를 ret으로 만든다.

예를 들어 main에서 func를 호출했다면, func를 호출하지 않았다면 실행되었을 main의 다음 명령어의 주소 -즉 RIP -를 func의 ret으로 생성하는 것이다. func 함수가 끝나면 다시 main의 명령어를 실행해야 되기 때문에 func의 명령어를 다 실행했다면 ret에 저장했던 값을 RIP에 넣어서 이제부터는 main의 명령어가 다시 실행되도록 한다) 

 

 

그리고 다시 돌아가야할 때 SFP에 있는 RBP값의 위치로 이동하는 것이다.

(즉 여기서 sfp는 main 함수의 맨 밑 주소이다. 무슨 뜻이냐면 main에서 func를 호출했다고 가정할 때,

RBP는 func의 맨 아래를 가리켜야 한다. 하지만 func가 끝난 후 어디로 돌아가야할지 알아야 하므로 RBP가 값이 바뀌기 전에 ret 위에 SFP를 만들어서 현재 RBP를 임시로 저장한다는 뜻이다. 이렇게 하면 나중에 func가 종료되었을 때 SFP에 저장했던 RBP 값을 꺼내서 RBP에 넣어준다면 main의 맨 밑 부분을 가리킬 수 있기 때문이다)

 

 

위 과정들 덕분에 main에서 func로 RSP와 RBP를 변경해도 func가 종료되면 다시 main에 맞춰 가리킬 수 있다.

 

그리고 일반적으로 스택 프레임에는 맨 아래부터 RET, SFP, 지역변수 순으로 쌓이는데 main에서 func를 호출해서 실행되는 상황이라면, func가 종료되면 위에서부터 하나씩 삭제 및 위에서 괄호 안에 추가적으로 설명했던 내용들을 수행하면서 다시 main 코드로 복귀한다.

스택 프레임 구조 - 찬우 선배님 ppt 참고


 

<어셈블리어 명령어 요약>

 

산술 명령어: add, sub, inc, dec, mul, div, cmp, test

데이터 이동 명령어: mov, lea, push, pop

흐름 제어 명령어: call, ret, jmp

 


*lea: 상수는 올수없는데 그 이유는 주소가 아니기 때문이다.
*push: 한 칸 올라가는 이유는 실행중인 함수의 제일 윗부분을 가리켜야 되기 때문이다.

'Layer7 과제 > 리버싱' 카테고리의 다른 글

[리버싱] 3차시 과제2  (0) 2022.07.27
[리버싱] 3차시  (0) 2022.07.27
[리버싱] 2차시 과제 (2)  (0) 2022.07.25
[리버싱] 1차시 과제 (2) - 리눅스 명령어, vim 명령어  (0) 2022.07.20
[리버싱] 1차시 과제  (0) 2022.07.20