Not So-Famous Solidity Attack Vectors, often missed/overlooked while Auditing! by Tejaswa Rastogi
ETHDubai 2023 · 16 Mar 2023 · 34:59
Tejaswa Rastogi examines Solidity attack patterns and coding mistakes that auditors can overlook when prioritizing business-logic vulnerabilities. He discusses their potential impact and protective techniques.
Speakers
Topics
From ETHDubai 2023, hosted by DeFi Dubai.
Transcript
hey everyone good morning so welcome to the talk and it would be a bit interesting right so [Music] how are you why thank you oh my God oh yes so hello everyone so in this talk I will be seeing some cool attack vectors that are not so popular in this world of smart contract security like we all know there's re-entrancy there are D5 bugs but it's already need some love as well come on
and that's our favorite language cool so a brief introduction who am I my name is stages for astogi uh pretty hard to pronounce name so just call me Teach what I do I'm a security auditor and researcher at consensus diligence besides that I work on some open source stuff like Korea sex Cypher shots and Unchained what are these these are just some platforms for the community so that we can grow and learn from each other cyber shots is a CTF platform Unchained again a blockchain security conference that we hosts so a couple of things yeah it doesn't matter like if
you want to follow me you can find me at Twitter Razer tweet where I tweet some stuff okay cool so this is the agenda there are a couple of things that we'll be discussing and I will try my best to keep it a bit spicy and here here you go so the first one we should discuss is something related to call data right so what is called later a brief introduction well if there is a function and you want to interact with it you have to suppose like you're supposed to pass some input data and that input data is scholar scroll data simple as that so below you will find ABI encoder call
later with the first four bytes as a function signature rest are the parameters which are ABI encoded yeah that's how it works cool so these are the parameters and this is how it looks when these get ABI encoded and right so let's talk about some dirty higher order bits and how these actually matter right so suppose you have a function which accepts a u and eight parameter right so what compiler needs or what's the utmost concern is the last one byte right nothing else like if you're passing any
other data with the call data it should not be uh it should not matter right but it actually doesn't work like that so here you can see uh this one the first one is the actual uh uh ABI encoder parameter or typically or logically that's how it should be but if you are adding some more bits that's going to work as well right like where where it can work for instance you're writing some very optimized code you want to work in assembly right your directly storing value using Esters and those in the
slots all right so this one actually works like this and compilers greater than 0.8 so how it works firstly solidity takes a parameter from the call later here below it extracts the one byte the last one by that like that's what we need because we are dealing with just you and date and after the extraction it does a EQ to validate it again that whatever we have extracted is actually the call data we want right so we have passed uh hex one seven as a unit 8 parameter we have uh we did a and with FF the last one byte to extract the
parameter that is one seven and after that the compiler actually did outperform an EQ I equal operation to verify that the actual call data is the same with whatever we have extracted but it doesn't work like that in compilers below 0.8 so in below 0.8 it just extracts the value whatever you have passed and it forwards to the next operations so imagine you're passing let's say a price and you expect that the unit 8 will be limited to 0 to 255 but the dagger can pass extra high order Bits And if you're using assembly and storing
directly you will be getting a higher price because that's how the higher order bits work so there's no restriction in compilers less than 0.8 which has been recently added end of 0.8 and that's how it works so this is the first one let's jump on to call it a malleability right so again many of the times we keep the message of data called Data as a kind of a unique identifier to say okay this identifier had recently got validated and now it should not be passed again so this is a simple scenario right from a CTF so let's say there is a pawn
function asking for a valid bytes 32 token it could be any token so firstly what we are doing we are verifying that it should not be empty it should contain some value and it should be the one that I have like in my protocol the verification token for the message sender so that it should not be passed any other random value then what we are doing we are hashing it down and keeping it as a unique identifier right so that's cool that's how the developer think that it will work always because if you try to pass a different token that will be a different uh call later and the identifier will be different so it won't work according to
the mindset of a developer but what if I pass the same token and add some data or append some data at the end right so this one you can see is exactly the same call later with the same function signature same parameter but these are two different hashes because I have appended some random data which which will not be uh utilized in this function because it will just be taking the first ABI encoder 32 bytes per meter which is a valid token so the second check token is equal to verification token will work because the token is right but it will
make a different hash right so this will this check the next one that they should not already be utilized will be bypass and yeah just because there are two different hashes so call data is a unique thing right so if we are writing something in assembly we should take care about these kind of stuff or we should add some checks so that we are dealing with the parameters we want right so this is a serious challenge right if you want to try out I've created one you can play it and it's available as ever shots uh like if you want to check go into the slides
and you will find it cool so next thing is we can talk about some gas griefing so before that like was gas uh simple introduction gas is like a fuel every transaction needs it to run some op codes some instruction set so every transaction needs some gas simple as that all right uh now in order to understand gas griefing we have to consider EIP 150 as well so what EIP 150 defines is every sub call like what's a sub call sub call is when a contract is trying to interact with another contract making a sub call or making a message call like this one in
green target.com so it's making a sub call to another contract so what EIP 150 defines that every subcontext every sub call context can only consume this much amount of gas and nothing more than that so but what if we do something good with it right we'll see so here below just a simple demonstration of how it looks great now let's talk about gas grieving what if we do something so just that we do not allow the transaction to execute entirely but partially
right so let's say there is a function which is kind of doing giving you some shares by taking some tokens from you like you're buying some shares right like you're giving it 100 tokens and you're buying some shares right so the above operation the sum operations the listed down what these are doing these are giving you some shares and the last call is trying to take tokens from you like as a trade-off like a trade is going on so it allocated you some hundred shares and now it has to take 100 tokens from you right but this target.call gonna not like not gonna happen like not gonna execute entirely and it's gonna fail why because we know
that the sub call can only consume like six to three out of 64 amount of gas typical like that so we are calculating the gas and we are giving it a gas accordingly so that the top level transaction will work but the sub goal will not be getting enough gas to execute right so this will fail silently because we are not checking the status of the call right so that the call where we are actually taking tokens from the sender or from our user is failing silently and the attacker is getting free shares because the top level transaction is getting executed right so this is one way we can do some
gas griefing all right there are other stuff as well so this is again a CDS challenge you might want to try out the name is Maya and it's again available on Cypher shots great moving on to the second gas craving the second pattern that we have so with this one we can actually perform some dos right in all of service how we can do that all right so previously we discussed about sub goals we can use sub calls and we can give a considerable amount of gas so that only the top level transaction works and not the sub call but let's use this sub course to exhaust the gas
deliberately and not allow the top level transaction to execute ever right so yeah what could be a possible scenario for this let's imagine yeah our very famous uh our beloved challenge King of The Ether right so you know how we solve these challenges like there is a king uh Define the king and if you want to Dethrone that King you are supposed to send more ether to whatever the last thing has sent so that he will be dethroned and you will be the new king now in order for the last king to be dethroned it is necessary and important that that transaction the transaction
where the ethers of the last uh okay will be sent to him should be executed or is necessary to be executed so that in order to move to the next operations but just imagine if we are not allowing the previous transaction to execute such that the whole transaction will not work now you must say we can do that by reverting uh the last transactions right but that's the point right we are not checking the status code we're not checking whether the last call executed or not right so if you are trying to revert if you're trying to revert the last king is trying to revert that's not
going to happen right because even though you're reverting I'm not checking the status code or the transaction and it will always execute right it will always work so but still we can do something right how we can do that uh utilizing the return data copy so what it does uh as a name says it returns uh uh the data from the last call uh that just got executed right so here you can see but the problem is whether you need it whether you want it or not return data copy will always be there after a call is executed right so let's say there are two instances first one we
don't want uh we are not requiring the call data second one we wanted we want this parameter with on data but it doesn't matter in both of the instances there will be an instruction written data copy right so it's like whether you need it or not it will always be copied right so now how we can do that let's take the same example right let's take the same function there are some operations going on then there is a sub call and then there are a few more operations and as we discussed the middle operation the middle uh sub context is needed to be executed such that the whole top level transaction should work but what we are
doing in this sub call this one here the attacker is trying to randomly copy a huge chunk of memory right now You Must Be Wondering okay yeah I got the point but the sub call can only consume 63 of 64 of gas so let it consume let it let it consume everything but there will still be 1 by 64 left for the top level transaction to execute the rest of the stuff right but it doesn't work like that because the copying of memory doesn't happen here in this gas it actually happens in the top level transaction right so whatever the attacker is trying to copy it is
actually not utilizing the gas is getting from the 63 by 64 gas it is actually utilizing the gas of the top level transaction right so what we can do we will randomly copy a huge chunk of memory in order to not allow the top level transaction to have anything left to process the rest of the op codes and what will happen it will reward right so even though I've created the name is King clear yeah a modified version of King of The Ether challenge so if you want to try you can try it out cool now let's talk about a new thing that recently got popped up and I'm pretty sure that will be a new attack
Vector just like read-only reentrancy uh you can uh think this one as a different pattern of read only it works uh kind of like the same way right so now what it is all about right so I wondered that a same transaction gives you a different value if you call it twice right now you will say yeah it's fine like if you're calling a function what I will do I will just check whether my function has been called once if yes I will give you one value if you are calling it twice I'll give you another value simple as that yeah I'll just keep a state counter right and I will just check it like if
you're calling it once or twice but what if it's a view function right what if it's a view function and you cannot do or update the state or do nothing right can you still manage it uh to give the victim two different values if called twice yes we can still do it right so this is how we can do it right so what we are doing is let's imagine uh like this is a very random uh example that I've created uh there might be some real world uh examples but yeah just for the understanding here you can see there is a function exact and what it's trying
to do is firstly fetching the Oracle price and just to make sure you're not giving a direct level of not giving a random value we are just having a check that the value should be at least equal to or more than the minimum price right and then again we are just updating uh variable current price with again fetching the Oracle or Price simple as that but what the Oracle is trying to do firstly is checking was the gas left right now if it's less than uh like we know for the first time this call oracle.price for the first time when the function will be called there will be enough gas like more than uh at least 11 800 so what it will return it will
return 500. to verify and validate the check the conditional check that we have that it should be at least 500 so we are verifying that but the next time you're calling the function again but what I will do now as some of the op codes some of the instruction has already been executed so there is less gas left than the 11 800. now the view function will give you zero so even though you have verified the check the conditional check you are still having a manipulated value that I have sent you right so here the Oracle can give you zero so what's the problem the problem is the uncached values so this is a new tag Vector that people should take care of while
auditing code while doing code reviews right so what what could be a solution right doesn't matter if there is a function call right just cache it down and use it for the rest of the operations right do not like fetch again and again because now we know it this can be done right and moving on to what our metamorphic contracts right so this is again a cool new thing right so we all know about the upgradable contract the proxy pattern the typical ones there is a proxy there is an implementation the delegate cause like when you call a function of the implementation the
delegate core will work and perform the logic in the implementation and take the state of proxy a simple button right so if you want to update if you want to update the logic the implementation what you usually do you go ahead upgrade the contract implementation contract and what happens there will be a new address right so if someone wants to check it will be a bit skeptical because there will be a new address containing so like doesn't matter there will be a new address so people will get to know that something has happened in the past right but what if we do it on the same address itself right we change the code altogether
right so now you must be saying okay like how we can do that right that's not a fundamental property or blockchain you're not allowed to overwrite the code yeah kind of but we are using self-destruct so we are making use of it right so okay uh yeah a good explanation is given here uh implementation uh sample implementation so we'll have a look all right here it is and cool so how it works firstly there's a byte code a metamorphic bytecode like what it is
trying to do we'll have a look uh in a moment and the first operation the first thing is trying to do is get your contract which you are trying to modify or morph after some time which you want to upgrade so firstly it will deploy that contract the implementation contract store the address into a mapping here and why we are doing it we will have a look then it will deploy the metamorphic code right so let's talk about was the byte code that we have just seen uh what is trying to do it's pretty simple right uh firstly uh let's run it yes
so here you can see uh is putting some parameters on the stack and that's a problem so we have to scroll up and down but yeah let me do it yes right so it is stored a four byte signature in memory and rest of the parameters are the parameters of the static call right the gas address the parameters of static all that we have and what are these uh four bytes or the second nature is all about well this is the signature of this function uh uh get implementation uh yes this one
so there is a function get implementation and now how it works when you deploy your metamorphic code here it actually calls the Constructor right because whatever we have will be called as a part of Constructor so this one will be executed right so we are storing the function signature that we will be calling out later right in the static call so what we are doing trying we are fetching the implementation that we have recently deployed the which was the first step here right remember we deployed our implementation contract this one so what it gonna do it's gonna fetch the value
from the mapping making a static call right and then simply uh fetch the size and simply do ixt code copy to copy the code of that implementation and override itself right a simple process that happens right so what it's gonna do it's gonna deploy a boilerplate metamorphic code and of course will run as a part of Constructor and these up quotes will take the value of the address from the factory contract fetch the address and take the code and override itself a simple process now if
you want to morph this code if you want to override if you want to upgrade the contract you just need to go back to the factory redeploy a metamorphic contract and redeploy implementation and you know what will happen it will take the same exact address why because we are utilizing create two and we can pre-calculate the address right so it's going to use the same address and the important part is the first one needs to be self-destructed because we cannot redeploy on the same address so you have to self-destruct the first address and then you can perform the same operation again it will calculate the same address deploy a metamorphic code and that metamorphic code will take the address
and override the new data the problem is the state will be lost because we cannot deal with it we are self-destructing so State cannot be maintained resolved right so imagine there are some non-technical people just randomly following a protocol having trust in it and let's take the same example of Oracle the Oracle is giving you a price from a plus b and the non-technical guys woke up next morning and finding now it's giving a price of a plus 2B and what happened they really don't know because it's the same address everything happened on the same address is the same
state now let's imagine there are some libraries we don't even need the state we are just referring to a contract to fetch some functions like as I mentioned a plus b or plus 2B so yeah it can do some damage right it can yeah the developer of the team can manipulate and modify the prices that you're getting so always for the auditors if there is a self-destruct and if it can be controlled by the authorities by the team you should always flag it right because nobody knows uh what could happen uh with this stuff all right so this was a metamorphic
contract this was again something that is very considerable and concerned and we have actually seen this I cannot disclose the project but we have actually seen the stuff uh live in a protocol that we audited and yes we have flagged it because we don't know if the team is trustful or not yeah we we trusted yeah because they are they came up to us for the audit but nobody knows right so this was something we discussed these are the steps that we just discussed cool all right uh yeah another cool thing core
injections right so we always talk about a web free when we talk about with three attack vectors right where three kind of feels alone in this world right because we are we are kind of missing our old school uh tag vectors our SQL injections rces why not invest three like let's use them together and let's have a beauty from both the walls right so what we can do uh let's let's imagine some accesses right so let's imagine uh there is a platform like openc right and there is an ft with the token URI that you can control and you can add any data that
you want and on the front end on the application there is no input centralization or input validation so free to pass in any data that you want and what you're trying to do you are randomly passing a script in the storage and that is being fetched on the front end without any validation so imagine creating a login form right in the platform and saying hey go into this site this is the nft that I created you just need to log in with your credentials and you will get your nft and that credential or that login form has been created just utilizing the vulnerability of the application and not
from the web 3 wall right so this is one example cool this is a practical you can say understanding that you can think about this is how openc fetches the data from URI so and it creates a page here right so you can see uh there is name author there are properties they are trades uh everything is there from the metadata right and that's what we discussed if I create if I pass in some values which are not being validated I can create anything possible and the page will render it so what could be a solution use some inner text and don't use inner
HTML do not render what you are getting do not execute the uh like inner HTML like even nstml is fine if you're keeping some validation now imagine your directly proposing or directly doing some exact JavaScript exec execute so whatever script is there in the storage you're directly running it now what could have been done they're going to have been an RC right so you can have created a shell on the application and can access the system so that can also happen as well and we have seen this kind of stuff in some protocols because they don't care about the front end because they think that my
protocol is like it doesn't make sense for them because it's just a plain front end and they're just fetching data from the storage like some usernames right the protocol is allowing you to create any username you want right so instead of doing a username you're just creating a script supposed to be executed on the front end right so that is one uh and there could be anything that you can do if there's no sanitization cool and let's talk and then some bad patterns uh that we should take care about there are multiple but I have Capital One uh static calls so static calls are a nice little thing
uh they they do don't harm they are cute yeah so but let's imagine we we are writing some assembly code we we love writing optimize code we are not writing in high level right so uh what the future of static call it doesn't allow you to modify as the state you just have to work with the the static state that you have you are not allowed to update it cool so but what if we are using a stale memory in order to execute the rest of the operations or my other functions are depending upon the stale memory that we
just have discussed right so what could happen so I've created one example here I'm storing a value at location zero and this x just to demonstrate like what the what was the value previously making a static call and then again uh fetching the value at 0 to verify what's the new value right so this is uh uh this is this is what happened in general in a normal scenario there's a function random which is returning a value or un 5000 these are the outputs like firstly the value was FFF and the memory location zero
we call the static we did a static call written 5000 this was a new value and said is the status whether the call succeeded or not right so this is all normal right but what if uh the function is rewarding and doing nothing right so doing a static call you're doing all the same thing but these are the results so earlier it had FFF the call failed you can see the status as zero the set value so the memory is still having FFF right so now imagine you are randomly you're believing your function that it
will always give you some value right and you are proceeding with the value return with it right so imagine again the same example of price you're saying hey the Oracle will at least give me some value at some point of time right and I will not work with the random value that I already had previously now in which scenarios it can happen the like I used uh zero location zero is usually kept for hashing uh the first 64 bytes so imagine there was some hash there was some stale memory there was some random data already there in the memory and you believed the static call will give me some value the return value
you and I will proceed with it right I will take the input and uh the next my operation the rest of my operations will work with that value but imagine the function is reverting so you are actually working the value that you had previously and not the value that you recently got right so this is how uh this is a bad pattern that you should always take care about if you're making a static call and what you can do simply check that what's the status code fs0 should not move ahead and keep it right there so I guess with this these are my tag vectors that I think would be worth sharing about and if you have any
question feel free to raise your hand or just shoot it out all right thank you so much and with that thank you for listening my talk and being here uh we'll see you around and yeah later