It`s my first time encountering this fancy pwn item, known as Web Pwn, on WACON2023. I have few experience on php coding, so it spent me a long time to figure out how ZendMM actually works.
The Memory Management of Zend Engine
PHP codes are explained through the Zend engine. Instead of directly using traditional malloc
and free
to manage memory, Zend uses ZendMM to allocate and release memory through emalloc
and efree
, which efficiently serves PHP request-bound machanisms (that`s another topic).
Basic Structure
As writen in zend_alloc.c
source code, all allocations are split into 3 categories: huge, large and small. Remember that zend_alloc allocates memory form OS by CHUNKS, which contains 2MB memories. Huge allocs are those who exceed a chunk. And zend_alloc use mmap
to allocate one. The concept of PAGE is commonly used in ZendMM, which usually contains 4KB memories. That`s to say, a chunk contains 512 pages. Small allocs are less than 3/4 of page size. The rest are Large allocs.
Each time a chunk is alloced, the first page of the chunk is used to record basic information about the chunk. The Structure recording information is _zend_mm_chunk
,(which doesn`t appear in huge chunk)
1 | struct _zend_mm_chunk { |
All the chunks form a double linked list (*next
, *prev
). A chunk records the usage and other detailed information of its 512 pages through zend_mm_page_map
, zend_mm_page_info
. Also, the zend_mm_heap
structure merits attention.
1 | struct _zend_mm_heap { |
Mind the zend_mm_free_slot
. The ZEND_MM_BINS
usually is 30, which means there are 30 fixed size for small runs. As a result, there are 30 single linked list.
Vulnerable Small Runs
We mainly focus on small runs, because its vulnerable.
1 | static zend_never_inline void *zend_mm_alloc_small_slow(zend_mm_heap *heap, uint32_t bin_num ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC) |
This function is mainly used for building the small run chain when allocating a chunk. It explains how 30 single linked chains are built. Because each part of the chain doesn`t have to contain a header about its size, only leaving the fd, we may find the weird scene (compared to glibc) in the memory.
When we allocate a small run:
1 | static zend_always_inline void *zend_mm_alloc_small(zend_mm_heap *heap, int bin_num ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC) |
When we release a small run:
1 | static zend_always_inline void zend_mm_free_small(zend_mm_heap *heap, void *ptr, int bin_num) |
Both of the functions lake security checks .If we replace the fd of it by out target address, we get an arbitrary address allocate! That makes small runs vulnerable.
WACON 2023-heaphp
A typical php-pwn, we are given a docker environment and a vulnerable php extension module heaphp.so.
It took me quite a long time building local php environment following the guideline on blogs. I complied php locally aming to debug it easily. This document helped a lot. However, if you try to complie php with debug-symbol, the ABI of the binary will change, which should match extensions build with same debug-symbol. It could be useful when developing extensions. But I recommend to turn it off in ctf game.
Reverse
All protection is on except Partial RELRO .The extension mainly consists of 5 functions: add, view, edit, list, delete. Because of zend engine, the pseudo-code are hard to understand (especially for noobs like me). There are tons of code of uncertain significance like:
Then we must dig into the basic data type in zend. That`s _zend_value
and _zval_struct
1 | typedef union _zend_value { |
That`s quite a complex structure, if we replace the meaningless __int64 xx
with corresponding zend data type, then it will be easier to comprehend.
by the way, the form of parameters looks weird
It doesn`t mean takeing exactly 2 parameters. In fact, a1
stands for the input args (parsed by something like zend_parse_arg
), while a2
stands for the return values. We may set type of a1
to be zend_execute_data *
and a2
to be zval *
. In practice, I set a1
to be _zval_struct *
for better comprehension.
After checking the declaration, the meaning of following parts a clear. (take zif_add_note
for example)
v2
represents the total number of parameters, and here should be 2.
Here comes a type check. Refering the table and we find ‘6’ represents string. So arg1 should be a string ptr, and will be copied to v4
.
Vulnerability
zif_add_note
use strlen
to caculate the length of input string and allocate corresponding memory. However, when using memcpy
to copy content, the 3rd argument taken is the actual length of string. The consequences is that string can be cut off by NULL, which means we can overwrite the next memory`s fd.
zif_add_note
also contains an off by NULL Vulnerability.But who cares?
Exploitation
Since Partial RELRO is on, we can overwrite GOT table. Before that, we must leak the address of heap.so and libc.so.
Debuging Tricks
To load the target extension, you should put the extension in correct path. To find the path, run
1 | php -i | grep -i extension_dir |
And modify the php.ini file. You can find
it by
1 | php --ini |
Usually it doesn`t exist at first and you have to create one manually.
Add the config at the file end
1 | extension=heaphp.so |
After that, you can check if it`s properly loaded by phpinfo
or checking the /proc/[pid]/maps
when running php.
To debug the extension, we run php with gdb attached first
1 | gdb php |
Then we run
it and press Ctrl+c
to interrupt it. Check the vmmap
, you may find heaphp.so is loaded.
We can set breakpoints now. Don`t forget to set our exploit script as argument.
1 | set args ./exp.php |
You can also write them in a gdb script.
Address Leak
Through overwrite the content pointer of any notes, we may get content of arbitrary address through zif_view_note
.
First step, we can leak an fd pointer (by zif_view_note
or zif_list_note
). Our heap memory was allocated by mmap anonymously, it doesn`t have a constant offset with libc.so or heaphp.so.
However, we may find a pointer related to libc.so or heaphp.so on the heap. It could be extremely hard to find one through analyzing the source code. But I found a useful tool in pwndbg.
1 | usage: leakfind [-h] [-p [PAGE_NAME]] [-o [MAX_OFFSET]] [-d [MAX_DEPTH]] [-s [STEP]] [--negative_offset [NEGATIVE_OFFSET]] address |
leakfind
is a powerful tool to leak address given a starting address, then we can find some libc pointers on the heap.
On obtaining the libc address, we get heaphp.so address since they have constant offset, then we can overwrite the [email protected]
on the heaphp.so with the actual address of system
on libc.so.
Payload
It`s not the final edition because functions like chr()
are banned in the docker, and getting shell is usually not allowed in PHP pwn. I preserved them to make it more readable.
1 |
|
D^3CTF 2024-PwnShell
I camp up with this pwnable challenge in d^3ctf 2024, to offer an entry-level php pwn challenge. To make it more interesting, I wraped it with a simple file uploading web challenge.
There is an off-by-null in addHacker function. Trigger the off-by-null to forge a fake fd and create a heap overlap to get a arbitary address write permitive. Then we are able to forge the GOT of efree to call system.
Btw the libc address leak can be obtained by including the /proc/self/maps or leaking certain pointers in the heap. That`s two typicall way to obtain adderss leak in php pwn.
1 |
|