codyebberson/vm

Out of bound read and write can lead to code execution

Closed this issue · 5 comments

Hi, I know that this project is unactive and that it's useless writing an issue but here I am.

Pretty much every instructions using an offset to write/read data are vulnerable to an out of bound vulnerabillity. For example, here is the code for the LOAD instruction:

case LOAD: 
                offset = code[ip++];
                stack[++sp] = stack[fp+offset];
                break;

The offset is a user controlled variable inside the VM and can be up to 8 bytes long (0xffffffff bytes) when the stack allocated for the VM is only 0x1f40 bytes long.

You (i don't really know if someone is reading this) should implement a security check to ensure that it's not bigger that the size of the stack.

PS: i actually wrote an article 🥸 about exploiting this specific vm in order to spawn a shell https://www.numb3rs.re/posts/popping_shells_1/

Wow, I'm honored that you researched this. I really enjoyed your blog post.

Yes, this is inactive, but you're right that it should be fixed.

The origin of this project was to prove to Terence Parr that C would be significantly faster than his Java implementation. It was faster, but obviously at the expense of memory safety, as you have demonstrated.

Hi @Numb3rsProprety - Thanks again for filing this.

I added a SAFE_MODE switch which checks sp and ip before all memory access.

I was curious how that would affect runtime performance. I changed the "counter" demo in vmtest.c to count to 2 billion.

On my machine:

  • SAFE_MODE enabled = 2.58 seconds
  • SAFE_MODE disabled = 2.28 seconds
  • (2.58 - 2.28) / 2.28 = 13% slowdown for the safety

The "counter" demo executes 1,800,000,008 instructions.

  • SAFE_MODE enabled = 2.58 seconds => 698 million instructions per second
  • SAFE_MODE disabled = 2.28 seconds => 789 million instructions per second

Quick comment on performance of VM vs JIT techniques: I was playing with cranelift-jit-demo, and ported the "counter" demo to the demo's toy language:

    fn counter() -> (r) {
        n = 200000000
        r = 0
        while r < n {
            r = r + 1
        }
    }

The full executable consistently runs in 0.1 seconds on my machine (same machine as above).

$ time ./target/release/toy.exe
the answer is: 200000000

real    0m0.108s
user    0m0.000s
sys     0m0.015s

That is a 20x+ speedup, with more functionality (AST parsing, Rust type safety, etc).

Hi @codyebberson, I'm really glad you enjoyed the blog post, and kudos for fixing the vulnerability! I never would have guessed that this security check could slow down the program like that—it's crazy