Obfuscating EOSIO smart contracts

in #eos4 years ago

Obfuscating EOSIO smart contracts

<p dir="auto">There are classes of smart contracts where one would rather not reveal how they work internally.<br /> It's a common theme with private smart contracts that are not intended for other users, for example, trading bot smart contracts because arbitrage trading is a very competitive zero-sum game. <blockquote> <p dir="auto">In software development, <a href="https://en.wikipedia.org/wiki/Obfuscation_(software)" target="_blank" rel="nofollow noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">obfuscation is the deliberate act of creating source or machine code that is difficult for humans to understand. <a href="https://en.wikipedia.org/wiki/Obfuscation_(software)" target="_blank" rel="nofollow noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">Wikipedia <p dir="auto">Usually, this refers to changing the source code to an unreadable mess that is hard to reverse engineer, like writing JavaScript using only <a href="http://www.jsfuck.com/" target="_blank" rel="nofollow noreferrer noopener" title="This link will take you away from hive.blog" class="external_link"><code>[]()+! characters.<br /> EOSIO smart contracts are compiled to WebAssembly and stored in this format on-chain. Meaning, the obfuscation needs to happen on the WebAssembly level as well.<br /> Remember that this only makes it <em>harder for an attacker to reverse engineer the contract but not impossible - given enough time the attacker will still succeed (<a href="https://en.wikipedia.org/wiki/Security_through_obscurity" target="_blank" rel="nofollow noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">"Security" by Obscurity). <blockquote> <p dir="auto">We are not talking about the cryptographic area of <em>Indistinguishability obfuscation here which can transform a program into a new one that is <strong>proven to not reveal anything besides its input-output behaviour. This would actually allow you to include private keys in the program and to implement signature creation in a smart contract but as of 2020, it's impracticable (even outside of smart contracts). <p dir="auto">In this first part we'll look at the simplest form of obfuscation: Changing the contract's ABI.<br /> This is very simple to implement and already has a big effect as it shows hex values on block explorers forcing someone to dive into the WASM code to decipher action arguments and table structures.<br /> Let's look at some approaches. <h3>1. ABI with obfuscated names <p dir="auto">This is the most common approach I've seen on-chain.<br /> Instead of using meaningful names for the action parameters one simply names them <code>a, <code>b, <code>c or similar.<br /> This is also the worst obfuscation because it retains all <em>type information of action parameters and table structures. <p dir="auto">I believe the reason why it is still so popular is that it allows easy interaction with the smart contract due to the correct ABI being on-chain and tools like <code>cleos, <code>eosjs or any wallets still work with this approach as they can serialize the actions. <h3>2. No ABI <p dir="auto">A different approach is to upload no ABI at all.<br /> This results in block explorers not being able to deserialize the data, instead, showing the plain hex data - losing all type information and how many arguments an action has.<br /> Let's test it with this example contract: <pre><code>CONTRACT obfuscate : public contract { public: using contract::contract; obfuscate(eosio::name receiver, eosio::name code, eosio::datastream<const char *> ds) : contract(receiver, code, ds), _storages(receiver, receiver.value) {} TABLE storage { name account; uint64_t value; std::string message; uint64_t primary_key() const { return account.value; } }; typedef eosio::multi_index<"storages"_n, storage> storage_t; storage_t _storages; ACTION test(const name &account, const uint64_t &value, const string &message) { auto storage = _storages.find(account.value); if (storage == _storages.end()) { _storages.emplace(get_self(), [&](auto &x) { x.account = account; x.value = value; x.message = message; }); } else { _storages.modify(storage, get_self(), [&](auto &x) { x.value += value; x.message = message; }); } } }; <p dir="auto">We can now set this contract code using the <code>eosio::setcode action but not upload the ABI.<br /> Invoking the <code>test action will look like this on eosq: <p dir="auto"><img src="https://images.hive.blog/768x0/https://cmichel.io/obfuscating-eosio-smart-contracts/no-abi.png" alt="eosq no ABI" srcset="https://images.hive.blog/768x0/https://cmichel.io/obfuscating-eosio-smart-contracts/no-abi.png 1x, https://images.hive.blog/1536x0/https://cmichel.io/obfuscating-eosio-smart-contracts/no-abi.png 2x" /><br /> <em>Invoking an action on a contract with no ABI <p dir="auto">The action data and table row simply show the plain hex data which is a lot better than the previous approach. But how do we actually call the contract action without the ABI? We need to serialize the action data ourselves.<br /> The easiest way to interact with such a contract is by using <code>eosjs and defining a custom <code>AbiProvider. <p dir="auto">The AbiProvider is responsible for fetching the ABI for any contract which is then used to serialize the action data upon sending a transaction with <code>api.transact.<br /> We can write our own AbiProvider that reads our contract's ABI from the file system and tries to fetch the ABI from the chain for any other contract. <pre><code>import { JsonRpc, Api, Serialize } from "eosjs"; import { AbiProvider, BinaryAbi } from "eosjs/dist/eosjs-api-interfaces"; import { TextEncoder, TextDecoder } from "util"; // converts JS object ABI to serialized ABI const jsonToRawAbi = (json) => { // ... }; const privateAbis: { [key: string]: Buffer } = {}; // load obfuscator ABI from file privateAbis[`obfuscator11`] = jsonToRawAbi(require(`./obfuscate.abi.json`)); export default class PrivateAbiProvider implements AbiProvider { rpc: JsonRpc; constructor(rpc) { this.rpc = rpc; } async getRawAbi(account): Promise<BinaryAbi> { // if we're interacting with the obfuscator contract use local ABI if (privateAbis[account]) return { accountName: account, abi: privateAbis[account], }; return (await this.rpc.getRawAbi(account)).abi as any; } } <p dir="auto">Using this custom AbiProvider in eosjs' API object is simple: <pre><code>const signatureProvider = new JsSignatureProvider(keys) const customAbiProvider = new PrivateAbiProvider(rpc) const api = new Api({ rpc: rpc, signatureProvider, abiProvider: customAbiProvider, textDecoder: new TextDecoder(), textEncoder: new TextEncoder(), }) // send test action const actions = [ { account: `obfuscator11`, name: `test`, data: { account: `obfuscator11`, value: 23, message: `hello this is just a string`, }, authorization: [{ actor: `obfuscator11`, permission: `active` }], }, ] await api.transact( { actions, }, { broadcast: true, sign: true, blocksBehind: 3, expireSeconds: 300, } ) <h3>3. Fake ABI <p dir="auto">We can go one step further with confusing anyone looking at the action in block explorers. Instead of uploading no ABI, we create a fake ABI where the action arguments and table structure types don't match the original ones.<br /> We can write the ABI file ourselves or let <code>eosio-cpp do it for us: <pre><code>CONTRACT obfuscate : public contract { // ... // no TABLE anymore struct storage { // ... same as before }; typedef eosio::multi_index<"storages"_n, storage> storage_t; storage_t _storages; // no ACTION anymore void test(const name &account, const uint64_t &value, const string &message) { // ... same as before } // this will just be used to create the fake ABI struct [[eosio::table("storages")]] fake_storage { bool stored; uint64_t primary_key() const { return 0; } }; [[eosio::action("test")]] void fake_test(const uint8_t &id) {} }; extern "C" { void apply(uint64_t receiver, uint64_t code, uint64_t action) { if (code == receiver) { switch (action) { EOSIO_DISPATCH_HELPER(obfuscate, (test)) } } eosio_exit(0); } } <p dir="auto">We remove the <code>ACTION macro from the original action (and therefore have to implement a custom <code>apply function) and create a fake ACTION with the same action name. Same for the table.<br /> This leads to the ABI generator picking up the fake types for the correct action/table names which the block explorers will use to deserialize the data. <p dir="auto">In our eosjs' AbiProvider we still use the correct ABI. The resulting action looks like this on eosq: <p dir="auto"><img src="https://images.hive.blog/768x0/https://cmichel.io/obfuscating-eosio-smart-contracts/fake-abi.png" alt="eosq with fake ABI" srcset="https://images.hive.blog/768x0/https://cmichel.io/obfuscating-eosio-smart-contracts/fake-abi.png 1x, https://images.hive.blog/1536x0/https://cmichel.io/obfuscating-eosio-smart-contracts/fake-abi.png 2x" /><br /> <em>Invoking an action on a contract with a fake ABI <p dir="auto">It looks like we send a <code>test action with a single <code>uint8_t id parameter and as if we stored a single boolean <code>stored.<br /> The remaining bytes that we used are just not deserialized and not shown at all.<br /> It's very easy to trick someone this way unless they check the action traces and look at the actual hex data. <p dir="auto">This already works well for protecting your contract from someone trying to make sense by looking at the actions or tables in a block explorer.<br /> In the next post, we'll look at obfuscating WebAssembly code. <p dir="auto"><a href="https://learneos.dev#modal" target="_blank" rel="nofollow noreferrer noopener" title="This link will take you away from hive.blog" class="external_link"><img src="https://images.hive.blog/768x0/https://cmichel.io/images/learneos_subscribe.png" alt="Learn EOS Development Signup" srcset="https://images.hive.blog/768x0/https://cmichel.io/images/learneos_subscribe.png 1x, https://images.hive.blog/1536x0/https://cmichel.io/images/learneos_subscribe.png 2x" /> <hr /> <p dir="auto">Originally published at <a href="https://cmichel.io/obfuscating-eosio-smart-contracts/" target="_blank" rel="nofollow noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">https://cmichel.io/obfuscating-eosio-smart-contracts/