본문 바로가기
Security/System Hacking

[Dreamhack System Hacking] STAGE 4

by 단월໒꒱ 2022. 1. 23.

[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

 

 

 

댓글