24kcsplus
文章23
标签36
分类0
Newstar CTF 2025 Week 2 Re方向 Write Up

Newstar CTF 2025 Week 2 Re方向 Write Up

采一朵花,送给艾达(1)

打开可以发现 IDA 无法将程序反编译为伪代码了

image.png

查看字符串和汇编可以发现程序添加了花指令

image.png

image.png

花指令是企图隐藏掉不想被逆向工程的代码块 (或其它功能) 的一种方法, 在真实代码中插入一些垃圾代码的同时还保证原有程序的正确执行, 而程序无法很好地反编译, 难以理解程序内容, 达到混淆视听的效果。

花指令通常用于加大静态分析的难度。

有关花指令可以看这几篇文章:

如果对汇编不熟悉,手动去花可能会有点麻烦,这里我们可以使用动调来去花

image.png

着重看这几个函数,在汇编处的类似的地方下断点:

image.png

按 F7 步入,IDA 会提示“是否要在RIP处直接创建指令?”

image.png

点击是,再按 F5 会发现有一些汇编能够被反编译了

image.png

继续按 F7 步入,IDA 会一步步重建,最后就可以看到正常反编译的情况了

image.png

这里使用了 RC4 加密,有密文和密钥,但是直接使用 Cyberchef 无法解出正常的 flag,可以猜测这是非标准的 RC4 加密

点击 rc4_init 查看,可以发现 IDA 也无法反编译

image.png

用同样的手法,在汇编中那几句类似的语句下断点,然后用动调一步步让 IDA 重建

image.png

rc4_crypt 也是一样的手法

image.png

这两个函数都是魔改了 RC4 算法,有关 RC4 的部分,可以问问 AI 或者 B站上有视频讲解,这里不再赘述。

魔改的部分如下:

  • 在 rc4_init 的 S 盒生成中,将标准 RC4 中的 S[i] = i 改为了 S[i] = -i

  • 而在 rc4_crypt 中,将标准 RC4 中使用的异或改成了加法,这也使得用同一个函数无法进行解密(RC4 的特性是加密函数同时是解密函数,这个特性是使用异或带来的可逆性,感兴趣的同学可以自行搜索位运算,了解他们的特性)

根据上面的内容,写出解密脚本:

# 这些是 16 进制的密文
CIPHER = [
    0x1175640343C17FC7,
    0xDF23C0F6558CB888,
    0xF2F082F69E2E0F4D,
    0xE1278329086B51BC,
    0x4E4F80B188C6BDCB
]

KEY = b"EasyJunkCodes"

def signed8(x):
    # 伪代码中将数强制转换为 8 位的 char 类型了,所以这里需要将数映射为 8 位
    return x - 256 if x >= 128 else x

def init_S_variant(key_bytes):
    S = [0]*256
    for i in range(256):
        S[i] = (-signed8(i)) & 0xFF # & 0xFF 是取低八位的操作
    i = 0
    keylen = len(key_bytes)
    for j in range(256):
        i = (S[j] + i + key_bytes[j % keylen]) % 256
        S[j], S[i] = S[i], S[j]
    return S

def prga_variant(S, length):
    # 生成密钥流,RC4 的一步
    i = j = 0
    ks = []
    for _ in range(length):
        i = (i + 1) % 256
        j = (S[i] + j) % 256
        S[i], S[j] = S[j], S[i]
        ks_byte = S[(S[i] + S[j]) & 0xFF]
        ks.append(ks_byte)
    return ks

def build_ciphertext():
    # 用小端序拼接密文,有关端序可以在《深入理解计算机系统》(CS:APP)的 2.1.3 中了解
    b = bytearray()
    for q in CIPHER:
        b += int(q).to_bytes(8, byteorder='little')
    return bytes(b)

def decrypt():
    cipher = build_ciphertext()
    S = init_S_variant(list(KEY))
    ks = prga_variant(S, len(cipher))
    # 题目程序是加法,所以要使用加法的逆运算减法才能解密
    plain = bytes((c - k) & 0xFF for c, k in zip(cipher, ks))
    try:
        print(plain.decode('ascii'))
    except Exception:
        print('error decoding as ascii')

if __name__ == "__main__":
    decrypt()

OhNativeEnc

