REVERSE
NSS茶馆!
IDA打开文件,发现是对输入的内容进行了tea加密
dword_CEE980
是输入的内容,unk_CEE000
是加密的密钥,加密结束后和dword_CEE010
进行比较,相同则为正确的FLAG
加密函数:
发现是修改了加密轮次和delta
编写解密代码
1 |
|
输出:
这里需要每4组字符进行一下逆序才能得到最终的FLAG:NSSCTF{tea_is_so_easy!!}
md5也能爆破?
IDA打开查看
经调试发现程序的核心代码为第20行,跟进
sub_651596
函数实际就是md5的加密函数,传进去的a1+8
用来存储md5计算的结果,a1+24
就是对应的明文
举个例子,如果输入14851234
那么第一轮传进去的就是用于md5加密的原始幻数和1485
结束sub_8C1596
函数后出来的就是1485的md5值7fb8ceb3bd59c7956b1df66729296a4c
然后第二轮进去的值就是第一轮出来的md5作为幻数和第二组输入的1234
一共10轮得到10个md5值,最后与byte_8D5050
进行匹配
1 | 7fb8ceb3bd59c7956b1df66729296a4c |
第一轮7fb8ceb3bd59c7956b1df66729296a4c
利用cmd5可直接解出为1485,第二轮由于幻数变了所以无法直接获取
解决办法就是自己修改md5的幻数然后进行四位数字的爆破
一开始是打算用python实现,找了个python的md5加密代码
1 | import binascii |
通过修改幻数发现计算结果和调试出来的结果不一样,遂改用c语言直接复制IDA中的伪代码进行加密
思路是用伪代码中加密逻辑进行加密,每一轮控制输入的幻数,爆破明文数字
这里以爆破最后一轮也就是第10组数字的代码为例(代码写得比较小学生,看个乐子):
1 |
|
输出:
得到最后一轮的数字为4261
最终的flag为NSSCTF{1485059684930231436754585566745634684261}
Reverse_or_Pwn
这个题目运行需要输入flag和key,利用IDA打开发现FLAG只是一个简单异或算法
解密POC:
1 | v8 = [0x2E,0x0D,0x1E,0x17,0x03,0x4F,0x2C,0x02,0x61,0x41,0x53,0x57,0x1E,0x21,0x57,0x44] |
提交发现flag不对!!!
看了下IDA提取的字符串,发现存在另一个函数
这个函数没有被其他函数引用,但是最终输出的内容和FLAG有关所以尝试解密
从字符串特征可以看到是base64加密,但每轮加密都替换了base64的原始字符顺序,解密脚本如下:
1 | def base64_encode(input_bytes,BASE64_CHARS): |
提交发现也不对,结合最后的输出内容发现前面的This_is_reserve?
是最终flag的前16个字符串,从第20位才是后面解出来的Pwn_and_base_is_so_Easy!
中间差了3位,结合题目信息flag是NSSCTF{your input}
推断和输入的key有关(实际完全没关系,但还是讲一下,代码不能白写)
解密代码如下:
1 | def fun(a1,a2): |
发现好像也不对,结合程序的提示信息12-16
以及题目名反应过来和pwn有关,主函数在最下面调用了fun1,在fun1中将16位的FLAG赋值给了长度为12的Destination
,而Destination
距离rbp栈上的返回地址刚好差了16位
所以需要利用栈溢出修改rbp的返回函数使其跳转到那个fun函数,函数地址是0x00402463
对应需要输入的字符串为c$@
,刚好三位
最终
提交后发现不对,问了下出题人,发现是因为跳转到0x00402464
也可以实现最终的效果,对应字符串d$@
所以FLAG为:NSSCTF{This_is_reserve?d$@Pwn_and_base_is_so_Easy!}
好像也是py?
利用pycdc.exe
进行反编译,但是这里的magic头有问题,所以需要先修改magic头
本地不同的python版本的头都不一样,可以打开一个本地的pyc文件查看
然后直接反编译即可,也可以使用在线反编译工具
1 | # Visit https://www.lddgo.net/string/pyc-compile-decompile for more information |
发现有两处函数无法反编译成功,所以利用pycdas反编译成opcode解析
得到opcode后针对两处无法反编译的代码可利用ai工具解读
1 | def x(text, key): |
simple_add_sub
有个shift变量有点奇怪,没找到具体的值是什么,但没关系我们可以爆破
最终的POC
1 | def generate(key, text): |
输出内容中找到符合flag的字符串
动态调试
直接将IDA获取的伪代码复制过来解密即可
POC:
1 |
|
输出:
这来做点数学题吧
解多元一次方程组
payload
1 | from scipy.optimize import fsolve |
得到FLAG:NSSCTF{Z3_Is_So_Easy}
CRYPTO
脚本跑不出来了吧
题目:
1 | e1*e2= 59653 |
首先分解质因数
1 | import math |
共模攻击
1 | import gmpy2 |
payload:
1 | e1e2= 59653 |
lf_rsa_sr
题目:
1 | from Crypto.Util.number import * |
这道题考了两个知识点,RSA和LFRS,RSA可参考https://blog.csdn.net/m0_73725283/article/details/134702242z中的P6.py
RSA_PAYLOAD:
1 | from Crypto.Util.number import * |
RSA解得后半段的FLAG,接下来解LFSR
关于LFSR(线性反馈移位寄存器)之前没怎么遇到过,这里重点学习一下
一个标准的LFSR代码如下:
1 | def lfsr(R,mask): |
代码解释起来有点复杂,原理和详细的解释可参考CTF中的LFSR考点(一)
我们这里可以通过找规律
来快速明白这段代码究竟做了什么。
代码传入两个参数R和mask,传出两个参数output和lastbit,如果将函数看作一个加密流程的话,R可以理解为明文,mask为密钥,output和lastbit均为密文,同时output一般会作为下一轮加密的明文。
若mask的第i位(从右开始数)为1,则将R的i位参与异或之后得到lastbit,R左移一位并将lastbit放入最右侧得到output。
举个例子:mask=0b0011,R=0b1101
mask的第一位和第二位为1,则将R的第一位和第二位异或得到0^1=1
,所以lastbit就等于1,output就等于((R<<1) & 0xffffff)+lastbit=0b11011
多循环几轮就能很明显的看出规律了,并且可以发现在经过32轮循环后,output的值从左往右排分别就是第1轮到第32轮的lastbit(由于R最高32位,所以如果mask超过了32位,后续的位数不用管)
1 | def lfsr(R,mask): |
输出
带着这个规律我们回到题目,题目中的提示
1 | a = getPrime(512) |
这块似乎没什么用,我们可以删掉
R为1,mask未知,key.txt内的内容是每一轮的lastbit和flag异或得到的,想要知道flag,得到lastbit即可解出
1 | key="0111100010111111010100001001110010000010010110011010000000010000111010100110000101000100010011010111000110110000110101001101111100110010000010010010011010001011111000010101001110000001011000011000001010011101100100111100110110001000111111001000100110011011111001001001110011110001110001011000101" |
思路:我们需要知道mask的哪些位为1,猜测flag的前几位为NSSCTF{
,利用这个条件我们可以得到前几位的lastbit值,然后利用lastbit进一步得到mask来推出后续的lastbit
1 | from Crypto.Util.number import * |
判断逻辑:
初始的R为0b00000000000000000000000000000001,通过key和flag异或得到第一个lastbit为1,可推出mask的第一位是1(如果mask第一位是0的话则R的第一位不参与异或,而R的其他位都是0,异或不可能为1)
第一轮结束后R的值为0b00000000000000000000000000000011,第二个lastbit为1,可推出mask的第二位是0(如果mask第二位是1的话,R的第二位就要参与异或,1^1=0,而lastbit为1,显然不符)
第二轮结束后R的值为0b000000000000000000000000000000111,第三个lastbit为1,可推出mask的第三位是0
第三轮结束后R的值为0b000000000000000000000000000001111,第三个lastbit为0,可推出mask的第四位是1
以此类推,上面程序输出的[0, 3, 4, 5, 8, 11, 12, 13, 16, 18, 20, 21, 23, 28, 29, 31]指的是mask中值为1的小标,也就是说mask中第1, 4, 5, 6, 9, 12, 13, 14, 17, 19, 21, 22, 24, 29, 30, 32位为1,利用这个条件我们可以解出后续的lastbit
paylaod:
1 | from Crypto.Util.number import * |
最终得到flag:NSSCTF{It1s_As_simPle_a3_It_lo0ks}
flag_where
1 | n = 96294984374753089080583610747240203389088051930341615602841335596072081913930052484580770899610689065293206976889303327507604080242460321817406117877072425663471808350427893332726383611142246218026112051129578226014713958882307096859175042839198895428723757405196020266267824586199807170149650434306779718677 |
解压压缩包得到一张图片,用010打开发现底部存在e、n和c
1 | n= 23613440611274671981103562117762261407657733032262530808365808460802265777404559689770212205664056651689849304576137012508441030782562719114824768464775304331357062635791932441797662757831660552844003759285119574349208127247279359306048367901670313942242293660325209254802053786362409960527461289549743183131793123086882884499956331652650601135698081029508500034843745858862415689306278980446458807168395881472172471730255424421648602988543988586027701543404817919626933524670439936405497236264107287151288962539180486176914120546870484668665264576021156708057529615192067896540955731053282702775053130195750940441499 |
e很小,直接上poc
1 | n= 23613440611274671981103562117762261407657733032262530808365808460802265777404559689770212205664056651689849304576137012508441030782562719114824768464775304331357062635791932441797662757831660552844003759285119574349208127247279359306048367901670313942242293660325209254802053786362409960527461289549743183131793123086882884499956331652650601135698081029508500034843745858862415689306278980446458807168395881472172471730255424421648602988543988586027701543404817919626933524670439936405497236264107287151288962539180486176914120546870484668665264576021156708057529615192067896540955731053282702775053130195750940441499 |
PWN
nocat
通过nc连接之后直接就能看到flag,但不允许使用cat命令
测试发现是过滤了cat关键字
通过shell变量拼接绕过
兄弟你的环境好香?
通过ida打开可看到
程序读取用户的输入,但变量长度只有80,最大输入长度为0x100,可尝试栈溢出
发现后门函数
编写payload
1 | from pwn import * |
这里的返回地址不能写
0x004011dd
,因为Ubuntu18.04 64位 和 部分Ubuntu16.04 64位 调用system的时候,rsp的最低字节必须为0x00(栈以16字节对齐),否则无法运行system指令。
不是哥们ret2text还阴啊?
这道题有点迷糊
首先用ida打开看到程序v1数组长度为56,却可以循环读取900个字符
发现后门程序
变量位置
一开始想得很简单,返回地址与v1的距离是0x40+0x8=0x48
,写了个poc发现没打成功,于是开始利用IDA配合gdbserver远程调试
首先在kali中开启gdbserver监听
1 | sudo apt install gdbserver |
IDA选择远程服务器
打下断点开始调试,然后就发现了是因为变量i的缘故,当i=60的时候v2 = read(0, &v1[i], 1uLL);
修改的v1[60]恰好是i的值,所以i就变成了0x41也就是65,
所以下一次修改的值为v1[65],而不是v[61],所以我们该传入0x48-(65-60)=0x43
个A,然后在传入要跳转的地址。
payload:
1 | from pwn import * |
出题人你到底干了什么?
题目暗示使用ret2lib
附件中给出了attachment和libc.so.6
思路:attchment中使用了write函数,可通过write函数的真实地址-write函数在libc.so.6中的偏移地址得到libc.so.6的基地址,然后利用libc.so.6中的system和/bin/sh得到shell
首先我们来找write函数的真实地址,payload如下
1 | payload = b"a" * offset #垃圾数据的填充 |
write函数原型是
int write(int fd, const void *buf, size_t count)
,具体对应的寄存器可见https://shell-storm.org/shellcode/files/linux-4.7-syscalls-x64.html
首先是pop_rdi_ret_addr和pop_rsi_ret_addr的地址,得到0x401233和0x401231
1 | ROPgadget --binary attachment --only "pop|ret" | grep rdi |
脚本跑出write_got,write_plt和main_addr的地址
1 | from pwn import * |
在attachment中可以看到可控局部变量buf到返回地址的偏移为0x60+0x08=0x68
开始尝试获取真实地址
1 | from pwn import * |
在libc.so.6中找system函数和write函数的偏移地址,得到0x10e280
和0x052290
1 | objdump -T libc.so.6 | grep write |
接下来找/bin/sh
的偏移地址,得到0x1b45bd
1 | ROPgadget --binary libc.so.6 --string '/bin/sh' |
poc:
1 | from pwn import * |