本文介绍了2024 KCTF 大赛的相关内容,包括比赛设置的评分体系、赛题的解析等。文章还详细描述了比赛中的一道题目《试探》的设计思路、解题过程以及对于相关混淆技术的处理。
KCTF大赛于2024年8月15日正式开赛,设置了多维度的评分体系,旨在引导竞赛的难度和趣味度,使其更具挑战性和吸引力。
赛题《试探》是一个采用拼图游戏设计的题目,初始状态和目标状态已知,需要通过移动元素达到目标状态。整个算法隐藏在一段shellcode中,涉及混淆技术。
解题过程包括使用IDA查看main函数,分析字符串加密和shellcode逻辑,处理混淆技术,包括字节替换和无效指令去除等。
通过解决棋盘问题得到的走法路径转换成三进制数,最终得到flag即注册码为“011110202122”。
2024 KCTF 大赛于8月15日正式开赛!比赛设置了多维度的评分体系,包括难度值、火力值和精致度积分,旨在引导竞赛的难度和趣味度,使其更具挑战性和吸引力。同时,也为参赛选手提供了更加公平、有趣的竞赛平台。
今天中午12点,
第十题《试探》已截止答题,本题共有10支战队成功破解,【hzqmwne】战队用时
1小时48分18秒
抢先拿下此题,第二名来自【Nepnep】战队、第三名来自【COMPASS】战队。
*注意:签到题《逐光启航》持续开放,整个比赛期间均可提交答案获得积分
题目名称:hidesc
运行环境:win10 win11
输出提示:key正确则输出提示ok!
{0, 1, 3},
{5, 2, 6},
{4, 7, 8}
{1, 2, 3},
{4, 5, 6},
{7, 8, 0}
通过移动元素0来到达目标状态。移动过程中0元素的坐标即为注册码。
整个算法隐藏在一段shellcode中,并且shellcode加入了大量的花指令干扰分析。
对shellcode的加载函进行了字符串隐藏 并通过系统调用隐藏API的方式干扰分析。
以下解析由看雪专家【wx_孤城】给出,来自【中午吃什么】战队。
丢进IDA查看main函数,有一些简单的字符串加密。
逐步断点,调用了以下函数:
ntdll.dll
NtAddBootEntry
TpAllocWait
TpSetWait
猜测为shellcode注入,简单分析下main函数逻辑。
18DB处的逻辑,将kctf + input + 6050处的一块shellcode拷贝到75C0
之后创建2个线程,线程A执行shellcode, 线程B等待答案并输出结果ok!或no!
选中140006050,使用IDA-->Edit-->Code转换为代码。
这时候我们是不能F5的,因为作者做了混淆。
混淆分析
第一部分将部分字节0x00换成了0x2A,导致IDA静态分析挂掉所以不能F5。
第二部分增加了一些无关的跳转和无效指令。
经过分析发现无效指令特征码只有这三种:
?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? FD EB 1F 3E 1C EB EB
?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? FF EB 15 3E 1D EB FB
?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? FE EB 18 3E 1C EB EB
去混淆脚本
# -*- coding: gbk -*-
def replace_bytes_and_preceding(file_path, search_bytes, replace_byte, preceding_length):
# 读取二进制文件内容
with open(file_path, 'rb') as file:
data = file.read()
# 将要查找的字节和替换的字节转换为字节类型
search_bytes = bytes.fromhex(search_bytes)
replace_byte = bytes.fromhex(replace_byte)
replace_length = len(search_bytes) + preceding_length # 替换的总长度
# 创建一个可变字节数组来进行操作
modified_data = bytearray(data)
# 初始化搜索开始位置
start = 0
while start < len(modified_data):
# 查找字节序列的位置
index = modified_data.find(search_bytes, start)
if index == -1:
break
# 计算需要替换的起始位置
replace_start = max(0, index - preceding_length)
# 将替换的范围全部设置为 `replace_byte`
modified_data[replace_start:index + len(search_bytes)] = replace_byte * replace_length
# 更新搜索开始位置,跳过当前替换的位置
start = index + len(search_bytes)
# 将修改后的数据写回到文件中
with open(file_path, 'wb') as file:
file.write(modified_data)
print(f"Replaced all occurrences of {search_bytes.hex()} and preceding {preceding_length} bytes with {replace_byte.hex()} in {file_path}.")
preceding_length = 11 # 替换之前的字节数
replace_bytes_and_preceding('test3.exe', 'FD EB 1F 3E 1C EB EB', '90', preceding_length)
replace_bytes_and_preceding('test3.exe', 'FF EB 15 3E 1D EB FB', '90', preceding_length)
replace_bytes_and_preceding('test3.exe', 'FE EB 18 3E 1C EB EB', '90', preceding_length)
# -*- coding: gbk -*-
def replace_specific_byte_in_range(file_path, search_bytes, search_byte, replace_byte, offset_start, offset_end):
# 读取二进制文件内容
with open(file_path, 'rb') as file:
data = file.read()
# 将要查找的字节和替换的字节转换为字节类型
search_bytes = bytes.fromhex(search_bytes)
search_byte = bytes.fromhex(search_byte)
replace_byte = bytes.fromhex(replace_byte)
# 创建一个可变字节数组来进行操作
modified_data = bytearray(data)
# 初始化搜索开始位置
start = 0
while start < len(modified_data):
# 查找字节序列的位置
index = modified_data.find(search_bytes, start)
if index == -1:
break
# 计算替换范围的起始和结束位置
range_start = index + offset_start
range_end = min(index + offset_end, len(modified_data))
# 替换范围内的所有指定字节
for i in range(range_start, range_end):
if modified_data[i] == search_byte[0]:
modified_data[i] = replace_byte[0]
# 更新搜索开始位置,跳过当前查找的位置
start = index + len(search_bytes)
# 将修改后的数据写回到文件中
with open(file_path, 'wb') as file:
file.write(modified_data)
print(f"Replaced all occurrences of {search_byte.hex()} with {replace_byte.hex()} in range [{offset_start:#X}, {offset_end:#X}] after each occurrence of {search_bytes.hex()} in {file_path}.")
replace_specific_byte_in_range('test3.exe', '57 50 51 56 E8 FF FF FF FF C0', '2A', '00', 0x2C, 0xEA7 + 0x2C)
用脚本去除混淆后的代码,可以发现无效指令都被换成nop了。
然后就可以愉快的F5了。
然后分析主校验函数:
分析后看起来是一个8-puzzle问题,要求从起始棋盘状态走到最终状态的最少步数。