Reverse5

1.1Creakeme

参考链接:

https://www.yunzh1jun.com/2022/05/27/WindowsSEH/

https://github.com/0xE4s0n/creakme_sctf2019/blob/master/creakme/creakme.cpp 题目源码

对照参考链接提到过的增强版的SEH结构体,我们可以发现入栈的参数是一一对应的:

入栈的参数是ebp、0xFFFFFFFE、stru_407B58、__except_handler4、large fs:0,分别对应_EXCEPTION_REGISTRATION中的_ebp、trylevel、scopetable、handler和prev(前面介绍TEB时已经说过,FS:[0]指向SEH起始地址)。

那么按照scopetable的定义,这个结构体中存储了lpfnFilter(当前try块的过滤函数)和lpfnHandler(当前try块的Handler)。双击stru_407B58,查看这个结构体:loc_4023DC是FilterFunc,loc_4023EF是HandlerFunc。这俩一个是过滤函数一个是处理函数

复制代码
.rdata:00407B58 stru_407B58     dd 0FFFFFFE4h           ; GSCookieOffset
.rdata:00407B58                                         ; DATA XREF: sub_402320+5↑o
.rdata:00407B58                 dd 0                    ; GSCookieXOROffset
.rdata:00407B58                 dd 0FFFFFFC4h           ; EHCookieOffset
.rdata:00407B58                 dd 0                    ; EHCookieXOROffset
.rdata:00407B58                 dd 0FFFFFFFEh           ; ScopeRecord.EnclosingLevel
.rdata:00407B58                 dd offset loc_4023DC    ; ScopeRecord.FilterFunc
.rdata:00407B58                 dd offset loc_4023EF    ; ScopeRecord.HandlerFunc
.rdata:00407B74                 align 8

过滤函数通过触发断点异常80000003h来转到处理函数

处理函数最终调用一个函数sub_402450,并且有两个参数,ecx,esi。ecx又是ebp+var_28+eac+C。

ebp+var_28又是程序初始时的ecx值。下面这些指令是标准的 PE 文件结构解析 动作。由于所有的偏移(如 3Ch, 0F8h)都是基于 ecx 计算的,这证明 ecx 存储的是 PE 文件的起始内存地址。

复制代码
mov  eax, [ecx+3Ch]                ; 读取 PE 头偏移 (e_lfanew)
movzx ebx, word ptr [eax+ecx+6]     ; 读取 NumberOfSections (节数量)
lea  esi, [ecx+0F8h]               ; 定位到节表 (Section Table)

再结合4024A0函数,其实ecx就是找.SCTF节的位置,然后进行函数402450处理。也就是自解密SMC。

知道加密逻辑和加密数据后,在 IDA 菜单栏点击 File -> Script command... ,选择 Python,然后输入以下代码:

python 复制代码
import idc
start_addr = 0x00404000  
length = 0x200           
key = b"sycloversyclover" 
key_len = len(key)
print(f"[*] Starting decryption at {hex(start_addr)}")
for i in range(length):
    current_byte = idc.get_wide_byte(start_addr + i)
    s = key[i % key_len]
    decrypted_byte = (~(current_byte ^ s)) & 0xFF
    idc.patch_byte(start_addr + i, decrypted_byte)
print("[+] Decryption finished!")

然后在404000这就会获得一段逻辑。按p后f5反编译

对密文做一个这样的处理,先--然后字符反转。

最终的解密脚本

python 复制代码
s='>pvfqYc,4tTc2UxRmlJ,sB{Fh4Ck2:CFOb4ErhtIcoLo'
print(len(s))
s=list(s)
for i in range(len(s)):
    s[i]=chr(ord(s[i])-1)
S=''.join(x for x in s)
S=S[::-1]
print(S)
from Crypto.Cipher import AES
import base64
ciphertext_base64 = "nKnbHsgqD3aNEB91jB3gEzAr+IklQwT1bSs3+bXpeuo="
key = b"sycloversyclover"
iv = b"sctfsctfsctfsctf"
raw_cipher = base64.b64decode(ciphertext_base64)
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(raw_cipher)
print(f"Flag: {decrypted.decode('utf-8').strip()}")

2.1hardcpp

逻辑比较简单,使用z3即可求解

