촉촉한초코칩
Window System Hacking [1장. 개요] 본문
윈도우 시스템 해킹 가이드 - 버그헌팅과 익스플로잇
1. PC 내부 구조
1.1 컴퓨터의 언어
컴퓨터는 소스 코드를 이해하지 못하기 때문에 내부적으로 컴파일을 거쳐 CPU가 이해할 수 있는 기계어로 대화한다.
Exploit 코드를 작성하기 위해서는 기계어인 어셈블리어와 컴퓨터의 내부 동작 구조를 이해해야 한다.
(참고 : https://h-factory.tistory.com/53)
1.2 CPU와 레지스터
CPU 구성요소
- 연산장치 : 수학적 연산
- 제어장치 : 메모리에서 기계어 코드를 읽고 해석한 뒤 실행하는 역할
- 레지스터 : 연산을 위해 CPU가 사용하는 데이터 저장소
인텔 IA-32구조
- 일반적인 32bit 구조의 인텔 CPU에서 사용되는 구조
- 대부분의 CPU는 IA-32와 호환되도록 설계된다.
레지스터 종류
- 범용 레지스터
- 명령어 포인터
- 세그먼트 레지스터
- 플래그 레지스터
→ 레지스터에는 메모리 주소나 값이 들어있고, CPU는 레지스터들을 이용해서 연산을 수행한다.
레지스터 | 설명 |
EAX | 산술 계산 및 리턴 값 전달에 사용 |
EBX | 범용적으로 사용 가능한 추가적인 레지스터 |
ECX | 일반적으로 반복문이나 문자열 복사 시 카운트에 사용 |
EDX | EAX와 비슷하게 주로 연산에 사용 |
ESI | 주로 문자열, 메모리 값을 복사할 때 원본 주소 가리킴 |
EDI | 주로 문자열, 메모리 값을 복사할 때 목표 주소 가리킴 |
EBP | 스택 프레임 시작 주소 (Base) 저장 |
ESP | 스택 프레임의 끝 지점 저장 |
EIP | 다음에 실행할 명령어 주소 저장 |
세그먼트 레지스터 | 각 세그먼트의 오프셋 저장 |
플래그 레지스터 | 연산 결과에 따라 다양한 상태 값 표시 |
레지스터 사용 실습
sum 함수 : 두 개의 인자 값을 받아서 더한 값을 리턴한다.
코드
int sum(int a, int b) {
return a+b;
}
어셈블리어 코드
...
mov eax, dword ptr [a]
add eax, dword ptr [b]
...
ret
- mov 명령어로 a 변수에 해당하는 값을 EAX 레지스터에 넣는다.
- add 명령어로 a 값이 들어있는 EAX와 b 변수에 해당하는 값을 더해서 EAX에 넣는다.
- 마지막에 ret 코드가 실행되면 sum 함수가 호출된 곳으로 돌아가게 되고, 함수를 호출했던 곳에서는 EAX에 저장된 리턴 값을 이용한다.
→ ret 명령이 실행되면 sum 함수를 호출한 곳에서는 EAX 레지스터에 저장된 값을 반환값으로 간주한다.
sum 함수의 호출과 반환 값 어셈블리어 코드
call sum (12A11AEh)
...
push eax
1.3 메모리 구조
가상 메모리
- 프로세스별로 유저 영역 2GB, 커널 영역 2GB로 총 4GB의 독립된 메모리 공간을 가진다.
- 실제로 커널 영역 2GB는 모든 프로세스가 공유한다.
- 장점
- 프로그램은 자신이 모든 메모리를 소유한 것처럼 주소 값에 신경쓰지 않고 메모리를 사용할 수 있다.
- 오류가 발생하더라도 다른 프로세스의 메모리와 격리되어 있어 안전성을 높일 수 있다.
프로세스별 메모리 확인 실습
Immunitiy Debugger를 이용하여 윈도우 메모리 구조를 확인한다.
1. 임의의 .exe 파일을 연다. (KakaoTalk.exe)
2. 메모리를 보기 위해 View → Memory 메뉴를 클릭하거나 Alt+M을 누른다.
유저 모드 2GB 영역에 로드된 다양한 PE 파일 이미지와 스택 등을 확인할 수 있다.
* PE 파일 : 윈도우 운영체제에서 사용하는 실행 파일 구조이다.
윈도우의 실행 파일 로더는 파일에 저장된 PE 헤더를 파싱하여 메모리에 실행 파일 및 DLL들을 로딩한다.
프로그램 하나가 동작하기 위해서는 연동된 다양한 모듈들이 함께 로드되며, Exploit을 작성함에 있어서 유용한 코드 조각들을 사용할 수 있다.
공격 코드 작성에 필요한 바이트 코드가 공격 대상 파일에 없더라도 함께 로딩되는 다른 모듈 내에서 찾아서 쓸 수 있다.
스택 : 높은 주소에서 낮은 주소로 할당된다.
힙 : 낮은 주소에서 높은 주소로 할당된다.
1.4 스택과 힙
스택과 힙 할당받는 예제
스택, 힙 주소 출력하는 코드
void _tmain(int argc, _TCHAR* argv[]){
int x = 9;
int *p;
p = (int*)malloc(sizeof(int));
*p = 9;
printf("Stack address : %p\n", &x);
printf("Heap address : %p\n", p);
}
스택
- 후입선출 (LIFO) : 마지막에 넣은 게 먼저 나온다.
- 메모리 관리를 위해 사용되며 ESP(스택 포인터) 레지스터와 EBP 레지스터가 사용된다.
- 함수 내부에서만 사용되는 지역 변수들이 주로 스택에 할당된다.
스택 예제
덧셈, 뺄셈 함수 호출 코드
int sum(int a, int b) {
int result = 0;
result = a+b;
return result;
}
int minus(int a, int b) {
int result = 0;
result = a-b;
return result;
}
void _tmain(int argc, _TCHAR* argv[]) {
int x = 9;
int y = 4;
printf("sum : %d\n", sum(x,y));
printf("minus : %d\n", minus(x,y));
}
sum과 minus 함수에 있는 result 변수는 함수가 종료된 후에는 존재할 필요가 없는 변수이다.
이러한 변수들이 메모리에 할당되어 남아 있으면 메모리를 쓸데없이 차지하게 된다.
이렇게 함수 내부에서만 사용되는 지역 변수들이 주로 스택에 할당된다.
힙
- 스택과 다르게 힙 관리자 및 힙 구조체를 통해 관리되며 프로그래머가 필요 시 할당 및 해제 할 수 있다.
- 스택보다 큰 메모리가 필요할 때 사용ㄷ하며 API를 통해 할당받거나 반환할 수 있다.
1.5 함수 호출과 리턴
함수마다 별도의 스택 공간을 가지며, 이를 스택 프레임이라고 부른다.
함수 호출 시 스택에 돌아올 주소를 저장하고, 스택 프레임을 생성한다. 그리고 함수 종료 시 스택 프레임을 제거하고 호출한 주소로 복귀한다.
(참고 : https://h-factory.tistory.com/53)
함수 내부에서 스택 메모리를 사용하는 것과 관계없이 종료 후에는 호출 전과 동일한 상태로 돌아가게 된다.
sum 함수 내부에서는 ESP의 변경과 상관없이 EBP를 기준으로 인자 x, y에 접근할 수 있게 된다.
mov eax, dword ptr [ebp+8]
add eax, dword ptr [ebp+0Ch]
EBP+8의 값을 EAX에 저장한 뒤, EBP+C의 값을 더해주고 있다.
이 코드는 result = a+b 코드이다.
즉, 함수 내부에서 인자 값에 접근할 때는 EBP 레지스터를 이용한다.
- 함수 프롤로그 : 함수의 시작 부분에서 스택 프레임을 만들어 주는 코드
- 함수 에필로그 : 스택 프레임을 해제하고 돌아가는 코드
- PUSH : 스택에 넣는 명령어
- MOV : 이동하는 명령어
PUSH EBP //스택 프레임 생성 (CALL)
MOV EBP, ESP
...
MOV ESP, EBP //스택 프레임 제거 (RETURN)
POP EBP
RET
함수 프롤로그 과정
(1) 함수 CALL
함수 호출 시 복귀 주소를 스택에 저장한 뒤 해당 함수 점수로 점프한다.
복귀 주소인 RET을 저장하게 되니까 최상위를 가리키는 ESP 위치도 변경된다.
(2) PUSH EBP
이전 스택 프레임의 EBP를 SFP(Stack Frame Pointer)에 저장한다.
스택 프레임 제거 후 원래 스택 프레임으로 복귀하기 위해 이전 스택 프레임의 EBP를 저장해야 하기 때문이다.
(3) MOV EBP, ESP
Stack Poiner를 EBP 레지스터에 저장한다.
가장 첫번째를 실행해야 하기 때문에 EBP 최상위 위치로 이동시킨다.
함수 에필로그 과정
(1) MOV ESP, EBP
사용 중이던 ESP를 EBP 레지스터에 저장된 스택 프레임 시작 주소로 복구한다.
EBP는 함수가 시작된 후부터 종료될 때까지 항상 스택 프레임의 기준점(시작 위치)을 가리킨다.
(2) POP EBP
ESP가 가리키는 주소에 저장된 이전 EBP 주소를 꺼내서 EBP 레지스터에 저장한다.
함수가 호출될 때마다 스택 프레임이 생성되는데, 각 함수의 EBP는 이전 함수의 EBP를 참조하게 된다.
이 구조 때문에 함수 호출이 끝난 후 POP EBP를 통해 이전 함수의 스택 프레임으로 돌아갈 수 있다.
(3) RET
POP EIP와 동일하며, 스택에 저장된 복귀 주소(스택의 최상위 값)를 꺼내서 EIP에 저장한다.
EIP는 현재 실행 중인 명령어의 메모리 주소를 저장하고 있다.
즉, RET가 실행되면 스택에 저장된 복귀 주소가 EIP에 로드된다.
그 결과, 프로그램은 함수 호출 이전의 위치로 돌아가서 계속 실행된다.
함수 호출 규약
- 함수를 호출하는 쪽과 호출당하는 함수 사이의 혼란을 방지하기 위한 일정 규약
- 컴파일 시 [프로젝트 속성] → [구성 속성] → [C/C++] → [고급] → [호출 규칙] 옵션을 통해 지정할 수 있다.
- _cdecl (C언어 호출 규약)
- 인자값 전달은 오른쪽부터
- 스택 정리는 caller(Add esp.n)
- _stdcall (WINAPI 호출 규약)
- 인자값 전달은 오른쪽부터
- 스택 정리는 callee(ret n)
- _fastcall
- 인자값 전달은 레지스터+스택
- 속도가 빠르나, 경우에 따라 오히려 코드가 길어질 수 있다.
호출 규약에 따른 어셈블리어 변화
(1) _cdecl
push로 인자 값을 역순으로 넣은 뒤 sum() 함수를 호출한다.
함수 호출 후 add esp, 8을 통해 인자 값 2개를 push 하며 늘어났던 8바이트의 스택을 함수 호출 이전의 상태로 만들어주는 것을 알 수 있다.
→ _cdecl 규약에서는 함수를 호출한 쪽이 스택을 정리해야 한다.
8바이트를 esp에 더해서 스택 포인터를 함수 호출 전 상태로 복원하는 것이다.
이 과정을 통해 push로 추가된 인자 값들을 스택에서 제거하게 된다. (스택은 높은 주소에서 낮은 주소로 가기 때문에 더하는 것은 스택에서 뺀다고 이해하면 된다.)
_cdecl | |
sum(a, b); | push b |
push a | |
call sum() | |
add esp, 8 |
...
push eax
...
push ecx
call 013811C7
(2) _stdcall
스택 정리는 함수 내부에서 리턴과 동시에 수행한다.
함수 복귀 시 ret 명령어가 아닌 ret 8 명령어를 실행했는데,
이는 함수 호출 전 두 번의 push로 늘어난 스택을 ret 8 명령어로 복귀와 동시에 줄여주는 것이다.
_stdcall | |
sum(a, b); | push b |
push a | |
call sum() |
#sum(a, b) 함수 호출
...
push eax
...
push ecx
call sum (0E01108h)
#sum(a, b) 함수 리턴
...
ret 8
(3) _fastcall
레지스터를 이용하여 인자 값을 전달한다.
인자 값을 레지스터에 옮긴 후 스택에 push 한다.
장점 : 다른 호출 규약에 비해 속도가 빠르다.
단점 : 인자 값의 수가 많아지면 스택을 사용한다.
edx와 ecx 레지스터에 인자값을 넣고 바로 sum 함수를 호출한다.
mov edx, dword ptr [y]
mov ecx, dword ptr [x]
call sum (10E11E0h)
'School > Windows' 카테고리의 다른 글
쉘코드 응용 - 노트패드 여는 쉘 코드 작성하기 (0) | 2022.05.02 |
---|---|
소프트웨어 취약점과 공격 실습 (0) | 2022.02.10 |
코드 패치와 저장하기 (코드패치 방법 2가지) (0) | 2022.02.05 |
어셈블리어 - if문 예제 실행하고 분석해보기 (0) | 2022.02.05 |
[어셈블리어] 1부터 100까지 더하고 최종값 저장하기 (0) | 2022.02.03 |