跳转至

2024

AppImage 创建“快捷方式”

AppImage是很 Linux 流行的应用打包方式。在运行的时候,他会将自身解压到/tmp/.mount_xxx文件夹下运行。

但是下载AppImage之后,难道每次使用都要打开下载目录然后运行吗?能不能在桌面,或者菜单,或者命令行直接打开呢?

想要在桌面/菜单打开,需要在~/Desktop/~/.local/share/applications创建一个Desktop文件。可以从/tmp/.mount_xxx复制Desktop文件,然后修改其中的可执行文件路径,以及Icon路径。必要的话将Icon文件复制出来。

想要在命令行打开,可以创建一个软链接到/usr/bin或者其他PATH中的路径。

ln -s xxx.AppImage /usr/bin/xxx

钉钉在部分桌面环境无法打开链接

我的桌面环境是Linux Mint。钉钉版本7.6.25-Release.4112601,无法通过点击打开链接(之前版本也是)。

钉钉默认安装在/opt/apps/com.alibabainc.dingtalk/files/<version>/com.alibaba.dingtalk,但是一般创建的快捷方式是运行上级目录的Elevator.sh,他里面链接了一个保护的动态链接库libcef.so(我并不知道具体是干啥的),导致无法通过浏览器打开链接。

可以修改Elevator.sh的这一行,将true改成false。或者通过修改其他地方让它不要被加载就可以了。

...
is_enable_cef109=true
if [ "${is_enable_cef109}" = "true" ]; then
    if [ "$os_machine" = "aarch64" ]; then
        if [ "${libc_lower_29}" = "true" ]; then
            preload_libs="${preload_libs} ./libm-2.31.so "
        fi
    fi
    preload_libs="${preload_libs} ./plugins/dtwebview/libcef.so "
else
if [ "$os_machine" = "mips64" ]; then
    echo mips64el branch
    preload_libs="${preload_libs} ./plugins/dtwebview/libcef.so "
fi
fi
...

ThirdWeb Attack Analysis

Summary

On December 5, 2023, Web3 development platform thirdweb reported security vulnerabilities in their prebuilt smart contracts. This flaw impacted all ERC20, ERC721, and ERC1155 tokens deployed using these contracts. In the following days, tokens deployed with the vulnerable contracts were progressively exploited in attacks.

Reason for selection

Thirdparty libraries play a critical role in software security, yet their importance is frequently underestimated. Modern software development relies heavily on reusing code from open source libraries. In smart contract programming, thirdparty libraries like OpenZeppelin are popular to incorporate security best practices and save time. But dependencies can still pose unforeseen dangers. The thirdweb hack is a typical example.

Vulnerability analysis

Simplified version

The root cause is that the forwarders (ERC-2771) were not designed to work with multicall.

Detailed version

ERC-2771

ERC-2771: This EIP defines a contract-level protocol for Recipient contracts to accept meta-transactions through trusted Forwarder contracts. No protocol changes are made. Recipient contracts are sent the effective msg.sender (referred to as _msgSender()) and msg.data (referred to as _msgData()) by appending additional calldata.

In practice, OpenZeppelin's ERC2771Context is widely used. The last 20 bytes of calldata from trusted forwarder is treated as _msgSender(). For common library users, it seems that they only need to replace all uses of msg.sender with _msgSender().

function _msgSender() internal view virtual override returns (address) {
    uint256 calldataLength = msg.data.length;
    uint256 contextSuffixLength = _contextSuffixLength();
    if (isTrustedForwarder(msg.sender) && calldataLength >= contextSuffixLength) {
        return address(bytes20(msg.data[calldataLength - contextSuffixLength:]));
    } else {
        return super._msgSender();
    }
}

function _msgData() internal view virtual override returns (bytes calldata) {
    uint256 calldataLength = msg.data.length;
    uint256 contextSuffixLength = _contextSuffixLength();
    if (isTrustedForwarder(msg.sender) && calldataLength >= contextSuffixLength) {
        return msg.data[:calldataLength - contextSuffixLength];
    } else {
        return super._msgData();
    }
}

Multicall

User can utilize multicall to integrate multiple calls into one single call. The calldatas of multiple calls extracted from the calldata of single call.  In practice, OpenZeppelin's MulticallUpgradeable is widely used. (The bug is fixed now)

function multicall(bytes[] calldata data) external virtual returns (bytes[] memory results) {
    results = new bytes[](data.length);
    for (uint256 i = 0; i < data.length; i++) {
        results[i] = _functionDelegateCall(address(this), data[i]);
    }
    return results;
}

ERC-2771+Multicall

The issue arises due to inconsistencies in how calldata is packed and unpacked between these two components (ERC-2771 and Multicall):

  1. As per ERC2771, the trusted forwarder should pack the message data and sender information together. The contract then uses _msgData() and _msgSender() to unpack the message data and sender information, respectively.

  2. However, the multicall function is not compatible with how ERC2771 packs data. In the correct implementation, multicall should use _msgSender() to unpack the sender information and append it to each message's calldata, so it can be unpacked properly in subsequent calls. But this step is missed.

  3. Meanwhile,  the contract following ERC2771 will try to unpack message data and sender information using _msgData() and _msgSender(). However, without the sender information appended to calldata, the sender information is unpacked as the last 20 bytes of _msgData(), which can be controlled by the attacker.

