WASM Artifact Analysis

WASM Artifact Analysis

As we have shown in our previous post there is a great deal of trust invested in the wasm artifacts when dealing with ink! smart contracts (or substrate runtimes). Here we present a simple tool to analyze those wasm artifacts statically and detect some type of backdoors and anomalies that could be found in wasm.

Goal: understand what is happening in the wasm module

An ink! wasm module must export only 2 functions:

  • deploy()

  • call()

deploy() is used as a constructor and it is called when the deployer instantiates the contract for the first time. call() is the main entrypoint used to execute all the implemented functionality. The dispatcher reads the input and select which internal method to jump to. As a simple first step, lets take a look at the wasm function call graph for a normal and a backdored flipper contract.

A normal Flipper

Note: It's important to keep in mind that function names may vary from one call graph to another. These function names, such as func1, func2, etc are dynamically generated based on the sequence of functions.

A backdoored flipper

Find the differences

From a quick inspection we can design a few static features to make good contracts from bad ones.

All good contracts written using ink! have a single path from call() to input(). Having two different paths to reach input from a call is so far a good way to determine if a contract is backdoored.

There are a few other very simple static checks that while at it we can add to the mix:

  • must export only call() and deploy()
  • must import only valid host functions
  • must import only the lastest versions (seal) of host functions
  • If it accepts payment it must have import functions to send it out ....

The tool

Check out our tool to statically check wasm artifacts generated by ink! smart contracts. Please let us know if you think of any other static feature to check or how to improve the ones we have.

We took our simple backdoor and run it through all know simple parity example contracts. Then check if the simple detector idea works. TL;DR it works!

 ~/inkscope/octopus $ poetry run inkscope --file ./tests/flipper.wasm --check_backdoor
Checking for backdoor
❌ Check failed

There are multiple paths from call to input.

Note that there are 2 paths from call() to input(). This is very uncommon in regular ink! smart contracts. There is a high chance that this smart contract has been manipulated.

image

Contract NameNumber of Paths from Call to Input
ink-examples/OriginalBackdoored
upgradeable-contracts/set-code-hash1 ✅2 ❌
upgradeable-contracts/delegator1 ✅2 ❌
upgradeable-contracts/delegator/delegatee1 ✅2 ❌
incrementer_copy1 ✅2 ❌
payment-channel1 ✅2 ❌
trait-erc201 ✅2 ❌
rand-extension1 ✅2 ❌
multi-contract-caller1 ✅2 ❌
contract-terminate1 ✅2 ❌
basic-contract-caller1 ✅2 ❌
basic-contract-caller/other_contract1 ✅2 ❌
erc201 ✅2 ❌
flipper1 ✅2 ❌
multisig1 ✅2 ❌
custom-allocator1 ✅2 ❌
vesting1 ✅2 ❌
contract-transfer1 ✅2 ❌
psp22-extension1 ✅2 ❌
e2e-call-runtime1 ✅2 ❌
trait-flipper1 ✅2 ❌
incrementer1 ✅2 ❌
erc7211 ✅2 ❌
dns1 ✅2 ❌
erc11551 ✅2 ❌

The future

We highlight the importance of being able to understand and analyze the low level artifact sent to the blockchain, here wasm. The plan is to extend this tool to extract more human readable features and to make it detect common pitfalls and bugs. We want to build a toolset that enable deployers to check the to the contract or runtime one last time before committing trust to it forever.