D3CTF 2024 forest复现

D3CTF 2024 Forest

写在前面:感谢REtard师傅的wp和zipkey师傅的wp,让我彻底搞懂了这题

指路:

D^3CTF2024逆向Writeup (qq.com)

微信公众平台 (qq.com)

先说结论:这个程序用异常处理控制流实现了一个类似迷宫的东西

我们用REtard师傅给的脚本可以打印出map,这个map实际上应该是一种二维的迷宫,但为了便于分析我们将他转换为一维的


import ida_bytes

for off in range(0, 18493, 64):
    i = off//64
    ea = 0x406030+off
    cc = ida_bytes.get_word(ea) == 0x3cd
    if ea == 0x4066f0:
        print(hex(ea), f'{i:4}', 'win')
    elif not cc:
        a = ida_bytes.get_dword(ea+0x0C+2)
        b = ida_bytes.get_dword(ea+0x17+2)
        c = ida_bytes.get_dword(ea+0x23+2)
        d = ida_bytes.get_dword(ea+0x2e+2)
        if a > 100000:
            print(hex(ea), f'{i:4}', f'int3')
        else:
            print(hex(ea), f'{i:4}', f'[{a+b*17},{c+d*17}]')
    else:
        print(hex(ea), f'{i:4}', "int3")
0x406030    0 [11,41]
0x406070    1 [1,1]
0x4060b0    2 [2,2]
0x4060f0    3 [3,3]
0x406130    4 [4,4]
0x406170    5 [5,5]
0x4061b0    6 [193,52]
0x4061f0    7 [7,7]
0x406230    8 [216,43]
0x406270    9 [116,13]
0x4062b0   10 [120,189]
0x4062f0   11 [82,244]
0x406330   12 [12,12]
0x406370   13 [8,1...5]
0x4063b0   14 int3
0x4063f0   15 int3
0x406430   16 [72,95]
0x406470   17 [233,272]
0x4064b0   18 [211,194]
0x4064f0   19 int3
0x406530   20 [59,230]
0x406570   21 [237,89]
0x4065b0   22 [60,190]
0x4065f0   23 [23,23]
0x406630   24 [243,232]
0x406670   25 int3
0x4066b0   26 [207,224]
0x4066f0   27 win
0x406730   28 [31,61]
0x406770   29 [151,179]
0x4067b0   30 [12,180]
0x4067f0   31 int3
0x406830   32 [76,147]
0x406870   33 int3
...

将以上打印出来的内容保存到1.txt,然后这是什么意思呢,我们从0位置开始,如果我们选择为"0",则跳到左边数字位置;如果选择"1",则跳到右边数字位置;

比如0位置是[11,41],如果我们选择"0",就跳到11,而11位置是[82,244],这时选择"1",就跳到244以此类推;

然后我们获胜的条件在27处,“win”

而如果是失败:1. 走到int3,直接结束 ;

  1. 走到类似 1 [1,1],2 [2 ,2]这种位置,直接陷入死循环 (这也是无法侧信道攻击爆破出flag的原因)

在这里插入图片描述

然后就可以写dfs脚本了


lines = open('./1.txt', 'r').read().splitlines(False)
game_map = []
for line in lines:
    idx, data = line[10:].strip().split(' ')
    if '[' not in data:
        game_map.append(data)
    else:
        game_map.append(eval(data, {}, {}))


def dfs(current, w: set, path: str,nodes: str):
    if current in w:
        return
    node = game_map[current]
    if isinstance(node, str):
        if node == 'win':
            print('win')
            print(path)
            print(nodes)
           # exit()
        else:
            return
    else:
        w.add(current)

        dfs(node[0], w.copy(), path+'0',nodes+'0: '+str(node[0])+'\n')
        dfs(node[1], w.copy(), path+'1',nodes+'1: '+str(node[1])+'\n')

dfs(0, set(), '', '' )
print(game_map) 
print('done')

# d3ctf{0ut_of_th3ForesT#}

打印出来的路径,即这串二进制字符,转换为16进制,再转换为ascii字符就可以拿到flag了

