[pdctf] 二叉树Wp

发布时间:2022-07-03 发布网站:脚本宝典
脚本宝典收集整理的这篇文章主要介绍了[pdctf] 二叉树Wp脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。

二叉树

Uh, you can give the key. IT's a binary seArch tree... I think?
Author: rBTree

二叉树是一个 ELF 文件,它接受 100 个字符输入并通过遍历“二叉搜索树”来验证它

解决方案

查看该_start函数,程序读取 100 个字节的输入,然后将字节的每一位映射到一个大的bit_array. 该程序还包含一个非常大的静态数据数组(Big_data_array此处命名)和一个“PASS”和“FaiL”字符串。

[pdctf] 二叉树Wp

真正有趣的函数是地址处的自修改函数0x400080

[pdctf] 二叉树Wp

在高级视图中,可以清楚地看到该函数根据rbx/指向的数据修改自身bda_pointer:自修改后,代码从 中读取一位bit_array,将bit_array指针增加一并运行test al, al以查看该位是否为零。之后的指令实际上并没有被执行,因为它们被之前的代码改变了。

[pdctf] 二叉树Wp

自我修改后的代码是这样的:

0x4000ad:       nop
0x4000ae:       je      0x4000bd
0x4000b0:       lea     rbx, [rdi + 0x40]
0x4000b4:       nop
0x4000b5:       add     r9, 0x49
0x4000b9:       jmp     0x400080
0x4000bb:       nop
0x4000bc:       nop
0x4000bd:       lea     rbx, [rdi + 0x20]
0x4000c1:       nop
0x4000c2:       nop
0x4000c3:       nop
0x4000c4:       add     r9, 0x11
0x4000c8:       nop
0x4000c9:       nop
0x4000ca:       nop
0x4000cb:       jmp     0x400080

 

现在这很有趣:实际执行的代码使用test指令设置的标志根据输入位的值进行分支。根据该位是 0 还是 1,执行两个外观相似的代码块之一。他们都将rbx/设置bda_pointer为一个新地址,绝对到rdibda_addressbig_data_array,添加一个值r9并递归地跳回到函数的开头。

作为rbx改变,这取决于分支自修改-XOR会显示不同的新代码。

输入的所有 800 位都显示相同的代码语义结构 bit_array

在第 800 位之后,将代码改回原始函数(现在假设所有路径恢复相同的原始函数):

0x4000ad:       mov     rax, 1
0x4000b4:       mov     rdi, 1
0x4000bb:       mov     rdx, 5
0x4000c2:       cmp     r9, 0x49da
0x4000c9:       jg      0x4000d2
0x4000cb:       mov     rsi, r10
0x4000ce:       Syscall
0x4000d0:       jmp     0x4000d7
0x4000d2:       mov     rsi, r11
0x4000d5:       syscall
0x4000d7:       mov     rax, 0x3c
0x4000de:       xor     rdi, rdi
0x4000e1:       syscall

 

正如挑战描述中提到的,这可以被视为具有加权分支的 800 深度二叉搜索树。目标可能是找到一个 800 位输入,其所采用分支的添加权重小于或等于MAGIC_VALUE 0x49da18906

问题是对所有路径进行完整搜索是不可行的,因为 800 深度的二叉搜索树将具有 2**800 = 6668014432879854274079851790721257797144758322315908160396257811764037237817632071521432200871554290742929910593433240445888801654119365080363356052330830046095157579514014558463078285911814024728965016135886601981690748037476461291163877376唯一路径。使用修剪分支的广度优先搜索一旦它们高于MAGIC_VALUE也似乎不可行,因为它似乎卡在深度 200 附近,这仍然是2**200 = 1606938044258990275541962092341162602522202993782792835301376唯一的路径。

可是等等这是一个二叉搜索树真的有意义吗?不,因为在二叉搜索树中,每个节点不仅有 2 个子节点(满足这个要求),而且只有一个父节点(这使它成为一棵树)。这意味着应该sum([2**i for i in range(801)])节点。由于big_data_array对节点进行编码并且816096大小仅为字节,因此它只能对25503节点进行编码所以这显然不能是一棵树,它更像是一个二分搜索图。

那么这个图实际上有哪些属性呢?有一个显式的起始节点,每个节点有两个孩子,图是有向的/每条边是单向的,有多个出口节点,每条边都有一个权重。现在让我们假设权重都是正的而不是零。此外,我们可以确定,从一开始,任何 800 次转换都会出现一个出口节点。

[pdctf] 二叉树Wp

由于MAGIC_VALUE不是一个精确的值,而只是允许正确解的最大值,我们可以通过找到从开始到退出的成本最低的路径来解决这个问题。一种方法是计算向后到达特定出口节点的最小成本:

[pdctf] 二叉树Wp

由于如果遍历了节点的所有子节点,则节点的反向计算成本不会降低,因此不再需要遍历所有路径。只需要遍历所有节点即可。

为了使用自修改代码,我使用了capstone以下脚本使用上面显示的方法来查找最短路径:

从 顶点 进口 *


loopFunc  =  0x400080 
loopFuncLen  =  0x4000e1 - loopFunc

bigdataArray  =  0x400176 
bigdataArrayLen  =  0x4c7556 - bigdataArray

MAGIC_VALUE  =  0x49da


md  =  Cs ( CS_ARCH_X86 , CS_MODE_64 )
 binaryFile  =  oPEn ( "main.elf" , "rb" )
 data  =  binaryFile。读()

loopFuncMem  = 数据[ 0x80 : 0x80 + loopFuncLen ]
 bigArrayMem  = 数据[ 0x176 : 0x176 + bigdataArrayLen ]