This allows an attacker to construct manipulated calldata that executes malicious logic with an arbitrary sender information, violating the expectations set by the specifications.

Attack analysis

Simplified version

Many tokens that use both of these libraries are vulnerable to similar attack methods. The attacker utilize the bug to impersonates the swap pool to burn its token, causing the price of token up. Then do a swap and draining the entire liquidity.

Detailed version

Let's take one instance of an attack transaction as an example.

https://phalcon.blocksec.com/explorer/tx/eth/0xecdd111a60debfadc6533de30fb7f55dc5ceed01dfadd30e4a7ebdb416d2f6b6

  1. Swap 5 $WETH for 3,455,399,346 $TIME.
  2. Invoke trusted forwarder with carefully crafted data, after being parsed by multicall, the burn function is called with Uniswap Pool as the _msgSender(). The $TIME balance of pool is decreased.

  1. Sync the pool, the price of $TIME is lifted.
  2. Swap 3,455,399,346 $TIME for 94 $WETH.

The step 2 is the key of attack. Attacker invoke Forwarder.execute to forward data:

However, it is parsed as bytes[] of length 1, then used as data to call the contract. 

Highlight & Lesson

In the DeFi space, it is crucial to also pay attention to the security of third-party libraries. Unfortunately, these libraries can sometimes interact with each other in unexpected and covert ways, posing a threat to the security of the contract. This highlights the importance of thorough auditing and monitoring to mitigate any potential vulnerabilities that may arise from such interactions.

零拷贝

减少内核和用户之间的拷贝

出于安全的考虑,内核不应该直接使用用户指针指向的内存,用户更不可能直接使用内核指针指向的内存。他们之间的信息交换,基本都要涉及拷贝,比如 Linux 上的 copy_to_usercopy_from_user

mmap 也是一个非常好用的东西。什么是 mmap?mmap 是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以使用操作内存的方式操作文件,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用 read,write 等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

所以,当你有若干个进程以只读的方式访问一个文件,或者是访问较大文件的大块,用 mmap 都是很好的选择。mmap 还可以用来做进程通信。不过并不是所有的文件都可以用 mmap 来访问,例如 socket 就只能用他专有的接口读写,还有 pipe 类型的文件就只能用 read/write 等。也不是所有能用 mmap 的文件都适合用 mmap,一个和 page 差不多大的文件,或者你就想从文件中读那么几个字节,用 mmap 适得其反。

使用 mmap

#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
    long checksum = 0;
    struct stat sb;

    int fd = open("disk.dd", O_RDONLY);
    fstat(fd, &sb);
    void *file = mmap(0, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);

    for (long i = 0; i < (sb.st_size >> 3); i++) {
        checksum ^= *((long *)file + i);
    }
    printf("checksum = 0x%lx\n", checksum);
    close(fd);
}

// checksum = 0xdcce4fdc7fd2151d
// real    0m5.762s
// user    0m2.910s
// sys     0m2.358s

mmap 为什么会比带 buffer 的 read/write 快呢,Page Size 是 4K,每次缺页加载 4K,和我之前手动 Buffer 不是很相似吗?因为 少了一次 CPU 的拷贝。试想 read 的过程,DMA 将数据从硬盘拷贝到 kernel space,CPU 再将数据从 kernel space 拷贝到 user space;而 mmap 就少了一次从 kernel space 拷贝到 user space 的过程。

不过在使用 mmap 的时候需要小心,如果有其他的进程截断了文件,那么你可能会访问到一个非法的地址,从而进程被杀死。

除了 mmapsendfile/splice 也可以实现类似的减少从 kernel space 到 user space 拷贝的作用,我们在后面介绍。他们提供的这种特性叫做 零拷贝 Zero Copy知乎上的这篇文章 提供了一个很好的介绍。

如果追求极致,你可以绕过内核做 I/O 操作,不过一般情况下这样做会牺牲相当的灵活性、可移植性,而且非常困难。

针对网络的零拷贝技术

下面我们要面对的场景,是我们从硬盘中读取数据到内存,进行处理,然后写到网卡中。这是一个很典型的网络应用的操作。

那么,最原始的使用 read/write 进行操作的话,我们将至少会有 4 次拷贝。中间两次是内存中的拷贝,虽然需要内核态到用户态的切换,但是相对 I/O 来说还是快的。但是我们还是要优化中间 2 次。

  • 硬盘拷贝到内存(内核态),DMA,慢
  • 内核态拷贝到用户态,CPU,快
  • 用户态拷贝到内核态,CPU,快
  • 内存(内核态)拷贝到网卡,DMA,慢

如果我们使用之前介绍的 mmap,我们可以优化到 3 次。

使用 sendfile,可以优化到 2 次,硬盘 ->内核态内存 ->网卡。但是你也发现了,没有经过用户态我们怎么处理数据,答案就是 没法处理数据,所以他可能只适合用于静态服务器。那么既然都无法处理了,还拷到内存干啥?能不能直接从硬盘 DMA 到网卡呢?答案是可以,新的 Linux 已经让 sendfile 可以做到这一点了,也就是可以优化到 1 次了。splice 也是类似的。

Copy On Write

其实这是一种思想,对于读多写少的场景的优化。在追求极致优化的内核中得到了广泛使用。你看到 rcu 字样的时候,多半就是 copy on write。