Resources

People often ask me "How did you learn how to hack?" The answer: by reading. This page is a collection of the blog posts and other articles that I have accumulated over the years of my journey. Enjoy!

EraLend Crypto Platform Hacked- 1212

Rekt    Reference →Posted 2 Years Ago
  • EraLend, a lending platform on zkSync Era, was hacked. Within the SyncSwap project, there is an LP token. The EraLend protocol was using a price oracle from SyncSwap.
  • Keeping all values in a good state is important. This is particular important when external calls are possible. In this case, the totalSupply is modified by a call to burn() but there is an external call prior to updating the reserves.
  • When calculating the oracle price, it uses the reserves. Since the external call exists, an attacker could leave the contract in a state where the supply and reserve do not match. This results in the oracle inflating the price of the asset. I personally do not understand why being in this state benefits the attacker and I cannot seem to find code from Eraland. So, just going to take their word for it.
  • What's funny, is that this behavior of the LP token is documented in the code and viewed as a feature. This is shown in the tweet. Read only reentrancy as a service!

Zenbleed- 1211

Tavis Ormandy    Reference →Posted 2 Years Ago
  • All x86-64 CPUs have vector instruction registers called XMM registers. Recent CPUs have increased these from 128-bits to 512 bits. 256 bit registers are called YMM and 512 bit registers are called ZMM. Besides number crunching, these are used in many libc calls for string based operations because of their speed and parallelism.
  • The author shows an example within strlen()
    vpxor  xmm0,xmm0,xmm0
    ...
    vpcmpeqb ymm1,ymm0,YMMWORD PTR [rdi]
    vpmovmskb eax,ymm1
    ...
    tzcnt  eax,eax
    vzeroupper
    ret
    
  • The first instruction is setting YMM0 to zero XORing it by itself. The next instruction is using a pointer to our string in $RDI to check which byes match YMM0 and stores the result in YMM1. This is essentially checking if null bytes will match. The vpmovmskb instruction allows us to transfer this to the general purpose register eax. tzcnt finds the amount of trailing zero bits. With 4 instructions, we have successfully found the position of the first null byte of a string!
  • The final instruction is vzeroupper. This is used to zero out the upper bits of the vector registers, which is important for performance reasons. A process has a special location for storing the state of these various registers: Register File and a Register Allocation Table (RAT). The RAT keeps track of what space in the register file is assigned to each register. For instance, when zeroing out an XMM register, the 'z-bit' flag is set in the RAT. So, vzeroupper just sets this flag to release the resources.
  • All of that was background! So, what's the bug? Modern processors perform speculative execution in order to process data faster. It turns out that the vzeroupper does not revert the changes made to the z-bit in the case of branch misprediction. In a way, this creates a use-after-free-like scenario where a RAT mapping has been removed but will still be used after the revert of the state.
  • How do we exploit this? Many of the string operations, such as strlen and strcmp use these instructions. So, we can target a string with these vector registers. To exploit the bug, a few steps must be taken:
    1. Force an XMM Register Merge Optimization to occur. This can be done using the cvtsi2sd instruction.
    2. A register rename, which can be triggered using the vmovdqa instruction.
    3. Mispredicted vzeroupper branch prediction. This is a standard thing to force conditional branches to mispredict for speculative execution bugs.
  • After optimizing the branch prediction, the author was able to steal 30kb per core per second. This can be done within VMs, since it's per core!
  • One thing I was wondering was how the bug was found? Manual review of Microcode? No, fuzzing! In this case, fuzzing is complicated since A) crashes will not occur so we need a different trigger and B) there is no guidance. To get around problem A) the author used an emulator to run the code. Then, they would run the code on the CPU itself to examine the state. If something was different, then it was potentially a bug. To make this more accurate, they added in pipeline instructions like sfence in order to ensure they had full control of what was being executed.
  • To solve the second problem of lack of guidance, the author used performance counters. These special registers store the counters of hardware-related events and there are a lot of them! By using this to guide the fuzzer, it would automatically find interesting paths, which is super neat.
  • Prior to this vulnerability, I had never heard of the bulk of these things and did had not ever considered microcode level bugs. Overall, an awesome write up on something that is out of my zone but the author did a good job making it comprehensible. LiveOverflow recently made a video about this as well.

