2019 *CTF

比赛时间:4月29日-4月30日

XCTF联赛内的*ctf,由上海复旦大学******战队(sixstar)主办。

在De1ta打的这场比赛,最终第七名。re的题目ak,还做了个misc,还是有点小高兴。

总体体验还是不错的,希望能多打打这种高水平的比赛,继续磨练!

下载地址

RE

fanoGO

香农-范诺编码,decode之后要为

1
If you cannot read all your books...fondle them---peer into them, let them fall open where they will, read from the first sentence that arrests the eye, set them back on the shelves with your own hands, arrange them on your own plan so that you at least know where they are. Let them be your friends; let them, at any rate, be your acquaintances.

编码表根据一个txt生成,具体细节不清楚,直接在程序中找字典。

另外程序里存字典的方式很奇特,用一个结构体表示。

fano___Fano__Decode是解码函数。一阵瞎调,最后在000000000045C618下断,rbx指向的就是解码字典的结构体,结构如下

1
2
3
4
5
6
7
00000000 dict struc ; (sizeof=0x30, mappedto_759)
00000000 byte dq ? ; offset point to decoded byte
00000008 unknown0 dq ?
00000010 code dq ? ; offset the encoded code
00000018 lenth dq ? ; char * encoded lenth
00000020 unknown1 dq ?
00000028 unknown2 dq ?

编码结果是从code指向的字符串开始的lenth位,我这里是手动复制出来在用文本处理得到字典的。。。 字典:

1
dic ={' ':'0001', '-':'00100100', ',':'0010001', '.':'00100101', ';':'001001111', 'I':'00101011', 'L':'00101100001', 'a':'0011', 'c':'010001', 'b':'010000', 'e':'0101', 'd':'01001', 'g':'01101', 'f':'01100', 'i':'1000', 'h':'0111', 'k':'1001001', 'm':'10011', 'l':'100101', 'o':'10110', 'n':'1010', 'q':'11000', 'p':'10111', 's':'1101', 'r':'11001', 'u':'111100', 't':'1110', 'w':'1111100', 'v':'111101', 'y':'1111110'}

注意到大于0x7f还有一些编码以及最终结束一般不会刚好凑齐8bit,猜测后面这些都是用来表示编码结束的。 之前的一段文字按字典编码后还剩4bit满一个字节,就找一个12bit的用来表示结束的编码 结束编码:111111100010 直接解码的时候所有不可见字符都被替换成了0xFD,就会出错 比如一开始的几个字符If you,编码后是: 00101011011000001111111010110111100 8bit一组: 00101011 0x2B 01100000 0x60 11111110 0xFE 10110111 0xB7 100......

前两个字节都能够照常解码,到了第三个字符第四个被莫名替换位0xFD了。 动调跟入解码过程,仔细看看过程。

fano_Bytes2Str函数的作用是字节转二进制字符串,在里面发现一个go的库函数runtime_stringiter2很可疑,进去的时候是0xFE,出来就变成0xFD了,跟进几步,发现他先校验当前字符是不是大于0x80,然后再根据后一位和后两位的数据修改,不满足某种规则会直接返回0xfffd,即变成0xfd。到这里就猜出有可能是utf8的编码问题。utf8是一种可变长的编码方式。

wiki上可以看到unicode转utf-8的规则

  • Octal 0–177 (hex 0–7F) is coded with an unchanged single byte.
  • Octal 0200–3777 (hex 80–7FF) shall be coded with two bytes. xxyy will be 3xx 2yy.

注意这里的八进制添加的3和2是按照每8bit结果看的,举个例子

FD将会变为 C3BD

11111101 375

补0

000 1111 1101 03 75

添加3和2,03 75变成303 275,如果直接把这个数转成2进制是不对的,因为303和275是独立的两部分,每个单独地转成8bit二进制数11000011和10111101

11000011 10111101 303 275

结果

C3 BD 1100001110111101 141675

也可以按百度百科的方法,补3个0然后填入110X XXXX 10XX XXXX中。 比赛时我是手动写了一个unicode到utf8的转换。赛后发现可以直接用unichr和encode转。。。

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
from pwn import *
context.log_level = 'error'
p = remote('34.92.37.22','10001')
dic ={' ':'0001', '-':'00100100', ',':'0010001', '.':'00100101', ';':'001001111', 'I':'00101011', 'L':'00101100001', 'a':'0011', 'c':'010001', 'b':'010000', 'e':'0101', 'd':'01001', 'g':'01101', 'f':'01100', 'i':'1000', 'h':'0111', 'k':'1001001', 'm':'10011', 'l':'100101', 'o':'10110', 'n':'1010', 'q':'11000', 'p':'10111', 's':'1101', 'r':'11001', 'u':'111100', 't':'1110', 'w':'1111100', 'v':'111101', 'y':'1111110'}
plain = 'If you cannot read all your books...fondle them---peer into them, let them fall open where they will, read from the first sentence that arrests the eye, set them back on the shelves with your own hands, arrange them on your own plan so that you at least know where they are. Let them be your friends; let them, at any rate, be your acquaintances.'

cipher = ''
for i in range(len(plain)):
cipher+=dic[plain[i]]
s = ''
cipher+='111111100010'
# print(cipher)
'''
# my unicode to utf8
for i in range(0,len(cipher),8):
temp = eval('0b'+cipher[i:i+8])
if temp>=0x80:
res = '000'+bin(temp)[2:]
res = '110'+res[0:5]+'10'+res[5:]
a = res[0:8]
b = res[8:]
s+=chr(eval('0b'+a))
s+=chr(eval('0b'+b))
else:
s+=chr(temp)
'''
for i in range(0,len(cipher),8):
temp = eval('0b'+cipher[i:i+8])
s+=unichr(temp)
s = s.encode('utf8')
# p = process('fanoGo')
p.recvuntil(':\n')
p.send(s)
try:
print(p.recv())
except EOFError:
p.close()

