前言
掌握一道题目的最好办法就是由做题人变成出题人()
本文所说的题目是长城杯某区逆向Time_Machine,题目反编译纯gs,所以本文主要讲解笔者出的题目
:笔者出的题目准备给iscc擂台的,但是没轮上(感觉擂台被控了,怎么说hh)
出题思路
额,不算是思路,站在前人的肩膀上罢了。
出这道题目的初心是因为比赛的时候用心的做了这道题写了出来感觉收获颇多,但当时只局限于写了出来,并不能全面理解,所以有了这么一个想法。
俗话说,实践出真知,熟能生巧,自己去亲手实操将题目敲一遍写一遍,对一个知识点理解的才到位,记忆也更加的深刻。
主要加密算法是使用 SuperFastHash 算法对flag逐字节进行哈希处理,然后通过改变环境变量来进入不同的分支,分支里面对内存进行复写和触发异常来实现父子进程的交互,子进程是一大坨代码块,通过以下指令块控制父进程调试子进程实现加密。
1 2 3 4 5
| movabs r11,%d xor r11,0x1337 ror r11,13 movabs r13,1 #0或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)
sub_401584 - enc function
解法一:梭哈!
因为是一个一个字节的输入,中间只会case 1
直接把1字节的256种结果全部算出来,后面梭哈就好
调回子进程的shellcode
现在就知道子进程在干嘛了,先从flag取一个字节到r12b,再计算r11d,给r13赋值,前面分析父进程知道只有在r13==1
时才进行校验,用idapython解析一下,在r13==1
时查表r11d得到r12b,即得到flag的一个字节
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| 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 算法对输入的每个字符进行散列,然后与一些硬编码的散列进行比较。
加密源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| 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代码块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| 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解密
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| #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; }
|
结语
希望本文能让大家对于父子进程的理解更好一点点,哈哈哈
还是那句话,掌握一道题目的最好办法就是由做题人变成出题人。
附录
如有想要实验的,附源码一份
如有需要,细节联系我
题目源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
| #include <cstdio> #include <cstdlib> #include <cstdio> #include <cstdlib> #include <windows.h> #include <unordered_map> #include <assert.h>
int counter = 0; #if !defined (Get16Bits) #define Get16Bits(d) ((((UINT32)(((const UINT8*)(d))[1])) << 8)\ +(UINT32)(((const UINT8*)(d))[0]) ) #endif
SIZE_T StringLengthA(LPCSTR String) { LPCSTR String2;
for (String2 = String; *String2; ++String2);
return (String2 - String); }
UINT32 HashStringSuperFastHashA(PCHAR String) { INT Length = StringLengthA(String); UINT32 Hash = Length; INT Tmp = 0;
INT Rem = Length & 3; Length >>= 2;
for (; Length > 0; Length--) { Hash += Get16Bits(String); Tmp = (Get16Bits(String + 2) << 11) ^ Hash; Hash = (Hash << 16) ^ Tmp; #pragma warning( push ) #pragma warning( disable : 6305) String += 2 * sizeof(UINT16); #pragma warning( pop ) Hash += Hash >> 11; }
switch (Rem) { case 3: { Hash += Get16Bits(String); Hash ^= Hash << 16; Hash ^= ((UCHAR)String[sizeof(UINT16)]) << 18; Hash += Hash >> 11; break; } case 2: { Hash += Get16Bits(String); Hash ^= Hash << 11; Hash ^= Hash >> 17; break; } case 1: { Hash += (UCHAR)*String; Hash ^= Hash << 10; Hash += Hash >> 1; } }
Hash ^= Hash << 3; Hash += Hash >> 5; Hash ^= Hash << 4; Hash += Hash >> 17; Hash ^= Hash << 25; Hash += Hash >> 6;
return Hash; } static void HandleException( const DEBUG_EVENT& ev, HANDLE ph, HANDLE th) {
CONTEXT ctx{}; ctx.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER; BOOL ret = GetThreadContext(th, &ctx); assert(ret);
BYTE buf[2]{}; BYTE nops[] = { 0x90,0x90 }; BYTE ret_nop[] = { 0xc3,0x90 }; SIZE_T actually_read = 0; ret = ReadProcessMemory( ph, (void*)ctx.Rip, buf, 2, &actually_read); assert(ret); char str_x[2] = { 0 }; if (buf[0] == 0x0f && buf[1] == 0x0b) { str_x[0] = ctx.R12 % 256; UINT32 hash = HashStringSuperFastHashA(str_x); if (hash == ctx.R11 && ctx.R13 == 1) { counter++; } if (counter == 24) { printf("Correct Flag :)\n"); ret = WriteProcessMemory( ph, (void*)ctx.Rip, ret_nop, 2, &actually_read); SetThreadContext(th, &ctx); } if (counter != 24) { ret = WriteProcessMemory( ph, (void*)ctx.Rip, nops, 2, &actually_read); SetThreadContext(th, &ctx); }
}
}
int Debugger(char* file_name) { _putenv("ix221iscc221=1");
STARTUPINFO si{}; si.cb = sizeof(si); PROCESS_INFORMATION pi{}; BOOL ret = CreateProcess( file_name, NULL, NULL, NULL, FALSE, DEBUG_PROCESS, NULL, NULL, &si, &pi);
DEBUG_EVENT ev; std::unordered_map<DWORD, HANDLE> threads;
threads[pi.dwThreadId] = pi.hThread;
bool the_end = false; while (!the_end) {
ret = WaitForDebugEvent(&ev, INFINITE); if (!ret) { printf("Weird failed: %u\n", GetLastError()); return 1; }
switch (ev.dwDebugEventCode) { case EXCEPTION_DEBUG_EVENT:
HandleException(ev, pi.hProcess, threads[ev.dwThreadId]); break;
case EXIT_PROCESS_DEBUG_EVENT: the_end = true; break;
case CREATE_THREAD_DEBUG_EVENT: threads[ev.dwThreadId] = ev.u.CreateThread.hThread; break;
case EXIT_THREAD_DEBUG_EVENT: CloseHandle(threads[ev.dwThreadId]); threads.erase(ev.dwThreadId); break;
case CREATE_PROCESS_DEBUG_EVENT: CloseHandle(ev.u.CreateProcessInfo.hFile); CloseHandle(ev.u.CreateProcessInfo.hProcess); CloseHandle(ev.u.CreateProcessInfo.hThread); break;
case LOAD_DLL_DEBUG_EVENT: case UNLOAD_DLL_DEBUG_EVENT: break;
default: printf("Weird: %u\n", ev.dwDebugEventCode); break; }
ContinueDebugEvent( ev.dwProcessId, ev.dwThreadId, DBG_CONTINUE); }
CloseHandle(pi.hThread); CloseHandle(pi.hProcess);
return 0; }
int Debuggee() {
unsigned char input[25] = { 0 }; printf("Enter The Flag:\n"); scanf("%s", &input); unsigned char data[] = { 0x44...... }; long size = sizeof(data); PVOID image; image = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); memcpy(image, data, size); void (*foo)(unsigned char*) = (void(*)(unsigned char*))image; foo(input); return 0; }
int main(int argc, char** argv) { if (getenv("ix221iscc221") == nullptr) { return Debugger(argv[0]); } else { return Debuggee(); } }
|