ConicFinance Multiple Exploits- 1210

Immunefi/Conic    Reference →Posted 2 Years Ago
  • Conic uses the Curve protocol for trying to earn rewards. It has a concept of omnipools where the same underlying asset is distributed among multiple Curve pools. Currently, I'm not seeing the benefit of this from reading their docs.
  • First, Immunefi spotted an exploit that occurred in the function _airdrop(). The code takes in three parameters: from, to and amount.
  • These parameters have calculations done on them to determine the airdrop address. This includes an XOR with the block number, from address and to address. Eventually, this is used for the address of a balance call with the amount provided by amount in the function.
  • It should be noted, there is no check on who the airdrop address is besides the manipulations that occur. The bug is that an attacker can set this to be an arbitrary user. Most importantly, they can set the address to be the address of the pairContract used for a pool. By setting this to 1 and calling sync() the pools price is very messed up, leading to an opportunity to make money.
  • The second vulnerability exists in the integration between Curve and Conic. The reentrancy protection on the Omnipool was only turned in in the case of ETH pools being used. This was checked by calling checking CurveHandlerV3() to see if the pool had interacted with ETH. The check was made by seeing if it was 0xeee...ee, which is a standard for ETH address. However, in reality, this used the WETH address.
  • Since the wrong address was being validated against, the reentrancy protection was broken. There is a known bug in Curve pools, which is read-only reentrancy. By manipulating the Curve pool then calling Omnipool contract for ETH allowed for an attacker to trick the pool into minting more cncETH LP tokens than they should have been rewarded. It's weird that this wasn't found in testing...
  • The protocol had reentrancy protections in place. On top of this, they also had a mechanism to ensure that imbalanced Curve pools could not be interacted with. However, the threshold was not tight enough, which allowed the attacker to slowly drain the pool. I thought this was a great idea that just did not work because of the ability to preform this attack several times.
  • Overall, two really interesting bugs! Manipulation of a pool by setting the amount of a particular contract to 1. Then, a known read-only reentrancy vulnerability exploit in Curve by finding a flaw in the prevention of this exact attack. What's the takeaway? Test your code to ensure it works!

CVE-2023-38408: Remote Code Execution in OpenSSH's forwarded ssh-agent- 1209