*CTF{NUY4a3E5D9186hVzejoyItr7xHBcmOpv}

yy

Ch4r1l3师傅先做了一部分,看了下文档继续往后做的。

看了下程序的逻辑,大概是 利用yacc进行词法解析,解析到的东西存到buffer 然后buffer再用encrypt这个函数进行加密,最后加密的结果与cmp进行比较 encrypt是用aes加密 测试了下,大概规则为 *CTF{xxxx},中间的xxx范围大概为0-9,a-z 然后大概解了下逻辑,那个switch里面那堆从上到下就是a-z 0-9 encrypt是用标准aes加密,用的密钥是提前生成好了,在bss段key开始的176个字节 很迷的就是,它比较是0xa0个字节.......但是很明显flag没那么长 感觉encrypt这个函数应该会被调用多次,但是实际调试的时候只调了一次? encrypt使用了一个全局变量cnt,用来记录加密了的长度

顺着Ch4r1l3师傅的思路,我看了一下词法解析的过程。

在词法分析器yylex中,找到了一个叫yy_ec的table,大小256,每个元素对应一个字节,除了测试过程中输入有效的字符*CTF{}0-9a-z外,大部分都为1,仔细看了一下发现漏了个下划线不为1。调试了一下发现,输入下划线后会对下一部分重新aes加密。做题过程中发现了每次加密都会异或之前一次的加密结果,其实这就是cbc加密模式。之前一直没了解过,所以是手动写的这个异或过程= =

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
from Crypto.Cipher import AES

def strxor(s1,s2):
s = ''
for i in range(len(s1)):
s+=chr(ord(s1[i])^ord(s2[i]))
return s

key = '2B7E151628AED2A6ABF7158809CF4F3C'.decode('hex')
aes = AES.new(key)
cipher = ['AE4614F82A40CF5031D3FE048C061212'.decode('hex'),
'23FAC726E861D9C3A93C45701AC7F03D'.decode('hex'),
'DFBEBC16AB6E37AC148B9C94F75D6278'.decode('hex'),
'FC16981DB231D35ADC3A60869ACA7BA3'.decode('hex'),
'B5D5F1B2D9FFD209D477D73DC0561902'.decode('hex'),
'B69B426CE8A277E399AC324091A92A86'.decode('hex'),
'F3FA473CC35C419BE80507D0D4305A9E'.decode('hex'),
'8D529BA3FBADB6443F72839C2277FE48'.decode('hex'),
'FE868412004EEDFFAC441923841F12CA'.decode('hex')]
dic = {'\x82':'a', '\x05':'b', '\x86':'c', '\x8A':'d', '\x0B':'e', '\x11':'f', '\x96':'g', '\x1D':'h', '\x27':'i', '\xA9':'j', '\x2B':'k', '\xB1':'l', '\xF3':'m', '\x5E':'n', '\x37':'o', '\x38':'p', '\xC2':'q', '\x47':'r', '\x4E':'s', '\x4F':'t', '\xD6':'u', '\x58':'v', '\xDE':'w', '\xE2':'x', '\xE5':'y', '\xE6':'z', '\x67':'0', '\x6B':'1', '\xEC':'2', '\xED':'3', '\x6F':'4', '\xF2':'5', '\x73':'6', '\xF5':'7', '\x77':'8', '\x7F': '9'}
flag = ''
xor = '00'*16
xor = xor.decode('hex')
for i in range(len(cipher)):
temp = cipher[i]
plain = aes.decrypt(temp)
plainp = strxor(plain,xor)
xor = cipher[i]
for j in plainp:
try:
flag+=dic[j]
except KeyError:
flag+='_'
break
flag = '*CTF{'+flag[:-1]+'}'
print(flag)

得到*CTF{yy_funct10nl_1s_h4rd_andc_n0_n33d_to_r3v3rs3}

然而提交不对,本地输入flag后返回congratulations。分析一下flag的语义,funct10n后面多了个l,and后面多了个c,去掉之后再提交就对了

原因:加密时每次用append:6124258631AB6EAFB114FE76783D1EFF填充明文,然后用输入映射的字节依次填充。append的字节在box里刚好有,比如B1对应字符l,86对应字符c,刚好跟我们求得一样。如果对应位置刚好是那个字符就会造成重复而多解。

最终flag: *CTF{yy_funct10n_1s_h4rd_and_n0_n33d_to_r3v3rs3}

Obfuscating Macros II

ddctf Obfuscating Macros的升级版,强化了算法。由于之前打ddctf的时候稍微分析了一下,比较上手一点。

混淆还是去不掉,只能硬调 首先随便输入个长度16的字符串,加密在sub_401006里。输入的16个字节分成了两个__int64,作为参数传进函数,幅值给v6 v7,推荐的做法是查找v6和v7的引用,在所有引用的地方下断,sub_4045F2sub_4043C0的引用不下断点。同时分析一下发现,v8是计算过程中的中间变量,也查找引用并下断点。

同时,根据打ddctf的经验,形如sub rax, 4的指令比较关键,控制了程序执行的流程。在函数内发现了两处这种可疑的语句,对应的反汇编分别是

1
2
if ( v7 & 1 )
v14 -= 4LL;

