USTC Hackergame 2021 writeup

云水遥
$ echo -n "NAME=${name}" | sha256sum
a81ac8ddf4d0d80ee6d3f4799277f5eda02b37167c6be4b6fd7021f2d39badde

忘了把标题删除了。

https://github.com/USTC-Hackergame/hackergame2021-writeups

Sign

date +%s 复制到 query param

Hex

看着图片把 hex-string 打出来,解码即可

Radio

用 Audacity 0.4 倍速播放,根据 NATO 字母表依次写出字母(和{})

Gua

考察 PHP 整数溢出

先 b9 = 9223372036854775807 (INT64_MAX)

结果 -9223372036854775808

如果加上大数使得结果变号,就会变成浮点数导致失败,所以先不要让他变号,输入 1024819115206086200 得到 -8,输入 1 得到 1

再输入一次 INT64_MAX 得到 -9223372036854775807

输入 1024819115206086200 得到 -7,此时差值 27 已经可以贝 9 整除,因此输入 3,得到 27

Transparent

考察 ANSI 转义字符序列(就是能在终端输出彩色字的,还能移动光标、擦除、清屏等)

sed 把 [ 换成 \x1b[ 居然显示不出来?还把我净屏了

简单看点 https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797

发现这个文件里面有以下几种元素:

tr ' ' '\n' < transparent.txt | sed 's/[[0-9;]*m//g' | grep -oP '\[[0-9
;]*H$' | grep -v '\[0;0' | xargs -I _ printf "\x1b[H\x1b%sW" _

即可播放动画

Travel

随风旅鸟与旅行准备.jpg

这游戏在网络上有点小火,玩法是根据一张照片挖掘信息

搜索关键词海边的肯德基,得到一个外墙颜色与照片完全一致的照片,确认该肯德基

Chopper

X-Forwarded-For 伪造 IP

复制 curl,每发一次 sleep 两秒

强制罚站 10 分钟这是坏的

Amnesia

1

编译出的 ELF 文件 .data 和 .rodata 会被清零,这个好弄,只要全部是代码就会储存在 .text,从而不会被清零影响。

main(){
putchar(72);
putchar(101);
putchar(108);
putchar(108);
putchar(111);
putchar(44);
putchar(32);
putchar(119);
putchar(111);
putchar(114);
putchar(108);
putchar(100);
putchar(33);
}

2

编译出的程序代码段 .text 会被清零,而 _start 会被编译器放置在代码段。

经过挂 GDB 可发现并不是因为 00 非法指令导致程序崩溃,而是不停的循环跳转最终爆栈,因此只要在代码走过的地方中间插入一段 hello world 并退出即可。

main(){}

__attribute__((section(".text1")))
void test() {
        __asm__(".byte 00\n" /* this zero is necesary */
                "movl $13, %edx\n"
                "movl $qqqxx, %ecx\n"
                "movl $1, %ebx\n"
                "movl $4, %eax\n"
                "int $0x80\n"
                "xorl %ebx, %ebx\n"
                "movl $1, %eax\n"
                "int $0x80\n"
                "qqqxx:\n"
                ".ascii \"Hello, world!\""
        );
}

GraphQL

先看下接口:

https://blog.yeswehack.com/yeswerhackers/how-exploit-graphql-endpoint-bug-bounty/

axios.post("/graphql", {
            query: "{ user(id:1) { privateEmail } }"
        })

RSA

p: $x, y$ 接近且 x prime,用 Wilson’s $(p-1)! \equiv p-1 \pmod{p}$ 倒着算

q: 解出 10 个质数的 multi-RSA

Oracle

1

只需知道对应的 contract creation code 即可计算地址。

pragma solidity =0.8.9;

import './challenge1.sol';

contract Pwn is Predictor {
    function predict(address challenge) external view returns (address) {
        bytes32 seed = Challenge(challenge).seed();

        bytes32 codehash = 0xfa9e82ddd8dbc9204f2d4547201e19c63d0c253a92ff97cc56ab55aff8eeb51e;
        // by keccak'ing compiled contract creation code of challenge1:Dummy
        bytes32 hash = keccak256(abi.encodePacked(
            bytes1(0xff), challenge, seed, codehash
        ));
        
        return address(uint160(uint(hash)));
    }
}

2

使用一个不会被 revert 倒回的变量传递地址信息即可

pragma solidity =0.8.9;

import './challenge2.sol';

contract Helper {
    function leak(address chall) public {
        address created = Challenge(chall).create_child();
        bytes memory encoded = abi.encodePacked(created);
        revert(string(encoded));
    }
}

contract Pwn is Predictor {
    function predict(address chall) public returns (address) {
        Helper h = new Helper();
        try h.leak(chall) {
            // nothing
        } catch Error(string memory encoded) {
            bytes memory mem = bytes(encoded);
            address created = address(uint160(bytes20(mem)));
            return created;
        }
    }
}

注意输入长度限制 4096,把没用的 revert 全部删除就能过了

LUKS

导出 master key

sudo cryptsetup luksDump --dump-master-key /dev/loop1p1

xxd -p -r 保存 master.bin

sudo cryptsetup --master-key-file ./master.bin open /dev/loop1p1 day1

Cook

1、2 写法过于显然故略

3

sha256d 挖矿(草)

CoP

1

这个表达式很工整,手动解析然后把 / % 替换成 UDiv 和 URem,特例不用实现也能过,z3 去解,跑一会就能出 90 个了

#!/usr/bin/env python3

from pwn import remote
import re
import z3

token = "<token>"

syms = dict()
def genexpr(s):
	global syms

	if len(s) == 1:
		if s == '0':
			return 0
		if s in syms:
			return syms[s]
		else:
			x = z3.BitVec(s, 36)
			syms[s] = x
			return x

	assert s.startswith('(') and s.endswith(')')
	if s[1] == '-':
		return - genexpr(s[2:-1])

	i = 1
	if s[i] == '(':
		indent = 1
		while indent > 0:
			i += 1
			if s[i] == '(': indent += 1
			elif s[i] == ')': indent -= 1
	i += 1

	op = s[i]
	if op == '+':
		return genexpr(s[1:i]) + genexpr(s[i+1:-1])
	elif op == '-':
		return genexpr(s[1:i]) - genexpr(s[i+1:-1])
	elif op == '*':
		return genexpr(s[1:i]) * genexpr(s[i+1:-1])
	elif op == '/':
		return z3.UDiv(genexpr(s[1:i]), genexpr(s[i+1:-1]))
	elif op == '%':
		return z3.URem(genexpr(s[1:i]), genexpr(s[i+1:-1]))
	else:
		raise RuntimeError

def solve(left, right):
	global syms
	syms = dict()
	e = genexpr(left)
	s = z3.Solver()
	s.add(e == right)
	if s.check() == z3.sat:
		model = s.model()
		txt = ""
		for name, var in syms.items():
			value = str(model[var].as_long())
			txt += f"{name}={value} "
		return txt.strip()

if __name__ == "__main__":
	io = remote("202.38.93.111", 10700)
	io.sendlineafter("token:", token)
	io.recvline()

	skip = 0
	for n in range(100):
		expr = io.recvline().decode().rstrip()
		val = io.recvline().decode().rstrip()
		print(f"{n+1}# {expr} == {val}")
		try:
			left = expr
			right = int(val)
			solved = solve(left, right)
			assert solved != None
			print(f"[+] {solved}")
		except:
			#solved = input(f"{skip}# ")
			solved = " "
			if solved.strip() == "":
				skip += 1
				print(f"give up #{skip}")
				if skip > 10:
					exit(1)
		io.sendline(solved)
	io.interactive()

2

首先往里面放数字常数:

选取适当的模数 k (7, 11, 13, 17, 19, 23) 可以使得 x+y 或者 x-y 互不同余,然后套 if 表达式进行插值,注意限制 4096

#!/usr/bin/env python3

from pwn import remote

token = "<token>"

x = 1
# Numeric Constant
def nc(i, r=2):
	if i == 0:
		return '(x-x)'
	elif i == 1:
		return '(x/x)'
	elif i == 2:
		return '((x/x)+(x/x))'
	else:
		ii = i // r
		txt = '(' + nc(r) + '*' + nc(ii) + ')'
		if i % r:
			txt = '(' + txt + '+' + nc(i%r) + ')'
		return txt

# INTERPolation
def interp(xya, mod):
	op = None
	mask = 2 ** 36 - 1

	ss = list(map(lambda v3: ((v3[0] + v3[1]) & mask) % mod, xya))
	if len(ss) == len(set(ss)):
		op = '+'
		vs = list(map(lambda v3: (((v3[0] + v3[1]) & mask) % mod, v3[2]), xya))
	ss = list(map(lambda v3: ((v3[0] - v3[1]) & mask) % mod, xya))
	if len(ss) == len(set(ss)):
		op = '-'
		vs = list(map(lambda v3: (((v3[0] - v3[1]) & mask) % mod, v3[2]), xya))
	else:
		return False

	vs.sort(key = lambda v: v[0])
	m = min(map(lambda v: v[1], vs))

	def ifc(rem, i1, i2):
		return f"if(((x{op}y)%{nc(mod)})<={nc(rem)},{i1},{i2})"
	expr = nc(m) + '+' + ifc(vs[0][0], nc(vs[0][1] - m),
			ifc(vs[1][0], nc(vs[1][1] - m),
				ifc(vs[2][0], nc(vs[2][1] - m),
					ifc(vs[3][0], nc(vs[3][1] - m),
						nc(vs[4][1]-m)
					)
				)
			)
		)

	def if_(rem, i1, i2):
		return f"if((x{op}y)%{mod} <= {rem}, {i1}, {i2})"
	hrexpr = str(m) + ' + ' + if_(vs[0][0], str(vs[0][1] - m),
			if_(vs[1][0], str(vs[1][1] - m),
				if_(vs[2][0], str(vs[2][1] - m),
					if_(vs[3][0], str(vs[3][1] - m),
						str(vs[4][1]-m)
					)
				)
			)
		)
	print(hrexpr)

	return expr

if __name__ == "__main__":
	io = remote("202.38.93.111", 10800)
	io.sendline(token)

	for _ in range(10):
		line = ""
		while not line.startswith("Challenge"):
			line = io.recvline().decode().strip()
			print("[+]", line)

		l5 = [io.recvline().decode().strip() for _ in range(5)]
		xya = []
		for l in l5:
			x, y, a = map(lambda s: s[s.index("=")+1:].strip(), l.split(","))
			print(x, y, a)
			xya += [[int(x), int(y), int(a)]]

		sent = False
		for mod in (7, 11, 13, 17, 19, 23, 29):
			expr = interp(xya, mod)
			if expr:
				io.sendline(expr)
				sent = True
				break
		if not sent:
			io.sendline(" ")
	io.interactive()

RAID

确定磁盘的顺序,RAID 0 通过 file * 可知有分区表的那个是第一个,之后通过程序代码和 PDF 中的对象序号大致递增来确定剩下的顺序。块大小和偏移量都是 128 KB,组合成完整的磁盘镜像,发现是 XFS 直接 mount 之

#!/bin/bash

let offset="128 * 1024"
let blocksize="128 * 1024"
let total="16777216"
let max="total - blocksize"

disks=(	\
	"wlOUASom2fI.img" \
	"jCC60mutgoE.img" \
	"1GHGGrmaMM0.img" \
	"5qiSQnlrA4Y.img" \
	"d3Be7V0EVKo.img" \
	"eRL2MQSdOjo.img" \
	"RApjvIxRlu0.img" \
	"ID7sM2RWkyI.img" \
)

let seek="0"
outfile="out.img"
while [ "$seek" -le "$max" ]
do
	let seek="seek + blocksize"
	for img in "${disks[@]}"
	do
		let t="seek+1"
		tail -c +"$t" "$img" | head -c "$blocksize" >> "$outfile"
	done
done

RAID 5 比 RAID 0 有点变化,块大小是 64 KB。file * 出来有两个都说自己是分区表,但其中那个每逢 0x50000 就出乱码的是因为与0异或导致出现了一模一样的内容,另一个是第一个盘。之后是实现 left symmetric layout,拼接数据,发现不能 mount,去掉前 1 MB 就可 mount 了

#!/bin/bash

let blocksize="65536"
let total="33554432"

disks=(\
"3RlmViivyG8.img" \
"IrYp6co7Gos.img" \
"3D8qN9DH91Q.img" \
"QjTgmgmwXAM.img" \
"60kE0MQisyY.img" \
)

let c20=0
let seek="0"
outfile="out.img"
# left symmetric
# http://www.reclaime-pro.com/posters/raid-layouts.pdf
while [ "$seek" -lt "$total" ]
do
	let t="seek+1"
	let i="c20 % 5"
	img="${disks[$i]}"
	#printf "%08x " "$seek"
	#echo "img#$i"
	tail -c +"$t" "$img" | head -c "$blocksize" >> "$outfile"
	if test $c20 -eq 3 -o $c20 -eq 7 -o $c20 -eq 11 -o $c20 -eq 15 -o $c20 -eq 19
	then
		let seek="seek + blocksize"
	fi
	let c20="(c20 + 1) % 20"
done

tail -c +1048577 out.img > real.img
rm -f out.img

Mosaic

打上被盖掉的定位点,每个马赛克最多与 9 个块有关系,然后穷举,有的解不出来也没有关系,因为纠错是 H,扫一下就出 flag 了

#!/usr/bin/env python3

from math import floor
from itertools import product
import numpy as np

pix_size, nblks = 11, 627//11
px, py, mosn, mosize = 103, 137, 20, 23

varmap = dict()
class Var:
        def __init__(self, sym):
                self.id = sym
        
        @property
        def sol(self):
                return self.id in varmap
        
        @property
        def val(self):
                assert self.sol
                return varmap[self.id]
        
        @val.setter
        def val(self, u8):
                varmap[self.id] = u8

        def set(self, u8):
                varmap[self.id] = u8
        
        def __eq__(self, right):
                return self.id == right.id

class Expr:
        def __init__(self):
                self.vars = []
                self.cnts = []
                self.const = 0
        
        def __iadd__(self, right):
                if type(right) == Var:
                        var = right
                        if var in self.vars:
                                self.cnts[self.vars.index(var)] += 1
                        else:
                                self.vars += [var]
                                self.cnts += [1]
                else:
                        self.const += right
                return self
        
        def __repr__(self):
                s = ' + '.join(str(self.cnts[i]) + 'v' + str(self.vars[i].id) for i in range(len(self.vars)))
                s += f' + {self.const}'
                return s

        @property
        def num_free(self):
                return sum(0 if var.sol else 1 for var in self.vars)

        def value(self, vec):
                s = self.const
                assert len(vec) == self.num_free
                n = 0
                for i, var in enumerate(self.vars):
                        cnt = self.cnts[i]
                        if var.sol:
                                s += cnt * var.val
                        else:
                                s += cnt * vec[n]
                                n += 1
                return s

from PIL import Image
im = Image.open("pixelated_qrcode.bmp")
ar = np.asarray(im, dtype='uint8')

def in_mos(xy):
        x, y = xy
        return (px <= x < px + mosn * mosize) and (py <= y < py + mosn * mosize)

def blkid(x, y):
        xx, yy = x // pix_size, y // pix_size
        return nblks * yy + xx

def blknid(i, j):
        return nblks * j + i

def unblkid(i):
        return (i % nblks) * pix_size, (i // nblks) * pix_size

def sample():
        y = py - 1
        for xx in range(px - 1, px + mosn * mosize + 2):
                bid = blkid(xx, y)
                if bid not in varmap:
                        varmap[bid] = ar[xx, y]
        
        y = py + mosn * mosize
        for xx in range(px - 1, px + mosn * mosize + 2):
                bid = blkid(xx, y)
                if bid not in varmap:
                        varmap[bid] = ar[xx, y]
        
        x = px - 1
        for yy in range(py - 1, py + mosn * mosize + 2):
                bid = blkid(x, yy)
                if bid not in varmap:
                        varmap[bid] = ar[x, yy]

        x = px + mosn * mosize
        for yy in range(py - 1, py + mosn * mosize + 2):
                bid = blkid(x, yy)
                if bid not in varmap:
                        varmap[bid] = ar[x, yy]

mos = []
avgs = []
def imports():
        global mos, avgs
        for i in range(mosn):
                for j in range(mosn):
                                x1 = px + i*mosize
                                x2 = px + (i+1)*mosize
                                y1 = py + j*mosize
                                y2 = py + (j+1)*mosize

                                acc = Expr()
                                for u in range(x1, x2):
                                        for v in range(y1, y2):
                                                v = Var( blkid(u, v) )
                                                if v.sol:
                                                        acc += v.val
                                                else:
                                                        acc += v
                                mos += [acc]
                                avgs += [ar[x1, y1]]

def put_eye(ci, cj):
        i = ci - 2
        for j in range(cj-2, cj+3):
                varmap[ blknid(i, j) ] = 0
        i = ci + 2
        for j in range(cj-2, cj+3):
                varmap[ blknid(i, j) ] = 0
        j = cj - 2
        for i in range(ci-2, ci+3):
                varmap[ blknid(i, j) ] = 0
        j = cj + 2
        for i in range(ci-2, ci+3):
                varmap[ blknid(i, j) ] = 0
        for di, dj in product((-1, 0, 1), repeat=2):
                varmap[ blknid(ci + di, cj + dj) ] = 255
        varmap[ blknid(ci, cj) ] = 0

def eyes():
        put_eye(28, 28)
        put_eye(28, 50)
        put_eye(50, 28)
        put_eye(50, 50)

eyes()
sample()
imports()

def times(n):
        return product([0, 255], repeat=n)

def solve_one():
        global mos
        for i, equ in enumerate(mos):
                nv = equ.num_free
                if nv == 0:
                        continue
                avg = avgs[i]
                nsols, solv = 0, None
                for vec in times(nv):
                        s = equ.value(vec)
                        a = floor(s / (mosize ** 2))
                        if a == avg:
                                nsols += 1
                                solv = vec
                if nsols == 1:
                        n = 0
                        for var in equ.vars:
                                if not var.sol:
                                        varmap[var.id] = solv[n]
                                        n += 1
                        return True
        raise RuntimeError

if __name__ == '__main__':
        try:
                while True:
                        solve_one()
        except:
                print('daijoubu')
        
        copyar = np.array(ar)
        for bid, color in varmap.items():
                bx, by = unblkid(bid)
                copyar[bx:bx+pix_size, by:by+pix_size] = color
        fix = Image.fromarray(copyar, mode='L')
        fix.save("fixed.bmp")

MC

加密是修改过的 TEA

F12,Deactivate breakpoints 或者 Save as override 去掉闹事的 debugger 语句

遇到源码不知道的关键词直接打在 Console 里获取

const encs = [
'6fbde674819a59bf',
'a12092565b4ca2a7',
'a11dc670c678681d',
'af4afb6704b82f0c'
]
const key = '1356853149054377';

function TEA_encrypt(plain, k4) {
    let v0 = plain[0], v1 = plain[1];
    const delta = 0x9E3779B9, ss = delta * 32;
    let sum = 0x0;
    while (sum != ss) {
        v0 += (v1 << 0x4 ^ v1 >>> 0x5) + v1 ^ sum + k4[sum & 0x3],
        sum += delta,
        v1 += (v0 << 0x4 ^ v0 >>> 0x5) + v0 ^ sum + k4[sum >>> 0xb & 0x3];
    }
    return [v0, v1];
}

function TEA_decrypt(cipher, k4) {
    let v0 = cipher[0], v1 = cipher[1];
    const delta = 0x9E3779B9, ss = delta * 32;
    let sum = ss;
    while (sum != 0) {
        v1 -= (v0 << 0x4 ^ v0 >>> 0x5) + v0 ^ sum + k4[sum >>> 0xb & 0x3];
        sum -= delta;
        v0 -= (v1 << 0x4 ^ v1 >>> 0x5) + v1 ^ sum + k4[sum & 0x3];
    }
    return [v0, v1];
}

function Str4ToLong(s) {
    let sum = 0x0;
    for (let i = 0x0; i < 4; i++)
        sum |= s.charCodeAt(i) << i * 8;
    return isNaN(sum) ? 0x0 : sum;
}

let k = new Array(4);
for (let i = 0; i < 4; i++) k[i] = Str4ToLong(key.slice(i * 4, (i + 1) * 4));

function Base16ToLong(hex) {
    return parseInt('0x' + hex);
}

function ToText(i) {
    const f = String.fromCharCode;
    return [f(i >>> 24), f((i >>> 16) & 0xff), f((i >>> 8) & 0xff), f(i & 0xff)].reverse().join('')
}

out = ''
for (let enc of encs) {
    let v0 = Base16ToLong(enc.slice(0, 8)),
        v1 = Base16ToLong(enc.slice(8, 16));
    let [c0, c1] = TEA_decrypt([v0, v1], k);
    out += ToText(c0)+ToText(c1);
}
console.log(out);

Fzuu

经过 afl 巨大多挖矿可以发现:

S100 申必咒语,拥有执行 shellcode 的能力。用 x64 execve

Light

1

#!/usr/bin/env python3

from z3 import *
from PIL import Image

im = Image.open("deng.png")

x, y, inc = 64, 64, 115
arr = []
for _1 in range(12):
	row = []
	x = 64
	for _2 in range(12):
		row += [ im.getpixel((x, y))[0] ]
		x += inc
	arr += [row]
	y += inc

vars = []
for y in range(12):
	for x in range(12):
		vars += [ BitVec("v" + str(12*y+x), 8) ]

slots = []
for y in range(12):
	row = []
	for x in range(12):
		row += [ 0 ]
	slots += [row]

s = Solver()
for y in range(12):
	for x in range(12):
		l3 = [(3, y, x)]
		l2 = list((2, *u) for u in filter(lambda u: 0 <= u[0] < 12 and 0 <= u[1] < 12, [(y-1, x), (y+1, x), (y, x-1), (y, x+1)]))
		l1 = list((1, *u) for u in filter(lambda u: 0 <= u[0] < 12 and 0 <= u[1] < 12, [(y-2, x), (y+2, x), (y, x-2), (y, x+2)]))
		l = l3 + l2 + l1
		acc = 0
		for t, u, v in l:
			acc += t * vars[12*u+v]
		s.add(acc == arr[y][x])

if s.check() == sat:
	model = s.model()
	taps = []
	for var in vars:
		taps += [ model[var].as_long() ]
print(taps)

Omake

https://t.me/hackergame2021 (大草)


Newer: PKU GeekGame 1st writeup

Older: HITCTF 2020 crypto writeup

Back to: Listing G2R