Qualys    Reference →Posted 2 Years Ago
  • ssh-agent is a program for hold private keys for authentication through ENV variables. Agent forwarding is the process of forwarding from further remote hosts, removing the need for authentication data to be stored on other machines. For obvious reasons, this is a slight security issue! It gives users the ability to bypass file permissions on a remote host; but, this is still widely used today. In our case, this gives a remote attack surface!
  • While browsing the source code of ssh-agent, the authors noticed an interesting bug: a user who has access to the remotely forwarded host can call dlopen() and dlclose() on /usr/lib* on the main workstation. A local privilege escalation by Jann Horn would load a library from /tmp was discovered in 2016. This resulted in an allowlist being made to filter out types. Initially, they looked for a filter bypass or a directory traversal to this fix but couldn't find one.
  • In 2010, Tavis Ormandy used a dlopen/dlclose primitive similar to this one in a local setting by abusing the side effects of the library loading and unloading process. The original author used local primitives (ENV variables, file writes, etc.) to get code execution. In this case, we only have the remote dlopen/dlclose primitive. Is this even exploitable? To determine this, they reviewed various default libraries that could be loaded.
  • What they found was startling from manual review. These are listed below:
    • Some libraries require an executable stack, which will make the make stack executable. 58 of these were found.
    • Many libraries are undeletable once loaded. 16577 were found.
    • Some libraries create a SIGSEVG signal handler and do not deregister that handler if closed. This turns into a use-after-free-like situation. 9 were found.
    • Some libraries instantly crash if loaded, since they are not loaded in the proper/expected context. 2 were found.
  • As soon as I read those primitives, I saw the potential for exploitation. Make the stack executable, create a signal handler in a mmaped section of memory that gets unmapped, reload code into that area, trigger the signal handler jump to executable stack for code execution. All of this sounds fine and dandy, but finding a proper path for this is tricky. So, the authors fuzzed the process with the various primitives from above. They injected their own signal handler with shellcode on the stack (0xCC opcode) to say if their shellcode as properly hit or not. They added their shellcode by writing to the socket of the connection directly.
  • They found two separate chains for the signal handle use after free to get code execution. While looking at the crashes, they found more primitives though. The next primitive was a callback function UAF. A shared library would register a userland callback function within a core library. Once this was unloaded, a different library could be replaced in that memory space. Finally, a shared library would make a call to the core library function that triggers the callback in our code.
  • Another primitive they found resulted from a strange SIGSEGV. A library is loaded, then a thread starts a timer in kernel-land. This library could be unloaded, but the timer never stops. Once the thread returns from execution, it was hop into unmapped code. Naturally, we can replace this with a different library to jump to the stack for execution.
  • While fuzzing, they kept getting the error message ... overflowed sigaltstack. Sigaltstack allows a thread to define a new alternate signal stack for execution of signal handlers. Similar to the previous bug, the library was implementing a separate signal stack but not unregistering it. Once the signal was hit, improper code was being hit, creating a sigaltstack use-after-free. Although they found some primitives, they couldn't take this to code execution after loads of testing.
  • Crazily enough, various combinations of library loads led to direct access over the instruction pointer. This happens as a consequence of signal handler overwriting then calling RET N. Once this occurs, the signal handler restoration process occurs, overwriting the userland addresses back to normal. In order to exploit this, they needed an ASLR leak to jump back to the proper location. They did not pursue this option further.
  • Another fuzzing crash was puzzling to them. With specific combinations of libraries, crashes occurred without any of the other primitives from above. The library LLVM libunwind.so is loaded is several locations. Some other libraries load libgcc_s.so for handling C++ exceptions. If both of these are loaded and an exception occurs, LLVM's _Unwind_GetCFA will be called within libgcc_s.so instead of the LLVM library! These have different types of structures, leading to a super bizarre type confusion vulnerability.
  • At the end, the authors mention there may be other ways to exploit this; they only looked on Ubuntu Desktop. Overall, another amazing article from Qualys that boggles my mind.

Why are you not an Elite Smart Contract Security Researcher?- 1208