1
2
if ( v16 <= 0x3FF )
v15 -= 3LL;

查找一下v16的引用,都下好断点。

这里其实不难猜出这是个循环,有赋初值0,有增量v16++,也有判定v16<=0x3FF。

这两处调试的时候应该重点关照一下。

下好断点,开调,注意时刻观察栈上v6和v7的变化情况。

由于加密是分两部分加密的,所以我们干脆两部分都输入为同一字符,这里输入了11111111aaaaaaaa

v6 = 'aaaaaaaa',v7 = '11111111'

果然首先来到v16 = 0,然后是判定v16<=0x3ff,接下来只要再回到这里就是一个循环了。

然后是v8 = ~v7

接下来来到了if(v7&1),这里为真,接下来边来到v8^=*v5,观看一下v8和v5的内容,发现v8变成了v6,v5指向v7,再下来其实是v6 = v8,然后是v7 = ~v7。接下来的执行流程不难看出是把整个128位的数循环左移了一位。从栈上的结果不难看出,这里的循环左移是按照v6在高位,v7在低位。

之后也没什么分支了,都是一条线直到循环结束。

整个循环内有分支的只有最开始的(v7&1),由于我们输入的最低位是1,这回输入个最低位位0的试试,我们输入00000000aaaaaaaa

与之前不同的,判定完(v7&1)的结果位0后,v8^=*v5这里,v8还是v6,而v5变成了指向~v7,再往后都一样了。

不难写出加密算法:

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
def rol(a,b):
tmp1 = (a&0x8000000000000000)//0x8000000000000000
tmp2 = (b&0x8000000000000000)//0x8000000000000000
a = a << 1
b = b << 1
a |= tmp2
b |= tmp1
a&=0xffffffffffffffff
b&=0xffffffffffffffff
return a,b

a = 0x6161616161616161
b = 0x3030303030303030
for i in range(0x400):
if b&1:
a^=b
else:
a^=(~b)
b = ~b
a &= 0xffffffffffffffff
b &= 0xffffffffffffffff
a,b = rol(a,b)
tmp = a
a = a+b
b = tmp
a,b = rol(a,b)
print(hex(a))
print(hex(b))

解密算法:

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
def ror(a,b):
tmp1 = (a&1)*0x8000000000000000
tmp2 = (b&1)*0x8000000000000000
a = a >> 1
b = b >> 1
a |= tmp2
b |= tmp1
a&=0xffffffffffffffff
b&=0xffffffffffffffff
return a,b

a = 0x50A2DCC51ED6C4A2
b = 0xA1E8895EB916B732
for i in range(0x400):
a, b = ror(a, b)
tmp = b
b = (a - b) & 0xffffffffffffffff
a = tmp
a, b = ror(a, b)
if b&1:
a^=b
else:
a^=(~b)
b = ~b
a&=0xffffffffffffffff
b&=0xffffffffffffffff
flag = ''
for i in range(8):
tmp = b&0xff
b>>=8
flag+=chr(tmp)
for i in range(8):
tmp = a&0xff
a>>=8
flag+=chr(tmp)
print(flag)

*CTF{fUnfl@tCf9}

对于去混淆,我自身angr还是没有研究透彻,我就不班门弄斧了,期待一个大佬讲解

matrix

看了下有很多冗余代码和花指令,然而没写过去除冗余代码的脚本,花指令倒是写了个脚本去除:

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
bg = 0x000027A0
end = 0x00010B80
main_return = 0x00005C10
addr = bg
print('start at %08x'%addr)
def patch_nop(begin,end):
while(end>begin):
PatchByte(begin,0x90)
begin=begin+1
def next_instr(addr):
return addr+ItemSize(addr)
while(addr<end):
next_addr =next_instr(addr)
MakeCode(next_addr)
addr = next_instr(addr)
MakeCode(addr)
if 'jmp $+5' in GetDisasm(addr):
print('ret :%08x'%addr)
patch_nop(addr,addr+5)
PatchByte(addr + 6,0xC3)
patch_nop(addr + 7, addr + 9)
addr = addr + 9
if 'xor' in GetDisasm(addr) and GetOpnd(addr,0) == GetOpnd(addr,1) and 'jnz' in GetDisasm(addr + 2):
patch_nop(addr, addr + 4)
print('xor jmp: %08x'%addr)
patch_nop(addr, addr + 4)
addr = addr + 4

if 'call $+5' in GetDisasm(addr):
print('nop: %08x'%addr)
patch_nop(addr,addr+0xB)
PatchByte(addr + 0xA, 0xE8)
addr = addr + 0xF
print('end')

然而去掉之后仍然不能f5,于是开始对着汇编撸。。。

程序流程

由于过程中有大量冗余代码,经常会干扰我们调试,所以要胆大心细,大胆的跳过一些无用的代码,找到真正有用的代码。

由于是动调的地址,我这里的基地址是0x56555000,可以根据这个偏移换算到0的地址。。。

首先能看到一些这样的跳转代码:

1
2
3
4
.text:56559FA3 cmp     ecx, eax
.text:56559FA5 jge loc_5655A2A0
.text:56559FAB jmp loc_5655A013

不难看出这是个循环。

输入后进入了第一个循环体,设内存断点跟一跟,或者直接断到跳出循环,可以发现它将输入的字符串转了hex。

之后在5655A3A0调用了函数565589C7,看一下栈内,参数就是转完hex的内容。

跟进函数,先看到一个循环,大小为字节的长度,继续跟进不难发现很多如下的代码:

