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/