使用 JADX 打开,在 FirstFragment 类中可以发现使用了 Native 层的函数

image.png

在资源文件 -> lib -> x86_64 -> libohnativeenc.so,右键导出,然后使用 IDA 打开

image.png

找到对应的函数,可以看到小改了 XXTEA 的 delta,将轮数固定为 12 轮

根据以上内容,可以写出解密代码:

from struct import pack, unpack

# 小端序转换
def bytes_to_words_le(b):
    return list(unpack('<' + 'I'*(len(b)//4), b))

def words_to_bytes_le(w):
    return pack('<' + 'I'*len(w), *[x & 0xFFFFFFFF for x in w])

def decrypt(v, k):
    delta = 114514       # 魔改的 Delta
    rounds = 12          # 固定轮数
    mask = 0xFFFFFFFF
    sum_ = (delta * rounds) & mask
    while sum_ != 0:
        e = (sum_ >> 2) & 3
        for p in range(len(v) - 1, -1, -1):  # 反向循环,XXTEA解密流程
            z = v[(p - 1) % len(v)]
            y = v[(p + 1) % len(v)]
            mx = (((z >> 5) ^ (y << 2)) +
                  ((y >> 3) ^ (z << 4))) ^ ((sum_ ^ y) + (k[(p & 3) ^ e] ^ z))
            v[p] = (v[p] - mx) & mask
        sum_ = (sum_ - delta) & mask
    return v

# 密钥与密文的 16 进制
key_hex = "54 68 69 73 49 73 41 58 58 74 65 61 4B 65 79 00"
data_hex = "B6 53 6E 4D 77 5D 08 D2 FB 2C 63 1E BB 7B 01 9B F5 04 6A F4 0E 84 27 47 64 A1 E4 D9 EF 12 44 37"

k = bytes_to_words_le(bytes(int(x, 16) for x in key_hex.split()))
v = bytes_to_words_le(bytes(int(x, 16) for x in data_hex.split()))

plain = words_to_bytes_le(decrypt(v, k)).rstrip(b'\x00')

print(plain.decode('utf-8'))

Look at me carefully

先用 IDA 打开,可以发现重复调用了很多遍一样的函数

image.png

查看这个函数的内部

image.png

其中,sub_401300 函数并没有对两个字符串做出任何更改

image.png

v4 变更后选择的 switch 的分支总是 case 4 ,而 case 4 里没有对两个字符串做出任何更改

image.png

sub_401100 函数中,因为 0x55 & 0xAA = 0,而任何数和 0 按位和总是等于零,所以 v5 为零,这样就只会进入 case 0 分支了,v5 变为 3,进入 case 3 分支,v5 变为 4,进入 case 4 分支,v5 变为 7,进入 default 分支,v5 变为 6,进入 case 6 分支,早前 v4 的最低位因最后与 1 按位或,所以最后一位为 1,现在与 0xFE 按位与,而 0xFE 最后一位为 0,所以 v4 最后一位也为 0 ,与只有一位的 1 按位和肯定为 0 ,所以这个分支执行完循环就会终止,而最终 v5 会变为 7,函数的返回值则为 170

静态分析很复杂,有非常多的位运算,但其实只需要动调就可以看最后 v5 是什么值了

在返回处下断点,运行到此处会暂停

image.png

光标放在变量上就可以查看变量值了

回到 sub_1A16E0 可以发现,程序的逻辑不过是将对应第 a3 位的字符分别与170、0xEF、0x45异或后,按顺序依次放在密文中

而 170 ^ 0xEF ^ 0x45 = 0,任何数和 0 异或都为其本身,所以程序的逻辑就是将对应第 a3 位的字符按顺序依次放在密文中

由此可以写出解密脚本:

ciphertext = 'cH4_1elo{ookte?0dv_}alafle___5yygume'
plaintext = [None] * 38
index_list = [27, 5, 6, 9, 28, 18, 32, 29, 4, 11, 15, 17, 22, 8, 34, 16, 19, 7, 26, 35, 2, 14, 21, 0, 1, 25, 13, 23, 20, 30, 33, 10, 3, 12, 24, 31]

for i in range(36):
    plaintext[index_list[i]] = ciphertext[i]

for i in range(36):
    print(plaintext[i], end='')

尤皮·埃克斯历险记(1)

这题需要用到除了 IDA 以外的东西了

使用 DIE(Detect It Easy) 来打开,查看程序属性,这步也叫查壳

image.png

可以发现使用了 UPX 壳

有关壳和 UPX 壳看下面的文章:

加了 UPX 壳的程序直接使用 IDA 打开函数会特别少,这是 UPX 壳的特征之一

image.png

UPX 壳去除也相对简单,使用工具即可

image.png

再用 IDA 打开就是正常的程序逻辑了

image.png

encrypt 函数如下:

image.png

根据 main 函数和 encrypt 函数的逻辑,可以写出解密脚本:

a = "isfhGJ\tt~cU\ny\nuTjcj\tT~cjQdu~w{\x04\x05qA"
for i in a:
    ii = ord(i) ^ 0x3c
    if 65 <= 187 - ii <= 90 or 97 <= 187 - ii <= 122:
        print(chr(187 - ii), end="")
    elif 48 <= 105 - ii <= 57:
        print(chr(105 - ii), end="")
    else:
        print(chr(ii), end="")

Forgotten_Code

附件是一个 .s 文件,是编译的一个中间阶段:由源代码生成汇编

关于编译的过程可以上网了解,此处不再赘述

Windows 环境下,安装 MinGW-w64,用以下指令生成最终的二进制可执行文件:

x86_64-w64-mingw32-gcc chal.s -o chal.exe

将二进制文件使用 IDA 打开

image.png

这个程序先判断 flag 是否符合格式,然后检查长度,使用 fn 函数加密,最后与 ezgm 数组对比

查看 fn 函数内容

image.png

题目稍微魔改了 TEA ,将左移改为了加上 16 倍的两部分,还有密钥硬编码

由此可以写出解密脚本:

#!/usr/bin/env python3
import struct
def u32(x): return x & 0xFFFFFFFF
def bytes_to_u32_le(b): return struct.unpack("<I", b)[0]
def u32_to_bytes_le(x): return struct.pack("<I", u32(x))

ezgm = [
    1210405119,
    710975774,
    -90350153,
    -1958008304,
    -745722482,
    67707510,
    -86515270,
    -1728462407
]
ezgm_u32 = [x & 0xFFFFFFFF for x in ezgm]
ng_bytes = b"sp\x7fvuctp|xeb|hv~"

# 分块,八字节一组
blocks = []
for i in range(0, 8, 2):
    lo = ezgm_u32[i]
    hi = ezgm_u32[i+1]
    blocks.append(u32_to_bytes_le(lo) + u32_to_bytes_le(hi))

# 密钥先异或 0x11
ng_words = [bytes_to_u32_le(ng_bytes[i*4:(i+1)*4]) for i in range(4)]
ng_xor = bytes([b ^ 0x11 for b in ng_bytes])
ng_xor_words = [bytes_to_u32_le(ng_xor[i*4:(i+1)*4]) for i in range(4)]
keys = [ng_xor_words, ng_words, ng_xor_words, ng_words]

def tea_decrypt_block(block8, key_words):
    v0 = bytes_to_u32_le(block8[:4])
    v1 = bytes_to_u32_le(block8[4:])
    k0, k1, k2, k3 = key_words
    delta = 0x9E3779B9
    sum_ = u32(delta * 32)
    for _ in range(32):
        v1 = u32(v1 - ( ((v0<<4) + k2) ^ (v0 + sum_) ^ ((v0>>5) + k3) ))
        v0 = u32(v0 - ( ((v1<<4) + k0) ^ (v1 + sum_) ^ ((v1>>5) + k1) ))
        sum_ = u32(sum_ - delta)
    return u32_to_bytes_le(v0) + u32_to_bytes_le(v1)

plaintext_blocks = [tea_decrypt_block(blocks[i], keys[i]) for i in range(4)]
flag_inner = b"".join(plaintext_blocks)
flag = b"flag{" + flag_inner + b"}"
print(flag.decode('ascii'))

本文作者:24kcsplus
本文链接:https://24kblog.top/posts/215059001/
版权声明:除非特别声明,否则本文采用 CC BY-NC-SA 3.0 CN 协议进行许可
×