win
0011000001110101011101000101111101101111011001100101111101110100011010000011001101000110011011110111001001100101011100110101010000100011
0: 11
0: 82
1: 26
1: 224
0: 66
0: 100
0: 229
0: 263
0: 164
1: 174
1: 64
1: 6
0: 193
1: 161
0: 28
1: 61
0: 223
1: 266
1: 9
1: 13
0: 8
1: 43
0: 16
0: 72
0: 258
1: 84
0: 117
1: 42
1: 251
1: 187
1: 114
1: 56
0: 20
1: 230
1: 191
0: 287
1: 208
1: 109
1: 112
1: 202
0: 199
1: 65
1: 214
0: 239
0: 98
1: 225
1: 21
0: 237
0: 184
1: 141
0: 78
1: 240
1: 277
1: 143
1: 103
1: 182
0: 160
1: 286
1: 29
1: 179
0: 181
1: 270
0: 40
0: 48
0: 45
1: 165
1: 37
0: 276
1: 63
0: 55
0: 280
0: 124
0: 231
0: 62
1: 265
1: 262
0: 254
0: 22
1: 190
1: 142
0: 134
1: 162
0: 186
0: 88
0: 106
1: 50
1: 92
0: 119
0: 32
1: 147
1: 34
0: 153
1: 157
1: 255
1: 94
1: 167
0: 241
1: 250
1: 268
1: 203
0: 183
0: 221
1: 10
0: 120
0: 267
1: 18
1: 194
0: 242
0: 172
1: 148
0: 188
1: 247
0: 17
1: 272
1: 127
1: 206
0: 35
0: 279
1: 110
1: 99
0: 259
1: 288
0: 159
1: 144
0: 71
1: 24
0: 243
0: 102
0: 269
0: 30
1: 180
0: 128
0: 145
0: 185
1: 47
1: 27

done


# 0011000001110101011101000101111101101111011001100101111101110100011010000011001101000110011011110111001001100101011100110101010000100011
# 3075745F6F665F746833466F7265735423
# 0ut_of_th3ForesT#

那么就下来开始正式进行分析本程序是如何实现这个maze的

main函数打开,前面就是常规的flag长度和格式的判断

在这里插入图片描述

然后关键就是在sub_401F50这个函数,点进去看,这里是将0x406030开始的内存页属性改为"PAGE_EXECUTE (0x40) ",也暗示了一会程序将会运行这部分的代码。

在这里插入图片描述

然后函数故意抛出了一个断点异常,那么我们就可以猜测是有SEH的存在,点开流程图,看到 filter函数的部分,而这个函数就是我们分析的关键。

在这里插入图片描述

进入到filter函数里,根据异常处理的知识,不难知道result指的就是异常类型 (ExceptionCode),而result = **this (result = this[0][0])

在这里插入图片描述

再来看看异常处理的相关结构,不难猜测 this 的类型为EXCEPTION_POINTERS,那么我们就可以修一下结构体

