New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
buffer overflow is not a buffer overflow #4
Comments
Actually, I know what is happening. the I will look at asm to confirm. Also thanks for the quick assignment. |
You're welcome! I usually do things on the latest available stable version, I don't know about @Creative0708 though. |
Ah woops, @Bright-Shard made the buffer overflow, my bad :v |
The buffers don't need to be in the same place. What needs to be the same is the location on the stack of the two On its own this is useless, except that the two buffers have different sizes, and Rust doesn't realise the size has changed. I'm not sure if it's a fat pointer thing or a type system thing - all that matters is that when we loop over every item in the buffer, Rust loops ten times, since the size of the original buffer is 10. Under the hood, we've swapped the pointer to point to a buffer of size 5, meaning we end up writing 10 items to a buffer with 5 items - a buffer overflow. Now that you've pointed this out, though, I think the printing logic is flawed; we should find a way to print 10 items from the second buffer's address, to prove that we've written 10 items to it. Currently I can't think of a way to do this in "safe" code though. |
If it's just for proof printing inside the CLI, maybe some unsafe printing is ok? Just like we do in tests. |
I don't see how Rust thinks the size changed. You are still writing to a &mut Box<[u8; 10]> with your first reference, which is what you are iterating over. So these two things are totally unrelated. you aren't overflowing a 5 byte buffer. You are just writing out of bounds into random memory. |
If you remove the println from your code, the language server will tell you that you are not using the five byte buffer at all, so how could you be overflowing it? |
As you say, the only time we use the five byte buffer is to print from it. So then how did the values change? Here's what happens in the code:
In short, all of this code would be correct if it was still using that box to a 10-byte buffer. But we dropped the box to that 10-byte buffer. We swapped it with a box pointing to a 5-byte buffer. So it's now overflowing. |
"buffer you are writing to" I put quotations, because it seems that the pointer is changing after some function calls. I can't quite figure out why yet. I do thing this is at least use after free. A colleague of mine pointed out that the heap caching won't count for the smaller size. I think possible there are two allocations here. One for the 10 byte buffer and one for the 5 byte buffer. |
Yes - the original bug all of our "features" exploit is a use-after-free. Here we exploit that use-after-free to change the pointer so it points to a 5-byte buffer. Rust doesn't realise the size has changed and thus still writes 10 times (the size of the old buffer). |
This is def Use After Free, with the potential to be a buffer overflow, depending on compiler and program. |
What I'm trying to explain is that we aren't trying to allocate twice to the same address. We're swapping the boxes themselves - ie, the pointers, not the data they point to. Please reread my step-by-step explanation. Probably the reason yours segfaults there is this exploit relies on both boxes getting put in the exact same place on the stack. This works for me on linux, but I guess it doesn't on windows. It's hard to guarantee that they do get put in the same stack location without unsafe code. |
But you aren't. You aren't telling Rust that you are writing 10 bytes to a 5 byte buffer. That would be overflowing the buffer. You are writing 10 bytes to memory that has already been freed. You aren't even using the 5 byte buffer. You are POTENTIALLY using it. But it doesn't have the same behavior in all cases. |
I think there's a misunderstanding here. The reference to the buffer is a |
Aha, it's a double reference indeed! I was starting to have doubts myself. |
You are compiling with gcc toolchain on linux, I assume? Only thing I can think of why we are getting different behavior. |
We're just compiling with Rust's official stable toolchain |
I am not sure exactly what it is. I hate it, for sure. It's definitely UAF and UB. I just don't think it's a buffer overflow, even though I would love it to be. We were just talking about this. I wanted to make a CTF with a buffer overflow in Rust. |
I agree that it's not much of a buffer overflow, @Bright-Shard may I modify it to be a more traditional stack-based buffer overflow like that of one written in C? |
The results I get definitely indicate that this is a buffer overflow. We are indeed overflowing outside of the allocation of the new, smaller buffer, by having the reference to the old buffer point to the new buffer instead thanks to them having been placed at the same position on the stack.
I think the one way the results could be different is if the hack of having the two boxes at the same place in the stack is actually not reliable. |
Why this is a buffer overflowExactly what Creative said. I don't care if the boxes point to the same place in the heap or not. We're just dealing with the stack. I'm going to go through a whole-ass breakdown here to hopefully clarify this once and for all. But to be clear, we do not care where the boxes' buffers get allocated to. I think the box is confusing here because it is used to refer to data in the heap, but there's really 2 things at play there - a pointer and a buffer. So I'm going to stop referring to boxes. I'm going to refer to pointers, which are on the stack and point to some allocated memory in the heap, and buffers, which are the actual data in the heap. Finally, I'll refer to the reference, which is our borrow of the original box and refers to the pointer. We first allocate a 10-byte buffer in memory and create a pointer to it. We create a static reference to that pointer, but then drop the pointer. TO BE CLEAR: Neither the 10-byte buffer nor the pointer to that memory exist anymore. They are both gone. However, our reference still assumes the pointer exists, and we still have that reference. Furthermore, the reference assumes that it refers to a pointer that is pointing to a 10-byte buffer. What happens next is the most crucial, and least stable, part of the exploit. We allocate a new buffer in memory, which is this time 5 bytes in size. The pointer to that buffer is then pushed to the stack. Because the old pointer has been removed from the stack, and the new pointer has been added to it, the new pointer effectively overwrites the old pointer on the stack. The reference that we had before is now referring to the new pointer, the one to the 5-byte buffer, because the old pointer has been overwritten by it. We now use the reference to write to our buffer. Let's clarify what the reference thinks it is doing, vs what it is actually doing, at this point:
So now, when we fill our buffer with the reference, Rust thinks we are accessing a pointer to a 10-byte buffer, and thus writing 10 bytes to a 10-byte buffer, which is safe. In reality, we are accessing a pointer to a 5-byte buffer, and thus writing 10 bytes to a 5 byte buffer, which is a buffer overflow. Why this isn't working for youI'm inclined to agree it's platform difference. I believe all 3 of us are on Arch Linux, and so the generated binary probably behaves differently. What's probably happening is that the pointers are not getting put in the same place on the stack. So when it goes to read the buffer from that pointer, it just reads garbage on the stack, not an actual pointer - resulting in either nonsensical printouts or an access violation. Maybe try running this in WSL? |
It's going to be very dependent on a lot of things, possibly. Rust doesn't even guarantee to generate the same machine code for the same crate in two different programs or on two different versions of the same compiler, and I have tested this with bindiff, and the same binary compiled on two different compiler versions generated a lot of different machine code. Like I said, it has the potential to overflow the buffer, but it's not in itself a buffer overflow. You do need to overflow the target buffer for it to be a buffer overflow. These aren't being place on the same spot on the stack in my environment, which is why it's not generating the same output. In no circumstances could I get it to overflow, and a colleague of mine did this on a Linux machine as well, although IDK if arch, and my only arch box is like 1.5 years old. I would bug report this as UAF, undefined behavior with safe Rust, and a potential to generate a buffer overflow noting the environment which the bug showed up. In any event, I think this bug has been around for a long time and they still haven't fixed it, and that is sad. I would love to see something that can reliably generate a buffer overflow. Stack or heap based. |
Yeah, it's definitely not the most reliable thing... Rust's stack unpredictability makes this difficult and I think I'm convinced at this point that this is the only point that could make this fail. @Creative0708 decided to start making a new buffer overflow example, we'll see how that goes. It'll probably be stack-based because it'll let us overflow a buffer that will corrupt some other data on the stack. I just don't know if we'll be able to do anything about it not working as expected on some |
My colleague is telling me the unreliability comes from it being a kernel issue. I am not really a Linux dev, but he says it's because Arch sets up the stack differently. |
Anyways, sorry for busting your balls. Still love what you guys are doing! |
This is a heap-based overflow. It's not reliable across platforms, I guess. We tried a stack-based overflow with slices but couldn't get them to align so it didn't ever work out. Hopefully creative's new attempt will and then we can have a heap-based and stack-based. |
I'm being told I misunderstood your statement, sorry xD |
Haha don't worry it's fine. We were committed to the joke being accurate anyways. xD |
Basically the gist is that it's only a buffer overflow on specific systems where the stack has the reliability we need. And well, apparently...
I didn't even know the Linux kernel could do this, but apparently it can. Oh well. 💀 |
also, this might help a bit with debugging, if you increase the length of buffer_ref to something above 32, it should break this example even on Arch. |
yeah, you're right, it does... not sure why atm, I'll have to look into that. Right now we're trying to make transmute more stable and then I think we're going to redo this with transmute instead of the current code. |
It's because the allocator will choose to make a new allocation for the greater than 32 byte sized chunk, rather than the smaller than 32 byte sized chunk. This is a caching optimization, I believe. Edit: see tcache bin for glibc |
But this should just rely on the pointer to the allocation, not the size of the allocation, that's what I don't understand. Unless it's making the stack more convoluted with more function calls or something |
You might want to check a disassembly of the compiled output. You may find that you aren't actually using the same spot on the stack. That is what I had found. I can actually get the overflow to work if I mess with the stack a bit in the code, on my end. For example, it will overflow if I use a hex formatter for anything, instead. I think this is because it is coincidentally writing the new buffer pointer to the place on the stack where the reference is pointing to, or something. Rust machine code generation is incredibly verbose, and the compiler will also optimize out fat pointers and in line sizes everywhere, and it gets really hard to track things down. I still gotta look in x96 dbg, and I also haven't looked at the code generated from the hex formatter trick. As I mentioned above, I have been doing some research on Rust code generation, and have found that Rusts code generation is pretty unreliable, and the compiler will make wildly different decisions sometimes based off the slightes difference in code. You might even find that the Rust compiler generated something different for the bigger buffer, too, in addition to the TCache optimization. |
Fixed in f09d049. Have fun! |
well it worked for him but not me. the stack is remarkably unstable. I'm gonna reopen this, and let's keep it open until it works for everyone (including nordgaren) |
Yea. Also didn't work for me. I tried just transmuting an array and another array, but no luck. let password = black_box([0u8; 8]);
// String is a Vec<u8> which is a (RawVec, usize) which is a ((Unique, usize), usize)
// Rust explicitly says that structs are not guaranteed to have members in order
// but this is sketchy enough anyways so :shrug:
let mut name = black_box(crate::transmute::<_, [u8;16]>(password));
println!("{password:?}");
for i in 0..name.len() {
name[i] = 1;
}
println!("{password:?}"); in this example, Rust still makes two different arrays. |
We're really just going to be at war with compiler optmisations here... and I don't think there's any way to completely turn them off. Creative found that the arrays were getting allocated the wrong way (ie password allocates before name), and I've not had any luck trying to write to the buffer manually. I think transmute isn't quite working as intended. I'm going to bed but will try to look into it more tomorrow. By the way, how are you managing to view the assembly? I'm just opening mine in binja but I can't find the functions I want to, and even with Edit: Yeah transmute appears copying the data, not modifiying it. I have this code: #[no_mangle]
#[allow(clippy::pedantic)]
pub fn buffer_overflow() {
let name_buf = black_box([0u8; 16]);
let zero = black_box(name_buf[0] == 0);
let password = if zero {
black_box([0u8; 7])
} else {
panic!("Dude wtf");
};
let mut name: [u8; 24] = transmute(name_buf);
let mut stdin_buffer = [0];
let mut idx = 0;
while stdin_buffer[0] != b'\n' {
stdin().read_exact(&mut stdin_buffer).unwrap();
name[idx] = stdin_buffer[0];
idx += 1;
println!("Read {idx}");
if idx > 24 {
println!("Hold it right there, partner! That name's too large for these parts.");
}
}
println!("name: {:?}", name_buf);
println!("password: {:?}", password);
println!("transmuted name: {:?}", name);
} So the first two buffers aren't getting modified, but the transmuted one is. |
We refactored the buffer-overflow example to be a little password overflow thing. I managed to write a small function that determines the layout of a @Nordgaren Could you try this new refactor? |
(ok it currently segfaults in release. I don't know why but at least it's gonna be reliable in debug mode.) |
In debug mode it just tells me I didn't modify the password, so that buffer is still all 0s In release mode, I get
It's funny, when I woke up, I was like "Maybe I should suggest trying to use |
And you tried with Interesting that you get that in release mode, for us it's immediate segfault lol. Oh well. |
Ah, yea. It works. I tried using a long password at first, but I must have just typed in 16 chars exactly lol. Whoops Yea, that seems to work |
Maybe black box the function that gets the new buffer, and it won't break in release? I will try to look at a release build sometime this week. Hopefully in the next day or two. |
Alright, I've completely redone the transmute function in d102be5 to rely on enum abuse instead of stack abuse, and it shouldn't panic now. Could you see if the buffer overflow works now? Thanks |
Works for me! Seems to also be reliable as a buffer overflow! Works on both debug and release builds! Asked my colleague to test on his Linux machine as well. :) |
Print out the addresses of each box.
There is no guarantee that Box will allocate the buffer in the same position on the heap.
Your code as it is now will have the old buffers address as 0x1, and not the same address as the new buffer.
If I
println!("{:?}", original_buffer.as_ptr();)
inget_dropped_buffer
it prints what you expect it to print for the address. I assume this as something to do with rust optimization even though you have everything inblack_box
. The buffer address still changes around, though. I will have to take a look at the actual asm to see what is causing this.Can you tell me what version of the compiler you guys did this on?
Thanks, and cool stuff!
Nordgaren
The text was updated successfully, but these errors were encountered: