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!
mm_struct_mmap will be null. Trying to access this will lead to a null dereference in the kernel, simply by reading process mapping files. Once the kernel oops occurs, a few things are left in weird states:
struct file, mm_users and task struct have a recount leak. mm_users has the potential for exploitation. Even with this though, it uses the overflow safe atomic_t type. With some Linux shenanigans that I don't fully understand, this doesn't matter though. After avoiding deadlocks and other mm_users specific problems, this is possible to overflow though.
uint256 denominator = stakedToken.balanceOf(address(this))
* factor / total_supply();
tokensToMint = amount * factor / denominator
balance of the contracts LP tokens is drastically decreased. Why is this bad? The denominator shown above takes into consideration the amount of LP tokens it owns! The smaller the denominator (hence amount of tokens in the contract) the more tokens that would be minted.Copy on Write is functionality in the Linux kernel for only remapping memory once it has been written to after a fork. This is a major optimization, since forked code can reuse memory from other processes. The copy only occurs only a write to the address space occurs.vm_map_copy_overwrite handles large copies with two different routes: unaligned and aligned. With the unaligned route, the extra condition checks whether the mapping is VM_PROT_WRITE. If this is true, it will create a shadow copy of the page only once it is writable.VM_PROT_WRITE should NOT be possible, with this code being later in the chain. The usage of needs_copy and VM_PROT_WRITE should not b possible. However, this can be raced! If we change the page mapping back from VM_PROT_WRITE after the verification in the upper code path but BEFORE the shadow copy call, we can hit this condition.uint256 _fromBalance = _balances[_id][_from]; uint256 _toBalance = _balances[_id][_to]; _balances[_id][_from] = _fromBalance - _amount; _balances[_id][_to] = _toBalance + _amount;
to being the same as from. Walking through the values if they are the same with the starting balance being 10 and the amount being 10:
mint() to provide liquidity and get an LP token in return. Additionally, it can call burn() to return the LP token to get their underlying assets as well as call collectFees to get the fees they are owed._beforeTokenTransfer is called to update the user debts. The hook only updates the cache fees via _cachefees() if the address is not 0 and the address does not equal the address of the pool. This was done since the contract is never expected to own the LP tokens._cacheFees is never triggered! This means that the time we got in (debt) does NOT get added to the calculations. When calling mint, the recipient of the token can be chosen. Because of further bad bookkeeping, the funds can be taken out as well. mint with the recipient being the contract itself. collectFees() with pair address as the account. It will send itself the fees. Now, the token believes that the user sent it money, even though the contract paid itself! This is because of it keeps an internal balance and comparing that to the actual balance of it. This difference convinces the protocol that we indeed set money to it!swap to collect all of the fees.unchecked block that allows for the fees calculation to go rouge. This allows the attacker to steal the entire reserves, instead of the total fees accrued only. I absolutely love this bug! Super crazy attack which starts by sending your money away.hisee driver.addr and size outside of the shared CMA region. We can either 0xAABBCC55 to our address + 0x4 or a value between 0x0 and 0xC to the address X + 0xC.CMD_HISEE_FACTORY_CHECK used in the logging component shown above. They chance the address of g_cma_addr to be 0xC and the size to 0xAABBCC55. This changes the range of allowed addresses from the CMA region to being practically infinite. With the secure monitor address space verification defeated, we can use other functions to perform even worse operations.CMD_HISEE_FACTORY_CHECK a third time will from the SE to a destination address of our choice. This primitive can be used to hijack many of the functions pointers in the data section; I am assuming there are not write protections in the Secure World. They overwrote one of the SMC handlers to be 0x14230238 since there the gadget BLR X2, which can be used to jump to an arbitrary location with the controlled X2 register. They use this to obtain a temporary arbitrary write primitive with ROP.buf_addr >= 0x3c000000 && buf_addr + buf_size - 1 < 0x40000000
hlog_header) is used to keep track of the current position in the shared memory buffer. The structure is in a modifiable location from our attack, allowing us to modify its fields, including a size and address value.memset gadget that sets bytes to zero, the author of the post overwrites a function pointer close to the gadget BLR X2 with 00 in the first byte (which is just where it points!). From there, they use the same exploitation strategy as before. claim to get their funds. During this, the signature is validated to originate from the backends signature. There is also a claim history to ensure this cannot be simply replayed.deadline (date) in the second contract was the same field as the reservedAmount variable. Since the amount was large enough (and seen as the deadline date), this verification would pass.0x01 and B being 0x0101. If we concatenate A + B together, we end up with 0x010101. However, the signed data would be the same if A was 0x0101 and B was 0x01! This violates Horton's Principle mentioned above. In the context of the smart contract, this means we can change the parameter that the bytes were signed for!calldata field to allow a callback with specific parameters. In some cases, the calldata must be mutated at the time of fulfillment. This can be done by a replacementPattern - a bitmask to alter the calldata. This is necessary for OpenSea, the address of the offer taker must be added to the calldata. calldata, the first 4 bytes are the function selector. In the context of OpenSea, we need to call transferFrom(address,address,uint256). Using the modification primitive from above, an attacker could shift bits between the callData and replacementPattern to modify the function selector! The closest selector is getApproved(uint) at only 10 bits of difference.getApproved(0) primitive, an attacker could have taken WETH from these users, despite them never approving or still owning the NFT.0x0. This is rated as a high finding because of the major loss of funds that could occur. This is especially bad since there is no way to change the storage, even as an administrator, to fix this mistake. Should they ALWAYS check that the location being sent to is valid? Hmmm. To me, in the context of this contract, setting beneficiary to the wrong address (not just 0x0), would be bad. I don't understand why only 0x0 is called out.0x0 address is very special. It is the ERC20/ERC721 specification that the burn function is used to destroy and the mint is used to transfer from the zero address. In this case, they are entirely separate. However, it is not uncommon to see this code shared between other functions. So, with the shared code path and a 0x0 address, this could lead to a burned NFT by accident. Yikes!Mint function is used to create tokens. The Burn is a function used to destroy tokens. Both of these are standards with ERC20 tokens. This is the case with cryptoBurgers (BURG). BURG is a token based on the Binance Smartchain.Mintand Burn functions should not be publicly callable. Normally, these are called internally once some operations has been performed, such as sending ETH to the platform in exchange for the token.Burn function is external in the source code. This can be seen here. This allows the number of tokens in the pool to be arbitrary decreased. Why is this bad? This breaks the prices of AMMs and tokens pairs.delegateCall pattern. This allows for the editing of state variables from any location but is a very dangerous case. Earn has a large collection of Actions, such as Swap and SendToken. The real meat of this call is the executeOp and its allowances:
executeOp will run within the context of a user's DSProxy. What can we do with this? Storage is unique per execution (as explained above) but a selfdestruct() would be a permanent brick if we could make that happen.delegateCall target must be within the allowlisted service addresses.InitializableUpgradeabilityProxy is a proxy for the AAVE LendingPool implementation. If the data provided for the initialize call is empty, and the previous implementation is 0, then we have created a strange state confusion problem. The best part: there's a delegateCall at the end of this with data we control! Now, we've taken a limited address delegateCall and turned this into an arbitrary address!verifyAction will reject it since LendingPool was not a registered action by the Oasis Earn. The function hasActionsToVerify validates that there is a list of actions; if so, it will verify. However, passing in a CustomOperation bypasses this verification while making the action still usable. Neat!