[Calling Convention] - 함수 호출 규약
1. 함수 호출 규약
1) 함수 호출 규약
- 함수의 호출 및 반환에 대한 약속
- 함수 호출 규약 적용하는 것은 일반적으로 컴파일러의 역할
2) 종류
- x86 아키텍처
① cdecl
② stdcall
③ fastcall
④ thiscall
- x86-64 아키텍처
① System V AMD64 ABI의 Calling Convention
② MS ABI의 Calling Convention
2. x86호출 규약 : cdecl
1) cdecl
- x86 아키텍처는 레지스터의 수가 적어 스택을 통해 인자를 전달
- 인자를 전달하기 위해 사용한 스택을 호출자가 정리함
- 스택을 통해 인자를 전달할 때는 마지막 인자부터 첫번째 인자까지 거꾸로 스택에 push함
- cdecl 함수 호출 규약
// Name: cdecl.c
// Compile: gcc -fno-asynchronous-unwind-tables -nostdlib -masm=intel \
// -fomit-frame-pointer -S cdecl.c -w -m32 -fno-pic -O0
void __attribute__((cdecl)) callee(int a1, int a2){ // cdecl로 호출
}
void caller(){
callee(1, 2);
}
- cdecl.c를 어셈블리어로 컴파일한 후 확인
; Name: cdecl.s
.file "cdecl.c"
.intel_syntax noprefix
.text
.globl callee
.type callee, @function
callee:
nop
ret ; 스택을 정리하지 않고 리턴합니다.
.size callee, .-callee
.globl caller
.type caller, @function
caller:
push 2 ; 2를 스택에 저장하여 callee의 인자로 전달합니다.
push 1 ; 1를 스택에 저장하여 callee의 인자로 전달합니다.
call callee
add esp, 8 ; 스택을 정리합니다. (push를 2번하였기 때문에 8byte만큼 esp가 증가되어 있습니다.)
nop
ret
.size caller, .-caller
.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits
3. x86-64호출 규약 : SYSV
1) SYSV
- 리눅스는 SYSTEM V(SYSV) ABI를 기반으로 만들어짐
- SYSV AVI는 ELF 포맷, 링킹 방법, 함수 호출 규약 등의 내용 포함
2) SYSV에서 정의한 함수 호출 규약 특징
- 6개의 인자를 RDI, RSI, RDX, RCX, R8, R9에 순서대로 저장하여 전달, 더 많은 인자를 사용해야 할 때는 스택을 추가로 사용
- Caller에서 인자 전달에 사용된 스택을 정리
- 함수의 반환 값은 RAX로 전달
- 자세히 분석하기 전에 주어진 코드를 작성하고 컴파일하였다.
(아래로는 강의를 따라가면서 한번 해본 것으로 중간중간에 끊겨서 흐름을 놓쳤을 수도 있습니다... 강의 내용에 있는 건 이해했어요.)
3) 인자 전달
4) 반환 주소 저장
5) 스택 프레임 저장
6) 반환값 전달
7) 반환
- 저장해뒀던 스택 프레임과 반환 주소를 꺼내면서 이루어짐
[Stack Buffer Overflow]
1. 스택 버퍼 오버플로우
1) 스택 버퍼 오버플로우
- 보안 취약점
- 스택의 버퍼에서 발생하는 오버플로우
2) 스택 오버플로우 vs 스택 버퍼 오버플로우 차이점
- 스택 오버플로우 : 스택 영역이 너무 많이 확장돼서 발생하는 버그
- 스택 버퍼 오버플로우 : 스택에 위치한 버퍼에 버퍼의 크기보다 많은 데이터가 입력되어 발생하는 버그
3) 버퍼
- 데이터가 목적지로 이동되기 전에 보관되는 임시 저장소
- 송신 측은 버퍼로 데이터를 전송 / 수신 측은 버퍼에서 데이터를 꺼내 사용
- 버퍼가 가득 찰 때까지는 유실되는 데이터 없이 통신 가능
- 빠른 속도로 이동하던 데이터가 안정적으로 목적지에 도달할 수 있도록 완충 작용을 하는 역할
- 데이터가 저장될 수 있는 모든 단위를 '버퍼'라 하기도 함
① 스택 버퍼 : 스택에 있는 지역 변수
② 힙 버퍼 : 힙에 할당된 메모리 영역
- cf) 버퍼링 : 송신 측의 전송 속도가 느려서 수신 측의 버퍼가 채워질 때까지 대기하는 것을 의미
4) 버퍼 오버플로우
- 버퍼가 넘치는 것
- 일반적으로 버퍼는 메모리 상에서 연속해서 할당되어 있음
-> 따라서 어떤 버퍼에서 오버플로우가 발생하면 뒤에 있는 버퍼들의 값이 조작될 수 있음
2. 중요 데이터 변조
- 버퍼 오버플로우가 발생하는 버퍼 뒤에 중요한 데이터가 있다면, 해당 데이터가 변조됨으로써 문제가 발생 가능
- 예제
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int check_auth(char *password) {
int auth = 0;
char temp[16];
strncpy(temp, password, strlen(password));
if(!strcmp(temp, "SECRET_PASSWORD"))
auth = 1;
return auth;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: ./sbof_auth ADMIN_PASSWORD\n");
exit(-1);
}
if (check_auth(argv[1]))
printf("Hello Admin!\n");
else
printf("Access Denied!\n");
}
main 함수는 argv[1]을 check_auth 함수의 인자로 전달한 후, 반환 값을 받아온다. 이 때, 반환 값이 0이 아니라면 "Hello Admin!"을, 0이라면 "Access Denied!"라는 문자열을 출력한다.
check_auth함수에서는 16 바이트 크기의 temp버퍼에 입력받은 패스워드를 복사한 후, 이를 "SECRET_PASSWORD" 문자열과 비교한다. 문자열이 같다면 auth를 1로 설정하고 반환한다.
그런데 check_auth에서 strncpy 함수를 통해 temp버퍼를 복사할 때, temp의 크기인 16 바이트가 아닌 인자로 전달된 password의 크기만큼 복사한다. 그러므로 argv[1]에 16 바이트가 넘는 문자열을 전달하면, 이들이 모두 복사되어 스택 버퍼 오버플로우가 발생하게 된다.
auth는 temp버퍼의 뒤에 존재하므로, temp버퍼에 오버플로우를 발생시키면 auth의 값을 0이 아닌 임의의 값으로 바꿀 수 있다. 이 경우, 실제 인증 여부와는 상관없이 main함수의 if(check_auth(argv[1])) 는 항상 참이 된다.
- 확인
SECRET_PASSWORD라고 정확한 문자열을 입력해주면 auth 값이 1이 되어 Hello Admin!을 출력하게 된다.
이번에는 SECRET_PASSWORD에서 일부 문자를 더하여 오버플로우가 발생하도록 했다.
그러면 if(check_auth(argv[1])) 는 항상 참이 되어 Hello Admin!을 출력하게 된다.
3. 데이터 유출
- 어떤 버퍼에 오버플로우를 발생시켜서 다른 버퍼와의 사이에 있는 널바이트를 모두 제거하면, 해당 버퍼를 출력시켜서 다른 버퍼의 데이터를 읽을 수 있게 됨
- 예제
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(void) {
char secret[16] = "secret message";
char barrier[4] = {};
char name[8] = {};
memset(barrier, 0, 4);
printf("Your name: ");
read(0, name, 12);
printf("Your name is %s.", name);
}
8바이트 크기의 name 버퍼에 12바이트의 입력을 받는다.
읽고자 하는 데이터인 secret버퍼와의 사이에 barrier라는 4바이트의 널 배열이 존재하는데, 오버플로우를 이용하여 널 바이트를 모두 다른 값으로 변경하면 secret을 읽을 수 있다.
- 확인
크기가 8바이트인 name 버퍼에 ABCDEFGHIJK라는 11 바이트의 값을 입력해주었다.
아직까지는 name 버퍼와 secret 버퍼 사이의 널바이트들이 다 채워진 게 아니라 정상적으로 입력한 내용이 그대로 출력되는 것을 확인할 수 있다.
위의 상태에서 하나를 더 입력하니 두 버퍼 사이의 널바이트들이 모두 사라지고 뒤의 secret 버퍼를 출력하여 이 버퍼 안에 저장된 데이터를 읽을 수 있게 되었다.
4. 실행 흐름 조작
- 함수를 호출할 때 반환 주소를 스택에 쌓고, 함수에서 반환될 때 이를 꺼내어 원래의 실행 흐름으로 돌아감
- 함수의 반환 주소를 조작하면 프로세스의 실행 흐름을 바꿀 수 있음
- 예제
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char buf[8];
printf("Overwrite return address with 0x4141414141414141: ");
gets(buf);
return 0;
}
- 확인
[함께실습] Stack Buffer Overflow
[혼자실습] Stack Buffer Overflow
'Security > System Hacking' 카테고리의 다른 글
[Dreamhack System Hacking] STAGE 4 - basic_exploitation_000 (0) | 2022.01.30 |
---|---|
[Dreamhack System Hacking] STAGE 4 - 함께실습 (0) | 2022.01.30 |
[Dreamhack System Hacking] STAGE 3 (0) | 2022.01.23 |
[Dreamhack System Hacking] STAGE 2 - shell_basic (0) | 2022.01.23 |
[Dreamhack System Hacking] STAGE 2 (0) | 2022.01.16 |
댓글