# 2020 湖湘杯初赛

努力挣扎到及格线边缘,最后半个小时过完,排名直接掉到101。不过misc AK,逆向接近AK(除了一道s390架构的奇葩题),还是挺开心的。

又菜,又找不到人py,只能靠做做水题开开心

# web

# 题目名字不重要反正题挺简单的

弱智程度的非预期,phpinfo Ctrl-F搜flag。

正常的做法大概是传一个巨大的文件上去让php来不及删除临时文件,打一个条件竞争。不过没打通……

原始flag:DASCTF{7f914b73611df31bcf679156167224b1}
提交flag:7f914b73611df31bcf679156167224b1

# reverse

# easyre

主函数里肉眼看上去没有判断逻辑,还看到了奇怪的push 233333,程序运行却能正常判断,ba断点也能断下来(在IDA不认函数的地方),觉得应该是什么地方把栈帧搞坏了。分析证实sub_40D920strlen()之外还顺手改了主函数的返回地址:

.text:0040D920 r_main          = dword ptr  50h
.text:0040D96D		mov     byte ptr [ebp+r_main], 0DAh ; STACK DESTROYED: LOWORD(r_main) = 0x48DA
.text:0040D971		mov     byte ptr [ebp+r_main+1], 48h ; 'H'
1
2
3

从而也就找到了用于检查输入的函数,sub_4048DA,之后的流程就简单了。因为栈帧坏掉了,所有的局部变量都用的是ebp寻址,但是拜F5所赐逻辑还原非常轻松。算法是循环移位+下标异或,写脚本跑出原始输入,由常量0x8f异或得到的输出字符串得知,md5一遍输入得到flag。

#!/usr/bin/env python3

target = bytearray([
    0x2B, 0x08, 0xA9, 0xC8, 0x97, 0x2F, 0xFF, 0x8C, 0x92, 0xF0, 
    0xA3, 0x89, 0xF7, 0x26, 0x07, 0xA4, 0xDA, 0xEA, 0xB3, 0x91, 
    0xEF, 0xDC, 0x95, 0xAB
])

n = target[23] & 0b00000111
for i in range(22, -1, -1):
    target[i] ^= i
    target[i + 1] = ((target[i + 1] >> 3) | (target[i] << 5)) & 0xff
target[0] = (target[0] >> 3) | (n << 5)

# bytearray(b'ea5yre_1s_50_ea5y_t0_y0u')
print(target)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

原始flag:ea5yre_1s_50_ea5y_t0_y0u
提交flag:18295eb198c57aa68728814fbc740a71

# ReMe

pyinstaller的程序,pyinstxtractor+uncompyle6一套操作直接得到源码。算法是对输入的每个字符做运算和比较,没有状态变化,跑彩虹表出flag。

#!/usr/bin/env python3

import hashlib

check = [
 'e5438e78ec1de10a2693f9cffb930d23',
 # ... 此处省略一堆MD5 ...
 '874992ac91866ce1430687aa9f7121fc']

def func(num):
    result = []
    while num != 1:
        num = num * 3 + 1 if num % 2 else num // 2
        result.append(num)

    return result

def gen_hash(ch):
    print(ch)
    ret_list = func(ch) # ord(ch)
    s = ''
    for idx in range(len(ret_list)):
        s += str(ret_list[idx])
        s += str(ret_list[(len(ret_list) - idx - 1)])

    md5 = hashlib.md5()
    md5.update(s.encode('utf-8'))
    return md5.hexdigest()

rainbow = [gen_hash(x) for x in range(0x20, 0xff)]

inp = bytes([rainbow.index(x) + 0x20 for x in check])
print(inp) # flag{My_M@th_3X+1_R3v_Te5t}

md5 = hashlib.md5()
md5.update(inp)
print(md5.hexdigest()) # 0584cfa2ce502951ef5606f6b99fc921
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

原始flag:flag{My_M@th_3X+1_R3v_Te5t}
提交flag:0584cfa2ce502951ef5606f6b99fc921

# easy_c++

F5就出来了,下标异或。

出这道题的是当我们科班的没学过C艹吗

target = b"7d21e<e3<:3;9;ji t r#w\"$*{*+*$|,"
flag = bytearray()

for i, b in enumerate(target):
    flag.append(b ^ i)

print flag # bytearray(b'7e02a9c4439056df0e2a7b432b0069b3')
1
2
3
4
5
6
7

原始flag:7e02a9c4439056df0e2a7b432b0069b3
提交flag:7e02a9c4439056df0e2a7b432b0069b3

# misc

# 颜文字之谜

