Complex Lab Systems Programming
Lecturer | Prof. Dr.-Ing. Horst Schirmeier, Dr. Michael Roitzsch |
Module | INF-B-510, INF-B-520, INF-B-530, INF-B-540, INF-04-KP, INF-DSE-20-E-ADSE, INF-DSE-20-M-INT, INF-E-4, INF-MA-PR, INF-VERT4, IST-05-FG-AVS, MINF-04-KP-FG2 (INF-BAS4 possible, too) |
Scope and Type |
4 SWS Lab (English) |
Cycle | Block event just before the winter semester starts |
Time and Place | 2024-09-23 to -10-01, 09:30–16:00, APB/E040 |
Enrollment | by subscribing to the mailing list (limited number of participants) |
Mailing List | |
Feedback | via email or our anonymous mailbox |
The course consists of two parts: The workshop takes place before the beginning of the winter term (Sep 23rd to Oct 1st, 2024). There will be a one-hour lunch break and additional breaks according to demand and time. Registration for the course works by subscribing to the mailing list. The practical assignments are to be solved individually after the workshop concludes. Solutions are handed in with the respective supervisor.
Workshop
This workshop teaches advanced skills in programming on the system level. Attendance is especially advised for students wanting to brush up their programming skills before taking one of our hands-on courses like Operating-System Construction, Microkernel-Based Operating Systems, Microkernel Construction, or the Complex Lab Microkernel-Based Operating Systems.
This is not a programming course for beginners. We want to teach you advanced skills and thus will expect the following:
- You have programming experience, for example in Java or Python.
- You know how to work on a Linux or Unix command line.
- You have seen a manpage.
- You can work with a text editor.
- You can use Google.
If you say yes to all of these points, this course is for you. The training enhances your C++ programming skills in areas such as compiler and tool chain, building a library, debugging, assembler, multi-threading, POSIX, and system calls.
Slides
Will be published during the workshop.
- Introduction
- Tools
- Rust
- Debugging & Assembler
- Threads
- No Libc
- C++ (source files)
Practical Part
You will work on the following four tasks on your own.
Each has an associated supervisor who can answer questions about the task.
You may use C, C++, and Rust along with their standard libraries for implementation. Additional programs or libraries/crates require supervisor consultation. Refer to the assignment for exceptions.
A comprehensive solution must include all features mentioned in the assignment and adhere to the specified syntax/grammar (for command line options and shell language).
Please make sure the code is of high quality:
- clear structure (functions/classes)
- defensive and offensive programming
- useful comments in English
- sensible naming of variables and functions
- limited scope for variables
- constants instead of “magic number”
- “Don't repeat yourself” (DRY)
- Avoid compiler warnings
Submission
To submit your solution, please send an email to the respective task's supervisor. Attach a compressed tar archive that includes the source code files and the corresponding Makefile or Cargo manifest. Please do not attach any binary files. Use the message text of the e-mail for explanations or answers to questions. You may write in either German or English.
There is no fixed deadline for submissions; you can submit solutions for the practical tasks as long as we offer the complex lab. We recommend, however, to complete the tasks by the end of the winter semester that follows the workshop. Please note, that due to the flexible submission regime, it may take some time for the supervisor to respond to your submission. If you require a quick response, please make sure to clearly indicate this.
In case there are any issues with your submitted solution, you may make corrections, but this could lead to a lower score. Therefore, please test your solution thoroughly before submitting.
Tutor: Robin Thunig
This exercise is about using GNU/Make to automate compiling and linking C files into an application.
-
Write a C application sincos that prints a table of sin/cos values for angles between 0 and 360 degrees. The application should receive exactly one argument which is the number of steps to use for printing.
A sample session with your program might look like this:
$> ./sincos 10
0.000 0.000 1.000
36.000 0.588 0.809
72.000 0.951 0.309
108.000 0.951 -0.309
144.000 0.588 -0.809
180.000 -0.000 -1.000
216.000 -0.588 -0.809
252.000 -0.951 -0.309
288.000 -0.951 0.309
324.000 -0.588 0.809
360.000 0.000 1.000
Your application shall consist of two C files:-
main.c: This file contains the main method of your application, checks the correctness of the command line parameter and then calls a function provided by the second file in order to perform real actions.
-
sincos.c: This file provides a function that iterates over the range between 0 and 360 degrees using the steps provided by the user. For each iteration it prints the current degree value as well as its sine and cosine.
-
The interface provided by sincos.c shall be defined in a header file that is included by the main application.
-
Write a Makefile to automate your application's build. The Makefile shall consist of two rules:
-
A rule to create the program from your two object files and
-
A clean rule to remove all generated files.
-
Dependency files should be created and included automatically by the Makefile.
Furthermore, the program shall be compiled with the compiler flags -Wall and -Wextra and compilation should not emit any warnings.
- Compile and link the application using GNU/Make.
- Test the correctness of your dependencies and file generation. A reference environment is given with the following VirtualBox image:
Tutor: Carsten Weinhold
The goal of this exercise is to familiarize yourself with the basic operations and data structures of the UNIX file system.
The UNIX utility find searches the file system for files that meet certain requirements. Your task is to implement a small find utility that accepts the following command-line syntax:
find <directory name> [-name <pattern>] [-type <f | d>] [-follow] [-xdev]
Your program should print a filename if the corresponding file or directory matches all the constraints specified on the command line (if any). Using the -type switch, the user can specify that either regular files or directories will match, but not both. The option -follow shall tell your program to follow symbolic links. The option -xdev specifies that 'find' shall not search directories whose contents are located in another file system (e.g., another disk partition). The option -name shall accept wildcards as explained in the following manpage excerpt:
A string is a wildcard pattern if it contains one of the characters `?', `*' or
`['. Globbing is the operation that expands a wildcard pattern into the list of
pathnames matching the pattern. Matching is defined by:
A `?' (not between brackets) matches any single character.
A `*' (not between brackets) matches any string, including the empty string.
...
(see also glob(7))
If you are in doubt about what the exact behavior of your tool shall be for the various options, see what GNU find outputs in your test environment (comes with your Linux distribution). Reading the GNU find manpage is also a good idea. You might find the functions readdir(), stat(), and fnmatch() or glob() to be useful when you develop the functionality to traverse directory trees. Note that it is not allowed use existing directory tree walkers such nftw() or ftw(), instead you are expected to build this functionality yourself.
Please make sure that you test your program thoroughly and any debugging output is disabled before you submit it. We look at the source code, but we primarily use automated tests to verify the correctness of your find tool. Therefore, your program should print all matching pathnames like GNU find does. Don't use output formats such as "found file: /path/to/file" or "found match: type=d, /path/to/dir"!
Important: To get good test coverage, you should download this tarball, which contains a shell script for creating a directory hierarchy for testing your find program. It is particularly useful for making sure that your implementation of the '-follow' option behaves in sensible way, like for example GNU find does! Also, see the included README for details on how to test the '-xdev' option.
Resources
Tutor: Till Smejkal
In this exercise you are going to write a simple shell that is able to execute command lines, interpret pipes and use input/and output redirection. You may use Flex and Bison to specify a parser for your command lines.
First, implement a shell with the following required features:
- Execute command lines with the following syntax:
command <args> [< input_redirect] (| command <args>)* [> output_redirect] [&]
As a starting point you may consider the files provided in the resources section below. - Support an arbitrary amount of arguments to programs as well as an arbitrary length for the pipe chain.
- Detect erroneous input and give useful feedback to the user.
Second, add convenience features to your shell:
- The readline library provides line editing features, such as deleting a word. libreadline comes along with libhistory which provides means to store strings in a list and navigate through this list. Use both libraries in order to implement easy-to-use line editing as well as access to previously entered commands using the arrow keys.
- Add support for the following shell builtins to your shell
- cd to change to another working directory
- pwd to print the current working directory
- kill <signo> <pid> to send a signal to a process
- alias and unalias to manage aliases (which are replaced by by their meaning during execution of commands).
Resources
- Flex and Bison are successors to Lex and Yacc, so all relevant information for Lex and Yacc is also useful for these tools.
- Manual for the Flex scanner generator
- Manual for the Bison parser generator
- Template Flex input file
- Template Bison input file
- The GNU readline library
- When implementing builtins you might have a look at the BASH reference manual.
- For executing processes, have a look at the man pages for fork(), execve(), and wait().
- For input and output redirection, take a look at open(), close(), and dup2().
- Pipes are created by pipe(), signal handling can be set up using sigaction().
Tutor: Jan Bierbaum
In this exercise you will apply widely used debugging tools, namely, gdb and objdump to reverse engineer and exploit a very simple and vulnerable login programme.
In Unix systems, a login programme is normally invoked by some login shell (like getty) together with a user-name. The login executable is often owned by the superuser and its SUID bit is set. Therefore, from an attacker's point of view, a vulnerable login programme means potential root access to the system.
Your task is to exploit a simplified version of a login programme that asks the user for a password and, depending on the input, will return a success or failure message. Note, that the given executable is an x86 ELF binary. You may need to set up support for running 32-bit binaries in your system. In case your machine cannot natively run x86 code, you can work remotely on the university's login server. If you need help with the setup, ask the tutor.
- First, extract the correct password by analysing the binary with objdump or gdb. Describe how to extract the password from the binary, e.g., by sending the output of a terminal session where you use objdump and/or gdb. In addition, please send the plain password.
- Next, exploit the buffer overflow vulnerability of this binary. By choosing a suitable input, you are able to skip the password check and force the program to jump directly to printing the success message. Note, that your task is to make the programme run the existing code, not to inject code that prints a similar message. Start your solution by answering the following questions:
- How does gcc translate function calls to assembly code? How are arguments and return values passed? How can the programme continue executing where ii “left off” when the function call happened?
- Which standard C-Library functions are vulnerable to buffer overflows and why? You don't need to name the individual functions but rather give examples and name the properties they share.
- How to use this knowledge to overwrite the return address of a function?
- Write a short programme in C, C++, or Rust that takes an address in hex format as input and produces a string as output. When using that string as input for the login program, it should directly jump to the given hex address. Additionally, send the hex address you would use to circumvent the authentication check and explain how you you found it. An example session using your resulting program should look similar to this one (replacing 1234ABCD with the correct value of course):
$> ./cracker 1234ABCD | ./simple_login
Enter the correct password:
Successful login! Now, we would execute a shell ...
- When simply overwriting the return address of the password authentication function, you will experience problems, namely a segmentation fault. Explain why this happens. How could you avoid this in theory? (Bonus: Extend your implementation so that the generated string does not result in a segmentation fault when fed into simple_login.)
- What practical countermeasures of contemporary UNIX systems and compilers complicate buffer overflow attacks?
Resources
Recommended Links
- Reference for C++ and the Standard Template Library
- Tutorial on using pointers in C
- PThreads Tutorial
- Coding Styles
- C++ standard
Grading
The results of the practical part are crucial in determining your final grade or whether you pass. Your implementation's correctness and completeness, the quality of your code, and any needed revisions are taken into account. Depending on the selected module, the solutions submitted are ultimately evaluated in an oral colloquium.
(We used DeepL and Grammarly in the creation the English version of this page.)