Overview
In this project, we’re going to learn about how multiple running programs can interact with each other, called inter-process communication or IPC. Note that “process” here means “running program”, not Scrum or the like. IPC comes in many forms, such as:
- Sockets: anything over a network usually involves socket interaction. The advantage is that you can use them both locally or over a network, at a slight performance cost.
 - Clipboard: this is how we manually transfer data from one program to another in modern operating systems.
 
Since those are familiar, we won’t be covering them today. There are, however, lesser-known forms of IPC that we’ll be covering:
- Signals. These are a feature of all modern OS’s that allow you to alert a process without sending any kind of payload.
 - Unnamed pipes (i.e. 
|and>) Also provided by all OS’s, this allow us to send data directly from one program to another, or directly to a file. They’re called “unnamed” because the pipes are temporarily created for just that command. - Spawning subprocesses. When your system boots, it starts with a single process. If we simply ran that program, that’s called embedded software development. Usually, however, we start up the operating system, which then takes that first process and forks itself into hundreds of other sub-processes. An oversimplified description of the term “operating system” is one giant IPC management ecosystem. Since the OS was originally booted into, it has ultimate control over what happens to our processes.
 
Part 0: Setup
- Log into http://git.gccis.rit.edu using your RIT username and password. We are not using gitlab.com.
 - Create a repository on the GCCIS GitLab, called 
ipc-project. It should be private. - Make your instructor and course assistants the 
Reporterrole on the repository. - Clone the reposistory locally using your favorite Git client. Note if you are in the SE labs, we recommend cloning to somewhere on 
c:\(e.g.c:\yourusername\ipc-project), notz:\ - Open your repository folder in your favorite text editor.
 - Create the following file and directory structure. All of these files will be empty to start with, and we’ll fill them in one-by-one.
 
ipc-project/
├── .gitignore
├── .gitlab-ci.yml
├── cat.py
├── listener.c
├── Makefile
├── secret.txt
├── test_cat_exploit.sh
├── test_cat.sh
├── test_full_dd.sh
├── test_listener.sh
├── litterbox/
│   ├── treasure.txt
- In our .gitignore, let’s ignore our built binaries, log, shell stuff, and python cache:
 
listener
output.log
.bash_history
*.pyc
- In our 
.gitlab-ci.yml, use this text. Note that our test cases are all commented out - this is so our initial job passes. As you work, you’ll be uncommenting thetest_*files. 
image:
  name: python:latest # don't change this
ipc-project:
  stage: test
  script:
    - make
    - chmod +x test_*
    # - ./test_cat.sh
    # - ./test_cat_exploit.sh
    # - ./test_listener.sh
    # - ./test_full_dd.sh
- In 
Makefile, add the following below. Note that Makefiles need a tab character, not spaces. 
compile:
	gcc -o listener listener.c
- 
In
secret.txt, addHey! This is a secret! - 
In
litterbox/treasure.txtaddTreasure found! - 
In both
listener.c, add the following: 
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
int main(){
	printf("Hello, world!\n");
	return 0;
}
- Push your code to GitLab and make sure the pipeline runs and passes. Check the console output to make sure the end of it looks something like this:
 
$ make
gcc -o listener listener.c
$ chmod +x test_*
- 
For this environment, we’re going to be making use of Docker. See the File Permissions Activity for how to work with Docker. In this project, we’re using the Python standard container straight from Dockerhub.
 - 
To start up a console inside our container, use this command:
 
docker run -it --rm --mount type=bind,source="$(pwd)",target=/root -w /root python:latest bash
(this will work on both Windows Powershell, Mac, and Linux)
Note: If you get an error something like this:
docker: Cannot connect to the Docker daemon at unix:///Users/andy/.docker/run/docker.sock. Is the docker daemon running?.
Make sure you’ve got Docker running.
- Let’s make sure we can compile, by running 
makefrom within your container, and then running./listenerand you should seeHello, World! 
Part 1: Signal Listener
Our first task is to learn about operating system signals. These are a feature you interact with and even use every day without realizing it. They’re simple, as lightweight as you can get, and dead simple to implement.
Most languages and operating systems support OS signals of some sort. In this example, let’s get some practice with C.
- 
Starting with
listener.c, make sure that yourHello, World!program works. Run yourbashshell as in the setup steps. Compile and run. - 
You may notice that we have some more
#includestatements above than your standard Hello, World program. This gives us access to the standard libary’s signals and processes API. In yourmainfunction, change yourprintffunction to instead print the Process ID. 
Hint: the function is called getpid(), and the format string for printing an integer is "%i".
Your output should look like this, though your PID will be different. You can even run this multiple times and see different PIDs.
Listener PID: 82
- 
Next, let’s practice using signals. Add a call to
pause()afterprintf, and rerun. - 
Notice how we’re not getting our prompt back. The program is set to wait until it receives a signal of any kind. Press
Ctrlandcat the same time. (Mac users: this is usually actualcontrolnotcmd). The program then exits. - 
In most shells,
Ctrl+cwill send aSIGINTor “interrupt” to the program. The GCC compiler by default adds a signal handler forSIGINT. Most compilers do this, so many command line programs can be exited withCtrl+c. But maybe we want to do some extra cleanup to exit gracefully. In fact, you might even see^Cin your shell - that’s a bash shorthand for saying “someone sent a SIGINT here” 
Let’s override this signal handler. Add this line to your main function, between printf and pause():
signal(SIGINT, sigint_handler);
- Compile. You’ll get an error. It’ll have a lot of info, but you’ll see this phrase:
 
