Skip to content

Instantly share code, notes, and snippets.

@DmitrySoshnikov
Last active April 6, 2016 08:44
Show Gist options
  • Save DmitrySoshnikov/31ee8c247d8075352da2 to your computer and use it in GitHub Desktop.
Save DmitrySoshnikov/31ee8c247d8075352da2 to your computer and use it in GitHub Desktop.
Stack buffer overflow
/**
* Stack buffer overflow.
*
* TL;DR: In older OSes, a buffer overflow of the data on the stack
* allowed overriding a return address, and execute an exploit code,
* instead of returning to a caller.
*
* Docs: https://en.wikipedia.org/wiki/Stack_buffer_overflow
*
* by Dmitry Soshnikov <[email protected]>
*/
var stack = [];
// Address 0:
//
// Main function from which we call our `foo` function,
// and tell where to return.
function main() {
// Nothing follows this call, so return
// directly from main, i.e. to OS.
call(foo, 'RETURN_TO_OS');
}
// Calls a function, returning after it
// to the preserved return address.
function call(fn, returnAddress) {
// Put the return address.
stack.push(returnAddress);
// And then actually pass the control to a function.
fn();
}
// Address 1:
//
// Our `foo` function, that allocates a buffer on the stack of
// length 3, and asks a user to fill the buffer (via `userInput`).
//
// After `userInput` work, we already have the stack frame actually
// corrupted, and the return address should be substituted to the
// `EXPLOIT_ADDRESS`.
//
function foo() {
// Once we entered the `foo`, confirm that the return address
// on the stack is set to `RETURN_TO_OS`.
// console.log('Stack', stack); // ['RETURN_TO_OS']
// The `data` is a buffer of 3 bytes on the stack.
var data = allocate(3);
// Here where the buffer overflow happens, and
// overrides the return address.
userInput(data);
// Confirm that exploit address was copied to the
// return address slot on the stack.
// console.log('Stack', stack); // [3, 2, 1, EXPLOIT_ADDRESS]
// Pop the allocated data.
deallocate(3);
// By this time there should be only the return address.
// And we don't return to RETURN_TO_OS, but to the EXPLOIT_ADDRESS.
returnToCaller();
}
// Address EXPLOIT_ADDRESS:
// This code was injected into memory "somehow".
//
// If we correctly overflowed the buffer, our code
// should be executed here.
//
function exploit() {
console.log('.PWNED');
}
var instructionAddresses = {
RETURN_TO_OS: null, // nothing to execute
// Below are addresses of our functions.
0: main,
1: foo,
EXPLOIT_ADDRESS: exploit,
};
// This guy just pops the return address which should be
// on the stack, and pass the control there.
// In our case it's an address of a function, or `null` if
// return to OS.
function returnToCaller() {
var returnAddress = stack.pop();
// Execute the next instruction at that address.
if (typeof instructionAddresses[returnAddress] !== null) {
instructionAddresses[returnAddress]();
}
}
// Just allocates needed bytes on the stack as a "local variable".
function allocate(bytes) {
stack.length += bytes;
return Array(bytes);
}
// Deallocates the stuff.
function deallocate(bytes) {
stack.length -= bytes;
}
// The actual buggy code of a compiler: it doesn't check for
// boundaries of the input, and allows an exploit to override
// any data there, including the return address.
function userInput(data) {
// From `foo`, data is supposed to be of length 3.
data[data.length - 1] = 1;
data[data.length - 2] = 2;
data[data.length - 3] = 3;
// Since the compiler doesn't do any boundary checks, we can
// push *more* data there. Put the address of our exploit code,
// overriding *actual* return address.
data.push('EXPLOIT_ADDRESS');
// *Actual* length, but not 3
for (var i = 0; i < data.length; i++) {
stack[stack.length - i - 1] = data[i];
}
}
// Run the program.
main(); // .PWNED
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment