记由长城杯初赛Time_Machine掌握父子进程并出题
一
前言
掌握一道题目的最好办法就是由做题人变成出题人。长城杯初赛,3h 20道题目(一道父子进程,一道VM,一道tea签到),虽然逆向三道题目质量确实高,但是感觉时间真的不够。
本文所说的题目是Time_Machine,题目反编译纯gs,所以本文主要讲解笔者出的题目(原题及笔者出的题目会放到原文附件)(本文如有不足之处,还请指教)。
二
出题思路
不算是思路,站在前人的肩膀上罢了。
出这道题目的初心是因为比赛的时候用心的做了这道题写了出来感觉收获颇多,但当时只局限于写了出来,并不能全面理解,所以有了这么一个想法。
俗话说,实践出真知,熟能生巧,自己去亲手实操将题目敲一遍写一遍,对一个知识点理解的才到位,记忆也更加的深刻。
主要加密算法是使用 SuperFastHash 算法对flag逐字节进行哈希处理,然后通过改变环境变量来进入不同的分支,分支里面对内存进行复写和触发异常来实现父子进程的交互,子进程是一大坨代码块,通过以下指令块控制父进程调试子进程实现加密。
movabs r11,%d
xor r11,0x1337
ror r11,13
movabs r13,1
ud2
三
解题
程序是64位的exe。
ida64分析
拖入ida64后映入眼帘,有一个小的主函数,用于检查环境变量的值,ix221iscc221如果为 null,则继续执行sub_401C38函数,否则调用函数sub_40195C函数。
也就是相当于一个父进检测。
检验的方式是通过一个环境变量,过掉的方式很简单,jz指令出下断点,然后改zf标志位即可过掉。
sub_401C38-输入函数
进入sub_401C38函数,它打印,然后接受用户输入,然后将内存中的一个区域复制到中间内存,分配一个新的区域,然后将中间内存复制到新分配的区域。(这边的内存就是父进程里面开的子进程并且调试)
这个区域是一个 shell 代码,最后调用这个 shell 代码,用作用户输入作为参数。也就是说输入函数是通过子进程进行调用的,并且VirtualAlloc(0x22A6ui64), --- 开辟的shellcode的长度0x22a6。
debugger
重新在main分支的地方下断点,过掉jz,然后手动进子进程分支,看看结尾出函数指针在干嘛。
f7进入shellcode。
这儿有一大堆代码块,调一下,发现此块块执行以下操作:
将用户输入的第一个字节加载到r12b寄存器中;
将一个奇怪的值与 0x1337 进行异或;
将结果右移 0xD 位;
将r13寄存器设置为0;
执行ud2定义的“未定义”操作码会使程序崩溃!
也就是说:
从输入的flag里取一个字节,也没发现有什么校验逻辑,但注意到ud2
指令(0F 0B
),运行到该指令会触发异常。
由于父进程在调试子进程,所以分析父进程的操作逻辑。
sub_40195C-父进程
设置环境变量ix221iscc221=1
,然后开一个子进程并调试,子进程进输入分支()。
当调试出现异常时去看输出Correct Flag :)\n
的函数。
superhash算法逐字节加密。
断点调试一下,
发现了0f 0b。
检查这两个字节是否为0F 0B
,很巧,刚好是子进程中的ud2
,很明显这里在捕获子进程在ud2
指令出错。
接下来就是看加密flag逻辑,可以看到
取r12b,进行加密操作后结果与r11d校验,最后检查r13是否为1,两个校验都通过,计数器+1,计数器等于0x18时输出正确(flag长度为0x18)。
enc
解法一:梭哈!
因为是一个一个字节的输入,中间只会case 1。
直接把1字节的256种结果全部算出来,后面梭哈就好。
调回子进程的奇怪代码-shellcode。
现在就知道子进程在干嘛了,先从flag取一个字节到r12b,再计算r11d,给r13赋值,前面分析父进程知道只有在r13==1
时才进行校验,用idapython解析一下,在r13==1
时查表r11d得到r12b,即得到flag的一个字节。
exp
import idc
m = {}
for c in range(256):
l1 = 1
l2 = ((c + l1) << 10) ^ (c + l1)
l1 = (l2 >> 1) + l2
l3 = (((8 * l1) ^ l1) >> 5) + ((8 * l1) ^ l1)
l3 &= 0xFFFFFFFF
l4 = (((16 * l3) ^ l3) >> 17) + ((16 * l3) ^ l3)
l4 &= 0xFFFFFFFF
r = (l4 << 25) ^ l4
r &= 0xFFFFFFFF
r = (r >> 6) + r
r &= 0xFFFFFFFF
print(hex(c), hex(r))
m[r] = c
start = 0x1E0000
end = start + 0x22a6 #VirtualAlloc
ea = start
while (ea < end):
# mov r12b, [rcx+i]
ins_len = idc.create_insn(ea)
ins = idc.generate_disasm_line(ea, 0)
if (ins == "retn"):
break
ea += ins_len
# mov r11, k
ins_len = idc.create_insn(ea)
ins = idc.generate_disasm_line(ea, 0)
k1 = idc.print_operand(ea, 1)
k1 = int("0x"+k1[:-1], 16)
ea += ins_len
# xor r11, k
ins_len = idc.create_insn(ea)
ins = idc.generate_disasm_line(ea, 0)
k2 = idc.print_operand(ea, 1)
k2 = int("0x"+k2[:-1], 16)
ea += ins_len
# ror r11, k
ins_len = idc.create_insn(ea)
ins = idc.generate_disasm_line(ea, 0)
k3 = idc.print_operand(ea, 1)
k3 = int("0x"+k3[:-1], 16)
ea += ins_len
r11 = (((k1 ^ k2) >> k3) | ((k1 ^ k2) << (64 - k3))) & 0xFFFFFFFFFFFFFFFF
# mov r13, 0/1
ins_len = idc.create_insn(ea)
ins = idc.generate_disasm_line(ea, 0)
r13 = idc.print_operand(ea, 1)
r13 = int(r13)
ea += ins_len
# ud2
ins_len = idc.create_insn(ea)
ins = idc.generate_disasm_line(ea, 0)
ea += ins_len
if (r13 == 1):
print(chr(m[r11]), end='')
得解
解法二:superfasthash算法
这个加密算法是使用 SuperFastHash 算法对输入的每个字符进行散列,然后与一些硬编码的散列进行比较。
加密源码
uint32_t hash(char *a1)
{
uint32_t v2;
uint32_t v3;
uint32_t v4;
uint32_t v5;
uint32_t v6;
uint32_t v7;
uint32_t v8;
uint32_t v9;
uint32_t v10;
uint32_t v11;
uint32_t i;
unsigned char* v13;
v13 = a1;
v4 = 1;
v2 = v4 & 3;
for (i = v4 >> 2; i > 0; --i)
{
v5 = (v13[1] << 8) + *v13 + v4;
v3 = v5 ^ (((v13[3] << 8) + v13[2]) << 11);
v13 += 4;
v4 = (((v5 << 16) ^ v3) >> 11) + ((v5 << 16) ^ v3);
}
switch (v2)
{
case 2:
v8 = (v13[1] << 8) + *v13 + v4;
v4 = (((v8 << 11) ^ v8) >> 17) ^ (v8 << 11) ^ v8;
break;
case 3:
v6 = (v13[1] << 8) + *v13 + v4;
v7 = (v13[2] << 18) ^ (v6 << 16) ^ v6;
v4 = (v7 >> 11) + v7;
break;
case 1:
v9 = ((*v13 + v4) << 10) ^ (*v13 + v4);
v4 = (v9 >> 1) + v9;
break;
}
v10 = (((8 * v4) ^ v4) >> 5) + ((8 * v4) ^ v4);
v11 = (((16 * v10) ^ v10) >> 17) + ((16 * v10) ^ v10);
return (((v11 << 25) ^ v11) >> 6) + ((v11 << 25) ^ v11);
}
常规解题方法步骤
1、提取shellcode代码块
with open("instructions.txt") as f:
lines = f.readlines()
blocks = []
block = []
for line in lines:
line = line.strip()
if line:
block.append(line)
if line == "ud2":
blocks.append(block)
block = []
with open("1.txt", "w") as out_file:
for b in blocks:
if "mov r13, 1" in b[4]:
out_file.write("\n".join(b) + "\n")
2、筛选出正确的代码块,得到正确密文的hash
def extract_ciphers(filename):
try:
with open(filename) as f:
lines = f.readlines()
except FileNotFoundError:
print("Error: File not found")
return []
ciphers = []
for i in range(0, len(lines), 6):
block = lines[i:i+6]
# 检查块的长度是否足够
if len(block) < 2:
print("Error: Block does not have enough lines")
continue
# 提取第二行中的十六进制数值
line = block[1].strip()
start_index = line.find("r11, ") + len("r11, ")
end_index = line.find("h", start_index)
if start_index != -1 and end_index != -1:
cipher_str = line[start_index:end_index]
try:
cipher_value = int(cipher_str, 16)
cipher_value ^= 0x1337
res = ror(cipher_value, 0xd, 64)
ciphers.append(res)
except ValueError:
print("Error: Invalid hex value")
return ciphers
def ror(val, r_bits, max_bits):
return ((val & (2**max_bits-1)) >> r_bits%max_bits) | \
(val << (max_bits-(r_bits%max_bits)) & (2**max_bits-1))
def main():
ciphers = extract_ciphers("clean_diassembly.txt")
if ciphers:
print("Ciphers:", ciphers)
if __name__ == "__main__":
main()
3、hash解密
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdint.h>
uint32_t hash(char* a1)
{
uint32_t v2;
uint32_t v3;
uint32_t v4;
uint32_t v5;
uint32_t v6;
uint32_t v7;
uint32_t v8;
uint32_t v9;
uint32_t v10;
uint32_t v11;
uint32_t i;
char* v13;
v13 = a1;
v4 = 1;
v2 = v4 & 3;
for (i = v4 >> 2; i > 0; --i)
{
v5 = (v13[1] << 8) + *v13 + v4;
v3 = v5 ^ (((v13[3] << 8) + v13[2]) << 11);
v13 += 4;
v4 = (((v5 << 16) ^ v3) >> 11) + ((v5 << 16) ^ v3);
}
switch (v2)
{
case 2:
v8 = (v13[1] << 8) + *v13 + v4;
v4 = (((v8 << 11) ^ v8) >> 17) ^ (v8 << 11) ^ v8;
break;
case 3:
v6 = (v13[1] << 8) + *v13 + v4;
v7 = (v13[2] << 18) ^ (v6 << 16) ^ v6;
v4 = (v7 >> 11) + v7;
break;
case 1:
v9 = ((*v13 + v4) << 10) ^ (*v13 + v4);
v4 = (v9 >> 1) + v9;
break;
}
v10 = (((8 * v4) ^ v4) >> 5) + ((8 * v4) ^ v4);
v11 = (((16 * v10) ^ v10) >> 17) + ((16 * v10) ^ v10);
return (((v11 << 25) ^ v11) >> 6) + ((v11 << 25) ^ v11);
}
int main()
{
uint32_t flag_hashes[] = { 3250317036, 2059931180, 831843374, 831843374, 2137638942, 291415938, 3060405360, 51373921, 291415938, 1891737825, 2577271396, 2682404089, 1319470528, 291415938, 2577271396, 2376513170, 291415938, 2685652659, 1867828354, 1457933662, 260209567, 2240464916, 3927678806, 2884595695 };
char val[2] = { 0 };
uint32_t all_hashes[256] = { 0 };
for (int i = 0; i < 256; i++)
{
val[0] = i;
all_hashes[i] = hash(val);
}
for (int i = 0; i < 39; i++)
{
for (int j = 0; j < 256; j++)
{
if (flag_hashes[i] == all_hashes[j])
{
printf("%c", j);
break;
}
}
}
return 1;
}
希望本文能让大家对于父子进程的理解更好一点点。
还是那句话,掌握一道题目的最好办法就是由做题人变成出题人。
看雪ID:IX221
https://bbs.kanxue.com/user-home-962534.htm
# 往期推荐
2、BFS Ekoparty 2022 Linux Kernel Exploitation Challenge
3、银狐样本分析
4、使用pysqlcipher3操作Windows微信数据库
5、XYCTF两道Unity IL2CPP题的出题思路与题解
球分享
球点赞
球在看
点击阅读原文查看更多