undefined reference to `sigint_handler'
- Our 
sigint_handleris intended to be a callback, or a function that is executed when the signal is issued. Define such a function with this signature, placing it above yourmain()signature line: 
void sigint_handler(int);
- Also, add the following implementation for this method:
 
void  sigint_handler(int _sig) {
  printf("SIGINT received!\n");
}
Note: you are welcome to format your curly braces as you see fit. We are ecumenical universalists in our source code formatting for this assignment.
- Compile and run. Your output will look something like this:
 
# ./listener
Listener PID: 142
^CSIGINT received!
- But what if we want to send a signal other than 
SIGINT? Shells don’t have a shortcut for everything. For that we usekill. The name is a little misleading because you’re not necessarily killing the process, but that is the most common use case. Let’s run our program in the background by adding a&to the command. Your output will look something like this: 
root@82cc68385377:~# ./listener &
[1] 145
root@82cc68385377:~# Listener PID: 145
- Now issue this comand: 
kill -s INT 145but replace145with whatever your PID is. You should seeSIGINT Received. And if you run another command, you’ll get a line showing that your process ended. Like this: 
[1]+  Done                    ./listener
- EXERCISE. Add a second signal handler that will catch the 
SIGUSR1signal. This should printSIGUSR1 received!. 
Tip: Make it call a separate function instead of reusing signt_handler.
Here’s what my full output looks like showing both:
root@82cc68385377:~# make
gcc -o listener listener.c
root@82cc68385377:~# ./listener &
[1] 170
root@82cc68385377:~# Listener PID: 170
kill -s USR1 170
SIGUSR1 received!
root@82cc68385377:~# ./listener &
[2] 171
[1]   Done                    ./listener
root@82cc68385377:~# Listener PID: 171
kill -s INT 171
SIGINT received!
- Exercise. Let’s make use of our process’ return code. Every program returns a single integer to the operating system to signal if it ended normally. A return code of 
0means “successful”, and a non-zero return code is an error. 
Yes, it’s confusing. In C-ish expressions, 0/non-0 mean false/true respectively, but with return codes 0 means “success” and non-0 means “error”. I don’t make the rules, but this ubiquitous convention is here to stay.
For us, GitLab checks the return code of its last statement to determine if the build passed or not. Add a return code here that returns 1 if SIGINT was caught, and 0 if SIGUSR1 was caught. Important: our handlers are void, so we can’t pass our return value back. Instead, the way I did it was to define a return_code integer outside of main(), and set it in each of the handlers. (Just don’t tell anyone I used a global variable…)
- CI Test Case. To show your code is working, add this to the 
test_listener.shfile. Be sure modify.gitlab-ci.ymlto make it run when you push to GitLab. We’ll be using a similar format for our bash-flavored “unit tests” in the next parts. 
#!/bin/bash
set -x # echo on
set -e # fail script on any failed command
./listener > output.log & # fire off listener in the background
LISTENER_PID=$!
echo -e "Listener PID: $LISTENER_PID"
sleep 1 # give it a moment to start up
kill -s USR1 $LISTENER_PID
actual=`cat output.log`
expected="SIGUSR1 received!*"
if [[ $actual =~ $expected ]]; then
	echo -e "\033[92m PASSED! \033[0m"
	exit 0
else
	echo -e "\031[92m FAILED! \033[0m"
	echo "expected: $expected"
	echo "actual: $actual"
	exit 1
fi
Tip: you can also run this locally. The easiest way is to replace bash in your docker run command with bash test_listener.sh. This saves you from coding on the CI. Here it is in full:
docker run -it --rm --mount type=bind,source="$(pwd)",target=/root -w /root python:latest bash test_listener.sh
- Well done! Make sure your code works on the CI, and then tag it with 
part1. By “works on the CI”, make sure that: 
- You’ve uncommented 
./test_listener.shin.gitlab-ci.ymland pushed - The console looks as expected.
 - The job passes (i.e. has a successful return code)
 
Part 2: Pipes and Distrustful Decomposition
In this section, we’re switching to Python. We could do any programming language for this part, but we’ll try to keep the code concise and simple.
Specifically, let’s simulate a path traversal vulnerability. If you recall from VOTD and the CWE 35: if the input to your program refers to a filepath, you likely want to restrict where the filepath can be. Since there are many, many ways to specify a filepath, getting this logic correct is more complex than you might assume.
- 
We’re going to be making our own version of the
catprogram that’s in every Unix-like environment. As you see from our script above,catwill print out the contents of a file. (It’s short forconcatenate, but it’s also a way to make every command line user a “cat person”. A very cat thing to do!). Try it out! - 
Now let’s open up our
cat.pyfile and make this its contents 
# cat.py - a safe(?) version of cat!
# The linux command `cat` prints out the contents of a file,
# if you give it a file name to open.
#
# Our version will only open files in the directory `litterbox`
# (also, unlike cat, it takes the filename from stdin, not args)
# INPUT: from stdin, we will take in the file name and open it
# OUTPUT: to stdout, the contents of the file if it's in `litterbox/`,
#			         or MEOW if not
import sys
- To demonsrate how pipes work, let’s use 
stdinandstdout. These are called unnamed pipes… which is confusing because didn’t I just say the name?? When you use stdin and stdout, they create temporary pipes. 
Make cat.py read from stdin using this:
filename = sys.stdin.read().strip()
This will read the entire input, wait until an EOF (end-of-file), then return the entire string.
Note: this line is not great in terms of resource exhaustion! There’s no limit here and it’ll all end up in memory.
Note: you can see the bad assumption I made by calling it filename not filepath - I assumed the user would only input the NAME of the file, but that string can include directories too.
- Next, let’s prepend our filename with our vulnerable code. As much as I cringe with this part, add this code, including the comment:
 
filename_in_litterbox_hopefully = f"./litterbox/{filename}"
#                  VULNERABLE HERE^^^^^^^^^^^^^^^^^^^^^^^^
- Because I can’t just leave vulnerable code without going against that tiny voice in my head, put this code in. Yes, it’s entirely a comment. Feel free to play with my (hopefully) correct implementation. For our purposes, though, let’s say we had this path traversal and didn’t know it.
 
# the secure way would be like this:
# (add import os to the top)
# abs_filename = os.path.abspath(f"./litterbox/{filename}")
# if abs_filename.startswith(os.path.abspath(f"./litterbox/")):
# 	filename_in_litterbox_hopefully = abs_filename
# else:
# 	raise Exception('Sandbox escaped!')
#
# But this is about Defense in Depth and WHAT IF we made the above mistake
- 
Uing a similar approach to above:
read()the entire contents of the file and print it out. I recommending using Python’swithandopensyntax. - 
Wrap the above step in a
try/except. In the error handler, addprint(f"MEOW"). - 
Get this code working by using
echoto send the filename you want to open. My output looks like this: 
root@149429f80f41:~# echo no-such-file.txt | python cat.py
MEOW [Errno 2] No such file or directory: './litterbox/no-such-file.txt'
root@149429f80f41:~# echo "treasure.txt" | python cat.py
Treasure found!
root@149429f80f41:~# echo "../secret.txt" | python cat.py
Hey! This is a secret!
- 
Great! Our code is as vulnerable as it should be. Uncomment the next two
.gitlab-ci.ymllines that start withtest_cat - 
Let’s add two test cases that do exactly what we did above and push them to the repository:
 
Put this in test_cat.sh
#!/usr/bin/bash
set -x # echo on
set -e # fail script on any failed command
actual=`echo "treasure.txt" | python cat.py`
expected="Treasure found!"
if [ "$actual" = "$expected" ]; then
	echo -e "\033[92m PASSED! \033[0m"
	exit 0
else
	echo -e "\031[92m FAILED! \033[0m"
	echo "expected: $expected"
	echo "actual: $actual"
	exit 1
fi
Put this in test_cat_exploit.sh:
#!/usr/bin/bash
set -x # echo on
set -e # fail script on any failed command
actual=`echo "../secret.txt" | python cat.py`
expected="Hey! This is a secret!"
if [ "$actual" = "$expected" ]; then
	echo -e "\033[92m PASSED! \033[0m"
	exit 0
else
	echo -e "\031[92m FAILED! \033[0m"
	echo "expected: $expected"
	echo "actual: $actual"
	exit 1
fi
- Hopefully they passed on the CI as well! Don’t move on until the following are true:
 
.gitlab-ci.ymlhas all buttest_full_dd.shuncommented- Your test files have been updated with the above shell code
 - GitLab’s console output looks like the output we had above
 - The job itself passed in GitLab
 
- 
Now let’s safeguard
cat.pyby using Distrustful Decomposition (DD). In short, DD is about running multiple processes at different permissions levels. In this example, we’ve already done the decomposition by makingcat.pylisten to a stdin pipe, and we can interact with it from another process (e.g. our shell, GitLab’s CI build system). Open uptest_full_dd.shand paste in thetest_cat_exploit.sh. - 
Before you run it, let’s change what we expect. Alter the test so that instead of
Hey that was a secret!being expected, let’s expect that an error was thrown. This isn’t a shell scripting tutorial, so just replace theexpected=andiflines with these: 
expected=MEOW*
if [[ "$actual" =~ $expected ]]; then
Note: Yes, the [ ] turned into [[ ]] - this is to take advantage of bash’s ability to do “globbing”, and get a fuzzy match to a string. It’s what enables us to use =~ and MEOW* without the quotes. Be glad you didn’t have to write that.
- Now, push and run your code on the CI, or run it locally. It should fail now. My output looked like this:
 
root@149429f80f41:~# ./test_cat_exploit.sh
+ set -e
++ echo ../secret.txt
++ python cat.py
+ actual='Hey! This is a secret!'
+ expected='MEOW*'
+ [[ Hey! This is a secret! =~ MEOW* ]]
+ echo -e '\031[92m FAILED! \033[0m'
[92m FAILED!
+ echo 'expected: MEOW*'
expected: MEOW*
+ echo 'actual: Hey! This is a secret!'
actual: Hey! This is a secret!
+ exit 1
- Instead, let’s set up our test case to now use different permissions. Right now, everything runs as root - gross! Update your test case so that it creates a separate user, called 
garfield. This user will have next to no privileges in the system, other than access to thelitterboxfolder. Here’s that code: 
# Create a new user who has no special permissions except access to the sandbox
# The "users" group is a default group with only access to /root/litterbox
adduser garfield --disabled-password --gecos ""
chgrp users ./litterbox
chown garfield ./litterbox
chmod g+rwx ./litterbox
ls -l --recursive
- Run your code again. It should still fail - we’re not actually doing anything with our new user just yet. But, we also added an 
lscommand to check our work. My output looked like this: 
root@149429f80f41:~# bash ./test_full_dd.sh
+ set -e
+ adduser garfield --disabled-password --gecos ''
Adding user `garfield' ...
Adding new group `garfield' (1000) ...
Adding new user `garfield' (1000) with group `garfield (1000)' ...
Creating home directory `/home/garfield' ...
Copying files from `/etc/skel' ...
Adding new user `garfield' to supplemental / extra groups `users' ...
Adding user `garfield' to group `users' ...
+ chgrp users ./litterbox
+ chown garfield ./litterbox
+ chmod g+rwx ./litterbox
++ pwd
+ WORKING_DIR=/root
+ ls -l --recursive
.:
total 264
-rw-r--r-- 1 root     root     67 Oct 22 14:34 Makefile
drwxr-xr-x 4 root     root    128 Oct 14 14:03 __pycache__
-rw-r--r-- 1 root     root   1082 Oct 22 15:05 cat.py
-rwxr-xr-x 1 root     root  70776 Oct 25 17:55 listener
-rw-r--r-- 1 root     root    653 Oct 26 13:14 listener.c
drwxrwxr-x 3 garfield users    96 Oct 22 14:05 litterbox
-rw-r--r-- 1 root     root     34 Oct 28 15:11 output.log
-rw-r--r-- 1 root     root     22 Oct 22 14:34 secret.txt
-rwxr-xr-x 1 root     root    335 Oct 22 14:45 test_cat.sh
-rwxr-xr-x 1 root     root    325 Oct 28 16:20 test_cat_exploit.sh
-rw-r--r-- 1 root     root    670 Oct 28 16:24 test_full_dd.sh
-rwxr-xr-x 1 root     root    499 Oct 22 15:15 test_listener.sh
./__pycache__:
total 8
-rw-r--r-- 1 root root 1302 Oct 14 13:21 test_cat.cpython-312.pyc
-rw-r--r-- 1 root root 1374 Oct 14 14:03 test_listener.cpython-312.pyc
./litterbox:
total 4
-rw-r--r-- 1 root root 15 Oct 22 14:06 treasure.txt
Note: if you are getting an error like this:
adduser: The user `garfield' already exists.
You’ve run your script multiple times within a single bash session. Instead, run the script as a docker run so that the environment starts clean every time.
- 
Some of your numbers might be different, but make sure the permissions match.
 - 
And finally, instead of running
python cat.pyas root, let’s change our run to run asgarfield. 
WORKING_DIR=`pwd`
actual=`echo "../secret.txt" | su - garfield -c "python $WORKING_DIR/cat.py"`
Note: when we use su, the system changes directories to garfield’s home directory. This command works around that by making the directories absolute.
- Run 
test_full_dd.shagain (on the GitLab or locally). My entire output looks like this. 
+ set -e
+ adduser garfield --disabled-password --gecos ''
Adding user `garfield' ...
Adding new group `garfield' (1000) ...
Adding new user `garfield' (1000) with group `garfield (1000)' ...
Creating home directory `/home/garfield' ...
Copying files from `/etc/skel' ...
Adding new user `garfield' to supplemental / extra groups `users' ...
Adding user `garfield' to group `users' ...
+ chgrp users ./litterbox
+ chown garfield ./litterbox
+ chmod g+rwx ./litterbox
+ ls -l --recursive
.:
total 188
-rw-r--r-- 1 root     root     67 Oct 22 14:34 Makefile
drwxr-xr-x 4 root     root    128 Oct 14 14:03 __pycache__
prw-r--r-- 1 root     root      0 Oct 22 16:28 can_w_string.pipe
-rw-r--r-- 1 root     root   1082 Oct 22 15:05 cat.py
-rwxr-xr-x 1 root     root  70432 Oct 25 13:11 hello
-rw-r--r-- 1 root     root    133 Oct 22 15:50 hello.c
-rwxr-xr-x 1 root     root  70776 Oct 25 17:55 listener
-rw-r--r-- 1 root     root    653 Oct 26 13:14 listener.c
drwxrwxr-x 3 garfield users    96 Oct 22 14:05 litterbox
-rw-r--r-- 1 root     root     34 Oct 28 15:11 output.log
-rw-r--r-- 1 root     root     53 Oct 28 16:27 repeater.py
-rw-r--r-- 1 root     root     22 Oct 22 14:34 secret.txt
-rw-r--r-- 1 root     root      0 Oct 22 16:26 talker.py
-rwxr-xr-x 1 root     root    335 Oct 22 14:45 test_cat.sh
-rwxr-xr-x 1 root     root    325 Oct 28 16:20 test_cat_exploit.sh
-rw-r--r-- 1 root     root    669 Oct 28 16:29 test_full_dd.sh
-rwxr-xr-x 1 root     root    499 Oct 22 15:15 test_listener.sh
./__pycache__:
total 8
-rw-r--r-- 1 root root 1302 Oct 14 13:21 test_cat.cpython-312.pyc
-rw-r--r-- 1 root root 1374 Oct 14 14:03 test_listener.cpython-312.pyc
./litterbox:
total 4
-rw-r--r-- 1 root root 15 Oct 22 14:06 treasure.txt
++ pwd
+ WORKING_DIR=/root
++ echo ../secret.txt
++ su - garfield -c 'python /root/cat.py'
+ actual='MEOW [Errno 2] No such file or directory: '\''./litterbox/../secret.txt'\'''
+ expected='MEOW*'
+ [[ MEOW [Errno 2] No such file or directory: './litterbox/../secret.txt' =~ MEOW* ]]
+ echo -e '\033[92m PASSED! \033[0m'
 PASSED!
+ exit 0
- You are done if:
 
- All tests are uncommented in 
.gitlab-ci.py - The console output resembles the above output.
 - The GitLab job passes
 - Tag your submission on GitLab 
submission 
Submission and Grading:
- Make sure your repository is shared with both instructor and course assistant with “Reporter” permissions.
 
Point breakdown:
- (5) Submission directions followed
 - (10) Part 1: passes on CI with the proper test case set up
 - (10) Part 2: passes on CI with the proper test case set up