loopFuncMem  = [ b  for  b  in  loopFuncMem ]
 bigArrayMem  = [ b  for  b  in  bigArrayMem ]

二进制文件。关闭()


# 实际最小路径计算

最短路径 = {}

DEF  registerPath(路径,附加):
     revAdd  =  0
    如果 路径[ - 1 ]在 最短路径:
         revAdd  = 最短路径[路径[ - 1 ]]
    否则:
         revAdd  = 添加[ - 1 ]

    为 [R 在 范围(1,len个(路径)+ 1):
        如果 - [R  >  1:revAdd  =  revAdd  + 附加[ - [R ]
        如果 路径[ - [R ]在 最短路径:
            最短路径[路径[ - [R ]] = 分钟(最短路径[路径[ - r],revAdd)
        否则:
            最短路径[路径[ - [R ]] =  revAdd

def  parsebranch ( mem , rbx , val , depth = 0 , path = [], add = []):

   
    如果 RBX 在 最短路径:
         registerPath(路径,附加)
        如果 最短路径[ RBX ] + VAL  >  MAGIC_VALUE:
            返回
    
    if  depth  ==  100 * 8 : 
         registerPath ( path , add )
        返回

    #应用自修改XOR
    用于 我 在 范围(8 * 4):
         MEM [我] ^ = bigArrayMem [ RBX +我]

    currentAdd  =  0 
    nextPointer  =  0 
    existingJumps  = []
    
    因为 我 在 md。disasm ( bytes ( mem ), 0x2d ):
         op  =  i。助记符
        if  op  ==  "nop" : 
             continue 
        if  op  ==  "je" :
             continue 
        if  op  ==  "jmp" :
             existingJumps。追加((nextPointer,currentAdd))
            继续
        ,如果 运算 ==  “添加”:
             currentAdd  =  INT(我。op_str。分裂(“”)[ 1 ]。条(),16)
            继续
        ,如果 运算 ==  “LEA” :
             nextPointer  =  INT(我。op_str。分裂(“+”)[ 1 ] .split ( "]" )[ 0 ] .strip (), 16 )
            继续
        PRint ( "0x%x: t %s t %s"  % ( i . address , i . mnemonic , i . op_str )) # 如果发生意外,则打印
    
    对于(nextPointer,currentAdd)  在 existingJumps:
         parseBranch(MEM + [],nextPointer,VAL + currentAdd,深度+ 1,路径+ [ nextPointer ],添加+ [ currentAdd ])
    
# 计算最短路径
parseBranch ( loopFuncMem [ 0x2d :], 0 , 0 )

 

有趣的是,脚本再次“卡住”在 ~depth 400 和 200 处,但是查看它,大部分时间占用的路径无论如何都不太可能导致解决方案,因此修剪它们可以更快地解决它:

        #这止不必要的计算
        ,如果 最短路径[ RBX ] + VAL  >  14000 和 深度 <  400:
            返回
            
        如果 最短路径[ RBX ] + VAL  >  12000 和 深度 <  200:
            返回

 

有了最短路径的数据,我们可以很容易地遍历这个图:

[pdctf] 二叉树Wp

def  verboseCalc ( mem , rbx , val , depth = 0 , bits = []):

    如果 深度 ==  100 * 8 : 
         barray  =  ''。加入('0'  ,如果 b  ==  0 否则 '1' 为 b 中 位)
         #打印标记
        打印('' 。加入([ Chr(INT(barray [我* 8 :(我+ 1)* 8 ] [: : - 1 ], 2 ))对于 我 在 范围内( len ( barray ) // 8 )]))
        返回
        
    #应用自修改XOR
    用于 我 在 范围(8 * 4):
         MEM [我] ^ = bigArrayMem [ RBX +我]

    当前添加 =  0 下一个
    指针 =  0
    
    
    现有跳跃 = []
    
    因为 我 在 md。disasm ( bytes ( mem ), 0x2d ):
         op  =  i。助记符
        if  op  ==  "jmp" :
             existingJumps。追加((nextPointer,currentAdd))
        如果 运算 ==  “添加”:
             currentAdd  =  INT(我。op_str。分裂(“”)[ 1 ]。strip (), 16 )
         if  op  ==  "lea" :
             nextPointer  =  int ( i . op_str . split ( "+" )[ 1 ]. split ( "]" )[ 0 ]. strip (), 16 )
    
    jo  =  shortestPath [ existingJumps [ 0 ][ 0 ]]
     jz  =  shortestPath [ existingJumps [ 1 ][ 0 ]]

    if  jo + val  ==  MAGIC_VALUE :
         verboseCalc ( mem + [], existingJumps [ 0 ][ 0 ], val + existingJumps [ 0 ][ 1 ], depth + 1 , bits + [ 1 ])
     if  jz + val  ==  MAGIC_VALUE :
         verboseCalc ( mem + [], existingJumps [ 1 ][0 ], val + existingJumps [ 1 ][ 1 ],深度+ 1 ,位+ [ 0 ])

# 使用最短路径映射并遍历它
verboseCalc ( loopFuncMem [ 0x2d :], 0 , 0 )

 

通过所有优化,我们只需运行脚本并在几秒钟内获得标志

>python solve.py
pbctf{!!finding_the_shortest_path_in_self-modifying_code!!_e74c30e30bb22a478ac513c9017F1b2608abfee7}

来自于博客:

https://rainbowpigeon.me/posts/pbctf-2021/#cosmo

 

脚本宝典总结

以上是脚本宝典为你收集整理的[pdctf] 二叉树Wp全部内容,希望文章能够帮你解决[pdctf] 二叉树Wp所遇到的问题。

如果觉得脚本宝典网站内容还不错,欢迎将脚本宝典推荐好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。