1
2
3
.text:56558BCF cmp     ecx, eax
.text:56558BD1 jnz loc_56558C2A

...

1
2
3
.text:56558C79 cmp     ecx, eax
.text:56558C7B jnz loc_56558CB5

...

这应该是个case或者是if elif elif ...

对比的是我们的输入和另外18个字节,提取出来,他们是:

10 15 11 14 12 13 20 25 21 24 22 23 30 35 31 34 32 33

如果相等的话,每个又会进入一个对应的函数,之后继续循环,如果不在这些字节之内,会直接返回。 先不管这18个函数干了什么,直接步过到返回。 紧跟着在5655A3D0调用了函数56559638,步入看看做了什么。 这里的循环跟之前的有些许不同:

1
2
3
4
5
6
7
.text:565596B0 cmp     eax, ecx
.text:565596B2 jge loc_56559E3D
.text:565596B8 mov eax, [ebp-4]
.text:565596BB test eax, eax
.text:565596BD jz loc_56559E3D
.text:565596C3 jmp loc_56559727

循环大小固定为6,每一轮结束后会test ebp-4的值,可以看到,初始为1,如果为0,会直接返回。可以看到最终的返回值是ebp-4,也就是说一旦ebp-4为0,就会返回0,循环完毕后它本身可能不变,也就是可能返回1。 看到这里有一些猜想了,这个循环可能是个check,每个循环是个单独的check,通过所有的check才会返回1,应该就拿到flag了,若返回0,可能就gg。 回到main函数验证一下。来到地址5655A3D0,稍微往下几行,看到了

1
2
3
.text:5655A409 test    eax, eax
.text:5655A40B jz loc_5655ABB9

...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:5655ABB9 loc_5655ABB9:                           ; CODE XREF: .text:5655A40B↑j
.text:5655ABB9 mov eax, offset aTryAgain ; "Try again!"
.text:5655ABBE push eax
.text:5655ABBF nop
.text:5655ABC0 nop
.text:5655ABC1 nop
.text:5655ABC2 nop
.text:5655ABC3 nop
.text:5655ABC4 nop
.text:5655ABC5 nop
.text:5655ABC6 nop
.text:5655ABC7 nop
.text:5655ABC8 nop
.text:5655ABC9 call near ptr _IO_puts

果然如猜想一般,如果返回0就Try again!了。 如果返回1会怎样,继续往下看,又是一个循环,直接到循环结束的地方,loc_5655AB90,可以看到这里输出了flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:5655AB90 loc_5655AB90:                           ; CODE XREF: .text:5655A55A↑j
.text:5655AB90 lea eax, [ebp-180h]
.text:5655AB96 push eax
.text:5655AB97 mov eax, offset aHereIsYourFlag ; "Here is your flag: %s\n"
.text:5655AB9C push eax
.text:5655AB9D nop
.text:5655AB9E nop
.text:5655AB9F nop
.text:5655ABA0 nop
.text:5655ABA1 nop
.text:5655ABA2 nop
.text:5655ABA3 nop
.text:5655ABA4 nop
.text:5655ABA5 nop
.text:5655ABA6 nop
.text:5655ABA7 call near ptr _IO_printf

我们试一试直接修改check的返回值为1,然后f9。 当然不会输出正确的flag,而是输出了一串乱码。

1
2
3
Input your key:31313131
Here is your flag: o�#k����,��19�\���К?�q

总结一下程序流程:

  1. 输入若干个十六进制数,在10 15 11 14 12 13 20 25 21 24 22 23 30 35 31 34 32 33 之内。
  2. 根据输入,进行了一些“操作”
  3. check结果
  4. 可能对结果又进行了一些计算,得到flag
  5. 打印flag,或是try again

算法

让我们来看看程序到底干了什么 重新调试,这回输入10151114测试 我么把位于565589C7的函数重命名为operate,位于56559638的函数重命名为check operate函数下断,跟进到10对应的分支5655BD47,我们把每个这种操作函数按重命名为operate+输入的字节,如operate10 慢慢步入,主体是一个长度为3的循环。 在5655BE7C发现取了一个data段的地址,跟入发现是一连串数据。稍微分析一下这里的结构。在下面一点发现了一连串的指针,直接打开后为修改的ida数据库是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.data:00013200 off_13200       dd offset dword_13020   ; DATA XREF: .text:00004796↑o
.data:00013200 ; .text:000055DF↑o ...
.data:00013204 dd offset unk_13044
.data:00013208 dd offset unk_13068
.data:0001320C dd offset unk_1308C
.data:00013210 dd offset unk_130B0
.data:00013214 dd offset unk_130D4
.data:00013218 off_13218 dd offset unk_130F8 ; DATA XREF: .text:00005701↑o
.data:00013218 ; .text:00005722↑o ...
.data:0001321C dd offset unk_1311C
.data:00013220 dd offset unk_13140
.data:00013224 dd offset unk_13164
.data:00013228 dd offset unk_13188
.data:0001322C dd offset unk_131AC

这一连串的指针,中间刚好隔了0x24字节,即9个int,一共12个指针。根据题目名叫matrix,猜测是个矩阵,感觉每个元素是int的概率比较大,这里先是猜的,也就是12*9的一个矩阵。注意到这12行是分开来的,也就是说有可能是两个6*9的矩阵。 指针和矩阵中间又有12个int数据。这个矩阵和数据到底是做什么的暂时不知道,继续往下分析。 之前说了,这道题充斥着很多冗余代码,比如刚刚引用这个矩阵的代码:

1
2
3
4
5
.text:5655BE7C mov     ebx, offset matrix
.text:5655BE81 adc ebx, edx
.text:5655BE83 sbb eax, ebx
.text:5655BE85 lea ebx, [eax]

调试一下可以发现,最终ebx被幅值成了ebx的反,这根本没有意义。 随后的一个对矩阵的引用也是如此,再下一个有些许不同了:

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
.text:5655BEE5 mov     ecx, offset matrix
.text:5655BEEA add ecx, eax
.text:5655BEEC mov eax, [ebp-4]
.text:5655BEEF sbb eax, edx
.text:5655BEF1 add eax, eax
.text:5655BEF3 mov eax, 0B4D418C0h
.text:5655BEF8 lea eax, [esi]
.text:5655BEFA mov eax, dword_5656829C
.text:5655BF00 shl eax, 0
.text:5655BF03 add eax, 0BE8634ABh
.text:5655BF09 sub eax, 70A1214Ch
.text:5655BF0F sub eax, 0CC322C9Bh
.text:5655BF15 add ecx, eax
.text:5655BF17 xor edx, eax
.text:5655BF19 mov ebx, 9F145970h
.text:5655BF1E lea ebx, [ebx-51A2725Fh]
.text:5655BF24 lea ebx, [edx+ebp*4-46h]
.text:5655BF28 mov edx, ecx
.text:5655BF2A add edx, edx
.text:5655BF2C mov ebx, 89B03C42h
.text:5655BF31 lea eax, [edi+7A2658Bh]
.text:5655BF37 lea eax, [eax-52554450h]
.text:5655BF3D mov eax, [ecx]
.text:5655BF3F mov [ebp-4], eax

在ida里有相同字符的高亮,可以看出,ecx先后加了两个数据,然后终于取数据了,这里取得是matrix+8,也就是第2个元素,紧跟着存入了[ebp-4] 不难发现,形如mov xxx,[xxx]的代码是可疑代码,有可能是真正有用的部分,分析时要重点关注这样的代码。 比如在5655C12C发现了这样一处代码:

1
2
3
.text:5655C12C mov     eax, [edx]
.text:5655C12E mov [ecx], eax

通过寄存器看到edx是matrix+BC的地址,也就是matrix[47],而ecx指向的是之前的matrix[2],这里有点像元素交换对不对,先把matrix[2]放到一个temp里,然后再把matrix[47]放到matrix[2]里 类似的,在5655C374又有一处代码:

1
2
3
.text:5655C374 mov     eax, [edx]
.text:5655C376 mov [ecx], eax

这里应该是 matrix[47] = matrix[11],所以不是交换元素了,继续看 又在下面发现了 matrix[11] = matrix[38] 最后,在结束循环之前,看到了 matrix[38] = temp,即是之前的matrix[2] 合并一下,本次循环做的事:

1
2
3
4
5
6
temp = matrix[2]
matrix[2] = matrix[47]
matrix[47] = matrix[11]
matrix[11] = matrix[38]
matrix[38] = temp

接下来两次循环干的应该是相似的事情了。 大胆猜测一下,其他17种操作应该也是某种变换。验证一下,一口气输完所有的18种输入,记录一下操作之前的矩阵,以及操作之后的矩阵 对比前后矩阵不难发现,虽然元素都被打乱了,但是元素的种类都没改变,还是之前的6*9个元素。 同时,matrix下面的一个6*9的矩阵没有被改变,应该是个常量,命名为constant好了 分析了两个变换之后,发现不出什么规律,比赛时就没有继续分析下去的动力了。 赛后想到一个可能好一点的分析方式,先将矩阵的内容patch成0-54的编号,每次输入一个不同的输入,分别记录下每次操作后的矩阵,再分析是怎么变化的。 试试换条思路,根据程序的流程,进行完操作之后是check,而操作的过程中矩阵元素的大小没有改变,只改变了元素的位置,我们有没有办法构造出最终过check的矩阵呢?

进入check函数看看如何check的: 循环长度为6,假设刚好对应了矩阵的每一行, 与之前变换的时候访问矩阵元素不太一样,这里是通过后面的6个指向矩阵的每一行的指针来访问元素的 用跟之前相似的方法,在5655984A发现了这样的代码:

1
2
3
4
.text:5655984A mov     edx, [eax]
.text:5655984C mov eax, [ecx]
.text:5655984E add edx, eax

这里加的是matrix[0]和matrix[2]

接下来跟着edx走不难发现一些类似的代码,最终的效果就是

1
2
temp = matrix[0]+matrix[2]+matrix[4]+matrix[6]+matrix[8]

刚好把偶数下标的元素求和了,然后不远处又能发现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:56559A8B cmp     edx, eax
.text:56559A8D mov eax, 0
.text:56559A92 setz al
.text:56559A95 mov ecx, [ebp-4]
.text:56559A98 and ecx, eax
.text:56559A9A mov eax, ecx
.text:56559A9C adc eax, ebx
.text:56559A9E lea eax, [esi]
.text:56559AA0 sbb edx, edx
.text:56559AA2 sub eax, ecx
.text:56559AA4 mov edx, 51160ED1h
.text:56559AA9 lea edx, [esi+2Ah]
.text:56559AAC mov ebx, 66600294h
.text:56559AB1 lea eax, [ebx+0Fh]
.text:56559AB4 mov eax, 0F4B4E967h
.text:56559AB9 mov eax, 13E96DD7h
.text:56559ABE lea edx, [esp+edi*4-0Dh]
.text:56559AC2 mov [ebp-4], ecx

