squ1rrel CTF 2025 wp

这个比赛印象还挺深,因为有一道camelcamel第一次用pintool爆破出来,万幸是单字节加密;re最后被ak了,总共4道题

第一道安卓没做看了下很简单

Intermediate Software Design

ida反编译分析c++算法即可

s = list(b"Yu*4-v)mXzjzso$~0vX_&Z   rB~")
print(len(s))
c = [0x00000003, 0x00000003, 0x00000001, 0x00000005, 0x00000032, 0x00000008, 0x00000001, 0x00000009, 0x00000005, 0x00000007, 0x00000009, 0x00000003, 0x00000002, 0x00000002, 0x00000006, 0x00000008, 0x00000005, 0x0000001E, 0x00000006, 0x00000003, 0x00000003, 0x00000001, 0x00000005, 0x00000004, 0x00000008, 0x00000001, 0x00000009, 0x00000005, 0x00000007, 0x00000009, 0x00000003, 0x00000002, 0x00000002, 0x00000006, 0x00000008, 0x00000005, 0x00000004, 0x00000006, 0x00000003, 0x00000003, 0x00000001, 0x00000005, 0x00000004, 0x00000008, 0x00000001, 0x00000009, 0x00000005, 0x00000007, 0x00000009, 0x00000003, 0x00000002, 0x00000002, 0x00000006, 0x00000008, 0x00000005, 0x00000004, 0x00000006, 0x00000003, 0x00000003, 0x00000001, 0x00000005, 0x00000004, 0x00000008, 0x00000001, 0x00000009, 0x00000005, 0x00000007, 0x00000009, 0x00000003, 0x00000002, 0x00000002, 0x00000006, 0x00000008, 0x00000005, 0x00000004, 0x00000006]
for i in range(len(s)):
    s[i] -= (c[i+13]-2)
    s[i] &= 0xff