流量包里一堆HTTP流量,把请求全部导出来,可以发现是个HTML5 UP的站点,绝大多数东西都没用。POI是index-demo.html,翻这个文件可以找到一段被隐藏的base64,逐行解码得到一屏幕的颜文字:

(。・∀・)ノ゙嗨Hi~ 
(@_@;)(@_@;)(@_@;)
(+_+)?(。>︿<)_θ(。>︿<)_θ
o(* ̄▽ ̄*)ブ゜
<(^-^)>(╯▽╰ )好香~~
ヽ(✿゚▽゚)ノ(@^0^)
[ ... 此处省略一屏幕的小人 ... ]
ಠ_ಠ(╯‵□′)╯炸弹!•••*~●(¬_¬ )
(╯‵□′)╯炸弹!•••
(╯‵□′)╯炸弹!•••
(╯‵□′)╯炸弹!•••
(╯‵□′)╯炸弹!•••(╯‵□′)╯炸弹!•••(╯‵□′)╯炸弹!•••(╯‵□′)╯炸弹!•••
flag被我炸没了哈哈哈
1
2
3
4
5
6
7
8
9
10
11
12
13

出题人有这功夫卖萌不去把题目和平台修一修吗

楞一看像aaencode,但实操不对,仔细看的话字符集也对不上(aaencode是个日本人发明的,使用的颜文字不会包含汉字)。回去看网页的源码,仔细看,可以注意到两个细节:

  • 选择复制base64的时候,可以发现某些行的最后有空白字符,而且空格和Tab都有。Ctrl-A全选,整个网页源码的前半截有很多行的最后都有空白字符。
  • 颜文字是逐行被base64的。

前者是SNOW加密的典型特征。网上找工具试图解密,发现数据能提出来,不过是一坨乱码。SNOW加密是有可选密码的,应该是密码不对。

后者想到了base64隐写(多谢大师傅暑期集训的时候提到了这个技术)。找脚本跑一遍,还真跑出来一个字符串key:"lorrie"。把lorrie当成密码给SNOW解密,得到最后一层加密的flag:

snwdos32> snow -p lorrie .\index-demo.html
flag{→_→←_←←_←←_←←_← →_→→_→←_←←_←←_← →_→←_←←_←←_← ←_←←_←←_←→_→→_→ ←_←←_←←_←→_→→_→ ←_← ←_←←_←←_←→_→→_→ →_→→_→→_→→_→←_← →_→←_←←_←←_← ←_←←_←←_←←_←←_← ←_←→_→→_→→_→→_→ →_→→_→→_→→_→→_→ ←_←←_←←_←←_←←_← ←_←←_←→_→←_← →_→←_←←_←←_← ←_←←_←←_←←_←→_→ ←_←→_→ ←_←←_←→_→→_→→_→ →_→→_→→_→→_→←_← ←_←←_←←_←←_←←_← ←_←←_←←_←→_→→_→ ←_←→_→ →_→→_→→_→→_→→_→ →_→←_←→_→←_← ←_← →_→→_→←_←←_←←_← →_→→_→→_→→_→←_← →_→←_←→_→←_← ←_←←_←←_←→_→→_→ ←_←←_←←_←→_→→_→ →_→→_→←_←←_←←_← →_→→_→→_→←_←←_←}
1
2

表情只有两种,每隔几个表情出现空格,有莫尔斯电码的味儿了,需要确定点划。按空格拆开,发现单独出现的表情只有←_←,那么它就是点了(莫尔斯电码表中对应单个字符的码点只有. => e一条),另一个是划。扔给在线解码器,得到flag。注意要转成小写。

原始flag:67b33e39b5105fb4a2953a0ce79c3378
提交flag:67b33e39b5105fb4a2953a0ce79c3378

# passwd

无脑windows内存取证,题目要密码,就vol hashdump把密码hash搞出来,扔给在线服务爆破。

原始flag:qwer1234
提交flag:db25f2fc14cd2d2b1e7af307241f548fb03c312a

# 虚实之间

明文攻击大杂烩

题给zip能解出一个明文txt文件,文件内容没啥有用的。binwalk压缩包能发现更多压缩包,内容为:

  • mingwen.txt,ZipCrypto加密压缩,和之前解出来的明文txt的CRC一致
  • flag.txt,AES256加密压缩

那么显然是要打明文攻击了,AES256又不能直接爆破。把压缩包倒进ARCHPR做明文攻击,让ARCHPR认这个压缩包还吃了几回瘪:

  • “压缩包损坏”:binwalk提出来的zip包某些数据好像不对劲,7zip也在抱怨。用WinRAR的文件修复功能解决;
  • “找不到用于明文攻击的文件”:这个没办法,得猜出题人用的是什么压缩软件。7zip不行,换了WinRAR就好了;
  • “压缩包不适合使用明文攻击”:AES256没办法明文攻击,需要把flag.txt删掉。虽然这么干的结果好像是在用明文攻击攻击一个不包含未知数据的压缩包,但是考虑到flag.txt的密码和mingwen.txt的密码有极大概率相同,可以利用明文攻击得到的ZipCrypto主密钥加快爆破密码的进程。

