ton
ton copied to clipboard
An attack on config SC (DoS-like) + surprising deployed config SC bug
I have discovered a possible dangerous attack, that can be carried out on config contract. To perform the attack you just have to be a validator (no matter how much % you have, even minimum to become validator is sufficient) and know your index (can be calculated from configuration).
The huge problem is that vote_dict
is persistent - therefore if a proposal is made it permanently remains in vote_dict
until the it is accepted by majority. This pretty closely looks like an issue in my C-1 contract (first minus point) that information would keep there forever. I of course understand that it would not directly damage the SC by draining it's balance or overflowing gas credit (because system contracts never pay for storage and their gas price is hardcoded to zero), however it still poses great danger, in a moment I would explain why.
The problem is that this would allow any validator to cast unlimited amount of votes therefore creating unconstrained number of entries in vote_dict
. Moreover, the proposal (cs
) is not validated any way and is stored in the tree - this way it is possible to drop in unconstrained huge amount of data (as much as fits into external message) therefore extremly bloating the tree. Such attack may allow unlimited permanent impact on the network because the data would be forever have to be carried around on vote_dict
of configurator contract and is never purged away.
The impact of such attack would be:
- ability to unrestrictedly bloat the masterchain, to be more precise, the config contract, which means that all that data would forever have to be kept in masterchain (!!!)
- i did not look into SC data restrictions, but if SC data is restricted in size, it is possible to even break the config contract by bloating it to the maximum extent, which would have disastrous consequences to the network
- spamming in tons of data into dictionary would also increase processing costs, while that does not pose imminent danger to the contract itself (because it is system), it may put unneccessary strain on validators, especially considering the fact that with filling the tree more and more it would require more and more operations to reformat tree to incoming hashes
- it is possible to break voting process altogether - I have seen that even accepted messages have some gas limit (although high, 1 with some zeroes) and if tree is bloated to the extent that it is overflowed, voting would not work anymore
The fix of the problem is very simple: installation of new validator set (0x4e565354) should nullify the vote_dict
dictionary, especially because at line 191 there is check that it behaves just like the proposal is brand new if vset id is changed. As of now I only see that it just installs next validator set (36) and it is activated in ticktock (32 <- 34 <- 36) later while preserving tail of cs
(including vote_dict
).
I hope that this can be at least a little considered as a candidate for Contest 1 Part 3 because despite having a disgusting DoS-flavoured taste and having some prerequisites (being a validator with any percentage of weight) the attack has permanent consequences on masterchain (data that would be stored forever, which I consider may have security impact by making masterchain unusable).
As for "exploitation", it proved to be a little difficult because there are no tools ready to cast votes. Alright, if there is none, lets make and use some for this task:
1. Extraction of ~determination~ config 34 and determining my index
I have found an index corresponding to my validator entry (also with help of my validator control utility).
So, my index is 2 and the script reports current sequence number 10, neat.
2. Generating some rubbish BoC, vote cast BoC, signing and merging
The next part is somewhat tricky. It is neccessary to prepare a body ("proposal"), construct an external message, sign it with validator key and prepend signature.
The challenge is that ATM sign
and signf
accepts data byte string (or data file), and cannot correctly handle complex cell tree (BoC file for example). So I had to pull out a ~power stone~ key from ~infinity gauntlet~ keyring manually and use it for signing.
3. And now, bombs away!
Sending the file to the network...
And... ~nobody came~ nothing happened!
4. Diving deep into situation
It seems that right now at the moment voting is broken.
Lets pull current code of config:
And try emulating the external message with the pulled code:
It seems that right now config is running broken version that differs in several ways: it does not check the 0x12 prefix, instead of which it skips over those 8 bits, but most importantly, it does not load 64 bits of total_weight and attempts to use the entire tail (with those bits) as a dictionary. That does not work. However, it does not cancel that there is logic problem with config smart contract code that allows to conduct an attack. If bug is fixed.
So now it is impossible to demonstrate the attack due to broken config contract (voting is not possible at all, and this attack obviously is based on the voting flaw).
Dropping some fift scripts that I used to do all this, for reproductbility after config is fixed: parse-confdata.fif - parses pulled data of config contract, to find the index.
$1 file>B B>boc
<s ref@+ =: cfg_dict 32 u@+ swap =: stored_seqno
256 u@+ swap =: public_key dict@ =: vote_dict
."Current validators set:" cr
34 cfg_dict 32 idict@ drop ref@ <s
." Pref (0x12): 0x" 8 u@+ swap x. cr
." UTime since: " 32 u@+ swap . cr
." UTime until: " 32 u@+ swap . cr
." Total: " 16 u@+ swap . cr
." Main: " 16 u@+ swap . cr
." Total weight: " 64 u@+ swap . cr
." [ index ] 73 8e81278a public_key weight adnl_address" cr
dict@ 0 { dup 2 pick 16 udict@ {
." [ " over . ."] " 8 u@+ swap x. 32 u@+ swap x.
32 B@+ swap Bx. ." " 64 u@+ swap . 32 B@ Bx. cr 1+ 0
} { -1 } cond } until
."Current sequence number: " stored_seqno . cr
vote_dict null? { ."Vote dictionary is empty!" cr } {
."Vote dictionary is not empty!" cr // Nothing to test :(
} cond
vote-build.fif - generates a message, signs with val.key and saves to vote-ext.boc
"TonUtil.fif" include
."Sequence number: " $1 type cr
."Validator index: " $2 type cr
<b
0x566f7465 32 u, // action
$1 parse-int 32 u, // msg_seqno
0xffffffff 32 u, // valid_until
$2 parse-int 16 u, // idx
// limit: upto 702 bits and 3 refs for body (packed proposal)
// limit: upto 399 bits and 4 refs (for message slice)
// result limit: 399 bits, 3 refs
"garbage" $,
<b "Justin Bieber for president of the world!" $, b> ref,
<b "Some another useless crap packed into" $, b> ref,
<b "The infinity Thanos proposal" $, b> ref,
b>
."To sign: " cr dup <s csr.
."Loading private key from val.key..." cr
<b "val.key" file>B B, b>
<s 4 B@+ swap ."Magic header: 0x" Bx. cr
32 B@ dup priv>pub ."Public key: " Bx. cr
over hashu swap ed25519_sign_uint
<b swap B, swap <s s, b>
256 1<<1- 15 / constant AllOnes
<b b{1000100} s, -1 AllOnes 5 * addr, b{00000} s, b{0} s, swap <s s, b>
."Resulting external message:" cr
dup <s csr.
2 boc+>B "vote-ext.boc" B>file
."External message query saved to vote-ext.boc" cr
vote-test.fif - pulls code from conf.cod, data from conf.cur and msg from vote-ext.boc
"TonUtil.fif" include
"conf.cod" file>B B>boc dup =: cCode <s =: sCode
"conf.cur" file>B B>boc dup =: cData <s =: sData
sData ref@ =: cfgDict
{
0 tuple 0x076ef1ea , // magic
0 , 0 , // actions msg_sents
now , // unix_time
1 , 1 , 0 , // block_lt, trans_lt, rand_seed
0 tuple GR$10 , dictnew , , // remaining balance
0 ,
cfgDict , // contract_address, global_config
1 tuple // wrap to another tuple
} : c7
"vote-ext.boc" file>B B>boc <s
7 8 + u@+ nip 256 u@+ nip 5 1 + u@+ nip s>c <s
-1 sCode cData c7 runvmctx
Well, the newer version of the configuration smart contract was not deployed yet over the one with broken voting precisely because the internal testing is not finished yet...
However, you make a valid point, useful also outside the specific context of the configuration smart contract. All smart contracts that store data supplied by a user (even by a paying customer) are subject to this problem. For example, a "notary smart contract" that stores user-provided data for a prearranged time interval for a fixed price would be subject to the same problem.
The "correct" solution as we envision it is to:
- Limit the storage time in advance (e.g., at most three next validator elections - because a proposal created right before the reelections is unlikely to have time to collect 2/3 of votes)
- Compute the storage fees for the user data that needs to be stored. One needs to know the storage prices (from configuration parameter 18), the storage time in seconds or at least an upper bound for it (see 1. above), and the storage size of the data item (total amount of cells and data bits).
- The storage size of a data item that may consist of more than one cell is not so easy to compute. We have recently added new TVM instructions
CDATASIZE
andSDATASIZE
for this purpose.
By the way, 0 tuple
is also called |
, and 1 tuple
is single
.
The correct way to sign a proposal by a validator key would be to compute the cell hash by invoking Fift word hash
, and sign this hash using sign
from validator-engine-console
. Exporting a validator private key for this purpose is not a good idea.
Well, right, combatting this was especially difficult in Automatic DNS smart contract, especially the recursive calculation of data size was pretty heavy and somewhat not accurate (not possible to count same cells referenced multiple times), however about the configuration contract I just noticed that fact when analyzed voting system and algorithm for whatever reason (was interesting to look into after max_factor change).
I emphasise on importance of this attack/vulnerability on the config smart contract because it is system contract and therefore has several magic features (free gas and storage) and is present strictly in masterchain and is tocking, therefore such attack may cause many unexpected network-wide problems, especially counting the fact that ext message limit is 16K and number of messages that can be thrown in is basicly unlimited.
If such vulnerability exists in simple smart contract the worst that would happen is just that the contract breaks down, becomes frozen and then is evicted from the blockchain. And here we get unlimited free masterchain storage (although programmatically RA-inaccessible).
- Good point, although I think that it's job of the one who creates the proposal to correctly coordinate voting in advance - if he is reelected as validator he can repeat proposal after reelections (and anyway votes are nullified if vset changes), and votes should be cast swiftly enough (if arrangements are made off-chain to do the voting, I see no reason to delay the action, the logic here is somewhat similar to multisignature wallet - initiator should in advance fix up the process)
- Good technical point, too bad I did not know about C-18 when making auto DNS contract and considered calculating prices responsibility of the contract owner.
- Very good addition, resolves the problem of very heavy recursive counting function and, at the same time, provides ability to set upper ceiling for calculations, pretty clever and useful.
Here I have an interesting question (not sure did I miss that from specification or what): if a cell is referenced several times from different places in DAG, storage price for that subtree is paid only once or many times or somewhat inbetween (once for storage of subtree and each time for reference)?
Thank you for the information, this is a very old code that someone posted right after c7 vm execution was introduced, changed by me several times here and there. Original stylistics preserved.
So, sign can sign a hash? I though it can only sign byte string (for the elections custom bytestring is used, no hash is passed between). Interesting feature, will definitely look into it. For the proof of concept I thought that it would be much faster and easier to directly grab a key from keychain and sign with it, due to highly temporary nature of key I do not think something really bad might happen.
Of course, absolutely, for normal usage encapsulation should be preserved and signature must be generated by VE directly with help of VEC, this is just a one-time event.
How is the progress of this issue?