This program provides a sample of overflow. When running the binary, we must modify the data in Red.
This is a representation of a Stack Frame. When we write to local variables, this data gets written to the Stack data segment. The stack is written in Blocks, whose size depends on the program's architecture.
In this case, we have a 64-bit program. We can confirm this by running file chall
:
When we start writing to this program, we see that we start writing to the space illustrated in our stack frame. This data is shown in hexadecimal. The most common way to fill this space is by using a character we know the hex value of, for example, A
(0x41
). If we write enough A
's, we will overwrite the data in Red! Let's try it.
This works! We modified the target variable as anticipated. However, we need to understand why this works at a lower level because we won't always have a visualizer to help us. Or will we…
GDB (the GNU Debugger) is an exploit developer's most powerful tool for understanding how programs work. We use GDB to perform dynamic analysis (program analysis during runtime) to understand what happens. This requires a moderate understanding of how to read Assembly.
First, we run gdb
on the binary with gdb chall
. We use info functions
to show the available functions in the binary.
Most of these are compiler-made; we are only concerned with the ones the developer made. The ones starting with an underscore (_
) are often compiler-made, and the ones ending with @plt
are library functions. We ignore these and go right to main
for our first function.
We use disassemble main
to show main
's assembly. There's a lot here, but the meat and potatoes are in this middle section here:
0x00000000000016ad <+212>: mov rdx,QWORD PTR [rip+0x297c] # 0x4030 <stdin@@GLIBC_2.2.5> 0x00000000000016b4 <+219>: lea rax,[rbp-0x20] 0x00000000000016b8 <+223>: mov esi,0x20 0x00000000000016bd <+228>: mov rdi,rax 0x00000000000016c0 <+231>: call 0x1100 <fgets@plt>
gets()
, fgets()
, or scanf()
calls. Eventually, we'll also want the comparison statement that checks if we modified the Red value.If we check the man
pages, we can see the arguments for fgets
:
char *fgets(char s[restrict .size], int size, FILE *restrict stream);
The first argument is a pointer to where we're writing, the number of bytes to write, and the source of where that data comes from. Let's analyze the above lines for this:
s
: We first see a mov rdi, rax
. Slightly above that, we see lea rax, [rbp-0x20]
. lea
, or Load Effective Address, computes what's inside the square brackets and puts that address into rax
. rbp
is the register used to reference stack variables, so this is a stack address. This proves we're writing to the stack!size
: We see mov esi, 0x20
, meaning we can write up to 0x20=32
bytes.stream
: We see mov rdx, QWORD PTR [rip+0x297c]
. rip
is used to reference global variables since it's the easiest way for the compiler to track the difference between the instruction and the global variable. gdb
hints that this refers to stdin
in the comment to the right (# 0x4030 <stdin@GLIBC_2.2.5>
). This makes sense since the input is coming from the console.Now that we understand how and where we're inputting data, we need to find the data to overwrite. We can see that happens here:
0x00000000000016e8 <+271>: mov eax,0xdeadbeef 0x00000000000016ed <+276>: cmp QWORD PTR [rbp-0x8],rax 0x00000000000016f1 <+280>: je 0x1701 <main+296> 0x00000000000016f3 <+282>: lea rdi,[rip+0x9c6] # 0x20c0 0x00000000000016fa <+289>: call 0x10c0 <puts@plt> 0x00000000000016ff <+294>: jmp 0x170d <main+308> 0x0000000000001701 <+296>: lea rdi,[rip+0x9e8] # 0x20f0 0x0000000000001708 <+303>: call 0x10c0 <puts@plt> 0x000000000000170d <+308>: mov edi,0x0 0x0000000000001712 <+313>: call 0x1110 <exit@plt>
Let's cover the important details:
mov eax, 0xdeadbeef
: This loads that value we want to compare into the register. We overlooked this value being loaded onto the stack, but that happened at the start of the program:0x0000000000001621 <+72>: mov eax,0xdeadbeef 0x0000000000001626 <+77>: mov QWORD PTR [rbp-0x8],rax
cmp QWORD PTR [rbp-0x8], rax
: This compares the Quad (8-byte) sized-value at that stack offset to what's in rax
(0xdeadbeef
). This sets the aEFLAGS
register based on the comparison results. CMP A, B
is equivalent to if (A ? B)
(?
since the operation is based on the J??
command).je 0x1701
: This is confusing at first. This actually tells us the operation is not equal (!=
). The compiler uses j??
to jump away from code. If the two values are equal, we skip code. We see this if we map this out like a tree:je
is not true, then we continue to the next line. This will execute the statements in that block and then jump over the block holding the true
case.From this, we gathered that the variable to overwrite starts at rbp-0x8
. We can do some basic math to see that we need to write bytes to reach there. Then, we must write data in that block to overwrite its contents. We call the number of bytes to reach important data the padding. This makes our padding 0x18=24
bytes. We commonly use A
for our padding since it's easy to identify. We'll use A
for padding and B
(0x42
) to overwrite the important block.
Rather than writing A
24 times and B
8 times, we can use Python to do it for us:
Then, we can take this string and use it in the program.
Or, we can use Pipes to push the input into the program automatically: