刷题记录
NKCTF20230318
PKMF
题目描述:flag格式为nkctf{xxx},flag全小写字母,中间的xxx为nkctf文件中写入的十六进制内容
直接进入主函数查看,这是简单整理后的主函数的样子,标上了一些比较显眼的变量和一些注释。
通过大体浏览后知道最后的“Congratulations! you found it!\n”是我们需要的最终结果,所以前面不能使程序退出。
ReadFile函数是从文件中读出数据,通过if语句可知,one_read_5的值只能是5,否则就会返回“Wrong”并且退出程序。同理可知下面的key_nkman的值只能是key_yilai数组的值,点进去查看是“6E6B6D616E”这样的一串。key_nkman就等于“6E6B6D616E”。
继续往下 HEX_0x15 += key_nkman[time++];
HEX_0x15是key_nkman数组的和,0x6E 0x6B 0x6D 0x61 0x6E
和为0x215,又由于HEX_0x15
是char类型,两个字节高位溢出,即HEX_0x15
值是0x15
再往下是一个迷宫的描述,可以找到迷宫的原来的字符组成拼接一下,用脚本把迷宫的样子画出来
1 |
|
每一行是18个元素,最后迷宫是长这样的
既然知道是迷宫了,可由下面while for双嵌套语句中知道上下左右分别是0(上)1(右)2(下)3(左)
while(time<16)得到每四个为一组的迷宫路径是:1122 3322 1223 2211 0111 1101 0000 0101 1211 0111 2223 2330 3323 2211 1112 2333
在线转获得 0x5A 0xFA 0x6B 0xA5 0x15 0x51 0x00 0x11 0x65 0x15 0xAB 0xBC 0xFB 0xA5 0x56 0xBF
再由路径的4进制转为16进制与值为0x15的HEX_0x15
进行异或操作。
最后可以使用脚本获得后面的flag数值
1 | enc=[0x5A,0xFA,0x6B,0xA5,0x15,0x51,0x00,0x11,0x65,0x15 ,0xAB,0xBC,0xFB,0xA5,0x56,0xBF] |
[‘0x4f’, ‘0xef’, ‘0x7e’, ‘0xb0’, ‘0x0’, ‘0x44’, ‘0x15’, ‘0x4’, ‘0x70’, ‘0x0’, ‘0xbe’, ‘0xa9’, ‘0xee’, ‘0xb0’, ‘0x43’, ‘0xaa’]
加上上面从文件读取到的
flag就是nkctf{056e6b6d616e4fef7eb0004415047000bea9eeb043aa}
ealier
运行程序后发现程序没有任何反应,查看发现存在花指令的干扰,所以首先要干的就是去花的工作。
先查看到的是call函数的花指令,同一种模式的去花,使用脚本完成。
对应的ida版本对应的api函数不同,所以要注意使用的函数
1 | import idautils |
ida_search.find_binary(cur_addr,end_addr,pattern,0,ida_search.SEARCH_DOWN)
五个参数分别是:起始位置、结束位置、
- cur_addr:起始地址,搜索将从该地址开始。
- end_addr:结束地址,搜索将在该地址处停止。
- pattern:要搜索的二进制模式,可以是字节字符串或16进制字符串。例如,**”\x33\xC0\x85\xC0\x74\x03\x75\x00\xE8“**或”33 C0 85 C0 74 03 75 00 E8”。
- **0 **默认值为0的参数用来表示标志的开关状态
- ida_search.SEARCH_DOWN:搜索标志,可以是以下之一或它们的组合:
- ida_search.SEARCH_DOWN:向下搜索。
- ida_search.SEARCH_UP:向上搜索。
- ida_search.SEARCH_NEXT:搜索下一个匹配项。
- ida_search.SEARCH_CASE:区分大小写。
- ida_search.SEARCH_REGEX:启用正则表达式搜索。
CTFSHOW-NKCTF-4月四周-20230417
flag白给
一道易语言的题目,主要就是UPX_0.89.3xx的脱壳,简单对比一下就出来了
数学不及格
分析代码
中间只有一个加密函数f,看起来像是斐波那契
直接用爆破脚本 求出v10 v11 v12
1 | from z3 import * |
再用16进制的转文本的脚本得到flag
1 | hex_string ='666c61677b6e65776265655f686572' |
签退
先反编译一下 安装了一个之前没安装好的新工具uncompyle6
1 | uncompyle6 -o pcat.py pcat.pyc |
反汇编出来的代码是
1 | # uncompyle6 version 3.9.0 |
解读代码后直接写解密脚本
1 | import string |
第二种思路
分为两段代码 一段是换表的base64加密,另一段是位移两位的凯撒密码加密
1 | import string |
1 | def rend(s): |
反过来就是先将 ‘BozjB3vlZ3ThBn9bZ2jhOH93ZaH9’ 凯撒密码位移两位解码,再base64换表解密 得到flag
CTFSHOW-BUUCTF-20230412
CrackRTF
加密算法的标识码:
https://learn.microsoft.com/en-us/windows/win32/seccrypto/alg-id
读反汇编代码
发现主要的思路就是上图这样,输入两次密码,分别定义为:input_1和input_2
input_1输入后会进行一次加密,进入加密算法,通过0x8004u知道该密码标识符是sha1加密算法,即input_1+“@DBApp”赋值input_1==SHA1==> “6E32D0943418C2C33385BC35A1470250DD8923A9”
写出碰撞的加密脚本
1 | import hashlib |
同样的方法,找到密码标识符后,确定是MD5加密方法,接着向下input_2+前面的input_1(加上“@DBApp”后)==MD5==> “27019e688a4e62a649fd99cadaafdb4e” 求得input_2== “!3a@0” (爆破的方法似乎行不通,不知道为啥这个MD5破解网站 可以 magic)
得到dbapp.rtf文件获得flag文件
存在的非预期解是在最后的函数里,存在一个调用函数
一个一个去查函数的作用,主要作用是将一个资源中的数据写入到一个名为”dbapp.rtf“文件中去,其他各个函数的含义:
FindResourceA 查找资源 参数包括资源 ID (0x65) 和资源类型 (“AAA”)。
SizeofResource 获取资源的大小 LoadResource 加载资源 LockResource 锁定资源
调用另一个函数 sub_401005,作用是将第一个参数 lpString 中的字符串与 lpBuffer 中的数据进行处理,即与前6个字符的异或,并将处理结果存储到 lpBuffer 中。
1 | import hashlib |
CTFSHOW 二进制玩家人均全栈
首先得到的是一个名叫zip的文件,用010打开查看,发现是个PK文件头的压缩文件,但是发现是的与正常压缩包文件的前几个字节存在偏差,所以需要手动修复文件
修改前文件
修改后的文件
个人觉得这一步不需要 ,14 00两位是使用的PKware的版本,08 00判断是否有加密,如果直接用更改后缀名的方式也是可以实现的
解出一个无后缀的文件,放在ubantu发现执行不了这个文件,原因是文件头错误,所以需要更改文件头继续执行文件。
查看原来文件头的数据,并更改成正确的elf文件的文件头
PE文件文件头数据是:
4D 5A 90 00
ELF文件文件头数据是:7F 45 4C 46
电脑暂时没有ELF文件,盗了张图(原链接戳这里 )
更改后重新测试,说明文件就改好了撒花(误
之后正常地分析文件,发现还需要将upx壳脱掉,自动脱壳。。。脱不掉。
planA:dumpfile脚本脱壳
1 |
|
下断点进行动态调式,找到oep(程序的入口点 What is OEP? )
首先,retn指令下断点F9跳到断点处
1.F8单步执行,找下一个retn指令
2.在retn处下断点,F9跳过,F8单步
3.继续向下找retn重复2步骤
重复个几次,之后一直F8,最终到达这里。
后面是一次大跳转,就会跳到OEP处,接着F8来到下图的位置
继续执行到返回弹窗
提示是返回的地方没有被定义为代码,问是否需要定义,选择Yes
这样就来到了OEP的位置
快捷键alt+F7点击上面的脚本,运行成功得到的dump文件就是最后我们需要的内存文件。
提示运行成功,报错dump err
就检查之前设置的dump文件路径有没有错误的,这种方法获得是加壳之前的内存文件,转移到了dump文件中,接下来分析的就是路径下的dump文件了。
planB:魔改UPX壳
已经判断了该程序的壳是UPX 但在010中会发现有vmp的壳,所以将upx壳的特征值改回来,之后再用机器脱壳
planC:其他方法
方法1:单步跟踪
只向下调不向上调
方法2:ESP定律法
dd XXXXX
hr XXXXX
方法3:2次内存镜像法
方法4:一步直达法
https://blog.csdn.net/m0_46296905/article/details/116049504
最后明显就是迷宫路线题,套路套路答案就出了,之前又搞过就不写了
https://www.cnblogs.com/r136a1/p/17101192.html
https://blog.csdn.net/weixin_53349587/article/details/124676648
CTFSHOW-9月刷题
密文解密网站:
https://txtmoji.com/
pwn02
栈溢出的题,直接接收溢出的字符串即可获得服务器权限
1 | from pwn import * |
pwn03
开了aslr保护,同时字符串并没有直接给出栈溢出的点 即bin/sh
的字样,涉及到plt和got表,
1 | from pwn import * |
CTFSHOW-3月四周-20230320
武穆遗书
fmf_my_reverse.exe
查壳upx先脱壳upx -d [fileaddress]
脱完后发现程序还是运行不了(程序是32位,所以无法在64位系统上运行)直接用ida反汇编
进入主函数发现使用了好多的api函数
分析一下这段代码
- 调用三个未知函数sub_4011D0()、sub_401200()、sub_401280()
- if判断,调用sub_401040()函数,如果函数返回值 !=0,即执行exit(0)退出程序;
- v4 = (const char *)operator new(0x1Cu); 使用C++中operator new运算符动态分配内存,申请了一块内存大小是0x1C字节,返回的指针存储在v4中
- sub_401390((int)v4, (int)v5, (int)&unk_4070F4, 28)调用了sub_401390函数,将v4, v5, &unk_4070F4, 28作为参数传递给函数
- 同2
- 进入while的无限循环,
- gets(v6);观察下面的函数,知道gets函数从输入中读取用户输入的字符串,存储在v6中,gets函数存在缓冲区溢出的风险
- fflush((FILE *)iob[0]._ptr);百度知道是为:清空输入缓冲区,防止缓冲区溢出
- if ( !strcmp(v4, v6) ) break; 相等的话就跳出循环,否则输出密码错误的提示信息
printf("password error!!! please try again!\nyour input is %s \n", v6);
- 跳出循环即输出成功信息
printf("win!!!the password and your input are all %s\n", v6);
,并打印用户输入的密码 - 最后执行Command的系统命令
下断点调试,老是会闪退,查了之后推测是用了反调试的手段
其他反调试手段:
- 检测调试器:软件会检测调试器的存在,并根据检测结果采取相应的行动,例如自我终止、运行不同的代码等。
- 检测断点:软件会检测是否在代码中设置了断点,如果检测到断点,可能会改变代码执行路径,或自我终止等。
- 检测调用栈:软件会检测调用栈,以查找是否存在调试器,如果存在,则可能会改变代码执行路径,或自我终止等。
- 加壳/加密:软件加壳或加密后,使得调试器无法直接读取程序代码和数据,这样可以防止反汇编和调试。
- 虚拟化:软件会在虚拟机中运行,使得调试器无法直接访问真实的程序代码和数据,这样可以防止反汇编和调试。
大概有两种解法:
第一种就是nop掉程序中的exit后,程序动调就可以出flag
第二种attach the process,需要先把程序运行起来再选择才行,这个还是有点问题,改了程序的属性外加wow64文件夹,但发现程序还是会出现闪退的情况
反汇编和反调试学习
FS寄存器偏移值指向:
000 指向SEH链指针
004 线程堆栈顶部
008 线程堆栈底部
00C SubSystemTib
010 FiberData
014 ArbitraryUserPointer
018 FS段寄存器在内存中的镜像地址
020 进程PID
024 线程ID
02C 指向线程局部存储指针
030 PEB结构地址(进程结构)
034 上个错误号
原文链接:https://cloud.tencent.com/developer/article/1142065
CTFSHOW-3月三周-20230316
re3(20230316-17)
先看伪代码,发现逻辑是非常清晰的,定义v17长度是8的整型数组,经过for循环给v16赋值.
1 | v7 = 0x50; |
需要得到的是输入的v5的值是什么四位数的值,v17数组最后一位是v5,暂且设置A是前面的得数 即A+v5==0xFFFF返回“ok”。
一开始的时候我是直接将v17上面给的固定的值全部加了起来,最后发现这个数已经不是符合flag条件的4位数字了 所以肯定是哪边算错了
后来发现for循环应该是一次一次的处理,v17[4]的值为0x5010,v17[5]的值为0xEF9,v17[6]的值是用户输入的数值。
v17[4]=0x5010 先将v11=5(DEC)的值化成二进制的0101,<<左移12在低位补0 得到16进制的(v11<<12)的值即0x5000. v12=0x10
v17[4]=(v11<<12)+v12 ==> 0x5000+0x10=0x5010
每一次循环都将v16加上数组元素v17[i]的值,每轮循环结束后,将检查v16的值是否超过了16位的最大值0xFFFF。如果超过了,则将v16高16位(即进位)的值保存在v15中,并将v16的低16位截断,只保留其低16位的值。
1 | for(i=0;i<=6;++i){ |
都将v16加上数组元素v17[i]的值。在每轮循环结束后,将检查v16的值是否超过了16位的最大值0xFFFF。如果超过了,则将v16高16位的值保存在v15中,并将v16的低16位截断,只保留其低16位的值。
每一次运行的结果第一次循环:
- i = 0
- v17[0] = 0x50
- v16 = 0 + 0x50 = 0x50
- v14 = 0x50
- v15 = 0x00
第二次循环:
- i = 1
- v17[1] = 0xFAE3
- v16 = 0x50 + 0xFAE3 = 0xFB33
- v14 = 0xFB33 & 0xFFFF = 0xB33
- v15 = 0xFB33 >> 16 = 0x00
第三次循环:
- i = 2
- v17[2] = 0xD7D3F7B
- v16 = 0xFB33 + 0xD7D3F7B = 0xD7D4EC0E
- v14 = 0xEC0E & 0xFFFF = 0xEC0E
- v15 = 0xD7D4 & 0xFFFF = 0xD7D4
第四次循环:
- i = 3
- v17[3] = 0xA43499F6
- v16 = 0xD7D4EC0E + 0xA43499F6 = 0x7B09E904
- v14 = 0xE904 & 0xFFFF = 0xE904
- v15 = 0x7B09 & 0xFFFF = 0x7B09
第五次循环:
- i = 4
- v17[4] = (5 << 12) + 0x10 = 0x5010
- v16 = 0x7B09E904 + 0x5010 = 0x7B09EE14
- v14 = 0xEE14 & 0xFFFF = 0xEE14
- v15 = 0x7B09 & 0xFFFF = 0x7B09
第六次循环:
- i = 5
- v17[5] = 0xEF9
- v16 = 0x7B09EE14 + 0xEF9 = 0x7B09FD0D
- v14 = 0xFD0D & 0xFFFF = 0xFD0D
- v15 = 0x7B09 & 0xFFFF = 0x7B09
第七次循环:
- i = 6
- v17[6] = v5
- v16 = 0x7B09FD0D + v5
最后逻辑崩盘,需要用动态调试搞一波,一开始写了一个C++脚本,
1 |
|
发现存在报错的情况,逻辑上感觉没啥错误,像是操作不当的原因。
不过今天拓展了ubantu的gdb的界面
![0]]2I6`M68H0NL14{9K~KKW.jpg](https://cdn.nlark.com/yuque/0/2023/jpeg/23148330/1679066038125-c62305af-8599-43f5-920a-0d2cd122daa3.jpeg#averageHue=%23360e2d&clientId=u9a778dd9-a3b4-4&from=paste&height=945&id=u0bd99a6f&originHeight=945&originWidth=1470&originalType=binary&ratio=1&rotation=0&showTitle=false&size=145816&status=done&style=none&taskId=uc38c23aa-33c4-4ba1-874d-aa7b5919727&title=&width=1470 )
![Z7NWICH8I]6))BK)D_](OH6.jpg](https://cdn.nlark.com/yuque/0/2023/jpeg/23148330/1679066054181-710e9645-46e5-49ea-8511-f6f29139b47c.jpeg#averageHue=%233f1c35&clientId=u9a778dd9-a3b4-4&from=paste&height=447&id=ue4024e80&originHeight=447&originWidth=833&originalType=binary&ratio=1&rotation=0&showTitle=false&size=96201&status=done&style=none&taskId=u30a1fabe-93f0-4141-9d55-2eff5179bed&title=&width=833 )
但貌似这个东西可以在ida里直接实现就好了,呜呜呜 准备明天去问问Pwn同学是咋搞的
一步一步跟进之后貌似发现了‘Error’的位置
mingyue.exe(20230318)
先关闭安全软件运行一个这个可执行文件,发现应该是有一串关于弹窗的代码出现,
一般的会使用MessageBox的函数,这样的话,源代码应该是
1 |
|
通过这个查找字符串的看是否存在与我们上面推测的弹窗的内容,可以看到有两段可疑字符和一个MessageBoxW的标志
查看字符的地址与所在函数之后,查看主函数的逻辑
貌似主函数看不出什么弹窗的源代码和提示flag的地方,突破口就是可以输入的地方sub_140001080函数,n一下将v3改为input 跟进一下函数,参数v3对应就是这里的char a1,同改a1为input_num。a4890572163qwe函数就是我们刚刚发现的可以字符串的函数,n为string 整理一下函数和变量的名字。这段的代码就是给刚刚第一段的’)(&^%489$!057@#><:2163qwe’进行加密的运算
其中两个重要的函数xor和judge函数
xor函数的分析
可以通过以下几个特征来看出这是一个链表节点
特征
- 所占空间大小判定:内存块大小为 16 字节,一个链表节点通常需要存储两个指针,即指向下一个节点的指针和指向前一个节点的指针,每个指针通常需要占用 8 个字节的空间(在 64 位系统中),因此一个链表节点通常需要占用 16 个字节的空间。
- 特征函数判定:通过 malloc 函数分配内存块,这是链表节点动态内存分配的一种常见实现方式,因为链表节点数量通常不固定,所以需要动态分配内存来保存它们。
- 指针数组判定:通过一个指针数组来表示链表节点,返回值是一个指向 _QWORD 类型的指针,它指向了一个内存块,这个内存块的大小是 16 字节,即一个链表节点的大小。
- 通过将新分配的内存块的地址存储在全局变量中,将新节点插入链表的头部。这表明这是一个单向链表的实现方式。
- 内存块的第一个字节用来存储字符类型的值,这表明这个链表节点不仅包含指针,还包含数据。
judge函数的分析
改过之后的函数
跟进最后两个函数,发现是两个我们上面猜测的弹窗的代码内容。
最后脚本
1 |
|
将数字输入之后返回正确的认证成功 即上面获得数字就是我们所要提交的flag
参考文章:
https://blog.csdn.net/OrientalGlass/article/details/129326915
re3车尾
上面卡在v5的输入那里,请教同学之后,可以使用动态调试将最后的结果爆破出来
ubantu运行./linux_sever64的程序,打好断点之后,Windows的ida动调程序开启
为了看起来比较简洁,将几个类型相同的变量组成一个新的数组
组成数组首先先选择最上面的变量,右键选择“Set lvar type”或者直接使用快捷键“Y”
定义好数组的名称和长度之后
点击set the type 最后就好了
输入00000fffff,记录每一次循环sum值
输入00000fffff每一次循环记录 v7[0] = 0x50; v9[0] = v7[0];
v7[1] = 0xFAE3; v9[1] = v7[1];
v7[2] = 0xD7D3F7B; v9[2] = v7[2];
v7[3] = 0xA43499F6; v9[3] = v7[3];
v7[4] = 5; v9[4] = (v7[4] << 12) + v7[5];
v7[5] = 0x10; v9[5] = v7[6];
v7[6] = 0xEF9; v9[6] = v5;
v7[7]=v5=0xffff;
i=1
sum==0x50;不符合sum>0xFFFF;加下一个v7数组中的数据;
i=2;
sum==0xFB33==0x50+0xFAE3==0x50+v9[1];
不符合sum>0xFFFF,加下一个v7数组中的数据;
i=3;
sum==0xD7E3AAE==0xFB33+0xD7D3F7B=0x50+v9[2];
符合sum>0xFFFF,v7[7]取sum低四位0x3AAE;v7[8]取sum高位0xD7E;下一轮sum为v7[7]+v7[8]
i=4;
sum==0x482c==v7[7]+v7[8]==0x3AAE+0xD7E;
不符合sum>0xFFFF,加下一个v7数组中的数据;
i=5;
sum==0xa434e222==0x482c+0xA43499F6==0x482c+v9[3];
符合sum>0xFFFF,v7[7]取sum低四位0xe222;v7[8]取sum高位0xa434;下一轮sum为v7[7]+v7[8]
i=6;
sum==0x18656==v7[7]+v7[8]==0xe222+0xa434;
符合sum>0xFFFF,v7[7]取sum低四位0x8656;v7[8]取sum高位0x1;下一轮sum为v7[7]+v7[8]
i=7;
sum==0x8657==v7[7]+v7[8]==0x8656+0x1;不符合sum>0xFFFF;
i=8;
sum==0xD667==0x8657+0x5010==0x8657+v9[4];不符合sum>0xFFFF;
i=9;
sum==0xE560==0xD667+0xEF9==0xD667+v9[5];不符合sum>0xFFFF;
sum=0xE560+0xFFFF=0x1e55f;
符合sum>0xFFFF,v7[7]取sum低四位0xe55f,v7[8]取sum高位0x1,循环结束。
由于最后一次循环的0x1e55f是>0xFFFF的,所以预测应该是返回”Error”,
最后返回也是“Error”即验证成功。
根据上面调试出来的结果,会发现一定的规律,sum是经过v7数组的元素进行相加分为两种情况
如果符合sum>0xFFFF;v7[7]取sum低四位;v7[8]取sum剩余高位,且下一轮sum==v7[7]+v7[8];
如果不符合sum>0xFFFF;sum+=v9[i];
同时还要注意到一开始的输入格式,观察代码输入的s长度赋值给v3,长度需要大于6,查看函数中v11的值的话就会发现是之前输入的前五位“00000”,经过多次输入都是输入的前五位数字没有影响,紧接着是算出来的最小值1a9f,第十位作为校验码,多次输入后发现也是无论任意值
1 | puts("plz input the key:"); |
确定flag{1a9f}
逆向5
call_1.exe 1.dll
运行call_1.exe文件,一个不完整的弹窗,ida进入主函数之后发现是
转化成代码语言
1 |
|
进入主函数发现根本没有上面推测的代码,所以说一个函数一个函数进入寻找,或者像之前的方法直接找是否存在字符串帮助更快的找到flag的线索。
直到进入sub_4015BD的函数,才发现存在可疑的地方:Str==“dba54edb0?d6>7??3ef0f1caf2ad3102”
alt+a查看是否是弹窗上的文字,根据unicode转变结果看,应该只是会用到的普通的内容性字符串
已知Str==“dba54edb0?d6>7??3ef0f1caf2ad3102”,所以,很明显Str[1]!=1,直接return result,查看sub_401520函数。
line7:使用LoadLibraryA调用题目给的“1.dll”文件
line8:GetProcAddress(hModule, “H”) 从所给的动态链接库(1.dll)中获取函数的地址,并将其转换成一个可以在程序中调用的地址。(具体可百度)
for循环依次取Str里的值,作为参数传给H。sub_40163E()函数的putchar函数打印经过H处理Str[i]的内容。
动态调试:由于Str[1]!=1,所以程序不会执行sub_401520函数中去,那么我们就想到可以直接改ZF符号,使他表示比较的结果是相等的,即在F7单步jnz后将ZF标志的0x00改为0x01
在汇编语言中,ZF标志经常用于比较和跳转指令中。例如,当执行cmp指令时,如果比较结果为相等,则ZF标志被置为1,程序可以根据ZF标志的值来判断跳转条件。如果ZF标志为1,则表示比较结果为相等,程序会跳转到指定位置;如果ZF标志为0,则表示比较结果不相等,程序会继续执行后续指令。
修改后进入sub_401520函数在sub_40163E函数处下断点,F9运行再F8执行sub_40163E函数,flag就出来了。
参考文章:
https://blog.csdn.net/weixin_45582916/article/details/118497453#5_107
https://wenku.baidu.com/view/63e8de76ccc789eb172ded630b1c59eef9c79a14.html?wkts=1679224381727&bdQuery=GetProcAddress%28hModule%2C+%22H%22%29
BUUCTF-NKCTF-4月五周
earlier
说是手动去除花指令,得靠感觉来,首先是看这个小东西,双击到位置改一改数据,一个就是u查看数据,将第一行的数据nop掉(ctrl+N)按c再将数据整理起来,函数也是,d展开,nop第一行,c整理起来,还一个就是push ebp那里大概会有函数,p一下如果可以建个函数。
DASCTF Apr.2023 X SU战队2023开局之战
【简单】easyRE
1 | #!/usr/bin/env python |
虽然说成功将exe文件剥离出了pyc文件,直接使用网站或者uncomply6无法直接逆向出源代码,所以使用pycdc工具来读python的字节码https://github.com/zrax/pycdc
1 | File Name: easyRE.py |
BUUCTF平台REVERSE刷题20230115.md
reverse1
直接快捷键shift+F12查找程序中的字符串找到类似flag的字符串
发现填入后不对继续跟进ctrl+x
在right flag那里看看有什么东西
看到这里是将flag中的o(ASCII的111)改为0(ASCII的48)所以得到的flag是{hell0_w0rld}
###flag{hell0_w0rld}
利用ascii将英文字符换为数字字符
reverse2
查看字符串shift+F12,顺着找到flag的所处位置
分析一下伪代码所要表达的内容:输入s2传入输入的flag,之后进行与正确flag字符的对比
这里的flag是7B,转化成十进制就是123,所以i要符合的条件值是???没有,所以for(…)循环是不存在程序运行过程中的。
上面自己的分析貌似与题目意思有偏差,并且本人发现IDA中a快捷键的插件貌似没了,这个也要处理一下
105、114与49都是ascii编码中的 即’i’、’r’与’1’,所以直接将一开始找的hacking_for_fun}中的i和r都改为1,即flag是{hack1ng_fo1_fun}
###flag{hack1ng_fo1_fun}
判断将指出的特殊字符利用ascii换为数字字符
内涵的软件
直接打开之后貌似就是flag
###flag{49d3c93df25caad81232130f3d2ebfad}
新年快乐
先放在DIE里判断一下文件的信息
发现是被upx加过壳的,所以首先要的就是先给程序脱壳
在upx目录下进行脱壳输入的命令:upx -d xxxx.exe
需要注意的是要将文件放在与upx同目录下或者说直接连文件的位置一起输入进去
所以现在得到的程序就是已经拖过壳的程序
可由上图知程序是32位的所以我们放在32位的ida进行下一步的程序分析
大概的逻辑思路是这样的
首先是
strcpy(Str2,"HappyNewYear!");
strcpy()是一种用于复制的函数声明,将后面的字符串”HappyNewYear”复制到Str2中去
Str1这里是我们需要输入的flag
之后到
if(!strncmp((const char *)&Str1, Str2,strlen(Str2)) )
strncmp()是用来比较的函数,strlen(Str2)表示的是获取到Str2的长度,在这个长度下进行比较
- 根据下面的
result = puts("this is true flag!");
可以知道上面的字符串就是我们所需要的flag
###flag{HappyNewYear!}
upx脱壳
xor
通过题目猜测这道题考的是异或,查看字符串上面的感觉像是需要异或处理的字符
写脚本并且运行
1 | enc = ['f',0x0A,'k',0x0C,'w&O.@',0x11,'x',0x0D, |
这里插入菜鸟教程的python内置函数界面
Python 内置函数 | 菜鸟教程
###flag{QianQiuWanDai_YiTongJiangHu}
helloword
下载后发现是一个apk文件,我们需要用别的安卓工具来打开这个文件,直接找到main函数就能够获得flag了
###flag{7631a988259a00816deda84afb29430a}
apk文件的处理
reverse3
直接查看字符串
在里面看见两串可以纳入考虑的字符串”rigth flag!\n” “e3nifIH9b_C@n@dH”
进入到该函数之后,看到函数的原伪代码的大概
v4那里进入后有一个sub_411AB0
的函数,点进去之后发现aAbcdefghijklmn
这个函数较可疑的点进去之后发现
是base64编码的特征,往上逆着推,
所以需要进行一个简单的减法将flag算出来,而且需要知道Str2的值就是我上面找到的奇怪的字符”e3nifIH9b_C@n@dH”,有两种方法
1 | import base64 |
1 | str2 = list("e3nifIH9b_C@n@dH") |
得到 **e2lfbDB2ZV95b3V9 **放在在线base64中解码
flag{i_l0ve_you}
自己做题的时候这里出现的一个错误
在base64包存在的情况下,为什么出不来结果
找了百度之后才知道原来是命名的时候出现错误了
因为当时命名的文件名是”base64.py“,与包base64重合了,所以会出现混乱,把名字直接改为exp.py 结果就跑出来了
不一样的flag
32位的程序,直接用IDA打开之后分析程序
发现有一串10组成的字符串,再根据下面的判断语句
根据11、14-18、51行可以判断为迷宫题,将上面的25位的字符串转为5x5的迷宫图
从*一直走到#结合上面的操作数,所以flag就是222441144222
###flag{222441144222}
迷宫问题
SimpleRev
第一步还是查看字符串,从里面找一些可以确定flag出现位置的函数与加密的伪代码
可以大致确定flag是通过这里进行加密的
进入v9与src之后发现都是以小端序排列的数据低位存放在低地址,高位存放在高地址处,所以需要将之前的字符倒过来看
而key3与key1不存在小端的情况
:::tips
text = join(key3,(const char *)v9)
::key3 –> ‘kills’
::v9 –> ‘wodah’ 倒过来就是 ‘hadow’
text = killshadow
:::
:::tips
strcpy(key,key1) 将key1中的字符串输到key
::key1 –>’ADSFK’
*(_QWORD *)src = ‘SLCDN’; 倒过来是’NDCLS’
strcat(key ,src)将key与src字符连接起来
::src –> ‘SLCDN’
key = key1 + src –> key = ‘ADSFKNDCLS’
:::
正看代码就是:
:::tips
输入自己的字符fake flag,通过getchar()函数赋给v1
后面就是通过对v1的加密得到最后正确答案的flag
加密的过程是str2[v2] = (v1 - 39 - key[v3 % v5] + 97) % 26 + 97;
所以说逆推v1的话就是v1 =str2[v2]-97+26+key[v3%v5]-58
并且根据上面来看我们需要得到的是v1的ASCII的编码的数值,需要将v1算出来是一个整数的形式
所以v1 = ord(str2[v2]-97)+26*i+ord(key[v3%v5])-58
:::
:::tips
同样要注意的是这里的小写字母要全改为大写字母
:::
1 | printf("Please input your flag:"); |
所以通过伪代码逆推写出得到flag的脚本
1 | str2 = 'killshadow' |
‘’’这里要给v1数值留作填充,观察到str2和key都是十位所以也是十位’’’
###flag{KLDQCUDFZO}
在flag加密:str2[v2] = (v1 - 39 - key[v3 % v5] + 97) % 26 + 97;
这里一开始没想通,所以花了蛮长时间去理解
1 | text = 'killshadow' |
当然还有C的脚本,但一般都使用python来写脚本
小端序问题、拼接问题
Java逆向解密
下载下来的题目是以class后缀的java语言的程序,所以使用JD-GUI的工具,将文件拖进去之后能够看到程序的整个过程
感觉像是异或处理
1 | key = [180, 136, 137, 147, 191, 137, 147, 191, 148, 136, |
###flag{This_is_the_flag_!}
JD-GUI[Java XOR逆向]
[GXYCTF2019]luck_guy
查看字符后好像找到了一半的flag
f1=GXY{do_not_
进入get_flag()的函数,可以看到flag的加密过程
通过观察flag的排序应该是case4->case5->case1最后能够得到flag的另一半f2
case4中将"icug
of\x7F”`复制给s,s给f2赋值,值得注意的是,计算机一般都是小端序排列,所以如果直接用字符的话肯定会报错,除非将字符倒过来,或者直接将字符转为ASCII编码再转为16进制的数。
case5中将上面的f2进行加密,可以看出是将字符按奇偶分开。
case1中是将f1与f2拼接起来
写脚本
1 | f1 = 'GXY{do_not_' |
##flag{do_not_hate_me}
上面的操作不知道是怎么回事,小端序被去了,忘记录屏看了呜呜呜~~
第二次重新开的时候就发现问题了
按下R之后变成了这串字符,这里就需要用到小端序了,所以第二个脚本就这样来的,这样看的话应该就是第一个开的姿势不对,什么勾八题目(╯▔皿▔)╯!!
1 | f2 = '\x7Ffo`guci'[::-1] #a[::-1]就是将序列a中的元素翻转 |
[BJDCTF2020]JustRE
直接搜索字符串,找到突破点
直接点击进入之后,发现sprintf()的函数直接将19999和0传入到占位符%d%d中,所以最后的flag是BJD{1999902069a45792d233ac}
###flag{1999902069a45792d233ac}
纯拼接类题目
刮开有奖
找到关键函数 DialogFunc ,其中strlen(String)==8
可知String是8位的字符,第57行的”U g3t 1T” “@_@”(you get it!)可知上面是程序加密的过程。
还能看到寄存器的位置也是连续的,以16进制的算法,
所以猜想函数sub_4010F0
是对上面的数字的数组化处理,并且暂时当作char类型来处理,
写脚本将上面的数字转换成字符
1 | ec = [90,74,83,69,67,97,78,72,51,110,103] |
进入sub_4010F0
函数 求出需要的v字符是3CEHJNSZagn
1 |
|
对应的变量
3 | C | E | H | J | N | S | Z | a | g | n |
---|---|---|---|---|---|---|---|---|---|---|
v7[0] | v7[1] | v8 | v9 | v10 | v11 | v12 | v13 | v14 | v15 | v16 |
通过返回的条件
String[0] = ‘3’+34= 51+34=85=’U’
String[1] = ‘J’
String[2] = (3v8+141)/4 =87 = ‘W’
String[3] = 2(v13/9)*4 = ‘P’
flag的前四位我们已经得出来了,还有后面的四位,查看v4 v5发现是base64加密的方法,直接用在线工具进行解密,得到v5 =’WP1’ v4= ‘jMp’,直接组装就得到flag了~~
###flag{UJWP1jMp}
这道题花了蛮长时间去理解 但是第一次真正写出能跑的脚本还是蛮高兴滴~
简单注册器
下载发现是一个apk的包,工具打开之后发现有一串可以的字符可以利用"dd2940c04462b4dd7c450528835cca15"
除此之后好像都看不懂了,使用jeb查看源代码
一开始没出来,右键点击MainActivity解析之后才出现能够看懂的代码
这样就可以清楚的看到flag的加密过程,解出v5是关键。
写脚本
1 | v11 = 0x1F |
###flag{59acc538825054c7de4b26440c0999dd}
[GWCTF 2019]pyre
下载后发现是一个pyc文件,直接用网站来进行对pyc文件的转换,转为可以阅读的py文件。
pyc文件是python文件经过编译后的二进制文件
py文件的原文是
1 | #!/usr/bin/env python |
isinstance(object, classinfo)
如果参数object是classinfo的实例,或者object是classinfo类的子类的一个实例, 返回True。如果object不是一个给定类型的的对象, 则返回结果总是False。
str与string
String是一个模块,str是一个字符类型
写脚本
1 | code = ['\x1f','\x12','\x1d','(','0','4','\x01','\x06','\x14','4', |
###GWHT{Just_Re_1s_Ha66y!}
pyd的逆向题
[ACTF新生赛2020]easyre
下载后的文件发现是加了upx壳的,所以首先要脱壳
查看这里_data_start__函数
看见qmemcpy(v4, “*F’"N,"(I?+@”, sizeof(v4));这条函数,将”*F’"N,"(I?+@”拷贝至v4中去,
着眼观察for循环,看出最后的flag长度应该是12,将flag的ascii值作为下标取值,与v4数组进行比较,利用v4数组在_data_start__中找位置,就是要找的flag
v4的数组是这样的[42, 70, 39, 34, 78, 44, 34, 40, 73, 63, 43, 64],使用python脚本进行转换
1 | enc = ("*F'\"N,\"(I?+@") |
写解题脚本
1 | #-*- coding:utf8- -*- |
###flag{U9X_1S_W6@T?}
find()函数
findit
直接用GDA打开之后找主函数,
发现一行像flag的字符串,直接将字符串扒下来是pvkq{m164675262033l4m49lnp7p9mnk28k75}
感觉像是凯撒密码,直接用解凯撒密码的在线工具,解出来flag
###flag{c164675262033b4c49bdf7f9cda28a75}
rsa
非对称算法(不传递密钥)
存在两个密钥:公钥和私钥
<1>c = (m ^ e) % n ,
=>E N组成一个公钥对(n,e)
<2> m = (c ^ d) % n
=>d是私钥
:::tips
应用流程
- 选取两个较大的互不相等的质数p和q,计算n=p*q
- 计算phi=(p-1)*(q-1)
- 选取任意e,使得e满足1<e<phi且gcd(e,phi)==1
- 计算e关于n的模逆元d,且d满足(e * d)% n ==1
- 加解密c = (m ^ e) % n , m = (c ^ d) % n 。其中m为明文,c为密文,(n,e)为公钥对,d为私钥,要求 0 <= m < n
:::
首先获得两个不知道怎么看的文件,直接将两个文件的后缀名改为txt,查看文件的内容,
使用在线工具SSL在线工具-公钥解析 将解rsa要用的e和n解出来
这里需要用到大整数的分解工具yafu
将解出来的p(n)使用factor()进行大整数的分解n得到p和q
p=285960468890451637935629440372639283459
q=304008741604601924494328155975272418463
利用代码求解
1 | import gmpy2 |
这里的首先是文件的命名问题,不要用与模板名字相同的名字命名一个新文件
其次是文件的引用,应该是题目中给的文件而不是说自己去建一个新的文件
[ACTF新生赛2020]rome
下载文件后找关键的字符函数,找到主函数后进行分析
1 | int func() |
第一个while循环
65-90的数,第二个循环是97-122的数,对大小写字母分别加密
第二个while循环是比较
1 | a='ACTF' |
跑出来的脚本再包上flag
###flag{Cae3ar_th4_Gre@t}
CrackRTF
看了writeup知道这道题是与winapi有关的题目,两个加密检验的部分
strcat()函数是字符串的拼接
进入主函数开始分析
先分析第一段,输入字符Destination,判断长度是否是6,感觉没啥,继续往下读,往下读发现读不通了,全是粉色的函数
点进sub_40100A函数中会发现更多的粉色函数,直接上网搜每一个函数大概是什么意思
CryptAcquireContextA:连接CSP,获得指定CSP的密钥容器的句柄
CryptCreateHash:启动数据流的散列。它创建并向调用应用程序返回加密服务提供程序 (CSP) 哈希对象的句柄。此句柄用于后续调用 CryptHashData 和 CryptHashSessionKey 以散列会话密钥和其他数据流。
CryptHashData:将数据添加到指定的哈希对象。可以多次调用此函数和 CryptHashSessionKey 来计算长数据流或不连续数据流的哈希值。
CryptGetHashParam:检索控制散列对象操作的数据。可以使用此函数检索实际散列值。
wsprintfA:将一系列的字符和数值输入到缓冲区。输出缓冲区里的的值取决于格式说明符(即”%”)。
lstrcatA:该函数将字符串lpString2附加在另一个字符串lpString1后面。
CryptDestroyHash:销毁由 hHash 参数引用的哈希对象。散列对象被销毁后,就不能再使用了。
CryptReleaseContext:释放加密服务提供程序 (CSP) 和密钥容器的句柄。
后面将Destination和”@DBApp”,v3赋值为Destination的长度,
[FlareOn4]login
M1–读代码解密
下载好了之后是这样的两个文件,
打开Description.txt文本文件给了flag的格式是以邮箱的形式
打开login.html文件可以发现下面的源代码里有加密的代码逻辑如下
1 | document.getElementById("prompt").onclick = function () |
看到一条关键信息的加密String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26);
看上去是一个超级三目运算,输入的c经过加密后输出
这样看的话很烦看着,所以直接设未知字符串enc1=(c <= "Z" ? 90 : 122)
判断输入的字符是大写还是小写,并且对应着’z’的ascii’码enc2=(c = c.charCodeAt(0) + 13)
将当前的字符的第一个字符的ASCII值+13再赋给enc2
js的charCode()方法
返回字符串第一个字符的 Unicode 编码(H 的 Unicode 值):
e.g:var str = "HELLO WORLD"; var n = str.charCodeAt(0);
输出结果:72
tips:
Unicode编码与Ascii编码之间的区别是一个是2个字节一个1个字节,统一为Unicode编码是为了不出现乱码的情况
最后的三目运算就是enc1>=enc2?enc2:enc-26
最后加完的值大于等于当前的”Z”,如果是执行enc2,即是+13后的结果,如果不是的话就是-26,反过来用flag的字符就是-13后小于当前字符对应的A时,就+26,否则就-13,写脚本
1 | enc = 'PyvragFvqrYbtvafNerRnfl@syner-ba.pbz' |
M2–ROT13的利用
没想啊,竟然是ROT13的加密
一个是+13一个是-26就是正巧是ROT-13。
只受26个字母的影响,数字、符号、空白以及其他的所有字元都不变( 英文字母的字符换成它13位之后的字符,越界之后再折回来)。
直接将获得的密文进行ROT13解码就行了,ASCII对照表可以方便数清字母之间相差的位数
ABCDEFGHIJKLMNOPQRSTUVWXYZ
NOPQRSTUVWXYZABCDEFGHIJKLM
flag{ClientSideLoginsAreEasy@flare-on.com }
[2019红帽杯]easyRE
查看字符串,点进提示字符所在的函数之后发现在主函数的伪代码
经过大佬的讲解貌似是经过十次base64的加密,最后与后面的字符进行比较,比较成功就会显示”You found me!!!
“
b64decode()
将二进制字符串解码为正常形式
Youngter-drive
下载后发现是被upx加壳的文件,直接脱壳,而且是32位的,直接放在ida里进行分析
分析主函数
相册
下载后是一个apk文件,放在模拟器里面发现打不开文件,所以直接使用GDA打开apk分析文件
查看所有字符后猜测是base64加密的,查看C2之后发现用到了NativeMethod方法,再进行base64的解密
Native Method
是一个java调用非java代码的接口,是由非java语言实现的。
我们可以在一个native method的本地实现中访问所有的java特性,但是这要依赖于你所访问的java特性的实现,而且这样做远远不如在java语言中使用那些特性方便和容易。
native method的存在并不会对其他类调用这些本地方法产生任何影响,实际上调用这些方法的其他类甚至不知道它所调用的是一个本地方法
找到.so文件右键导出(找了半天才找到怎么操作这个东西)
将.so文件放在ida32位里面进行分析,发现有三串感觉像是base64加密后的密文,直接用在线转换的工具转换。
后面按照题目的意思flag是一个邮箱,所以flag应该是18218465125@163.com ,包上flag{}就正确了
###flag{18218465125@163.com }
BUUCTF平台MISC刷题20221217
二维码
下载后是一张照片,二维码的图片,直接用软件破解后填入是错的
直接将图片放入到010editor进行分析,发现这个图片里包含着压缩包,kali中的binwalk进行分离binwalk -e xx.png
分离出压缩包且是加密的,使用爆破命令fcrackzip -b -c 1 -l 4-4 -u /home/kali/Desktop/1D7.zip
4-4是由于文件打开后的文本文件写的是4位数的密码所以是4,得到解压密码是7639 解压出来密码得到了flag文件
##flag{vjpw_wnoei}
N种方法解决
下载后的文件执行不了,直接拖入010editor中进行分析
根据开头的data:image/jpg;base64知道应该是将base64的编码转换成照片
二维码扫描工具
将KEY换成flag就好了
##flag{dca57f966e4e4e31fd5b15417da63269}
大白
显示屏幕太小,所以大概是长宽被改变了。
放入010之后也报了CRC错误,改变图片的高度与宽度相同 02 A7,得到flag
###flag{He1l0_d4_ba1}
wireshark
根据题目的提示
登录后的流量包是POST的,只有这一条是符合条件的
找到管理员的密码得到flag
###flag{ffb7567a1d4f4abdffdb54e022f8facd&captcha}
rar
下载后是一个压缩包,直接进行密码的爆破,4位纯数字,设置好后等待,接触的密码是8795,打开加密的压缩文件得到flag
###flag{1773c5da790bd3caff38e3decd180eb7}
zip伪加密
下载得到加密的压缩包,如果像上面的题目的话,我们是得不到密码的
放入010editor看看
这里需要补充知识点
文件的头文件类型
一个 ZIP 文件由三个部分组成: 压缩源文件数据区+压缩源文件目录区+压缩源文件目录结束标志
1、压缩源文件数据区
在这个数据区中每一个压缩的源文件/目录都是一条记录,记录的格式如下:[文件头+ 文件数据 + 数据描述符]
a 文件头格式
组成 长度
文件头标记 4 bytes (0x04034b50)
解压文件所需 pkware 版本 2 bytes
全局方式位标记 2 bytes
压缩方式 2 bytes
最后修改文件时间 2 bytes
最后修改文件日期 2 bytes
CRC-32校验 4 bytes
压缩后尺寸 4 bytes
未压缩尺寸 4 bytes
文件名长度 2 bytes
扩展记录长度 2 bytes
文件名 (不定长度)
扩展字段 (不定长度)
b、文件数据
c、数据描述符
组成 长度
CRC-32校验 4 bytes
压缩后尺寸 4 bytes
未压缩尺寸 4 bytes
这个数据描述符只在全局方式位标记的第3位设为1时才存在(见后详解),紧接在压缩数据的最后一个字节后。这个数据描述符只用在不能对输出的 ZIP 文件进行检索时使用。例如:在一个不能检索的驱动器(如:磁带机上)上的 ZIP 文件中。如果是磁盘上的ZIP文件一般没有这个数据描述符。
2、压缩源文件目录区
在这个数据区中每一条纪录对应在压缩源文件数据区中的一条数据
组成 长度
目录中文件文件头标记 4 bytes (0x02014b50)
压缩使用的 pkware 版本 2 bytes
解压文件所需 pkware 版本 2 bytes
全局方式位标记 2 bytes
压缩方式 2 bytes
最后修改文件时间 2 bytes
最后修改文件日期 2 bytes
CRC-32校验 4 bytes
压缩后尺寸 4 bytes
未压缩尺寸 4 bytes
文件名长度 2 bytes
扩展字段长度 2 bytes
文件注释长度 2 bytes
磁盘开始号 2 bytes
内部文件属性 2 bytes
外部文件属性 4 bytes
局部头部偏移量 4 bytes
文件名 (不定长度)
扩展字段 (不定长度)
文件注释 (不定长度)
–>这里是有判断是否存在加密情况的判断点
识别真假加密
无加密
压缩源文件数据区的全局加密应当为00 00
且压缩源文件目录区的全局方式位标记应当为00 00
假加密
压缩源文件数据区的全局加密应当为00 00
且压缩源文件目录区的全局方式位标记应当为09 00
真加密
压缩源文件数据区的全局加密应当为09 00
3、压缩源文件目录结束标志
组成 长度
目录结束标记 4 bytes (0x02014b50)
当前磁盘编号 2 bytes
目录区开始磁盘编号 2 bytes
本磁盘上纪录总数 2 bytes
目录区中纪录总数 2 bytes
目录区尺寸大小 4 bytes
目录区对第一张磁盘的偏移量 4 bytes
ZIP 文件注释长度 2 bytes
根据题目说是伪加密
直接将上面提及的09 00 改为00 00改好后保存,这时压缩包就可以打开了,得到flag文件
###flag{Adm1N-B2G-kU-SZIP}
被嗅探的流量
抓取的文件传输的数据,直接寻找POST包http.request.method==POST
跟踪TCP流找到flag
###flag{da73d88936010da1eeeb36e945ec4b97}
安全测试员职业技能赛
01hidden key
赛题:
1 | from Crypto.Util.number import * |
由题目可知,key左移12位,因此在2669175714787937右移12位后减去4096到加上4096长度的区间内进行爆破,解题脚本:
1 | from Crypto.Util.number import * |
other method
1 | print(bytes_to_long(key)>>12) |
脚本
1 | from Crypto.Util.number import * |
02bad python
010打开附件,发现缺少pyc文件头,由附件名可知,缺少的是python3.6版本的pyc文件头,于是补全,然后得到在线反编译代码:
1 | #!/usr/bin/env python |
可知是tea加密,写出解密脚本:
1 |
|
然后转成byte类型组合在一起可得Th1s_1s_A_Easy_Pyth0n__R3veRse_0
flag{Th1s_1s_A_Easy_Pyth0n__R3veRse_0}
03ereeee
下载附件,打开ida分析可知是rce加密和base64换表加密
解题脚本:换表base64:
1 | import base64 |
rc4解密:
1 | import base64 |
得flag{RC_f0ur_And_Base_s1xty_f0ur_Encrypt_!}
将flag{}包裹的字符串md5后再用flag{}包裹得flag{7d3357ea9ae1a4b2746147bc053c190d}
- Title: 刷题记录
- Author: Juana_2u
- Created at : 2023-09-13 21:29:43
- Updated at : 2023-10-14 11:43:16
- Link: https://juana-2u.github.io/2023/09/13/刷题记录/
- License: This work is licensed under CC BY-NC-SA 4.0.