# 前言

已经是四月底了,但是却突然发现这一整个月都没有在博客写些什么,感觉又是在不明所以中度过了,想看的 magisk , frida 源码没有看完,想做的小夏的 live2d 也迟迟没有动工,下个月努努力,多学点,多做点,加油加油

总觉得自己还是要写下点什么才好,恰好这周听闻有个 d3ctf, 于是便想来看看逆向题,去学习一下新知识开拓视野,同时也为了度过一段充实的周末时光

回想去年的 d3ctf, 感觉那时的自己有太多不了解的地方了,于是乎情理之中的也是一题都没做出来了,复现的题目的计划在我的 todolist 里面躺了很长一段时间,然而不知不觉间变成了 undo, 最后到现在的 forget do,never do

同时我也想来看看一下这一年的自己有了什么变化,感觉现在可以称自己是一位入门逆向工程师了吧哈哈哈,总共五道逆向题做好了三题,可惜周日因为要调休的缘故 (谁发明的调休?!) 便没有再继续看题了,题目挺不错的,不过要是可以有安卓题的话我应该会有百分之三百的精气神去做吧 ^.^

翻了翻自己过去写的 ctf 文章,那也仅仅是 wp 出个 flag 而已,但是 flag 不是题目的终点,二进制文件中蕴藏的技巧,手法才是真正值得总结一篇文章的地方,我可不希望自己写的文章在未来连我自己都不愿重新翻看 (●ˇ∀ˇ●)

题目附件点这里下载~

# forest

很好的一道题,将 MSVC 的 SEH 异常处理使用的十分巧妙,同时点和图的思想也在这题有了充分的体现,在做这题的时候有一个失败的解法,不过我感觉很有意思所以也在这里记录下来啦

这里你会看到地址都是 61 开头,那时因为用 od 的时候里面的基址就是 0x610000 , 所以当时为了和 ida 中对应起来我也改成了 61 开头,不过最后 od 还是没有排上用场全靠 ida 动态调试啦

这里通过主动设置 int 3 断点触发 0x80000003 断点异常进入 sub_611A00

image-20240427182852533

在 sub_611A00 中,对异常码进行判断,这里可以重定义 this 参数类型为 _EXCEPTION_POINTERS , 这样看起来更方便些,感觉是一个小小的突破口哦

image-20240427182955955

首次触发断点异常,解密由 VirtualProtect 分配的内存地址的值,并设置 byte_616028 标志为 0, 下一次触发断点异常将视为 flag 错误,进程退出

image-20240427183012954

解密完成后设置 EIP, 并通过 this->ContextRecord->EFlags |= 0x100 设置单步调试模式,使标志寄存器第 8 位 TF 为 1

image-20240427183214681

随后程序会通过调用 cli 特权指令实现 shellcode 的跳转

image-20240427184042129

image-20240427184106659

int 2Dh 表示 flag 错误image-20240427184528371

# [失败] frida 侧信道

说的高大上一点叫侧信道,实际上就是爆破哈哈哈,我调试的时候发现每一次输入的 flag 不一样,调用 cli 特权指令触发 0xC0000096 的次数也不一样,所以我就在想,要是我可以一位一位的爆破,通过在处理 0xC0000096 异常的地方下断点统计调用次数,然后然调用最多的那个字符作为这一位的 flag, 那不就得到最后的 flag 了嘛~

所以这回用上了我的老伙计 frida, 想要通过 frida 实现爆破,那么需要使用 frida 的 python 脚本,同时输入也不能再是 console.log , 而是需要使用 send 将结果返回到 python 中去做处理

function my_hook() {
    var module = Process.findModuleByName(exe_name);
    Interceptor.attach(module.base.add(0x1BDE),
        {
            onEnter: function (args) {
                if(this.context.eax.compare(0xC0000096)===0){
                    count=count+1
                    //console.log(this.context.eax)
                    //console.log(ptr(module.base.add(0xAD20).readS32()).readS32().toString(16))
                    //console.log(ptr(module.base.add(0xAD24).readS32()).readS32().toString(16))
                    send(count)
                }
            },
            onLeave: function (retval) {
            }
        }
    );
}
// d3ctf{01234567890123456}
setImmediate(my_hook);

这个 python 的代码虽然很简单但是也巧妙,嘻嘻

import os
import subprocess
import frida
import string
#d3ctf{0ut00431101002001}
def on_message(message, data):
    global max_count,max_ch,current_ch
    if message['type'] == 'send':
        if not max_count:
            max_count = int(message['payload'])
            max_ch = current_ch
        elif int(message['payload'])>max_count:
            max_count = message['payload']
            max_ch = current_ch
        elif int(message['payload'])==max_count:
            print(f"NOTE! {max_ch} and {current_ch} have the same max_count {max_count}")
    elif message['type'] == "error":
        print(message["description"])
        print(message["stack"])
        print(message["fileName"], "line:", message["lineNumber"], "colum:", message["columnNumber"])
jscode = open("trace_forest.js","rb").read().decode()
t_flag = list("d3ctf{01234567890123456}")
for i in range(6,23):
    max_count, max_ch = None,None
    break_flag = 0
    for c in string.printable:
        current_ch = c
        t_flag[i] = c
        process = subprocess.Popen("forest.exe",
                                   stdin=subprocess.PIPE,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE,
                                   universal_newlines=True)
        session = frida.attach("forest.exe")
        script = session.create_script(jscode)
        script.on('message', on_message)
        script.load()
        process.stdin.write(''.join(t_flag))
        output, error = process.communicate()
        process.terminate()
    t_flag[i] = max_ch
    print(f"{''.join(t_flag)}, max: {max_count}")

但是可惜的是程序跑完出来了这个 d3ctf{0ut00431101002001} , 虽然是错的但是回显不一样了 ^.^

这何尝不是一种胜利~不过换个方法咯

image-20240429013314486

# [成功] 图 点 路径

如果想要产生正确的回显,必须触发 0xC0000005 异常

image-20240427210553263

输入的字符串将会被全部转换为二进制的形式,每执行 cli 特权指令触发 0xC0000096 异常后都会读取一位

image-20240427211258536

如果为 0, 则下一次触发 0x80000004 单步调试异常且当前指令的偏移逻辑 &0x3F 等于 7 时,将会让当前的 Eip 加上 23

image-20240427211317024

进入下一个块的位置由 unk_EAA720 和 unk_EA8658 进行控制,算法为 (unk_EAA720+unk_EA8658+(unk_EA8658<<4))<<6

image-20240427211804141

观察后发现这明显就是一个图呀,所以可以用 idapython 找到图的所有边

# d3ctf{0ut00431101002001}
import idautils
import idc
import ida_bytes
base = 0x616035
pattern = ["B8 FF FF FF FF C7 00 ?? 00 00 00 B8 FF FF FF FF C7 00 ?? 00 00 00 FA"]
pl = []  # point list
for i in range(len(pattern)):
    cur_addr = 0x616035
    end_addr = 0x616035 + 0x483D
    while cur_addr < end_addr:
        cur_addr = idc.find_binary(cur_addr, idc.SEARCH_DOWN, pattern[i])
        if cur_addr == idc.BADADDR:
            break
        else:
            a = ida_bytes.get_byte(cur_addr + 7)
            b = ida_bytes.get_byte(cur_addr + 18)
            pl.append({
                "cur": cur_addr,
                "off": (a + b + (b << 4)) << 6,
                "s": 0
            })
        cur_addr = idc.next_head(cur_addr)
pattern = ["8B 00"]
for i in range(len(pattern)):
    cur_addr = 0x616035
    end_addr = 0x616035 + 0x483D
    while cur_addr < end_addr:
        cur_addr = idc.find_binary(cur_addr, idc.SEARCH_DOWN, pattern[i])
        if cur_addr == idc.BADADDR:
            break
        else:
            # if cur_addr-(base+5) &0x3F==7,eax->1, eip+23
            pl.append({
                "cur": cur_addr,
                "off": cur_addr + 23 - base,
                "s": 1
            })
        cur_addr = idc.next_head(cur_addr)
print(pl)

然后打印一下没有对应端点的点,这里只是部分,实际上还有很多…

image-20240427232406322

之后一个一个试过去,就有 flag 了 (为什么不用 for 循环尝试所有可能的点,因为用 for 循环 all_simple_paths 不出结果呜)

base = 0x616035
pl = [...]# ida python 的结果
import networkx as nx
G = nx.DiGraph()
for p in pl:
    G.add_node(p["cur"])
out_way = []
for p in pl:
    if not p["s"]:
        if G.has_node(base+p["off"]):
            G.add_edge(p["cur"],base+p["off"])
        else:
            if base+p['off']>0x616035 + 0x483D:
                print(f"find a way out: {hex(p['cur'])} --> {hex(base+p['off'])}")
                G.add_node(base+p['off'])
                G.add_edge(base,base+p['off'])
                out_way.append(base+p['off'])
            else:
                print(f"no taget point: {hex(p['cur'])} --> {hex(base + p['off'])}")
                out_way.append(base + p['off'])
    else:
        if G.has_node(p["cur"]+2+23):
            G.add_edge(p["cur"],p["cur"]+2+23)
        if G.has_node(p["cur"]+2):
            G.add_edge(p["cur"], p["cur"] + 2)
        else:
            print("ERRR")
start = 0x616035
tem = []
out = 0x616c0e
for path in nx.all_simple_paths(G, source=start, target=out):
    tem.append(path)
    #print(tem)
    #print(len(tem[0]))
    pll = []
    for t in tem[0]:
        for p in pl:
            if p["cur"]==t:
                #print(p["cur"],p["s"])
                pll.append(p)
                break
    start = True
    flag=""
    for p in range(len(pll)):
        if p!=0:
            if not pll[p]["s"]:
                if pll[p]["cur"]-pll[p-1]["cur"]==2:
                    flag+="0"
                else:
                    flag+="1"
    for i in range(0,len(flag)//8):
        print(chr(int(flag[8*i:8*i+8],2)),end='')

# ezjunk

从整个程序的入口 start 函数开始分析,在执行 main 函数前,还会调用 sub_401CC0 函数

image-20240429004720898

在这个函数中,会对 sub_401550 进行调用

image-20240429004856409

但是却没法直接反编译,既然是栈的问题,把 sub rsp, 30h nop 掉就好啦

image-20240429005457581

有个反调试,过一下就好了

image-20240427151306234

image-20240429005548792

main 函数的花指令也很简单,去一下就看到逻辑啦

image-20240429010852125

进到 sub_401917 里面,只是一个 tea 算法

image-20240429010317189

之后就是一个类似 crc 的算法

image-20240429010928318

不过在调试的时候还是不能直接把花指令 nop 掉的,因为这里读取的是 loc401A1C 花指令的汇编,如果 nop 掉变成 90 的话,tea 的常数值会出错的

from ctypes import *
def encrypt(v, key):
    v0, v1 = c_uint32(v[0]), c_uint32(v[1])
    delta = 0xff58f981
    total = c_uint32(0xE8017300)
    for i in range(32):
        v0.value += (((v1.value << 4) ^ (v1.value >> 5)) + v1.value) ^ (total.value + key[total.value & 3]) ^ 0x44
        v1.value += (((v0.value << 5) ^ (v0.value >> 6)) + v0.value) ^ (
                    total.value + key[(total.value >> 11) & 3]) ^ 0x33
        total.value -= delta
    return v0.value, v1.value
def decrypt(v, key):
    v0, v1 = c_uint32(v[0]), c_uint32(v[1])
    delta = 0xff58f981
    total = c_uint32(0xE8017300 - 32 * delta)
    for i in range(32):
        total.value += delta
        v1.value -= (((v0.value << 5) ^ (v0.value >> 6)) + v0.value) ^ (
                    total.value + key[(total.value >> 11) & 3]) ^ 0x33
        v0.value -= (((v1.value << 4) ^ (v1.value >> 5)) + v1.value) ^ (total.value + key[total.value & 3]) ^ 0x44
    return v0.value, v1.value
# test
if __name__ == "__main__":
    # 待加密的明文,两个 32 位整型,即 64bit 的明文数据
    # fakeflag{Is_there_anywhere_else}
    # value = [0x5406CBB1, 0xA4A41EA2, 0x34489AC5, 0x53D68797, 0xB8E0C06F, 0x0259F2DB, 0x52E38D82, 0x595D5E1D]
    value = [0xB6DDB3A9, 0x36162C23, 0x1889FABF, 0x6CE4E73B, 0x0A5AF8FC, 0x21FF8415, 0x44859557, 0x2DC227B7]
    value = [c_uint(v) for v in value]
    for i in range(len(value)):
        for _ in range(32):
            if value[i].value&1:
                value[i] = c_uint(((value[i].value ^ 0x84A6972F)//2) | 0x80000000)#2086826726
            else:
                value[i] = c_uint((value[i].value//2))
    value = [v.value for v in value]
    #for v in value:
        #print(hex(v))
    # value = []
    # 四个 key,每个是 32bit,即密钥长度为 128bit
    key = [0x00005454, 0x00004602, 0x00004477, 0x00005E5E]
    v = [0, 0]
    for i in range(len(value) // 2):
        v[0], v[1] = value[2 * i], value[2 * i + 1]
        res = decrypt(v, key)
        print(f"{res[0].to_bytes(4, 'little').decode()}{res[1].to_bytes(4, 'little').decode()}", end='')

# RandomVM

VM 题型,不过算是比较方便的那种真正用到的指令不是很多,ida trace 在关键函数 trace 一下,不过有个 ptrace 反调试注意一下就好啦

syscall (0x65) ptrace 反调试,跳过这条指令就好了

image-20240427014538097

比较重要的有这些函数

image-20240427032349722

idapython trace 一下

#xor
import idc
import ida_bytes
import idaapi
ea = ida_bytes.get_byte(idaapi.get_imagebase()+0xB072)
ecx=idc.get_reg_value("ECX")
eax=idc.get_reg_value("EAX")
print(f"final[{ea}] = {hex(ecx)}^{hex(eax)} = {hex(ecx^eax)}")
#mov
import idc
import ida_bytes
import idaapi
ea = ida_bytes.get_byte(idaapi.get_imagebase()+0xB072)
eax=idc.get_reg_value("EAX")
print(f"final[{ea}] = {hex(eax)}")
#circleRmov
import idc
import ida_bytes
import idaapi
import ctypes
ea = ida_bytes.get_byte(idaapi.get_imagebase()+0xB072)
edx=idc.get_reg_value("EDX")
ecx=idc.get_reg_value("ECX")%8
res = ctypes.c_uint8(edx)
if ecx!=0xffffffff:
    res = ctypes.c_uint8((res.value>>ecx) | (res.value<<(8-ecx)))
else:
    res = ctypes.c_uint8(0)
print(f"final[{ea}] = {hex(edx)}>>{ecx} | {hex(edx)}<<{8-ecx} = {hex(res.value)}")

输入 0123456789ab 得到输出

final[0] = 0x0^0x30 = 0x30
final[1] = 0x1
final[1] = 0x30>>3 | 0x30<<5 = 0x6
final[1] = 0x6^0x3 = 0x5
final[1] = 0x5^0x31 = 0x34
final[2] = 0x2
final[2] = 0x31>>5 | 0x31<<3 = 0x89
final[2] = 0x89^0x32 = 0xbb
final[3] = 0x3
final[3] = 0x32>>6 | 0x32<<2 = 0xc8
final[3] = 0xc8^0x33 = 0xfb
final[4] = 0x4
final[4] = 0x33>>7 | 0x33<<1 = 0x66
final[4] = 0x66^0x7 = 0x61
final[4] = 0x61^0x34 = 0x55
final[5] = 0x5
final[5] = 0x34>>4 | 0x34<<4 = 0x43
final[5] = 0x43^0x4 = 0x47
final[5] = 0x47^0x35 = 0x72
final[6] = 0x6
final[6] = 0x35>>4 | 0x35<<4 = 0x53
final[6] = 0x53^0x36 = 0x65
final[7] = 0x7
final[7] = 0x36>>7 | 0x36<<1 = 0x6c
final[7] = 0x6c^0x7 = 0x6b
final[7] = 0x6b^0x37 = 0x5c
final[8] = 0x8
final[8] = 0x37>>7 | 0x37<<1 = 0x6e
final[8] = 0x6e^0x38 = 0x56
final[9] = 0x9
final[9] = 0x38>>2 | 0x38<<6 = 0xe
final[9] = 0xe^0x39 = 0x37
final[10] = 0xa
final[10] = 0x39>>4 | 0x39<<4 = 0x93
final[10] = 0x93^0x61 = 0xf2
final[11] = 0xb
final[11] = 0x61>>4 | 0x61<<4 = 0x16
final[11] = 0x16^0x62 = 0x74
final[12] = 0xc
final[12] = 0x62>>7 | 0x62<<1 = 0xc4
final[12] = 0xc4^0x7 = 0xc3
final[2] = 0xbb^0x34 = 0x8f
final[3] = 0xfb^0x8f = 0x74
final[4] = 0x55^0x74 = 0x21
final[5] = 0x72^0x21 = 0x53
final[6] = 0x65^0x53 = 0x36
final[7] = 0x5c^0x36 = 0x6a
final[8] = 0x56^0x6a = 0x3c
final[9] = 0x37^0x3c = 0xb
final[10] = 0xf2^0xb = 0xf9
final[11] = 0x74^0xf9 = 0x8d
final[12] = 0xc3^0x8d = 0x4e

exp 如下

import ctypes
def circleR_rev(s, r):
    res = ctypes.c_uint8(s)
    res = ctypes.c_uint8((res.value << r) | (res.value >> (8 - r)))
    return res.value
key = [0x9D, 0x6B, 0xA1, 0x02, 0xD7, 0xED, 0x40, 0xF6, 0x0E, 0xAE, 0x84, 0x19]
circle_R = [3, 5, 6, 7, 4, 4, 7, 7, 2, 4, 4, 7]
xor = [3,0,0,7,4,0,7,0,0,0,0,7]
flag = [0 for _ in range(12)]
for i in range(len(key)-1,-1,-1):
    if not flag[-1]:
        flag[i] = circleR_rev(key[i]^key[i-1]^xor[i],circle_R[i])
    else:
        flag[i] = circleR_rev(key[i]^key[i-1]^xor[i]^flag[i+1],circle_R[i])
    if not i:
        flag[i] = circleR_rev(key[i]^xor[i]^flag[i + 1], circle_R[i])
print(''.join(map(chr, flag)))
更新于 阅读次数