This post was originally published by pancake on his Substack, trufae.substack.com, and is republished here with permission.
Reverse engineering is often associated with the offensive aspect of security, revealing secrets, finding vulnerabilities, writing exploits or cracking software.
However, there are many other aspects in the field that cover defensive or education aspects like verifying compiler artifacts, learning about compilers, having fun participating in CTFs or binary patching binaries for various purposes.
I’ve been messing with assembly for more than 28 years, and patching bits to fix vulnerabilities or crashes was in my top 10 of fun things to do with binaries.
Today’s aphpcalypsis was a great excuse to once again use it as an excuse to teach some r2 oneliners and help everyone be a little bit safer in this crazy dead Internet.
Twenty-four bytes away from safetyland
In October 21, 2026, Calif released two serious vulnerabilities affecting the PHP runtime were disclosed:
- CVE-2026-109618: memory corruption caused by unsafe out-of-memory handling.
- CVE-2026-109619: use-after-free caused by global state surviving between requests.
Both issues can lead to remote code execution under certain conditions, the bugs are fixed upstream, but the time gap between crafting a release, distros picking it, sysadmins updating, and users scraping the whole Internet for vulnerable servers and maintaining takes its time.
First quick actions to disable multipart/form-data in your Apache or rewrite some client headers in your frontend proxies, the other option is, if you can afford it, just remove the mod_php or shutdown the server.
Alternatively you can also prompt your favourite LLM to rewrite your php files into a Rust FastCGI, or just rebuild the last php from git but that will take some time and will be less fun than analyzing a 10 MB binary by hand.
Understanding the bugs
Before jumping into the disassembly we need to collect information about each vulnerability, understand the affected code and prepare an strategy to address the exploitable path.
I won’t go into much details because I let long posts rot in tabs and I prefer to go straight to the point and deliver some good r2 oneliners for the kids to enjoy, and maybe if i’m bored and people show some interest I will write in a separate post the full details.
Resolving out of memory situations (CVE-2026-109618)
Critical software recommends to just crash and restart when the OS can’t provide enough memory for the program to run, but we don’t run PHP in critical infrastructures in baremetal, but most userland software ends up managing the situation and move along.
The this case, PHP reports an error and calls zend_bailout() to abort execution. This leaves a half initialized BG(unserialize) struct which the attacker controls, and trick the engine into perform calls to other functions in order to overwriting other function pointers like zend_function to pivot the attacker controler shellcode.
How can we avoid all this mess to happen? exit early and do nothing! So the easiest patch to do here is to patch the zend_bailout function to just exit(1) instead of trying to recover from the OOM with longjmp leaving a corrupted global state around.
Apache will properly exhume the child and respond to the user request accordingly.
Digging into the assembly
Let’s jump into our Alpine-x64 server, install the php85-apache2 package and get the .so:
$ apk add php85-apache2
$ apk info -L php85-apache2 | grep so$
usr/lib/apache2/mod_php85.so
$ shasum /usr/lib/apache2/mod_php85.so
7d64a834e2fe22d82f349f8b703cee1d86fdacf9 /usr/lib/apache2/mod_php85.so
Copy the library in our home and open it with radare2:
$ cp /usr/lib/apache2/mod_php85.so .
$ cp mod_php85.so mod_php85.so.orig
$ r2 mod_php85.so
[0x0005fe70]> f~bailout # filter flags matching bailout
0x00515e30 102 sym._zend_bailout
0x00939218 8 reloc._zend_bailout
[0x0005fe70]> /r 0x00515e30 # search code references to it
Bailout internals
As you can clearly read here, executor_globals[0x1a0] tells the bailout to run exit(-1) or siglongjmp. and as long as we just want to force the exit, the easiest patch we can do here is to nop the jne. One of the many ways to do that is with the wao command which can perform a nop for ANY architecture so your script will be portable. Then we reanalyse and disassemble the function again:
If you are unable to see an exit(-1) here, You can use r2ghidra which can remove dead code with -e r2ghidra.ccode=true
Decompilation of the patched zend_bailout function
If we want to go picky, we can actually investigate the 28 xrefs to this function and find the one referencing the Allowed memory size of %zu bytes exhausted string, which leads us to 0x3a4a30, and that one ends up calling the infamous zend_bailout.
A nice detail here is that we can see the sitgsetjmp and source paths and line number references stated right before the bailout.
Listing strings in the function that have the exhausted bytes string.
Those little details gives us hints to navigate thru the disassembly when we have the source available, here’s a better decompilation of the very same function:
Beware the leftovers (CVE-2026-109619)
PHP keeps some global pointers around between requests, this is specially interesting for Apache workers because they reuse a single process for multiple clients.
This is used to optimize memory usage and everything should be fine if that global state is properly locked and deinitialized without leaving half baked work.
The bug is that a fatal error leaves a backtrace array (allocated on the request heap) in a global variable EG(last_fatal_error_backtrace). Because that global isn’t cleared during shutdown, the next request handled by the same worker inherits a dangling pointer to memory that has already been freed, leading to the Use After Free.
Good global are the ones that are never touched
What if we simply prevent the global to be referenced by just avoiding it’s creation? For that purpose we will lose a precious backtrace for debugging, because that structure holds a reference to the request that caused it.
Personally I prefer no debug details in production before being owned by a worm, so let’s check which codes makes use of it:
- The function
zend_error_zstr_atis called in every error path and it checks for thefatal_error_backtrace_onglobal variable
In this function we scroll down to observe the conditional jumps, the first one that depends on a dereference is the one that
If we graph just that portion of the function we can clearly see that this condition is the one that fills the backtrace into the observed errors, so the patch we need to do is to just patch this conditional je into an unconditional jmp so the branch follows the ttrue path and keeps the observed path without leaving a stale backtrace from another request available.
To patch this we can use wao nocj@0x51643b the same way we did before and call it a day.
» nocj stands for non-conditional-jump, that ‘write assembly operation’ checks if the instruction in the given address is a conditional jump and removes its conditional bit.
Bonus Points
The CVE-2026-109619 mentions the last_error* globals also affected by an Use-After-Free, so it’s worth patching this vulnerability too.
This one is tricky because we lack some symbols and strings, so we depend on reading the source code:
- During php_request_shutdown()
- It’s calling clear_last_error() — if we check the git diff this happens too early
- Upstream calls clear_last_error() again right before the memory manager shutdowns.
The patch and the problem is clear in C, but we none of those symbols are available in release binaries, so we must find some xrefs to relocs that the function calls.
- php_request_shutdown calls zend_interned_strings_deactivate
0$ rabin2 -R mod_php85.so| grep zend_interned_strings_deactivate
0x009363c8 0x009353c8 SET_64 6 zend_interned_strings_deactivate
0$ r2 -qc "aar; axt @ 0x9363c8" mod_php85.so
(nofunc) 0x33e61f [CALL:--x] call qword [reloc.zend_interned_strings_deactivate]
There’s only one XREF! Bingo, we can now dig into the function to find a way to near the memory teardown code, and luckily the compiler optimization pass inserted a 6 byte NOP to align a loop, that’s enough room to place a relative call (which requires 5 bytes).
Rearranging the calls to keep original semantics is necessary, so we will select those bytes in visual mode V using the cursor c and Shift+Arrow keys then y to yank (copy in vim-slang), then move the cursor 5 bytes forward to have room for the call and do Y (to paste the instructions we copied).
We will need to add a nop wao nop@..e60f to patch the nopsled and then write the call in the cursor position with A key in visual or wa command.
Note the sigsetjmp refs in the screenshot above.. as a reverse engineer I know that when there’s such call I must look carefully in how the memory is managed.
Once patched, we can use radiff2 to verify the changes.
Write Cache
In all the scripts above we didn’t opened the binary in write-mode, so in order to patch the executable you can:
- Use -w flag to open en read-write
- Run oo+ to reopen a read-only file in read-write
- Enable io.cache=true to permit patching in memory
If you opted for the last one, which is probably the safest option, once you finish patching everything you’ll need to review the cache writes and commit it to disk using the wci command like this:
This final screenshot
Fraudulent Versioning
Finally I would like to share another silly trick I used to do in the 90’s when Nessus was the kiddiest way to pwn the rest of peers in the IRC chat which implies patching the fingerprinting that’s exposed to the users via HTTP headers, this way, attackers will probably skip your server considering it not vulnerable because no version info or wrong version info is exposed.
Obviously this versioning details can contain SQL statements to make fun of those who are still writing vulnerable vulnerability scanners. 😄
Patching this string was as easy as searching for a pattern and writing the new text:
Final Words
There are many posts and sites explaining how to update, verify if you are safe or how to apply some safeguards or workarounds to keep your WordPress running.
It is not the purpose of this post to help you exploit or update your system, but instead to teach you how reverse engineering can help you understand vulnerabilities and fix 1days before the official fix is out.
Hope you learned some new stuff and enjoyed the reading. Spread the dword!
—pancake















