脚本宝典收集整理的这篇文章主要介绍了[pdctf] 二叉树Wp,脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。
二叉树是一个 ELF 文件,它接受 100 个字符输入并通过遍历“二叉搜索树”来验证它
查看该_start
函数,程序读取 100 个字节的输入,然后将字节的每一位映射到一个大的bit_array
. 该程序还包含一个非常大的静态数据数组(Big_data_array
此处命名)和一个“PASS”和“FaiL”字符串。
真正有趣的函数是地址处的自修改函数0x400080
:
在高级视图中,可以清楚地看到该函数根据rbx
/指向的数据修改自身bda_pointer
:自修改后,代码从 中读取一位bit_array
,将bit_array
指针增加一并运行test al, al
以查看该位是否为零。之后的指令实际上并没有被执行,因为它们被之前的代码改变了。
自我修改后的代码是这样的:
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
为一个新地址,绝对到rdi
/ bda_address
/ big_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
0x49da
/ 18906
。
问题是对所有路径进行完整搜索是不可行的,因为 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 次转换都会出现一个出口节点。
由于MAGIC_VALUE
不是一个精确的值,而只是允许正确解的最大值,我们可以通过找到从开始到退出的成本最低的路径来解决这个问题。一种方法是计算向后到达特定出口节点的最小成本:
由于如果遍历了节点的所有子节点,则节点的反向计算成本不会降低,因此不再需要遍历所有路径。只需要遍历所有节点即可。
为了使用自修改代码,我使用了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: 返回
有了最短路径的数据,我们可以很容易地遍历这个图:
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,请注明来意。