key = [0x2A, 0x51, 0x63, 0x4D, 0x22, 0x5C, 0x37, 0x7E, 0x19]
for i in range(0, len(s), 2):
    s[i] ^= key[i//2%len(key)]
print(list(map(hex, s)))
print("".join(map(chr, s)))

Russian Games

勾八玩意,写的什么鬼逻辑

题目给了个russian-games.vm (下面的省略了简单函数),里面硬编码的数据经过转字符串发现nand2tetros,查询可知是一种抽象编程语言

function Main.MMMMN 0
    push argument 0
    push argument 1
    not
    and
    push argument 0
    not
    push argument 1
    and
    or
    return

function Main.MMMMM 1
    push argument 1
    pop local 0
    push argument 1
    neg
    push constant 1
    sub
    push argument 0
    and
    pop local 0
    push argument 1
    push argument 0
    not
    and
    push local 0
    or
    return

function Main.MMMNM 2
    push argument 0
    push argument 1
    not
    pop local 0
    not
    push argument 1
    and
    pop local 1
    push local 0
    push argument 0
    and
    pop local 0
    push local 1
    push local 0
    or
    return

function Main.MMNMM 0
    push argument 0
    push argument 1
    not
    and
    push argument 0
    not
    push argument 1
    and
    or
    push constant 0
    return

function Main.MMNMN 1
    push argument 0
    push argument 1
    push argument 0
    not
    and
    push argument 1
    not
    push argument 0
    and
    or
    pop local 0
    return

function Main.MMNNM 1
    push argument 1
    push argument 1
    push argument 0
    not
    and
    push argument 1
    not
    push argument 0
    and
    or
    pop local 0
    return

function Main.NMNNM 0
    push argument 0
    pop pointer 1
    push constant 110
    pop that 0
    push constant 97
    pop that 1
    push constant 110
    pop that 2
    push constant 100
    pop that 3
    push constant 50
    pop that 4
    push constant 116
    pop that 5
    push constant 101
    pop that 6
    push constant 116
    pop that 7
    push constant 114
    pop that 8
    push constant 111
    pop that 9
    push constant 115
    pop that 10
    push constant 0
    pop that 11
    push argument 0
    return

function Main.MMMNN 5
    push argument 0
    pop pointer 0
    push argument 1
    pop pointer 1
    push argument 2
    pop local 1
    push constant 3
    pop local 2
    goto _____

    label _____
    push this 0
    if-goto ____
    goto .

    label ____
    push that 0
    if-goto ___
    goto ______

    label ______
    push argument 1
    pop pointer 1

    label ___
    push this 0
    push that 0
    call Main.MMMMN 2
    push constant 94
    call Main.MMMNM 2
    push constant 132
    call Main.MMNMN 2

    push local 3
    if-goto __
    goto .__

    label _
    push local 2
    push constant 31
    add
    pop local 2
    push local 2
    call Main.MMMMM 2
    push pointer 1
    pop local 0
    push local 1
    pop pointer 1
    pop that 0
    goto .__.

    label .__.
    push pointer 0
    push constant 1
    add
    pop pointer 0
    push pointer 1
    push constant 1
    add
    pop local 1
    push local 0
    push constant 1
    add
    pop local 0
    push local 0
    pop pointer 1
    goto _____

    label .
    push argument 2
    return

    label __
    push local 3
    push constant 1
    add
    pop local 3
    push pointer 1
    pop local 0
    push local 1
    pop pointer 1
    pop local 4
    push local 4
    push local 1
    call Main.MMMMM 2
    push local 1
    call Main.MMMMM 2
    push constant 431
    call Main.MMMNM 2
    pop that 0
    push pointer 1
    push constant 1
    add
    pop local 1
    push local 0
    pop pointer 1
    push local 4
    goto _

    label .__
    push local 3
    push constant 1
    sub
    pop local 3
    goto _


function Main.NNMNM 3
    push argument 0
    pop pointer 0
    push constant 24576
    pop pointer 1
    push constant 0
    pop local 0
    push constant 0
    goto ___

    label ___
    push that 0
    pop local 2
    push local 2
    if-goto ____
    pop local 1
    push constant 0
    goto ___

    label ____
    push local 2
    push constant 128
    eq
    if-goto _____
    push local 2
    eq
    not
    if-goto DIF
    push local 1
    goto ___

    label DIF
    push local 2
    pop local 1
    push local 1
    pop this 0
    push pointer 0
    push constant 1
    add
    pop pointer 0
    push local 0
    push constant 100
    eq
    not
    if-goto ______
    goto _____

    label ______
    push local 0
    push constant 1
    add
    pop local 0
    push local 1
    goto ___

    label _____
    push constant 0
    pop this 0
    push argument 0
    return

function Main.NNMNN 0
    push argument 0
    pop pointer 0
    push constant 97
    pop this 0
    push constant 481
    pop this 1
    push constant 15
    pop this 2
    push constant 37
    pop this 3
    push constant 420
    pop this 4
    push constant 116
    pop this 5
    push constant 128
    pop this 6
    push constant 503
    pop this 7
    push constant 229
    pop this 8
    push constant 130
    pop this 9
    push constant 489
    pop this 10
    push constant 189
    pop this 11
    push constant 333
    pop this 12
    push constant 496
    pop this 13
    push constant 358
    pop this 14
    push constant 321
    pop this 15
    push constant 497
    pop this 16
    push constant 297
    pop this 17
    push constant 461
    pop this 18
    push constant 429
    pop this 19
    push constant 439
    pop this 20
    push constant 410
    pop this 21
    push constant 496
    pop this 22
    push constant 428
    pop this 23
    push constant 588
    pop this 24
    push constant 486
    pop this 25
    push constant 632
    pop this 26
    push constant 587
    pop this 27
    push constant 496
    pop this 28
    push constant 560
    pop this 29
    push constant 736
    pop this 30
    push constant 468
    pop this 31
    push constant 726
    pop this 32
    push constant 689
    pop this 33
    push constant 463
    pop this 34
    push constant 651
    pop this 35
    push constant 779
    pop this 36
    push constant 486
    pop this 37
    push constant 864
    pop this 38
    push constant 891
    pop this 39
    push constant 482
    pop this 40
    push constant 810
    pop this 41
    push constant 975
    pop this 42
    push constant 445
    pop this 43
    push constant 951
    pop this 44
    push constant 969
    pop this 45
    push constant 483
    pop this 46
    push constant 943
    pop this 47
    push argument 0
    return

function Main.NNNNN 0
    push argument 0
    pop pointer 0
    push argument 1
    pop pointer 1

    label ___
    push this 0
    push that 0
    eq
    not
    if-goto ____
    push this 0
    push constant 0
    eq
    if-goto _____
    push pointer 0
    push constant 1
    add
    pop pointer 0
    push pointer 1
    push constant 1
    add
    pop pointer 1
    goto ___

    label ____
    push constant 0
    return

    label _____
    push constant 0
    push constant 0
    eq
    return

function Sys.init 0
    push constant 2048
    call Main.NNMNM 1   # 输入
    push constant 4096
    call Main.NMNNM 1   # key
    push constant 10240
    call Main.MMMNN 3   # 加密
    push constant 8192
    call Main.NNMNN 1   # 返回要比较的数组
    call Main.NNNNN 2   # 比较
    pop pointer 0
label WHILE
    goto WHILE              // loops infinitely

代码给ds分析更清楚些(gpt分析有点拉),可以知道前面几个简短函数的功能,均是异或以及数组返回

def MMMMN(a, b):
    return (a & (~b)) | ((~a) & b)

def MMMMM(a, b):
    local0 = (-b - 1) & a
    return (b & ~a) | local0

def MMMNM(a, b):
    local0 = ~b
    local1 = ~a & b
    local0 = local0 & a
    return local1 | local0

def MMNMN(a, b):    # 自己手动分析下,ds给的不对,local0相当于没用
    local0 = (b&(~a))|((~b)&a)
    return a

def MMNNM(a, b):    # 自己手动分析下,ds给的不对,local0相当于没用
    local0 = (b&(~a))|((~b)&a)
    return b

def NMNNM():
    s = ''.join(chr(c) for c in [110, 97, 110, 100, 50, 116, 101, 116, 114, 111, 115])
    return s
  
def NNMNM():
    return input()

def NNMNN():
    data = [
        97, 481, 15, 37, 420, 116, 128, 503, 229, 130,
        489, 189, 333, 496, 358, 321, 497, 297, 461, 429,
        439, 410, 496, 428, 588, 486, 632, 587, 496, 560,
        736, 468, 726, 689, 463, 651, 779, 486, 864, 891,
        482, 810, 975, 445, 951, 969, 483, 943
    ]
    return data

def NNNNN(a, b):
    return a == b

Sys.init重点分析,可以知道输入、加密的位置,最关键的代码是MMMNN函数,里面很多跳转,同时调用了很多异或操作,下面是我一点点扣代码将逻辑转为了python,并使用了flag头作为测试

def func(arg0, arg1, arg2):
    pointer0 = arg0  # this指针基地址
    pointer1 = arg1  # that指针基地址
    local_vars = [0, arg2, 3, 0, 0, 0]

    p0 = 0
    p1 = 0
    while True:
        # 标签_____
        this0 = pointer0[p0]
        if this0 == 0:
            print(pointer0)
            return arg2
        # 标签____
        that0 = pointer1[p1]
        if that0 == 0:
            # 标签______: 重置pointer1到arg1
            pointer1 = arg1
            that0 = pointer1[0]
            p1 = 0
        # 标签___: 计算异或结果
        xor1 = this0 ^ that0
        xor2 = xor1 ^ 94
        res = xor2  # MMNMN返回第一个参数,忽略第二个参数132
        # 检查local3
        if local_vars[3] != 0:
            # 标签__: 处理local3非零的情况
            local_vars[3] += 1
            local_vars[0] = p1
            p1 = local_vars[1]
            local_vars[4] = res
            xor3 = local_vars[4] ^ local_vars[1]
            xor4 = xor3 ^ local_vars[1]
            xor5 = xor4 ^ 431
            pointer0[p0] = xor5
            local_vars[1] = p1 + 1
            p1 = local_vars[0]
        else:
            # 标签.__: local3减1
            local_vars[3] -= 1
        # 标签_: 处理local2和存储结果
        local_vars[2] += 31
        # 计算异或并存储到that0
        xor_local = res ^ local_vars[2]
        # 调整指针和局部变量
        local_vars[0] = p1
        p1 = local_vars[1]
        if local_vars[3] != 0:    # 函数里两个pop that经过测试发现只能取一个,所以这道题目有一个奇偶不同处理
            pointer0[p0] = xor_local    # 判断点在于local_vars[3],初始为0,然后-1然后0然后-1做不同操作
        p0 += 1
        local_vars[1] = p1 + 1
        local_vars[0] += 1
        p1 = local_vars[0]

func(list(b"squ1rrel{\x00"), list(b"nand2tetros\x00"), 10240)

输出终于让我看到了一点眉目

[97, 481, 37, 420, 128, 503, 130, 489, 333, 0]
[97, 481, 15, 37, 420, 116, 128, 503, 229, 130,
489, 189, 333, 496, 358, 321, 497, 297, 461, 429,
439, 410, 496, 428, 588, 486, 632, 587, 496, 560,
736, 468, 726, 689, 463, 651, 779, 486, 864, 891,
482, 810, 975, 445, 951, 969, 483, 943]

和data对比发现每三个一组前两个对应flag加密完的结果,勾八的那第三个是干啥用的

解密就很简单了

key = b"nand2tetros"
data = [
        97, 481, 15, 37, 420, 116, 128, 503, 229, 130,
        489, 189, 333, 496, 358, 321, 497, 297, 461, 429,
        439, 410, 496, 428, 588, 486, 632, 587, 496, 560,
        736, 468, 726, 689, 463, 651, 779, 486, 864, 891,
        482, 810, 975, 445, 951, 969, 483, 943
    ]
new_data = []
for i in range(0, len(data), 3):
    new_data += data[i:i+2]
data = new_data
for i in range(len(data)):
    if i % 2 == 0:
        data[i] ^= (3+31*(i+1))
    else:
        data[i] ^= 431
    data[i] ^= 94
    data[i] ^= key[i%len(key)]
print("".join(map(chr, data)))  

squ1rrel{n4nd2t3tr1s_VM_1s_gr8!}

做题思路:这题算是一种vm,见多了就知道,必须一步步分析,碰到控制流这种就得多测试数据,一点点调看输入输出怎么变化的

camelcamelcamel

读ida反编译代码读了一晚上,ocaml编译得,和haskell貌似一样的抽象语言

最后才想到可以单字节爆破,使用了pintool,魔改了下原来的inscount0.cpp脚本如下,使得其在运行程序完直接输出跑的指令数,需要编译成so文件,具体可以参考https://www.cnblogs.com/level5uiharu/p/16963907.html

/*
 * Copyright (C) 2004-2021 Intel Corporation.
 * SPDX-License-Identifier: MIT
 */

#include <iostream>
#include <fstream>
#include "pin.H"
using std::cerr;
using std::endl;
using std::ios;
using std::ofstream;
using std::string;
using namespace std;
ofstream OutFile;

static UINT64 icount = 0;

VOID docount() { icount++; }

VOID Instruction(INS ins, VOID* v)
{
    INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)docount, IARG_END);
}