python 复制代码
from z3 import *
data=[0xF3, 0x2E, 0x18, 0x36, 0xE1, 0x4C, 0x22, 0xD1, 0xF9, 0x8C, 0x40, 0x76, 0xF4, 0x0E, 0x00, 0x05, 0xA3, 0x90, 0x0E, 0xA5] 
v18 = [BitVec(f'v18_{i}', 8) for i in range(21)]

s = Solver()
for char in v18:
    s.add(char >= 32, char <= 126)
for i in range(20):
    s.add((((v18[i]^18)*3)+2)^((v18[i]%7)+(0 ^ v18[i+1]))  == data[i])
# 求解并输出
if s.check() == sat:
    m = s.model()
    print("解出 v18 数组结果 (Hex):")
    for i in range(20):
        val = m[v18[i]].as_long()
        print(f"v18[{i}] = {hex(val)}")
    
    # 将结果转换为字符串(CTF 中常见的 Flag 格式)
    import struct
    try:
        flag = b""
        for i in range(20):
            flag += struct.pack("<Q", m[v18[i]].as_long())
        print(f"\n可能的 Flag 字符串: {flag.decode(errors='ignore')}")
    except:
        pass
else:
    print("无法求解,请检查约束公式是否输入有误(特别是括号匹配)。")

3.1powerPacked

upx解壳即可。

4.1Repyc

5.1Shit

改名为put和cin,按Y分别定义,put这里不指定ecx和edx识别不出来,cin不用指定就能识别。主要是汇编那。

复制代码
void __usercall put(void *this<ecx>, const char *str<edx>);
void* __thiscall cin(void *this, char *buffer);

看见genkey那有花指令,写一个idc脚本把花指令去除掉,手动nop也许就是比较麻烦。然后把整个函数U掉,再按P还原函数后F5得到如下结果:

python 复制代码
import idc
def patch_junk(start_addr, end_addr):
    curr = start_addr
    while curr < end_addr:
        if (idc.get_wide_byte(curr) == 0xE8 and 
            idc.get_wide_byte(curr + 1) == 0x03 and 
            idc.get_wide_byte(curr + 2) == 0x00 and 
            idc.get_wide_byte(curr + 3) == 0x00 and 
            idc.get_wide_byte(curr + 4) == 0x00): 
            for i in range(13): 
                idc.patch_byte(curr + i, 0x90)
            curr += 13
        else:
            curr += 1
patch_junk(0x401460,0x401630)

然后把encode按相同的脚本和方法,先去花,全部U掉,然后P,F5

查看encode函数。里面有内联函数ROR4,也就是循环右移4字节(32位)。循环左移(Rotate Left)的本质是:把移出去的高位,重新补到低位。循环右移(Rotate Right)的本质是:把移出去的低位,重新补到高位。width取32是ROR4,width取8是ROR1

python 复制代码
def ROL(val, n, width=32):
    return ((val << (n % width)) & (2**width - 1)) | (val >> (width - (n % width)))

def ROR(val, n, width=32):
    return (val >> (n % width)) | ((val << (width - (n % width))) & (2**width - 1))

反调试:

尝试修改运行逻辑也并不能调试

尝试使用attach方法获取key。

选择本地,下个断点,再附加程序,直接search找到shit即可

按F9跑起来,再输入一个长度为24的字符串,跑到这按F7进去,

在这通过修改标志位过去

python 复制代码
import struct
def ROL(val, n, width=32):
    return ((val << (n % width)) & (2**width - 1)) | (val >> (width - (n % width)))
def ROR(val, n, width=32):
    return (val >> (n % width)) | ((val << (width - (n % width))) & (2**width - 1))
data=[0x8C2C133A, 0xF74CB3F6, 0xFEDFA6F2, 0xAB293E3B, 0x26CF8A2A, 0x88A1F279]
key=[0x00000003, 0x00000010, 0x0000000D, 0x00000004, 0x00000013, 0x0000000B]

for i in range(5,0,-1):
    data[i] ^= data[i-1]

for i in range(6):
    temp = data[i] ^ (1 << key[i])
    temp = (temp>>16)&0xffff | (~(temp<<16)&0xffff0000)
    data[i] = ROL(temp, key[i], 32)
flag=""
for i in range(6):
    flag+=struct.pack(">I", data[i]).decode("utf-8", errors="ignore")
print(flag)

6.1Crash