结果符合预期,主密钥出来的相对较快但是没有直接用处,ARCHPR自动开始爆破压缩包密码了。到了饭点去吃饭,一个多小时之后密码就出来了,为123%asd!O。解出来flag.txt,得到最后一层:

仅需5,跳过去
ffd5e341le25b2dcab15cbb}gc3bc5b{789b51
1
2

这个“跳过去”以及这串东西的状态让我想到了去年暑期集训grating那道题,是个栅栏密码。可以手动分析分组长度(flag{开头,定位字符可以找到周期),也可以找个在线的栅栏密码工具解。

原始flag:flag{febc7d2138555b9ebccb32b554dbb11c}
提交flag:febc7d2138555b9ebccb32b554dbb11c

# 隐藏的秘密

又一道Windows内存取证。看进程列表有个记事本,提取文字内容如下:

什么?计算机又被不知名账户登录了?明明在计算机管理中没有这个用户,为什么还会被这个用户登录呢?电脑跟前的你能帮我找到原因吗?flag为该用户的用户名以及密码的md5值。

格式:md5(用户名:密码)
1
2
3

(volatility不支持中文,会打出来一堆问号。用-D把原始数据dump出来,然后把UTF-16 LE转成其他编码。)

“计算机管理中没有这个用户”,典型情况是用户名以$结尾,这个账户会被认作系统保留而不显示在一般的用户管理工具中(如计算机管理、控制面板、net user等)。用vol hashdump导出所有用户和密码hash,得到了好多用户的信息,都以$结尾,都是隐藏用户:

Administrator:500:f0d412bd764ffe81aad3b435b51404ee:209c6174da490caeb422f3fa5a7ae634:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
SUPPORT_388945a0:1001:aad3b435b51404eeaad3b435b51404ee:8d9221b8e70124641a83291d3d21f7e0:::
9w3a6J0$:1003:e761601f5cf981c136077a718ccdf409:ec9dc7d0895ad3dae1feba8ffdeacffd:::
4hiU9ZK$:1004:de5eea9d3fd12c34aad3b435b51404ee:2f2d544c53b3031f24d63402ea7fb4f9:::
A4W7iKb$:1005:61339c1be342167eaad3b435b51404ee:b6e6f6a85f90219d619aca4706f354fc:::
oeTQczq$:1006:b4d2cf4a862f6fcaaad3b435b51404ee:3fbc1f9dc4416f6fb3666de834185cb4:::
CAlrXyU$:1007:8ea6fb8594a1b952aad3b435b51404ee:51d603c77a884df049f7ed4dabed4fd4:::
[ ... 此处省略好几个屏幕的账号密码 ...]
c7Hz9Pp$:1347:0f9f8dbb40720d3baad3b435b51404ee:700d50c788cb1d8222f602d6b49c5056:::
mhuJAts$:1348:7e3313e6fcdba43af1d170cc66c6502d:3675fb98099e3a48eb3a7bb109c3c1cd:::
wOdFUca$:1349:0f9d3dae7810e1c325ad3b83fa6627c7:61d503863ff8115432c5f43a3a6d4433:::
6jOc4eN$:1350:f52de6fb2fb87796f517f264cbe9141b:bfcce05afc41a41c6cefb38f275fdc9b:::
pNrAPQk$:1351:b1ddc1f6035fa511dd4218f5e59dd23a:f408b7150e6234e877c8cf816548ff73:::
1
2
3
4
5
6
7
8
9
10
11
12
13
14

(这台机器是被日了多少回)

怎么确定哪个用户登录了……当时在这里卡了半天。

看了眼桌面,记事本之外还有个注册表编辑器的窗口。想起来去翻SAM:

$ vol hivelist
Virtual    Physical   Name
---------- ---------- ----
[ ... ]
0xe1757860 0x12d8e860 \Device\HarddiskVolume1\WINDOWS\system32\config\SAM
[ ... ]
$ vol hivedump -o 0xe1757860
Last Written         Key
[ ... 来自Windows注册表的问候 ... ]
2019-12-20 14:02:09 UTC+0000 \SAM\SAM\Domains\Account\Users\Names\f6peHPT$
2019-12-20 14:02:09 UTC+0000 \SAM\SAM\Domains\Account\Users\Names\F78CjKn$
2019-12-20 14:02:09 UTC+0000 \SAM\SAM\Domains\Account\Users\Names\Fat864v$
2019-12-20 14:02:09 UTC+0000 \SAM\SAM\Domains\Account\Users\Names\fbWO4jI$
2019-12-20 14:02:09 UTC+0000 \SAM\SAM\Domains\Account\Users\Names\FdZr2pW$
2019-12-20 14:07:00 UTC+0000 \SAM\SAM\Domains\Account\Users\Names\FHREhpe$
2019-12-20 14:02:09 UTC+0000 \SAM\SAM\Domains\Account\Users\Names\fKhcxqB$
2019-12-20 14:02:09 UTC+0000 \SAM\SAM\Domains\Account\Users\Names\flicduJ$
2019-12-20 14:02:09 UTC+0000 \SAM\SAM\Domains\Account\Users\Names\fM8xa10$
2019-12-20 14:02:09 UTC+0000 \SAM\SAM\Domains\Account\Users\Names\FtNHiY2$
2019-12-20 14:02:09 UTC+0000 \SAM\SAM\Domains\Account\Users\Names\FZ6rJyb$
[ ... Windows内存是个装满信息的宝库 ... ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在问候我的十几屏信息中,还真的发现了点东西:这堆用户的SAM信息中,有且只有用户FHREhpe$的信息修改时间和其他用户的不一样,而且更新,但不清楚是不是真的因为登录。把它的hash拿去解,得到密码,算个MD5出flag。

原始flag:FHREhpe$:NIAIWOMA(我不爱你,滚)
提交flag:8cf1d5b00c27cb8284bce9ccecb09fb7

# crypto

# LFSXOR

读读代码就能发现LFSR只是幌子,在LFSR之外还有额外的随机变换,预测序列基本不可能。视线转向异或的过程,两个密钥的长度固定而且循环作用到明文上:

m = ('至少512字节的随机数据') + flag
k1 = ('15字节的随机数据')
k2 = ('31字节的随机数据')
e1 = m ^ k1 # 已知
e2 = m ^ k2 # 已知
1
2
3
4
5

e1 ^ e2可以拿到k1 ^ k2,记作k0。POI是,两个密钥都循环使用,但是长度刚好差一倍零一个字节(31 == 15 * 2 + 1k1每循环两次就比k2“落后”一个字节。如果在k0中标记所有包含k1[0]的字节,可以得到:

  • k0[0] = k1[0] ^ k2[0]
  • k0[15] = k1[0] ^ k2[15]
  • k0[30] = k1[0] ^ k2[30]
  • k0[45] = k1[0] ^ k2[14]
  • k0[60] = k1[0] ^ k2[29]
  • ……

通式为k0[15 * i] = k1[0] ^ k2[(15 * i) % 31]k1[0]刚好可以循环到k2的每个字节。因为k0足够长(题给e1e2都是810字节,上式的循环周期为15 * 31 == 465字节),从k0中是可以完全还原k1[0] ^ k2的。k1[0]因为是一个字节,解空间只有8位,完全可以爆破(assert提供了判定条件),从而得到k2k2 ^ e2得到m

最终脚本如下:

#!/usr/bin/env python3

e1 = # ...
e2 = # ...

def xor(a, b):
    return str(chr(a ^ b)).encode('latin1')

# 题给函数,计算循环异或content ^ key
def encode(content, key):
    tmp = b''
    for i in range(len(content)):
        tmp += xor(content[i], key[i % len(key)])
    return tmp

# k0 = e1 ^ e2
k0 = encode(e1, e2)

# k1[0] ^ k2
k1u0_xor_k2 = [0] * 31

idx = 0
for i in range(31):
    k1u0_xor_k2[idx % 31] = k0[idx]
    idx = idx + 15

print(bytes(k1u0_xor_k2))

# 爆破k1[0]
for k1u0 in range(256):
    k2 = encode(k1u0_xor_k2, [k1u0])
    m = encode(e2, k2)
    if b'DASCTF' in m:
        print(m[512 : ]) # DASCTF{7cc33bd1c63b029fa27a6a78f1253024}
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

原始flag:DASCTF{7cc33bd1c63b029fa27a6a78f1253024}
提交flag:7cc33bd1c63b029fa27a6a78f1253024

# 古典美++

这道题不是我(wp作者)交的。下面是我自己试着做的过程。

维吉尼亚密码破解。网上搜一搜,理论一大堆,但是犯懒,不想自己写破译脚本。捡到了个可以直接用的破译工具:https://www.guballa.de/vigenere-solver (opens new window),得到密钥ORDERBY,MD5一下出flag。

原始flag:ORDERBY
提交flag:c82bbc1ac4ab644c0aa81980ed2eb25b