这里的edx是求和的结果,eax跟过去一看,是之前那12个不知道有啥用的数据中的第一个,命名为cmp好了。 比较的结果果然被放在了[eb-4]中 继续往下跟,不难看到第二个比较,不同的,这次求和的内容并不是奇数下标的元素,而是

1
2
temp = matrix[1]+matrix[3]+matrix[4]+matrix[5]+matrix[6]

在两次过程中matrix[4]都被加进去了,最终比较的对象是cmp[1] 猜测一下check过程。cmp是个6*2的矩阵。每次比较matrix的每一行,下标为02468的元素求和要为cmp[i][0],下标为13457的元素求和要为cmp[i][1] 稍微验证一下之后的循环,下标果然如此

穷举求解

既然相加的元素都在这6*9个之中,我们只要找到某5个元素,加和等于cmp就行了

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
from itertools import *
a = [i for i in range(54)]
mat = [0x0FDFE0BA1, 0x9A915052, 0x0C96F3527, 0x0F5201FCD, 0x0FE32ED8F, 0x0DB8E3EF9, 0x51EF954, 0x0FE217F1C, 0x7B33A8BB, 0x9CF903A1, 0x0C381E2CD, 0x22B35BE4, 0x4550E6AE, 0x0DC9E8F3C, 0x0A9B44EAF, 0x3372486A, 0x51329F58, 0x5F2F456E, 0x9B555A08, 0x0EB1A8529, 0x9B009084, 0x9B0B7B06, 0x9967F311, 0x91FB13AB, 0x18952236, 0x6F7B9915, 0x0EDD9D6D1, 0x0FB67FE21, 0x259911B0, 0x3DC4EE74, 0x98936FF0, 0x0DF7502CE, 0x0C3DF1016, 0x0BC1220F9, 0x0F54C810C, 0x715A634C, 0x3E1637A6, 0x80F07B8D, 0x0FB9CA491, 0x0AD254C2E, 0x0FB5A012F, 0x1AEF5581, 0x0B9CC1351, 0x9A3B536D, 0x0BD7FAF0F, 0x0F49AD883, 0x2C55324, 0x83BC3205, 0x43846281, 0x19382448, 0x0FADB2B18, 0x9335D185, 0x94C6BF5A, 0x591685AE]
m = combinations(a,5)
res = [[0x0D481DD44, 0x0E66CF0E0], [0x6C86565D, 0x0EF6C2A6D], [0x0D170230A, 0x9159B169], [0x3DCF0D3F, 0x0D9331E76], [0x64691AF0, 0x0DBF384CF], [0x69E3E3A, 0x7122DE4D]]
# get part of result
for i in m:
summ = 0
for j in i:
summ+=mat[j]
summ&=0xffffffff
for k in range(len(res)):
if summ == res[k][0]:
print(k,0)
for j in i:
print(hex(mat[j]))
print('')

if summ == res[k][1]:
print(k,1)
for j in i:
print(hex(mat[j]))
print('')
# then combine these output in Notepad++, get the a
exit()

把输出稍微整理一下,选出同一行有相同元素的(matrix[4]),把相同元素提取出来,得到

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
(0, 0)			(0, 1)
0xfdfe0ba1L 0x51329f58
0x7b33a8bb 0x6f7b9915
0x3dc4ee74 0x2c55324
0x3e1637a6 0x43846281

0xdf7502ce

(1, 0) (1, 1)
0xc96f3527L 0xf5201fcdL
0x51ef954 0xdb8e3ef9L
0x715a634c 0xeb1a8529L
0x9335d185L 0x9a3b536dL

0x9967f311L,

(2, 0) (2, 1)
0x9b009084L 0xc381e2cdL
0x18952236 0x98936ff0L
0xbd7faf0fL 0xc3df1016L
0x83bc3205L 0x94c6bf5aL

0xdc9e8f3cL,

(3, 0) (3, 1)
0x22b35be4 0x91fb13abL
0x3372486a 0x80f07b8dL
0xedd9d6d1L 0xad254c2eL
0xfb9ca491L 0x1aef5581

0xfe32ed8fL,

(4, 0) (4, 1)
0x9cf903a1L 0xfe217f1cL
0x9b555a08L 0xa9b44eafL
0xb9cc1351L 0x259911b0
0x591685ae 0xf54c810cL

0x19382448,

(5, 0) (5, 1)
0x5f2f456e 0x9a915052L
0xfb67fe21L 0x4550e6ae
0xbc1220f9L 0x9b0b7b06L
0xf49ad883L 0xfadb2b18L

0xfb5a012fL,

那么现在问题在于,我们只是得到每一行有哪些元素,并不知道它们是如何排列的,毕竟加法满足交换律。

一行可能的结果数:4! * 4! = 576

由于每一行互相是互不影响的,所以我们只需要穷举576*6种结果,找出其中经过计算得到flag后,有可见字符的结果拼接即可。

一开始我的解决方案是想办法patch程序中的内存,然而找不到好的方法写脚本,所以还得分析计算flag的方法

继续调试,check函数之后就是计算flag了,首先是长度为6的循环,八成是对应矩阵的每一行了

还是根据之前调试的方法,不难发现,先将matrix和constant的行数的指针放到[ebp-184h]和[ebp-188h]

之后进入了一个长度为9的循环,先猜测对应行中的每一个元素