可以先使用GoReSym恢复一下符号

GoReSym.exe -t -d -p xxx.exe > xxx.json,生成json文件后 选择脚本文件,选择idapython下的python脚本,让后选择生成的json即可。随后便会恢复文件符号。

在ida里找到了string和interface的定义,分别定义v1到v6,按T后搜索可以直接选择

输出函数的原型是

Go 复制代码
void fmt_Fprintln(runtime_itab *a1, void *os_Stdout_ptr, BUILTIN_INTERFACE *a3, __int64 a4, __int64 a5);

输入函数的原型是

Go 复制代码
void fmt_Fscanln(runtime_itab *a1, void *os_Stdin, BUILTIN_INTERFACE *a3, __int64 a4, __int64 a5);

大概就能还原成这个样子

按照逻辑,开始逆向main_check,在main_check里先调用main_encrypto,

void *runtime_newobject(type *t)

  • 输入参数 t:这是一个指向 runtime._type 结构体的指针。它描述了要分配的对象是什么类型(大小、对齐方式、是否包含指针等)。

  • 返回值:返回分配好的内存空间的起始地址(指针)。

在结构体里找到了main_secretdata类型,看来是分配这个类型的指针

在 Go 语言中,string 是不可变的,而 []byte 是可变的。为了保证安全,当你把字符串转成字节切片时,Go 必须在堆上开辟一块新内存,并将字符串的内容拷贝过去。这个分配内存并拷贝的过程,就是由**runtime.stringtoslicebyte** 完成的。

  • 第一个参数 (0): 这是一个临时缓冲区 buf

    • 如果编译器通过逃逸分析发现这个转换后的切片不会逃逸出当前函数,它可能会在栈上分配一个小缓冲区(通常是 32 字节)传进来,以避免堆分配。

    • 如果传入 0(NULL),则表示该切片会逃逸,必须在堆上分配内存。

  • 第二个参数 (ptr): 字符串的地址。

  • 第三个参数 (n): 字符串的长度

Go 编译器在处理切片(Slice)传参时,会将每个切片拆分为 (指针, 长度, 容量) 三个参数传给函数。

Go 复制代码
ptr_1 = Encrypt_DesEncrypt(
    ptr_cast, a5, v5,      // 参数 1: 待加密数据 (你的输入) 的 Slice (ptr, len, cap)
    key_ptr_cast, a5, v5,  // 参数 2: 密钥 Key 的 Slice (来自 JSON 的 "WelcomeToTheGKCTF2021XXX")
    iv_ptr_cast_1, a5, v5  // 参数 3: 偏移量 IV 的 Slice (来自 JSON 的 "1Ssecret")
);

Encrypt_DesEncrypt的返回类型

Go 复制代码
struct desret  {
    byte *data;
    int64 len;
    int64 cap;
    void *err_itab;
    void *err_data;
};

大概能还原成这样

最复杂的加密就逆向完了

check显示得并不完整,jz改成jmp即可。那整个逻辑就一目了然了

相关推荐
j7~18 小时前
【MYSQL】基本查询(表的增删查改)--详解
数据库·mysql·select·create·聚合函数·update·groupby
爱喝水的鱼丶18 小时前
SAP-ABAP:变量、常量、结构与内表声明(10篇博客合集) 第八篇:复杂业务场景下的声明组合:结构嵌套内表、内表包含结构的实现方法
运维·数据库·学习·算法·sap·abap
这个DBA有点耶18 小时前
集中式 vs 分布式:2026数据库选型决策树
数据库·分布式·决策树
鸽芷咕18 小时前
KingbaseES系统视图与Hints调优:从诊断到性能优化的进阶之路
数据库·oracle·性能优化
Pocker_Spades_A18 小时前
没公网IP怎么远程连数据库?PostgreSQL + cpolar,在任何网络环境下都能连上
网络·数据库·tcp/ip
数据与后端架构提升之路18 小时前
RAG 实战指南:深入浅出向量数据库 Milvus
数据库·milvus
一只fish18 小时前
Oracle官方文档翻译《Database Concepts 26ai》第11章-服务器端编程
数据库·oracle
一只fish18 小时前
Oracle官方文档翻译《Database Concepts 26ai》第13章-事务
数据库·oracle
. . . . .18 小时前
mysql常用SQL
数据库·sql·mysql