Capture the Flag
Intro: JSON Web Tokens
A JSON Web Token (JWT) is a “a compact and self-contained way for securely transmitting information between parties as a JSON object” [jwt.io]. You've probably been using them every day for years: every time you log in somewhere using OIDC, the login provider (e.g. Feide, ID-porten, Google, Facebook, Github, Gitlab, etc.) issues a JSON Web Token with your login information, signs it and gives it to the place you're logging in to (the client, or relying party). The client can then check the signature against the provider's public key, and trust that the provider has verified that you are who you say you are – i.e., use it to authenticate you. JWTs are also commonly used for authorization – e.g., “Would you like to give $APP_NAME access to your Google Drive?”
A JWT consists of:
- A payload – the transmitted information. For OIDC, this might look like
{'iss': 'https://git.app.uib.no', 'sub': '788', 'aud': '5ee48…', 'exp': 1758018479, 'iat': 1758018359, 'nickname': 'Anya.Bagge', … }
- A header – specifying which key and algorithm was used to sign the token. For example,
'header': {'typ': 'JWT', 'kid': 'bA0ZTKpAgOh7A0XPyBwm9juhGOHo6Bg_FMjEGQBg_WM', 'alg': 'RS256'}
- A cryptographic signature that authenticates the above information.
It's the same concept that we've been using for thousands of years, except with a digital cryptographic signature instead of seals, stamps and hand-written signatures.
Your Personal Assignment-1 Token
For this assignment, you're going to use JWTs to prove who you are and that you've completed the assignment. For each part of the assignment, you have to use buffer overflows to break into a server. Once you're successful, you'll be issued a signed token that proves you've completed the task. (It's like completing a scavenger hunt with a passport, where each station on the hunt will stamp your passport!)
To get you're initial token that prove that you're you, log in below with UiB's Gitlab server, git.app.uib.no
. If you don't have an account there already (26 of you don't), log in to UiB's GitLab first picking the option “Students and employees at UiB, UiO... (via Feide)”, not “External Users (GitHub)” – you'll have to wait a few minutes before your account is ready.
and the information inside looks like this:
{
"sub": "anya", // the username
"iss": "https://inf226.puffling.no", // who issued the token
"iat": 1758110453, // when it was issued
"scope": ["id"], // the purpose, in this case identification
// when you complete a task, it'll say ["id","task-1"]
}
Rules & Practical Information
Before You Start
You should do Pwnexercise and Ropexercise first, so that you're somewhat familiar with pwntools
and buffer overflow techniques. The instructions and hints will use pwntools
and Python, though you may use any language you like to implement your solution.
- This note explains buffer overflows
- This note on Return Oriented Programming goes through step-by-step how to solve challenges like these.
- This note on Binary Stuff explains more about memory addresses and such.
- This small exercise on Object Files and Disassembly walks you through compilation, inspecting object files, disassembling code and how calls to dynamic libraries work. Examples from Linux, MacOS, x86-64, ARM, AVR and Java/JVM. (This is more details than you need to know.)
Note that the tasks have been designed so that the output from the server typically ends with a :
or a ?
. This makes it easier to use recvuntil()
(or next_qa()
from ctf-helper.py
). Also, in the cases where you need to input the address of getFlag()
, you may have to add 5 to it, for reasons explained elsewhere.
Learning Objectives
Afterwards, you should be able to explain…
- how a buffer overflow attack works,
- how the computer knows which code to run,
- how function calls work,
- roughly how code and data for a program is represented on disk and in memory
and you should be able to…
- identify data on a stack frame,
- find memory addresses of code / data (symbols)
- use
pwntools
(or similar) to connect to and communicate with a program, locally or over the network
Also, you should have a rough idea of how to mitigate / prevent buffer overflow attacks. Additionally, you might have so fun (or find the whole thing extremely frustrating).
Deadline, Deliverables and Grading
- The deadline is Friday, October 3rd 2025 at 23:59.
- This assignment counts towards your grade. Each task counts as 2% of your final grade, for a total of 10%. The next 20% will be the other assignments, and the final 70% the exam. All the assignments and the exam must be passed in order to pass the course.
- The assignment is individual and the results/report you submit must be your own.
- All you need to do is complete the tasks, so that the Current Status above shows Success. You don't have to deliver anything.
- To pass this assignment, all you need is an honest attempt at completing the assignment – there is no minimum score required. (If there's any doubt, we can tell from the logs whether you've made an honest attempt or not.)
Plagiarism / Cheating / Collaboration
- You may discuss / sit together with / try and figure things out together with other students, but your work must be your own.
- You can ask AI for assistance in explaining things, but not for generating a solution. Generally, it is better that you ask us or other students to explain stuff (no question is too stupid and no time (before the deadline) is too late to ask!).
- When/if helping each other, please try to not ask for / give solutions, but rather explanations of how things work, or other hint. Explaining how [you think] your [non-functioning] code works can be useful for both you and the your collaborator. Anya can provide you with your own personal rubber duck if you ask (Acme, Inc.'s rubber duck division seems to be struggling with deliveries nowadays, for some reason.)
- If possible, try to ask for help on a Discord channel rather than with private messages – there are probably others who wonder about the exact same thing, but are too shy to ask!
- But: don't post screenshots/listings with your (almost) complete solution, or showing the flag, etc. There are still lots of useful things to ask/answer regarding library function, how does return-oriented-programing work, etc.
Deadline extensions, etc
If you for some reason see that you can't deliver on time, you can get a deadline extension. You don't have to explain why – we trust that you have a sensible reason (and certainly understand if you don't!). It's better to spend a bit of extra time and get it right, than to take shortcuts and learn nothing.
-
You can get an (up to)
4864 hour extension if you need it. However: for the next compulsory exercise, you will then have to deliver a draft of that exercise4864 hours before that deadline, so we know you're on the right track next time! -
If you really need a longer extension, please include the code of whatever you have so far (even if it's nothing) and a brief plan for how you intend to complete the exercise in the remaining time. Unless we have arranged otherwise due to special circumstances, any work done more than
4864 hours after the deadline won't be counted. -
If you've solved the assignment and just think you could do a bit better with a bit more time – don't bother, you probably have more useful stuff to do, and small fixes won't make any practical difference. Perfection is overrated!
The Tasks
Task-0 (Warming Up)
You'll find task-0
running on inf226.puffling.no
at port 7000
. Connect, send your id token, and you will then get the following question:
- Are you ready for a challenge (no/maybe/yes)?
The task will be more difficult depending on whether you answer n
/no
(easy mode), m
/maybe
(moderate mode, the default) or y
/yes
(hard mode).
To solve it, you must answer four simple questions of the form What does the <ANIMAL> say?
. If you enjoy guessing, you can try to guess, or you can just read the source code. The difficulty modes have the following effect:
no
/easy mode: the questions are always presented in the same order (so, you can just send all the answers without reading the questions)maybe
/moderate mode: the questions appear in random orderyes
/hard mode: the fox sound is a random byte string, 16 bytes long, including\0
terminator (it's “just” pseudo-random, so it's theoretically predictable, but you should be able to find an easier way)
Purpose: Make sure that your token and basic setup works, before you move on to more advanced stuff.
Success criterion: To get your success token (“capture the flag”), you'll need to succeed in yes
/hard mode.
Hints:
- You will never know what the fox says.
- Keep in mind that C strings are terminated by a null byte,
'\0'
. - You can solve this one by connecting manually to the server.
Files
View source code
const char rodata[] = "abcd";
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <ctype.h>
void getFlag(unsigned int mode);
char fox_sound[16] = "pwnpwnpwnpwnpwn";
const char *riddles[] = {
"rubber duck", "squeak",
"non-rubber duck", "quack",
"parrot", "squawk",
"fox", fox_sound};
void randomize(char *s, unsigned int n);
int main(int argc, char **argv)
{
setbuf(stdout, NULL); // turn off output buffering
srand(time(NULL));
/////////// INTERESTING CODE BELOW //////////////
unsigned int nRiddles = sizeof(riddles) / sizeof(char *) / 2;
int start = 0, challenge = 1;
char animal[16];
char sound[16];
char buffer[16];
printf("Are you ready for a challenge (no/maybe/yes)? ");
if (!gets(buffer))
return 1;
if (tolower(buffer[0]) == 'y')
{
randomize(fox_sound, sizeof(fox_sound));
start = rand() % nRiddles;
challenge = 2;
}
else if (tolower(buffer[0]) == 'n')
{
start = 0;
challenge = 0;
}
else
{
start = rand() % nRiddles;
challenge = 1;
}
int i;
for (int k = 0; k < nRiddles; k++)
{
i = (start + k) % nRiddles;
strcpy(animal, riddles[i * 2]);
strcpy(sound, riddles[i * 2 + 1]);
printf("What does the %s say? ", animal);
if (!gets(buffer))
return 1;
if (strcmp(buffer, sound) != 0)
{
printf("Nope :(\n");
exit(0);
}
}
printf("You are truly wise in the ways of the %s. Proceed.\n", riddles[i * 2]);
getFlag(challenge);
return 0;
}
void getFlag(unsigned int mode)
{
const char *modes[] = {"easy", "moderate", "hard"};
if (getenv("TASK_NAME"))
{
char cmdline[48];
snprintf(cmdline, 47, "python ctf.py check %d", mode);
system(cmdline);
}
else
{
printf("Success (%s mode)!\n", modes[mode]);
}
}
void randomize(char *s, unsigned int n)
{
int i = 0;
while (i < n - 1)
{
s[i++] = rand() % 256;
}
s[i] = 0;
}
Task-1 (E-commerce Fraud)
A rubber duck vendor is operating on port 7001
on inf226.puffling.no
, and you've been given $1000 credit to buy ducks. Their developer has been reading the INF226 textbook – specifically the example about a bookstore that lost money to customers who fraudulently ordered a negative number of books – and devised a way to protect against such fraud. But with your experience in buffer overflows, you may still be able to scam them out of some money.
Note the order
data structure – we'll use struct
s to make the variable layout more predictable. (The C compiler is free to choose how local variables are laid out on the stack since local variables are only accessible inside a function. But data structures have to be compatible across function calls, so they have a well-defined layout. GCC will typically place arrays and buffers above scalar variables, to protect them from buffer overflows. order.buffer
will overwrite order.nDucks
, which might come in handy for this task.
Files
View source code
const char rodata[] = "abcd";
#include <stdio.h>
#include <stdlib.h>
#define _GNU_SOURCE
#include <string.h>
struct account {
long money;
const char *name;
};
void retrieve_account(struct account *acc);
void check_flag(const struct account *acc);
int main(int argc, char **argv){
struct account acc; // current user and their money
retrieve_account(&acc);
/////////// INTERESTING CODE BELOW //////////////
// these are the interesting local variables, packed in a struct so the layout doesn't change
struct {
char buffer[16];
int nDucks;
} order;
const long price_per_duck = 20;
setbuf(stdout, NULL); // turn off output buffering
printf("Hi, %s! Welcome to *Acme Rubber Ducks, Inc.*! We sell rubber ducks for the low price of $%ld per duck. You have $%ld. How many rubber ducks would you like to order? ", acc.name, price_per_duck, acc.money);
fgets(order.buffer, 512, stdin);
order.nDucks = atoi(order.buffer); // atoi converts a string to an int
if(order.nDucks < 1) { // we're certainly not going to get tricked by anyone ordering a negative amount of ducks!
printf("Sorry, minimum order size is 1\n");
return 0;
}
printf("And what is your shipping address? ");
fgets(order.buffer, 512, stdin); // read up to 512 bytes into our 16 byte buffer (what could possibly go wrong?)
strchrnul(order.buffer, '\n')[0] = '\0'; // replace newline with end-of-string
long total = order.nDucks*price_per_duck;
if(total > acc.money) { // input validation is important. we're checking everything here!
printf("Sorry! You don't have enough money!\n");
} else {
acc.money -= total;
printf("Thank you for your order of %d ducks!\nWe have charged your account $%ld, and you now have $%ld left.\n", order.nDucks, total, acc.money);
check_flag(&acc);
}
return 0;
}
void retrieve_account(struct account *acc) {
const char* m = getenv("MONEY");
if(m != NULL)
acc->money = atol(m);
else
acc->money = 1000;
acc->name = getenv("USER");
// ^^^ the arrow operator (->) gets a field value from a *pointer to a struct*; it is
// equivalent to dereferencing the pointer (*acc) and then selecting the field: (*acc).name
}
// check if the flag was captured
// ctf.py will check if money > 1000 and if so issue a success token
void check_flag(const struct account *acc) {
if(getenv("TASK_NAME")) {
char cmdline[48];
snprintf(cmdline, 47, "python ctf.py check %ld", acc->money);
system(cmdline);
} else {
printf("Success!");
}
}
Success criterion: You start with $1000 in your account. To get your success token (“capture the flag”), you need to complete a transaction and end up with more than $1000 in your account.
Task-2 (Sorting Service)
Everything worth doing is done in the cloud these days, and sorting is no different. Acme, Inc.'s new number sorting service is running on inf226.puffling.no
at port 7002
, and rumour has it the key to their success is their revolutionary (and proprietary) new O(n log log n) sorting algorithm. You could make a fortune (in citations, at least) if you could unlock their secret...
- The server reads line-by-line into a character
buffer[]
Each line is converted to an integer and stored in an arraylocals.numbers[]
. - You can select ascending or descending sort by entering
A
orD
- If a line doesn't start with a digit,
A
orD
, input stops, the list is sorted then printed
Success criterion: To get your success token (“capture the flag”), you need to use jump-oriented or return-oriented programming to call the getFlag()
function.
Files
View source code
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
void getFlag();
int compare_ascending(const void *a, const void *b);
int compare_descending(const void *a, const void *b);
int main(int argc, char **argv)
{
char buffer[32];
int n;
struct
{
int numbers[16];
volatile int (*comparator)(const void *, const void *);
} sort_data;
sort_data.comparator = compare_ascending;
setbuf(stdout, NULL); // turn off output buffering
printf("Welcome to *Acme, Inc.*'s number sorting service. Please enter some numbers, end with an empty line:\n");
n = 0;
while (fgets(buffer, 128, stdin) != NULL)
{
if (buffer[0] == 'A')
sort_data.comparator = compare_ascending;
else if (buffer[0] == 'D')
sort_data.comparator = compare_descending;
else if (isdigit(buffer[0]))
sort_data.numbers[n++] = atoi(buffer);
else
break;
}
printf("Sorting %d numbers...\n", n);
qsort(sort_data.numbers, n, sizeof(int), sort_data.comparator);
printf("Here are your sorted numbers:\n");
for (int i = 0; i < n; i++)
printf("%d\n", sort_data.numbers[i]);
return 0;
}
void getFlag()
{
if(getenv("TASK_NAME")) {
system("python ctf.py success");
exit(0);
} else {
printf("Success!");
exit(0);
}
}
int compare_ascending(const void *a, const void *b)
{
return *((int *)a) - *((int *)b);
}
int compare_descending(const void *a, const void *b)
{
return *((int *)b) - *((int *)a);
}
Task-3 (Bird Trivia)
This single-question trivia quiz runs on inf226.puffling.no
at port 7003
. Your goal is to call the getFlag()
function, by overwriting the return address from main()
. But the trick you used last time won't work, since the stack is protected by a stack canary. You need to figure out the canary value and include that in your payload when you overwrite the return address.
Files
View source code
#include <stdio.h>
#include <stdlib.h>
void getFlag();
int main(int argc, char **argv){
int offset = 0;
char buffer[16] = "0123456789abcdef";
setbuf(stdout, NULL); // turn off output buffering
printf("What's the typical max weight of an Atlantic canary? ");
fgets(buffer, 512, stdin);
offset = atoi(buffer);
printf("Here's a hint: %lx\n", *(unsigned long*)(buffer+offset));
printf("Please don't overwrite my stack!\n");
fgets(buffer, 512, stdin);
return 0;
}
void getFlag()
{
if(getenv("TASK_NAME")) {
system("python ctf.py success");
exit(0);
} else {
printf("Success!");
exit(0);
}
}
Success criterion: To get your success token (“capture the flag”), you need to discover the stack canary, then use return-oriented programming to call the getFlag()
function.
Task-4 (A Very Basic Language Server)
Young software mogul Jill Yates has developed a new online BASIC programming environment for enthusiastic hobbyists*, and you've been hired to do penetration testing. The BASIC interpreter is running on inf226.puffling.no
port 7004
. Connect to it, and it will prompt you to enter a BASIC program line-by-line (put a -b
in front of your token to get verbose output from the BASIC interpreter):
[pnxd5t] Please enter your token: -b eyJhbGciOiJFZERTQ…
[pnxd5t] Welcome, Jill.Yates! Best of luck with task-4!
Ready for some BASIC programming?
100 LET W$="WORLD"
110 PRINT "HELLO "; W$
120 END
BASIC> LIST
BASIC: 100 LET W$="WORLD"
BASIC: 110 PRINT "HELLO "; W$
BASIC: 120 END
BASIC> RUN
HELLO WORLD
You've notice two potential issues in the main loop:
while (locals.buffer[0] != '\0')
{
printf("%-4lu ", *locals.line_pointer);
gets(locals.buffer); // reads line, with newline replaced by '\0'
output_line(outfile, line_no, locals.buffer);
line_no = line_no + 10;
}
- Line 3 in the fragment prints the current line number by reading it through a pointer.
- Line 5 calls
gets()
which is unsafe because it doesn't protect agains buffer overflows – in fact, it's so unsafe that it's been removed from the C standard. GCC says this about it if you compile against the older standard that still includedgets()
:
/usr/bin/ld: /tmp/ccz7bQiC.o: in function `main':
/home/anya/inf226/oblig1/server/deploy/task-4/task-4.c:31:(.text+0x8a): warning: the `gets' function is dangerous and should not be used.
Using gets()
, you should be able to overwrite the return address, but the stack is protected against buffer overflows. In combination, these two things should allow you to read arbitrary memory addresses, which you'll need to find the stack canary.
Success criterion: To get your success token (“capture the flag”), you need traverse the stack and find the stack canary, then use return-oriented programming to call the getFlag()
function.
Hints:
- You need to figure out where the stack is, and search until you find the active stack frame (put something there that you'll recognize).
- Unlike the other tasks, this one has output terminated by a space.
- In BASIC, comments are introduced by a
REM
command. See if you can solve the task without getting a syntax error! (Add a-b
in front of your token to see errors from the BASIC interpreter.) - There may be a way to solve this task without using a buffer overflow...
Memory map:
This is the (formatted) output of/proc/$PID/maps
for task-4
running on the server:
start addr– end addr mode offset dev inode file path –––––––––––– –––––––––––– –––– –––––––– ––––– –––––– ––––––––––––––––––––––––––––––––––––––––––– 00400000- 00401000 r--p 00000000 08:10 262210 /srv/oblig1/task-4 00401000- 00402000 r-xp 00001000 08:10 262210 /srv/oblig1/task-4 00402000- 00403000 r--p 00002000 08:10 262210 /srv/oblig1/task-4 00403000- 00404000 r--p 00002000 08:10 262210 /srv/oblig1/task-4 00404000- 00405000 rw-p 00003000 08:10 262210 /srv/oblig1/task-4 00405000- 00426000 rw-p 00000000 00:00 0 [heap] 7ffff7dbf000-7ffff7dc2000 rw-p 00000000 00:00 0 7ffff7dc2000-7ffff7dea000 r--p 00000000 08:01 155684 /usr/lib/x86_64-linux-gnu/libc.so.6 7ffff7dea000-7ffff7f4f000 r-xp 00028000 08:01 155684 /usr/lib/x86_64-linux-gnu/libc.so.6 7ffff7f4f000-7ffff7fa5000 r--p 0018d000 08:01 155684 /usr/lib/x86_64-linux-gnu/libc.so.6 7ffff7fa5000-7ffff7fa9000 r--p 001e2000 08:01 155684 /usr/lib/x86_64-linux-gnu/libc.so.6 7ffff7fa9000-7ffff7fab000 rw-p 001e6000 08:01 155684 /usr/lib/x86_64-linux-gnu/libc.so.6 7ffff7fab000-7ffff7fb8000 rw-p 00000000 00:00 0 7ffff7fbf000-7ffff7fc1000 rw-p 00000000 00:00 0 7ffff7fc1000-7ffff7fc5000 r--p 00000000 00:00 0 [vvar] 7ffff7fc5000-7ffff7fc7000 r-xp 00000000 00:00 0 [vdso] 7ffff7fc7000-7ffff7fc8000 r--p 00000000 08:01 154931 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7ffff7fc8000-7ffff7ff0000 r-xp 00001000 08:01 154931 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7ffff7ff0000-7ffff7ffb000 r--p 00029000 08:01 154931 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7ffff7ffb000-7ffff7ffd000 r--p 00034000 08:01 154931 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7ffff7ffd000-7ffff7ffe000 rw-p 00036000 08:01 154931 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0 7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]Start address (inclusive), end address (exclusive) is the memory area; mode is the permissions:read, write, execute, private; if the memory area is mapped from a file, offset is how far the mapped memory is from the start of the file; dev is the device number, which together with inode identifies the file, and file path is the corresponding path in the file system. The [stack] is probably the most interesting thing here. With ASLR, the library and stack addresses will be randomized, and with position-independed executable enabled, the first five entries would be randomized as well.
Files
View source code
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
void getFlag();
void output_line(FILE *outfile, unsigned long line, char *s);
// int gets(char *s);
int main(int argc, char **argv)
{
setbuf(stdout, NULL); // turn off output buffering
FILE *outfile = fopen("/tmp/prog.bas", "w");
unsigned long line_no;
struct
{
char buffer[32];
unsigned long *line_pointer;
} locals;
line_no = 100;
locals.buffer[0] = ' ';
locals.line_pointer = &line_no;
printf("Ready for some BASIC programming?\n");
while (locals.buffer[0] != '\0')
{
printf("%lu ", *locals.line_pointer);
if(!gets(locals.buffer)) // reads line, with newline replaced by '\0'
break;
output_line(outfile, line_no, locals.buffer);
line_no = line_no + 10;
}
if (outfile) {
fclose(outfile);
system("python ctf.py run /tmp/prog.bas");
}
return 0;
}
void output_line(FILE *outfile, unsigned long line, char *s)
{
static int state = 1;
if (outfile && state)
{
fprintf(outfile, "%lu ", line);
if (*s < ' ' || strncmp(s, "END", 4) == 0 || strncmp(s, "end", 4) == 0)
{
fprintf(outfile, "END\n");
*s = '\0';
state = 0;
}
else
{
while (*s >= ' ')
fputc(toupper(*(s++)), outfile);
fputc('\n', outfile);
}
}
}
void getFlag()
{
if (getenv("TASK_NAME"))
{
system("python ctf.py success");
exit(0);
}
else
{
printf("Success!");
exit(0);
}
}
Resources
These resouces may help you get started.
Python pwntools skeleton
You can use this – together with ctf_helper.py
(below) – as a starting point for your solutions.
from ctf_helper import *
id_token = "" # insert your token here
io : tube = start(7000)
read_welcome()
### REPLACE WITH YOUR CODE
io.sendline(b'something')
next_qa()
### May help you decode the final success/fail response
log_answer(read_answer())
CTF Helper
Implements a few functions for connecting and receiving data that may be useful.
You can also download the file here: ctf_helper.py
.
from pwn import * # type: ignore
import sys
id_token = "" # insert your token here
# Set to 'info' if you don't want a trace of data sent and received
context.log_level = "debug"
io : tube = None # type: ignore
def start(port: int):
"""
Connect to INF226 server. Port number should be 7000-7004.
With command line arguments (in sys.argv):
- `FILE` -- load ELF file (probably requires Posix system)
- `--process FILE` -- start local process instead (requires Linux)
- `--gdb FILE CMDS` -- start local debugger process instead (requires Linux)
- `--remote` -- connect to server (default)
*(Call this first, then proceed with `read_welcome`)*
"""
global io, _elf_file
action = "--remote"
file = None
# very crude commandline option parser
if len(sys.argv) < 2:
pass
elif sys.argv[1].startswith('--'):
action = sys.argv[1]
file = sys.argv[2] if len(sys.argv) > 2 else None
else:
file = sys.argv[1]
if file:
try:
_elf_file = ELF(file)
except Exception as e:
error("Failed to load ELF file %s", file)
if action == "--gdb":
gdb_script = ''.join([f'{a}\n' for a in sys.argv[3:]])
io = gdb.debug(file, gdb_script or "break main\n")
elif action == "--process":
io = process(file)
# By default, we connect to the official assignment servers
if not io:
io = remote("inf226.puffling.no", port)
io.timeout = 2 # wait max 5 seconds when reading data
return io
_elf_file = None
def elf_file():
"""Return the ELF executable for the current task, if available."""
return _elf_file
question_delims = [b": ", b"? ", b":", b"?"]
def read_welcome(tok: str | None = None):
"""
Send token and read the welcome/first question. Questions end with '?' or ':'.
*(It's a good idea to call this first for all the tasks!)*
"""
tok = tok or id_token
question = io.recvuntil(question_delims, timeout=1024).decode() # type: ignore # read welcome or token request
if "Please enter your token:" in question:
if not tok:
io.warning_once("WARNING: No token specified!")
io.sendline(b"" + tok.encode()) # send token
question = io.recvuntil(question_delims).decode() # read welcome
return question
def next_qa() -> str:
"""
Read a question/answer from the tube. (In practice, reads until the next `?` or `:`.
*(Might be useful for any task)*
"""
return io.recvuntil(question_delims).decode().strip()
def read_answer() -> tuple[str, str | None]:
"""
Read and decode answer once you've completed the challenge.
Call this after you've sent your exploit. Print the result with `log_answer`.
*(Might be useful for any task)*
"""
a = io.recvuntil(question_delims).decode()
if "signed success token" in a:
new_tok = io.recvline().decode()
return a, new_tok
else:
rest = io.recvall().decode()
return a + rest, None
def log_answer(answer_and_token: tuple[str, str | None]) -> None:
"""
Combine with read_answer() to get pretty output.
*(Might be useful for any task)*
"""
answer, token = answer_and_token
if token:
info("Success!!")
info("")
info(
"*** Please submit the new token to https://inf226.puffling.no/oblig1/ as proof you have completed the task:"
)
info("")
info(" " + token)
info("")
else:
for line in answer.splitlines():
warning(line)
def chars(n: int):
"""
Reinterpret a 64-bit integer as a string. Non-ASCII characters are displayed as `.`
*(Might be useful for task-4)*
"""
def convert_char(c):
return chr(c) if 32 <= c < 127 else "."
return "".join([convert_char(c) for c in unpack_many(p64(n), word_size=8)])
def print_mem_entry(addr: int | str, data: int | str | bytes):
"""
Helper for printing a memory location.
Arguments:
addr (int): the memory address the word was found at
word (int|str|bytes): contents of memory location, as an integer (possibly encoded as str or bytes)
*(Useful for task-4)*
"""
if addr == 0:
info(f' {"Address":12s} {"Word":16s} {"Chars":8s} {"Original":24s}')
info(f' {"-" * 12} {"-"*16} {"-"*8} {"-"*24}')
else:
if isinstance(data, bytes):
text = repr(data)
word = int(data, 10)
if isinstance(data, str):
text = repr(data)
word = int(data, 10)
elif isinstance(data, int):
text = ""
word = data
if isinstance(addr, int):
info(f"------ {addr:12x}: {word:016x} {chars(word):8s} {text:24s}")
else:
info(f"{addr:20s}: {word:016x} {chars(word):8s} {text:24s}")
Using gdb
If you're on Linux (or something close enough to be able to run Linux programs) and you have gdb
and gdb-server
installed, you can use the GDB debugger to see what's going on while the program is running.
For example, here is a small script of GDB commands that would help you debug task-0
:
# put breakpoint at main+370, right after gets() – this will be breakpoint 1
break *main+370
# give list of commands to run when reaching breakpoint 1
commands 1
# print stack pointer, address of buffer and contents of buffer, sound, animal
print $rsp
print &buffer
print buffer
print sound
print animal
end
# now, run until breakpoint
continue
You can type them in manually, or give them as the gdbscript
argument to io.remote
; or if you're using ctf_helper.py
, you can save them to a file gdb_script
and run with:
$ python my_solution.py --gdb ./task-0 "$(cat gdb_script)"
The ELF file includes debugging information (in the DWARF format (pun apparently intended)), so gdb
knows where to find variables, how long the buffer
array is, and so on.
Without pwntools
you could also start gdb
with:
$ gdb ./task-0 -x gdb_script
If so, you should replace continue
with run
in the script.
Other useful gdb
commands include disas
(print disassembled code) and help
.
Hints
- If you get confused, it may be useful to draw a diagram of the memory, pointers and variables. (Or, if you're lazy, use Explore stack frame layout in the sidebar.)
- Kenneth's support material from 2020 may be useful, with hints for setting things up and using
pwntools
. - ctf101.org has a useful intro to C and overview of binary exploitation, buffer overflows, return oriented programming, and bypassing stack canaries.
- If you try to run the code for
task-4
yourself (on Linux or in a virtual machine), you'll need to turn off ASLR. You can do this withsetarch -R task-4
(orsetarch -R gdb task-4
for debugging withgdb
), or for the whole system by doingecho 0 > /proc/sys/kernel/randomize_va_space
(remember to turn it back on again afterwards withecho 2 > /proc/sys/kernel/randomize_va_space
)
Connecting manually:
If you want to talk directly to the servers, you have (at least) a few options:
pwntools
hasio.interactive()
– though it seems be make trouble sometimes- netcat (
nc
) is a popular tool for interacting with network sockets. For example:
$ nc inf226.puffling.no 7000
[uiianz] Please enter your token:
[uiianz] Proceeding as unauthenticated user.
[uiianz] Welcome, 2a01:799:…! Best of luck with task-0!
Are you ready for a challenge (no/maybe/yes)? n
What does the rubber duck say? glurbledörk
Nope :(
- If your system doesn't have
nc
, you might have still have old-fashionedtelnet
installed. Although it's originally meant to connect to a Telnet server (which isn't used much anymore, now that we have SSH), you can also connect directly to a port, just like withnc
. If the connection hangs, you can “escape” by pressing Ctrl-] (which is Ctrl-AltGr-9 on a Norwegian keyboard), and quit.
$ telnet inf226.puffling.no 7000
Trying 2001:700:2:8300::2264...
Connected to inf226.puffling.no.
Escape character is '^]'.
[uipmv9] Please enter your token: ^]
telnet> quit
Connection closed.
- Task-0 and Task-1 should be solvable this way.
Address space:
- These files are compiled without the position-independent executable (more info) option, so the code is always loaded at the same address (somewhere around
0x401000
). Linux (or at least the our server) uses this memory layout: 0x7ffffffffff0
– top of memory, the stack typically starts ~-0x1000
from here (depends on environment variables – should be the same for each run). Each stack frame is aligned so that the address ends with a0
– some system calls may crash with aSIGSEGV
if this is not the case. (See ROP#Solving thesystem
crash.)- With a non-PIE executable, the addresses of code and data are hardcoded, so what you see in the ELF file (
0x401xxx
) will be the actual memory addresses when the program is running.objdump -t
shows you all the symbol addresses. - The main function gets called from one of the dynamic libraries (
__libc_start_call_main
in/lib/x86_64-linux-gnu/libc.so.6
), so you'll see a return pointer to there on the stack. - Dynamic libraries get put somewhere below the stack, addresses may start with
0x7f…
. The output of runningldd
on a file shows you the libraries. With ASLR, these addresses will be random:
$ ldd task-4
linux-vdso.so.1 (0x00007c3f9e53c000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007c3f9e200000)
/lib64/ld-linux-x86-64.so.2 (0x00007c3f9e53f000)
$ ldd task-4
linux-vdso.so.1 (0x00007b61fa0bc000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007b61f9e00000)
/lib64/ld-linux-x86-64.so.2 (0x00007b61fa0bf000)
- With ASLR turned off, the libraries will appear at the same location every time:
$ setarch -R ldd task-4
linux-vdso.so.1 (0x00007ffff7fbe000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7c00000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffff7fc1000)
$ setarch -R ldd task-4
linux-vdso.so.1 (0x00007ffff7fbe000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7c00000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffff7fc1000)
Compiler command lines
- If you try to compile the C files yourself (on Linux), you may want to turn of PIE (position-independent executable mode). Use this command
gcc -g -no-pie -fno-pie 00.c -o 00
(both-no-pie
and-fno-pie
are necessary; the first is an option to the linker and the latter to the compiler). If you compile with PIE, the code will be placed wherever Linux finds convenient. With ASLR turned off, this will be in the middle of memory, somain
might be at0x5555555551fc
, for example. - The C programs should work similarly on Mac and Windows – feel free to try! In particular, if you have an M1/M2 Mac, see what difference that makes to the disassembled code, and if the exploits still work.
The commands below are what was actually used to compile the task programs. The option -Wstringop-overflow=0
makes the compiler stop complaining about potential buffer overflows, -g
enables debug info (needed to visualize the stack and to use variable names in gdb
), and -o
specifies the output file (the default is a.out
, which is not very useful).
- Task-0 needs C99 (
--std=c99
) to use gets().-fno-stack-protector -fno-pie -no-pie
makes no difference in this task:
$ gcc -Wstringop-overflow=0 --std=c99 -g -fno-stack-protector -fno-pie -no-pie task-0.c -o task-0
- Task-1 is relocatable, and will have randomized addresses:
$ gcc -Wno-stringop-overflow -g task-1.c -o task-1
- Task-2 has stack canary disabled (
-fno-stack-protector
):
$ gcc -Wstringop-overflow=0 -g -fno-stack-protector -fno-pie -no-pie task-2.c -o task-2
- Task-3 has stack canary enabled:
$ gcc -Wstringop-overflow=0 -g -fno-pie -no-pie task-3.c -o task-3
- Task-4 needs C99 (
--std=c99
) to use gets(); must be run withsetarch -R
$ gcc -Wstringop-overflow=0 --std=c99 -g -fno-pie -no-pie task-4.c -o task-4