KNOB< string > KnobOutputFile(KNOB_MODE_WRITEONCE, "pintool", "o", "inscount.out", "specify output file name");

VOID Fini(INT32 code, VOID* v)
{
    // 修改这里:删除"Count "字符串,直接输出icount
    cout << icount << endl;
    OutFile.close();
}

INT32 Usage()
{
    cerr << "This tool counts the number of dynamic instructions executed" << endl;
    cerr << endl << KNOB_BASE::StringKnobSummary() << endl;
    return -1;
}

int main(int argc, char* argv[])
{
    if (PIN_Init(argc, argv)) return Usage();

    OutFile.open(KnobOutputFile.Value().c_str());

    INS_AddInstrumentFunction(Instruction, 0);
    PIN_AddFiniFunction(Fini, 0);

    PIN_StartProgram();

    return 0;
}

然后就要借助pwntool,这题其实比较幸运,只能输入40位否则报错,同时每次检查单字节走过的指令数相同

import string
from pwn import *
import time

context.log_level = 'error'  # 减少调试输出
total_length = 40
# 优先使用常见字符提高效率,经测试主要是小写+数字+下划线+大写字母
charset = "_{}" + string.ascii_lowercase + string.digits + string.ascii_uppercase + "!?@#*+-"
# prefix = "squ1rrel{0caml_1s_c00l_4nd_we1rd_nU8X3N"
prefix = "s"
s = "s"+"!"+"A"*38
p = process(["./pin -t inscount0.so -- ./camelcamelcamel"], shell=True)

p.sendline(s.encode())
out = p.recvall().decode()
print(s)
print(out, end="")
count = int(out.split("\n")[1])
max_count = count
time_start = time.time()
while len(prefix) < total_length:
    for c in charset:
        # 构造当前payload
        current = prefix + c
        current = current.ljust(total_length, 'A')[:total_length]

        # 执行Pin命令
        p = process(["./pin -t inscount0.so -- ./camelcamelcamel"], shell=True)

        # 发送输入(不带换行符)
        p.sendline(current.encode())
        out = p.recvall().decode()
        print(current)
        print(out, end="")
        count = int(out.split("\n")[1])
        if len(prefix) != 39:
            if count > max_count + 30:  # 测试发现每正确一个字符多执行39个指令,因此在这里多设置些防止pin有时候不准确
                max_count = count
                prefix += c
                break
        else:
            if count > max_count:  # 最后一个测试发现每正确一个字符多执行14个指令
                max_count = count
                prefix += c
                break
print(f"Bruteforce took {time.time()-time_start}s")

挺优雅的解题思路hhh

这里还有一篇wp直接分析ocamel代码的

最后修改:2025 年 06 月 23 日
如果觉得我的文章对你有用,请随意赞赏