有了先前分析的经验,现在在分析就上手一些了,最终有用的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.text:5655A8AC mov     eax, [ebp-8]           ; j
.text:5655A8AF shl eax, 2
;...
.text:5655A8F2 mov ecx, [ebp-184h]
.text:5655A8F8 add ecx, eax
;...
.text:5655A9B6 mov eax, [ebp-8]
.text:5655A9B9 shl eax, 2
;...
.text:5655AA01 mov edx, [ebp-188h]
.text:5655AA07 add edx, eax
;...
.text:5655AA19 mov eax, [ecx]
.text:5655AA1B mov ecx, [edx]
.text:5655AA1D imul eax, ecx
;...
.text:5655AA95 mov ecx, [ebp-18Ch]
.text:5655AA9B add ecx, eax
;...
.text:5655AAC7 mov [ebp-18Ch], ecx ; m[i][j]*c[i][j]
.text:5655AACD jmp loc_5655A818 ; j++

其实就是两个矩阵对应元素相乘,同一行再相加

外循环干的事:

1
2
3
4
5
6
7
8
9
10
.text:5655AB0A mov     eax, [ebp-4]           ; i
.text:5655AB0D shl eax, 2
;...
.text:5655AB4C lea ecx, [ebp-180h] ; flag
.text:5655AB52 add ecx, eax
;...
.text:5655AB83 mov eax, [ebp-18Ch] ; the result of every row
.text:5655AB89 mov [ecx], eax
.text:5655AB8B jmp loc_5655A565 ; i++

那么加完的内容就是最后要输出的东西了,开始穷举:

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
from itertools import *
res = [[0x0D481DD44, 0x0E66CF0E0], [0x6C86565D, 0x0EF6C2A6D], [0x0D170230A, 0x9159B169], [0x3DCF0D3F, 0x0D9331E76], [0x64691AF0, 0x0DBF384CF], [0x69E3E3A, 0x7122DE4D]]
constmat = [0x0B849CD19, 0x55E00017, 0x844966B, 0x80C181EC, 0x686C0B3C, 0x55400592, 0x0CD42168A, 0x4039E81, 0x0D9DE549F, 0x2034677D, 0x144ABD, 0x49100D00, 0x0E003A0E0, 0x80F0006D, 0x8307ADD6, 0x4CF60781, 0x0A0352643, 0x0C580C3DE, 0x0EA8C4E24, 0x68603008, 0x687FBFFF, 0x19DE4BF9, 0x271A1179, 0x99791C4D, 0x29CBFFC, 0x2B82801E, 0x3C0307FB, 0x0DAE61CD6, 0x8F7B1BF0, 0x0C56CEF1D, 0x0D6493A96, 0x1808018, 0x0F48001B9, 0x3712519, 0x9294F318, 0x6DE20384, 0x0F3750B04, 0x256A122A, 0x257290B, 0x0C4582056, 0x204E8BC0, 0x79C7ADE7, 0x0C4C20203, 0x5B961570, 0x66034856, 0x78329E3A, 0x1D07C00, 0x4AC240E6, 0x854CFBBE, 0x0ABFEC404, 0x5BD80037, 0x0E94CBCD8, 0x1, 0x0C4CA280D]
a= [[[0xfdfe0ba1, 0x7b33a8bb, 0x3dc4ee74, 0x3e1637a6],[0x51329f58, 0x6f7b9915, 0x2c55324, 0x43846281],0xdf7502ce],[[0xc96f3527, 0x51ef954, 0x715a634c, 0x9335d185], [0xf5201fcd, 0xdb8e3ef9, 0xeb1a8529, 0x9a3b536d],0x9967f311],[[0x9b009084, 0x18952236, 0xbd7faf0f, 0x83bc3205], [0xc381e2cd, 0x98936ff0, 0xc3df1016, 0x94c6bf5a], 0xdc9e8f3c],[[0x22b35be4, 0x3372486a, 0xedd9d6d1, 0xfb9ca491], [0x91fb13ab, 0x80f07b8d, 0xad254c2e, 0x1aef5581], 0xfe32ed8f],[[0x9cf903a1, 0x9b555a08, 0xb9cc1351, 0x591685ae], [0xfe217f1c, 0xa9b44eaf, 0x259911b0, 0xf54c810c], 0x19382448],[[0x5f2f456e, 0xfb67fe21, 0xbc1220f9, 0xf49ad883], [0x9a915052, 0x4550e6ae, 0x9b0b7b06, 0xfadb2b18], 0xfb5a012f]]
m = permutations([0,1,2,3],4)
n = []
for i in m:
n.append(i)
cont = 0

for i in range(0,6):
print('')
print('#########part %d#########'%i)
for p in n:
for q in n:
cont+=1
ress = ''
resr = []
resr.append(a[i][0][p[0]])
resr.append(a[i][1][q[0]])
resr.append(a[i][0][p[1]])
resr.append(a[i][1][q[1]])
resr.append(a[i][2])
resr.append(a[i][1][q[2]])
resr.append(a[i][0][p[2]])
resr.append(a[i][1][q[3]])
resr.append(a[i][0][p[3]])
sumr = 0
check0 = resr[0]+resr[2]+resr[4]+resr[6]+resr[8]
check0&=0xffffffff
check1 = resr[1]+resr[3]+resr[4]+resr[5]+resr[7]
check1&=0xffffffff
if check0 != res[i][0]:
print(i)
for k in resr:
print(hex(k))
print('')
print(hex(check0))
print(hex(res[i][0]))
print(p)
print(q)
exit()
assert check0 == res[i][0]
assert check1 == res[i][1]
for k in range(len(resr)):
sumr+=resr[k]*constmat[i*9+k]
sumr&=0xFFFFFFFF
ff = 0
for _ in range(4):
temp = sumr&0xFF
if temp < 0x20 or temp >= 0x7f:
ff = 1
break
ress+=chr(temp)
sumr>>=8
if ff == 1:
continue
print(ress)