typedef struct _EXCEPTION_POINTERS {
  PEXCEPTION_RECORD ExceptionRecord;
  PCONTEXT          ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
typedef struct _EXCEPTION_RECORD
 { 
 DWORD ExceptionCode;  //异常码
 DWORD ExceptionFlags;  //标志异常发生后是否还可以继续执行
 struct _EXCEPTION_RECORD* ExceptionRecord; //指向下一个异常节点的指针,这是一个链表结构
 PVOID ExceptionAddress; //异常发生的地址
 DWORD NumberParameters; //异常附加信息
 ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //异常的字符串
} EXCEPTION_RECORD,  *PEXCEPTION_RECORD;
typedef struct _CONTEXT {
     DWORD ContextFlags; //用来表示该结构中的哪些域有效
     DWORD   Dr0, Dr2, Dr3, Dr4, Dr5, Dr6, Dr7; //调试寄存器
     FLOATING_SAVE_AREA FloatSave; //浮点寄存器区
     DWORD   SegGs, SegFs, SegEs, Seg Ds; //段寄存器
     DWORD   Edi, Esi, Ebx, Edx, Ecx, Eax; //通用寄存器组
     DWORD   Ebp, Eip, SegCs, EFlags, Esp, SegSs; //控制寄存器组

     //扩展寄存器,只有特定的处理器才有
     BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;

选中this,然后右键选中"Convert to struct *"

在这里插入图片描述

然后选中_EXCEPTION_POINTTERS,点击ok

在这里插入图片描述

这样我们就可以修好结构体了,然后下面ContextRecord的成员也成功修复了,这样我们就可以更容易地分析了

在这里插入图片描述

接下来是本程序中主要的异常处理代码及其类型

EXCEPTION_ACCESS_VIOLATION         0xC0000005     
程序企图读写一个不可访问的地址时引发的异常。例如企图读取0地址处的内存。
EXCEPTION_BREAKPOINT               0x80000003     
触发断点时引发的异常。
EXCEPTION_PRIV_INSTRUCTION         0xC0000096     
程序企图执行一条当前CPU模式不允许的指令时引发该异常。
EXCEPTION_SINGLE_STEP              0x80000004     
标志寄存器的TF位为1时,每执行一条指令就会引发该异常。主要用于单步调试。
EXCEPTION_ACCESS_VIOLATION         0xC0000005     
程序企图读写一个不可访问的地址时引发的异常。例如企图读取0地址处的内存

在进入filter函数前,我们已经触发了一个断点异常,那么我们将进入到0x80000003的部分

在这里插入图片描述

这个函数其实就是对0x406030开始的指令进行一个初始化,将原地址0xFFFFFFFF分别替换成r2,r1,r0指向的地址(这里只有图中识别出来的指令是有效指令,其他的都是垃圾数据)

初始化前:

在这里插入图片描述

初始化后:(这里动调后基址发生了变化,看偏移就好了)

在这里插入图片描述

而由上往下分别对应r2,r1,r0的地址

在这里插入图片描述

在这里插入图片描述

程序中有

*r2 = *(v8 + v7) != '0'

r2指向的地址的值,就是我们输入的flag转换为二进制后取的第n位

比如我们输入的flag,他的二进制为0011000001110101011101000101111101101111011001100101111101110100

那么第一次*r2 = 0,然后是0,1,1,0,其实就是实现了路径选择

初始化完后,将context中的eip改为0xD06030,然后 Eflags I= 0x100,以下是Elflags寄存器各位的描述

c653b426d9590ea3dd5c71d8c02cfc9c

而0x100的二进制为 1 0000 0000,那么 Eflags= |=0x100其实就是将 TF 位设为 1

TF标志位为1时,执行一条指令后就会触发0x80000004异常

当eip恢复后,事实上,在0xD006030区域运行的代码(除了cli),每执行一行后,就会马上触发0x80000004的异常,又重新进入filter函数中

比如第一次执行 mov eax, 0xC07670,然后触发0x80000004异常,处理完后运行mov eax,[eax],然后又马上触发0x80000004异常

接下来我们来看看ExceptionCode==0x80000004时的处理

在这里插入图片描述

1.(Context ->Eip - 0xD06030 & 0x3F) == 7,

由offset = 0x6030处的代码我们可以知道,从一个mov eax , 0C7670(r2)到下一个mov eax,0C7670(r2)的出现间隔了0x40 (0xD06070 - 0xD06030)。

那么0x40其实就是如下迷宫的一个节点的实际大小

在这里插入图片描述

而这条式子的含义其实就是判断此时运行的是不是在mov eax,[eax]处,这里从r2处取值,也就是拿出我们第n位的二进制数,然后放到eax

2.Context ->Eax ==1

当我们的第n位为1时,Context-> Eip += 0x17 (23),可以看到在执行完mov eax,[eax]后,Eip =0xD06037时,若Eip += 0x17,则新的Eip = 0xD0604E,

在这里插入图片描述

那么这里其实就已经实现了路径选择,两个部分的两个值,可以理解为不同的横坐标和纵坐标

当[0xC07670] (r2)= 0 时,执行上面这部分;当[0xC07670] (r2)= 1 时,执行下面的部分

在这里插入图片描述

代码块中还有cli指令,由于cli指令是特权指令,ring3无法执行,因此运行到cli指令会触发0xc0000096异常,分析对应的处理代码

给*r1 , *r0赋值后,执行 cli 指令,然后会触发0xc0000096异常,那么接下来就进入这异常处理的部分

在这里插入图片描述

关注v14 = *r0 + 17 * *r1,然后将eip设为 0xD06030 + 0x40 * v14,那么这里就是设置跳转的位置

比如我们第一次的选择是"0",那么*r1 = 0xB , *r0 = 0x0,New_Eip = 0xD06030 + 0x40 * 0xB = 0xD062F0

这里就是下一块的地址,就这样形成了一个完美的迷宫结构

在这里插入图片描述

错误路径:int3异常

在这里插入图片描述

错误路径:死循环

在这里插入图片描述

正确路径:

将给0x1处的内存赋值以触发读写内存错误0xC0000005,然后进入到filter中输出正确的部分

在这里插入图片描述

后记

这篇东西其实5月就写好了,但忘了有博客这玩意了55
很好的一道题目,当时没做出来感觉有点可惜。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值