gmhacker    Reference →Posted 2 Years Ago
  • In every field, there are people at the top and bottom. Why is this? What makes somebody elite at a subject? This is what the post is about. With so many aspiring people, there has to be a secret. It's told from the perspective of an elite smart contract hacker.
  • Everyone wants to be a smart contract auditor for the money. The biggest bounty ever given out if $10m and there is so much other money going around. Spearbit DAO has crazy salaries and many people on Code4rena/Sherlock have made 100K+.
  • The reality of the thousands of people flocking to audit smart contracts is grime: it is really hard and competitive. On Code4rena, only 29 people have a lifetime earning of 100K+, 57 of 50K and 170 of 10K. Damn, that's really not that lucrative or helpful. On Immunefi, the numbers are in the millions for several people though.
  • What's interesting, is that this thing doesn't work on a full time job for many people. Even Pashov, the most lucrative private auditor, has only doing 30ish audits total. To hit my salary as an auditor, I would need to make $700 a day on Code4rena and Immunefi, which would put me in the top 0.1% of auditors; this simply is not realistic for me or very many people.
  • What are the top auditors secrets? Success is not the default outcome. There are two keys: perseverance and focus. Everyone claims they want to be the best but very few spend the actual time to do so.
  • For perseverance, do you spend 12 hours a day on smart contract auditing? Do you read every report that is released? Do you reproduce hacks that occur? If not, you're already growing slower than some people in the space. Progress is not always obvious either.
  • For focus, it is more complicated. Being able to sit down for hours upon hours to get good; you've got to put in the time. Here, we also need to consider efficiency. Are you learning the right content? Is your time sitting down only hacking or are you on Twitter? Being efficient is hard to do with your focused time. The call to action is simple to say but hard to execute: wake up on time, setup a real work schedule for this and be disciplined with your time.
  • If you're not here, that's okay! Armada, a famous Super Smash Brother Melee player ruined my Melee career. Why? He told me the amount of effort to reach the top. At this point, I realized I did not want to reach the top but that was okay. I do other things I enjoy! If you want to live a full time, with friends, sports, family and so on, you'll probably never be at the 1%. That's what these articles don't tell you.
  • A few other things, imo, make the space hard to get into:
    • Required Knowledge: Most projects integrate with other projects with integrate with other projects. If you are missing some understanding, it makes a project hard to understand.
    • Competitive: Everyone wants to make the money. The easy to find stuff is likely not going to be there.
    • Bug classes are unique: Finance issues, denial of service, frontrunning and reentrancy are all unique to the space. Getting up to speed with everything is difficult.
    • Moves fast: Every day there's a new hack, new technique, new article... it's easy to get behind in the space and miss something.
  • Overall, a good article! I wrote some of my own opinions in here as well, since the truth isn't always easy to hear.

Solidity Deep Dive: New Opcode 'Prevrandao'- 1207

Markus - Injective    Reference →Posted 2 Years Ago
  • When Ethereum moved from proof of work to proof of stake, it added some new functionality. One of these with the replacement of block.difficulty with block.prevrandao. Although, the opcode is still the same, the values have different meanings now.
  • The blockchain is completely deterministic, forcing randomness to come from things like Chainlink VRF off-chain. prevrandao is meant to be a source of randomness on chain that is created using decentralized information. This is generated with the following steps for each validator:
    1. Sign over the current epoch number.
    2. Compute hash of signature.
    3. Calculate new randomness as hash XOR with the previous randomness.
  • Is this secure? That's a complicated question. The randomness is based upon signature data of the block. So, you cannot directly affect it. However, you can choose not to sign the data in your slot. If this is the case, the randao update is simply skipped. For every validator at the end of a slot, an attacker has control over a single bit of influence by deciding or not deciding on signing the data.
  • To use this in a secure way, we need to pick a prevrandao from the future. According to the EIP-4399 specification, this should be 4 epochs into the future. The reason for this is that we can limit the influence of an attacker by forcing them to guess earlier on.
  • A naive solution is to enforce a guess early then 4 epochs (greater than 128 blocks). in the future to do the validation. However, this is manipulable by censorship attacks by withholding a particular transaction until the prevrandao opcode returns a favorable value. To fix this problem, enforce that the transaction happens on a particular block in the future.
  • Overall, an interesting article on Ethereum randomness. It is still not secure enough for people to use on a lottery, but does it's job for simple on chain things, like ordering of validators.

saleReceiver and feeReceiver can steal refunds after sale has ended- 1206

Code4Rena    Reference →Posted 2 Years Ago
  • In this protocol, it's a standard auction but the lowest price wins. If a user gets outbid, they get a refund but must call a function in order to perform the refund.
  • The sale is considered ended when the last NFT is sold. This triggers the payout to the seller and fee collector. The verification for paying out is done by checking if the current ID matches the final sale id.
  • There is an additional check that checks that that the current id does not equal the final id. Otherwise, we've tried to get an NFT that already been sold.
  • In the code, the function buy(uint256 _amount) amount variable is for the AMOUNT of NFTs you want to buy. If a user buys 0 NFTs, then the currentId is not set properly. The check from before about the auction being over doesn't work since the currentId is not set from the iteration within a loop. It's obviously lower than the final ID, since it's set to 0.
  • This results in the sale being made with a larger value than the current bid. Since the auction has ended, we're able to get a refund from this still. More importantly, the check on whether to transfer the funds to the feeReceiver and saleReceiver can be hit over and over again because of the if statement failing.
  • Zero iteration in the array causes this problem. Overall, a pretty neat bug that is common in the web3 space.