把输出按语义拼接一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>uG8   \w8]   J|@>   Oh)!   ye$B   ukxG
xU"a Gj3o S_Cu }]nU h`1) uHnr
W8F? {7h1 X4G\ 63_i *[T3 ;73-
P[jk KP6o Xp,T -gF: s_m4 a/_j
|/Zj v9E[ oeps -S96 \Q}] g1c}
:!); B::( '-SR ERNJ gJO( AR&|
*CTF a'/0 SA;V nL?K SZL 4nEE
uP@J uk6W @GhI RV&b "-R! YOcm
erkU XP#3 2\t$ 5zB( AkT6
Mc=F hx-A Q7^< -l/l
;|.b X .; <1I7 YctL
=!M3 4gjL 1I@B VRae
A6V\ /_"u jke&
k?!R ^I;-
:q(V

*CTF{7h1S_Cu63_is_m4g1c}

总结

因为刚开始学花指令,没找去除冗余代码的方法,脚本写不出来,所以干脆就纯看汇编做的,感觉方法比较蠢,需要胆大心细,但是做的过程中也多多少少对这种混淆的方法有了一定的了解,有空的话深究一下高级一点去花方法。

此外,比赛时并没有发现这道题的算法是什么,赛后跟大牛交流了一下其实这是一个魔方,6*9个元素对应6个面每个面9个块,18种输入对应18种标准魔方的操作,每个面顺时针逆时针旋转90度加3个中间块的两个方向旋转90度。不过知道是魔方之后也想不到有什么很好的解决方法。颜色不知道,最终还原的魔方的状态也不知道,只知道还原的魔方能过check。 还有一点,由于魔方边块和角块必须是挨着的,在矩阵上就能满足某种特定的规则,这样在我之前的穷举时就能去除那些不紧挨的情况了,可能最终的输出会被缩小很多,如果遇到flag没有语义的情况会有优势。

misc

she

根据提示,杀死左小角小屋内的鸡拿flag。

先跟右下角的女生对话后会不能存档。

存个档,进右上角小屋杀只怪,升一级的点前后再存档,比较两个存档的不同。

再存档内可以看到gold,exp,level等后面的数值不一样。稍微修改一下,发现存数据的规律:

0x69表示类型位整型,下一个字符表示用到多少字节存数据,只用1字节的时候不要,最多四字节,因此所有数据都是5开始的。我们把金钱,经验值调大,进游戏一刀满级,然后去左上角的商店买装备。

然而还是打不过,想了想鸡的攻防可能超过我们已有的最高攻防了。

这是可以改游戏文件,把鸡的攻防调为最低。

在data文件夹内有个叫enemies的文件,在里面可以看到大概是两个敌人,一个叫Julia,就是那只鸡,一个叫Trainer训练假人。

稍微摸索一下数据,看到几个比较关键的比如def,atk等,或者直接干脆把所有的都改成05 或者06。

再去打鸡,可以一刀秒了。

接下来还不能直接拿flag,根据提示,打开所有的门中的宝箱。有些门不能直接开,开完后面的门才能开。基本这里就是比走位躲幽灵的时间了。

所有门开完后最后的多了个门,里面提示把之前宝箱内的数字连起来md5即是flag。

数字要按顺序从左到右

213697

*ctf{d6f3fdffbcb462607878af65d059f274}

otaku

下载下来是一个文档Anime_Intro.doc和压缩包flag.zip,压缩包加密了

文档打开后是一段英文,稍微看一下内容,讲的是紫罗兰永恒花园。注意到一些地方格式不太对,有几个字符是空白的。把空白字符复制到谷歌文档里竟然多了很多东西,很迷。。。

也可以把doc用压缩包打开,在xml里面能看到少的东西,是另外一串字符串。大意是个梗,跟炉石有关,差不多就是要388去救薇尔莉特之类的balabala。。。先不管

去看flag.zip,里面有张图片和一个叫last words的txt,txt的大小为432字节。之前隐藏的一部分文字大概也就这么长,复制到txt里看一下,然而却有433字节,用010 editor打开,发现有个地方不太对,有个I'm的单引号占了三个字节,如果是英文引号的话应该占1个字节,那么大小应该为431字节(一共确实是有431个字符)。而zip里的txt有432字节,这里用的确实是中文引号,那只能是编码方式的问题了。用notepad++的转码功能,注意是转码,而不是上面的编码,转码乘ascii,中文部分会自动用gbk编码,这样得到的txt就有432字节了,大小跟zip内的一样,考虑明文攻击。

2019starctf01

在制作zip的时候踩了很多坑,最后得出结论:

同为zip压缩,7z和winrar的结果竟然不同,就算所有的设置都一样,压缩结果也不同!而且这里的不同不仅是时间这些无关紧要的东西不同,是最终压缩结果的压缩数据都不一样!这里坑了很久。然后试了很多压缩软件,只有winrar的跟其他的不一样!7z,好压,linux命令行压缩等的结果都是一样的,这里确实是被坑了。。。

最后使用winrar制作的zip,azpr一把梭,得到密码解开zip,拿出图片。

图片是紫罗兰永恒花园的角色薇尔莉特。然后直接lsb隐写得到结果。

队里队友用zsteg工具一把梭,然而我怎么都搞不好环境,跑不起来。。。只好用stegsolve了

这题还是挺有意思的,彩蛋很多,做起来挺欢乐的。然而比赛时都去刚re了,这题最终是阿烨出的。