Analysis CVE-2023-29300: Adobe ColdFusion Pre-Auth RCE- 1205

Harsh Jaiswal - Project Discovery    Reference →Posted 2 Years Ago
  • The authors knew about a potential RCE in Adobe ColdFusion. So, they went to the Java code and started diffing from the previous version.
  • While doing this, they found the function validateWddxFilter() had been added. This did verification on the type attribute of the object to ensure it starts with coldfusion.
  • The sink is a call to getClassbySignature() that gets an instance of an arbitrary class. Then, it calls a function that must start with set. Being able to call arbitrary calls with a semi-restricted function is a good primitive to start from!
  • Their test payload was java.util.Date.setDate(). After verifying that this worked in a debugger, they were set to look for more primitives. With the class com.sun.rowset.JdbcRowSetImpl, setDataSourceName() sets a JNDI lookup name. Then, by calling setAutoCommit(), we can create a JNDI injection vulnerability, like with log4shell.
  • To get code execution, the authors used a ysoserial java serialization payload with commons-beanutils to get code execution. Pretty neat bug and unique primitive.

Rodeo Finance Hack- 1204

Immunefi    Reference →Posted 2 Years Ago
  • Rodeo Finance is a leveraged yield farming protocol. Apparently, it also supports loan functionality.
  • The service was uses a price oracle to determine the price of assets when borrowing and lending. In particular, it was using a Time Weighted Average Prices (TWAP) which averages out the price over a given interval. This prevents instantaneous changes in the price from manipulation. In this case, updates were every 45 minutes and looking at the previous 4.
  • TWAP oracle are vulnerable when their price is being updated. By sandwiching price updates, it's possible to get a position briefly for some amount of money, see the update occur, then make money from the update even though very little funds were provided.
  • The attacker manipulated the price by performing a sandwich attack on its source - ETH-unshETH pool - over the 3 updates. By sandwiching the update process, the attacker was able to get all of their money back but was still able to manipulate the price.
  • This was a risky tactic. At any point, the oracle protocol could have been arbitraged to steal funds but was not. Once the TWAP had the inflated price, the attacker opened multiple large leveraged positions.
  • To eventually make money, they borrowed much more funds than they should have been able to. Since the oracle price was manipulated, they were also able to arbitrage it by swapping with the same pool as they manipulated. This led to a massive price difference from the amount that was swapped by the Rodeo Finance Protocol.
  • According the Quill Audits, there is another aspect to this though. The strategy address was unconfigured, which allowed for bypassing the sanity check on the values being used.
  • What was wrong then? The oracle used the reserve ratio to determine the price. Additionally, multiple oracles should be used to prevent attacks like this. Or, have limits on how much the price can change over a given interval.

One more problem with ERC777- 1203

Daniil Ogurtsov - MixBytes    Reference →Posted 2 Years Ago
  • ERC777 is a well known token standard for Non-Fungible Token (NFT). There are hooks defined on these NFTs in order to allow for users to perform operations on either receiving or sending NFTs.
  • How are these hooks implemented though? Instead of checking the sender/receiver of the check, it looks into the ERC1820 registry contract. This is done by taking a hash of the interface then checking if the sender/receiver has added a hook for this. Once this is set, it will execute the hook at the address provided.
  • Only the owner of an address can specify these hooks. This is where the novel technique comes into play... what if you have arbitrary call to an address within the context of the contract? An attacker could set the hook for the contract!
  • This can be used as an unexpected reentrancy hook. This becomes particular troublesome with swap paths being calculated before the transaction assuming that the hook will not modify the state. Additionally, an attacker could force all transactions within the context of the contract to fail.
  • Arbitrary calls within a contract are typically very bad anyway. This registry setting is just another way to make use of it.