目录 见龙在田 - 注册... 1 飞龙在天 -login.htm... 3 龙跃在渊 - IgniteMe.exe... 7 潜龙勿用 -greek_to_me.exe... 10 神龙摆尾 -notepad.exe... 16 密云不雨 -pewpewboat.exe... 22 突如其来 -payload.dll... 28 双龙取水 -zsud.exe... 35 震惊百里 -flair.apk... 46 时乘六龙 -remorse.ino.hex... 59 龙战于野 -shell.php... 64 履霜冰至 -covfefe.exe... 72 亢龙有悔 - 一次 APT 攻击分析 -[missing]... 80 Layer 0: 抛砖引玉 ( 攻击场景介绍 )... 80 Layer 1: 金蝉脱壳 ( 下载程序 )... 81 Layer 2: 无中生有 ( 程序框架 )... 82 Layer 3: 树上开花 ( 插件分析 )... 84 Layer 4: 顺手牵羊 ( 信息窃取 )... 85 Layer 5: 声东击西 ( 内网渗透 )... 86 Layer 6: 暗度陈仓 ( 偷取重要文件 )... 88 Layer 7: 反客为主 ( 还原 lab10.zip.cry)... 90 Layer 8: 釜底抽薪 (get flag!!!)... 91
见龙在田 - 注册 9 月 2 日凌晨,Flare-on 开通了第四届的逆向挑战赛, 网址为 https://2017.flare-on.com
国内的很多用户在填写完信息提交的时候发现页面有如下提示 : 什么鬼, 难道注册就是第一关吗? 其实, 注册页面使用了 google 的人机身份验证, 所以注册需要使用 VPN 才可以注册成功 通过 VPN 访问的页面如下 :
飞龙在天 -login.htm 序 : 注册成功后就看到了第一关, 显示要下载名为 login.html 的文件,
通过文件名可以知道通过的此部分就正式开启逆向之路了, 在浏览器 中打开此文件, 显示如下 : 随便输入一段字符串, 提示如下 : 看来得看源代码了 <html> <head> <title>flare On 2017</title> </head> <body> <input type="text" name="flag" id="flag" value="enter the flag" /> <input type="button" id="prompt" value="click to check the flag" /> <script type="text/javascript"> document.getelementbyid("prompt").onclick = function () { var flag = document.getelementbyid("flag").value; var rotflag = flag.replace(/[a-za-z]/g, function(c){return String.fromCharCode((c <= "Z"? 90 : 122) >= (c = c.charcodeat(0) + 13)? c : c - 26);}); if ("PyvragFvqrYbtvafNerRnfl@syner-ba.pbz" == rotflag) { alert("correct flag!"); } else { alert("incorrect flag, rot again"); } } </script> </body> </html> 红色部分为关键的加密代码, 经过分析, 发现关键代码为 ROT13 的算法, 这个算法为一个对称算法, 加密后的字符串再次加密就会还原明文
ROT13: 只对字母进行编码, 用当前字母往前数的第 13 个字母替换当前字母, 例如当前为 A, 编码后变成 N, 当前为 B, 编码后变成 O, 以此类推顺序循环 那现在有两种方式可以获得 flag: Method 1. 一种是将比较的参考字符串填入到输入框中, 利用 WEB 调试器, 让代码运行到和参考比较的地方, 查看比较的字符串, 便会发现 flag Method 2. 另一种是编写代码实现 ROT13 的算法如下 : ''' File:rot13.py Auth:SkyPlant CreateTime:2017-09-02 10:30 CopyRight:@nsfocus ''' import sys def rot13(instr): outstr = ""; for ch in instr: if ch.isalpha(): if chr(ord(ch)+13).isalpha(): outstr += chr(ord(ch)+13) else: outstr += chr(ord(ch)-13) else:
return outstr outstr += ch if name == ' main ': instr = sys.argv[1] outstr = rot13(instr) print outstr 运行结果如下 : $ python rot13.py PyvragFvqrYbtvafNerRnfl@syner-ba.pbz ClientSidefoginsAreEasy@flare-on.com
龙跃在渊 - IgniteMe.exe 序 : 拿到程序后, 首先在命令行上运行一下, 看有什么提示
F:\>IgniteMe.exe G1v3 m3 t3h fl4g: aaaaaaaaaaaa N0t t00 h0t R we? 7ry 4ga1nz plzzz! F:\> 根据提示可以知道如果输入了正确的 flag, 那么程序应该会有不同的提示 将程序载入到 IDA, 看一下验证流程 void noreturn start() { DWORD NumberOfBytesWritten; // [sp+0h] [bp-4h]@1 } NumberOfBytesWritten = 0; hfile = GetStdHandle(0xFFFFFFF6); dword_403074 = GetStdHandle(0xFFFFFFF5); WriteFile(dword_403074, ag1v3m3t3hfl4g, 0x13u, &NumberOfBytesWritten, 0); sub_4010f0(numberofbyteswritten); if ( sub_401050() ) // Key : Must Be 1 WriteFile(dword_403074, ag00dj0b, 0xAu, &NumberOfBytesWritten, 0); else WriteFile(dword_403074, an0tt00h0trwe_7, 0x24u, &NumberOfBytesWritten, 0); ExitProcess(0); 函数 sub_401050 的返回值来决定在终端显示成功还是失败 signed int sub_401050() { int len; // ST04_4@1 int i; // [sp+4h] [bp-8h]@1 unsigned int j; // [sp+4h] [bp-8h]@4 char v4; // [sp+bh] [bp-1h]@1 len = strlen(inputbuf); v4 = sub_401000(); // return 0x04 for ( i = len - 1; i >= 0; --i ) { outbuf_403180[i] = v4 ^ inputbuf[i]; // xor V4 v4 = inputbuf[i]; // Set V4 Value } for ( j = 0; j < 0x27; ++j ) { if ( outbuf_403180[j]!= (unsigned int8)refbuf_403000[j] )// Must Equal
} } return 0; return 1; // return from here 验证的算法为从输入的末尾开始读取每一个字节与 V4( 初始值位 0x04) 进行异或, 将结果传送给指定的数组, 并将输入的当前位置字节赋值给 V4 参与比较的结果如下 : 编写解密代码如下 : #Solution for flare-on challage 2 #Create By SkyPlant #Create Time : 2017/09/02 13:51 key = "\x0d\x26\x49\x45\x2a\x17\x78\x44\x2b\x6c\x5d\x5e\x45\x12\x2f\x17\x2b\x44\x6f\x6e\x56\x09\x5 F\x45\x47\x73\x26\x0A\x0D\x13\x17\x48\x42\x01\x40\x4D\x0C\x02\x69" flag = "" ch = 0x4 for i in range(0,0x27): ch = ord(key[0x26-i]) ^ ch flag = flag + chr(ch) flag = flag[::-1] print flag 运行后结果如下 : $ python solution2.py R_y0u_H0t_3n0ugH_t0_1gn1t3@flare-on.com
潜龙勿用 -greek_to_me.exe 序 :
闯入第三关之后, 会看到 FLARE 给了你一些赞赏, 但同时对你的能力还有一点的怀疑, 他们是这么说的 我的天! 怀疑我的能力? 不能忍! 下载下来, 就是干! 双击运行, 眼前一片漆黑, 既无法输入数据, 又没法获得数据 使用 IDA 打开, 静态看一下到底是怎么回事, 通过查看 IDA, 只有四个函数, 从 start 函数往里面跟进, 函数 sub_401121 创建套接字, 监听 2222 端口, 接收数据, 并从接收到的数据中取出一个字节, 与 loc_40107c 处的数据进行异或操作, 操作完之后, 将异或后的 loc_40107c 传入 sub_4011e6 进行校验, 整体流程图为 :
经过分析流程图, 解决的思路就比较简单了 - 暴力破解 通过动态调试, 将 loc_40107c 处的数据取出来, 长度为 0x79 可以定义一个 for 循环, 从 0x0 开始, 每次加 1, 与我们取出来的 loc_40107c 数据进行程序中的操作, 然后带入 sub_4011e6, 并判断返回值是否为 0xFB5E, 如果是的话, 就说明找到的 key 参考代码如下 :
#include "stdafx.h" #include<windows.h> #include<iostream> using namespace std; WORD sub_4011e6(byte *a1_code, unsigned int a2_size); void xorfunc(byte index); int main(int argc, char* argv[]) { for (byte index = 0x0; index <= 0x10000; index++) { xorfunc(index); } return 0; } void xorfunc(byte index) { byte TargetCode[] = { 0x33,0xE1,0xC4,0x99,0x11,0x06,0x81,0x16, 0xF0,0x32,0x9F,0xC4,0x91,0x17,0x06,0x81, 0x14,0xF0,0x06,0x81,0x15,0xF1,0xC4,0x91, 0x1A,0x06,0x81,0x1B,0xE2,0x06,0x81,0x18, 0xF2,0x06,0x81,0x19,0xF1,0x06,0x81,0x1E, 0xF0,0xC4,0x99,0x1F,0xC4,0x91,0x1C,0x06, 0x81,0x1D,0xE6,0x06,0x81,0x62,0xEF,0x06, 0x81,0x63,0xF2,0x06,0x81,0x60,0xE3,0xC4, 0x99,0x61,0x06,0x81,0x66,0xBC,0x06,0x81, 0x67,0xE6,0x06,0x81,0x64,0xE8,0x06,0x81, 0x65,0x9D,0x06,0x81,0x6A,0xF2,0xC4,0x99, 0x6B,0x06,0x81,0x68,0xA9,0x06,0x81,0x69, 0xEF,0x06,0x81,0x6E,0xEE,0x06,0x81,0x6F, 0xAE,0x06,0x81,0x6C,0xE3,0x06,0x81,0x6D, 0xEF,0x06,0x81,0x72,0xE9,0x06,0x81,0x73,0x7C }; int i = 0; do { (TargetCode[i]) = (index^(targetcode[i])) + 0x22; i++; } while (i < 0x79); if (sub_4011e6(targetcode, 0x79) == 0xFB5E) { printf("%x",index); getchar(); } } WORD sub_4011e6(byte *a1_code, unsigned int a2_size)
{ } int v2_size; // edx@1 WORD v3; // cx@1 BYTE *v4_code; // ebx@2 WORD v5; // di@3 int v6; // esi@3 WORD v8; // [sp+0h] [bp-4h]@1 v2_size = a2_size; v3 = 0xff; v8 = 0xff; if (a2_size) { v4_code = a1_code; do { v5 = v8; v6 = v2_size; if (v2_size > 0x14) v6 = 0x14; v2_size -= v6; do { v5 += *v4_code; v3 += v5; ++v4_code; --v6; } while (v6); v8 = (v5 >> 8) + (unsigned int8)v5; v3 = (v3 >> 8) + (unsigned int8)v3; } while (v2_size); } return ((v8 >> 8) + (unsigned int8)(v8&0xff)) ((v3 << 8) + (v3 & 0xFF00)); 经过运行此程序, 可以得到答案是 a2, 接着使用 OD 进行调试 ( 在 程序等待连接时, 可以写个脚本进行连接, 并发送数据 ), 将参与异 或操作的那个字节修改为 a2
if 语句条件成立, 继续使用 OD 往下跟踪, 发现程序往一块内存中 不断的放入数据, 在数据窗口中定位到目标地址, 可以看到程序正在 将 flag 放到那块内存中
神龙摆尾 -notepad.exe 序 : 题目提示我们是否在 VM 中运行, 意思就是暗示我们需要在虚拟 机中运行 ( 在本机运行了, 果然没有反应 ) 运行之后, 首先弹出了
一个时间的对话框, 然后就显示 notepad 界面 如果直接定位到弹出对话框的地址处进行分析, 完全看不出来是什么意思, 因为程序调用的函数都是经过动态获得的, 所以还是使用 IDA 从头开始看 从 IDA 中可以看到, 此程序有 99 个函数, 不过不要被这个数字吓到, 跟着程序流程走, 还是比较简单的 调用 sub_1013f30 时, 传入的第二个参数是 C:\Users\username\flareon2016challenge, 如果没有这个文件夹的话, 就需要创建此文件夹, 因为后期程序将遍历此文件夹中的文件 在 FindFirstFile 循环遍历中, 只有一个函数 sub_1014e20, 此函数的第二个参数是遍历到的文件的全路径, 无疑, 我们需要跟进这个函数, 查看这个函数对遍历到的文件所做的操作 这个函数有很多 if else 分支, 不过它调用的函数, 经过注释之后, 流程还相对简单, 此函数将遍历到的文件映射进内存中, 并判断此文件是否为 EXE 文件, 由此可知, 程序要查找的文件是 EXE 形式的 PE 文件
继续往下看代码, 程序还会判断当前运行的平台的文件属性, 如果平台不正确, 则不执行操作 ( 难怪我在本机运行没有效果 ) 通过最开始定位 MessageBox 代码的位置, 可以得知它是在 sub_10146c0 里面调用的 这个函数也是重点函数, 需要重点分析 后面的代码只是一些文件的内存对齐操作, 先不用理会 在 sub_10146c0 中, 有几个 if 判断语句, 是用来比较时间戳的 : 图中的 v62_ntheader 是当前运行程序的 NT 头,a2_NtHeader 是遍历到的文件的 NT 头,NtHeader+8 得到的数据是 TimeDateStamp, 如果符合 if 条件, 则会将时间格式进行转换并会弹出对话框, 显示时间, 程序会显示的时间整理如下 : 2016/09/08 18:49:06 UTC 2016/09/09 12:54:16 UTC
2008/11/10 09:40:34 UTC 2016/08/01 00:00:00 UTC 在调用函数 sub_10145b0 时, 传入的第三个参数是 key.bin 文件的绝对路径, 所以我们还需要在 flareon2016challenge 文件夹下创建名为 key.bin 的文件 这个函数的作用是将遍历到的文件的特定偏移处的数据写入 key.bin 文件中, 写入的大小是 8 字节 将 if 语句走完之后, 后面又会读取 key.bin 中的数据, 经过函数 sub_1014670 运算后, 调用 MessageBox 弹出结果 从读取的数据长度可知, 前面的 if 语句都需要进入, 也就说明了 flareon2016challenge 文件夹下要有 4 个 EXE 并且这四个 EXE 的时间戳要对应上 从文件夹的名称 flareon2016challenge 和遍历此文件夹中的应用程序可以猜测到, 这些应用程序有可能就是 2016 年的 flareon 的题目 所以就将 2016 年的题目下载下来, 找到时间戳正确的 EXE 放到 flareon2016challenge 文件夹下 现在有两种解题思路 : 第一种 : 修改时间戳
多次运行 notepad.exe, 修改程序的时间戳, 让它进入到不同的 if 语句中, 按照 if 语句的顺序进行修改即可, 原始的时间戳为 0x48025287, 后期需要修改四次, 依次为 0x57D1B2A2,0x57D2B0F8, 0x49180192,0x579E9100, 运行后将弹出 flag 第二种 : 写脚本进行破解通过对比时间戳, 我们可以知道程序要找的四个文件分别是哪几个, 我们也知道了程序要读取的文件的位置, 分别为 0x400,0x410, 0x420,0x430 只有函数 sub_1014670 对这些数据进行了操作 所以我们可以将对应文件对应位置处的数据读取出来, 编写程序对这些数据进行运算, 同样可以得到答案 参考脚本 : memdata = "\x37\xe7\xd8\xbe\x7a\x53\x30\x25\xbb\x38\x57\x26\x97\x26\x6f\x50\xf4\x75\x67\xbf\xb0\xef\xa 5\x7A\x65\xAE\xAB\x66\x73\xA0\xA3\xA1" key1_from_2016ch_1 = "\x55\x8b\xec\x8b\x4d\x0c\x56\x57" key2_from_2016ch_2 = "\x8b\x55\x08\x52\xff\x15\x30\x20"
key3_from_2016ch_6 = "\xc0\x40\x50\xff\xd6\x83\xc4\x08" key4_from_2016ch_4 = "\x00\x83\xc4\x08\x5d\xc3\xcc\xcc" flag = "" key = key1_from_2016ch_1 + key2_from_2016ch_2 + key3_from_2016ch_6 + key4_from_2016ch_4 len = len(key) print len for i in range(0,len): flag = flag + chr(ord(memdata[i]) ^ ord(key[i])) print flag 结果如下 :
密云不雨 -pewpewboat.exe 序 : 穿过弯弯曲曲的盘山路, 来到了河边, 旁边有块牌子提示, 请放
松一下, 来做个游戏, 只有通关了, 才会有通往彼岸的吊桥 使用 file 命令查看文档格式, 发现是一个 64 位的 ELF 程序 $ file pewpewboat pewpewboat: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=580d3cee15362410c9e7b0ae44d65d57deb52912, stripped 运行后界面如下 : 1 2 3 4 5 6 7 8 A _ _ _ _ _ _ _ _ B _ _ _ _ _ _ _ _ C _ _ _ _ _ _ _ _ D _ _ _ _ _ _ _ _ E _ _ _ _ _ _ _ _ F _ _ _ _ _ _ _ _ G _ _ _ _ _ _ _ _ H _ _ _ _ _ _ _ _ Rank: Seaman Recruit Welcome to pewpewboat! We just loaded a pew pew map, start shootin'! Enter a coordinate: 这是一个射击游戏, 通过猜测目标位置来进行射击, 成功打到所有目标即可过关 如此以来, 需要逆向看看靶子上目标的坐标是如何生成的了 先来看看他的限制条件 : 1. 尝试的次数 当射击达到一定次数以后, 提示弹药使用完毕 2. 相互关联 无法通过修改内存直接得到坐标, 必须通过计算完成 3. 输入限制 只能输入一个坐标, 多个坐标同时输入会产生
错误的结果 注意到以上条件, 则需要看的是如何生成坐标的了 首先要确定, 坐标生成所需要内容是什么, 下图中展示的靶和目标坐标生成的代码段 for ( i = 0; i <= 99; ++i ) { memcpy(dest, (char *)&unk_6050e0 + 576 * i, 0x240uLL);// GetMapData sub_40304f(dest, 576LL, v8); // DecryptMap if ( (unsigned int)sub_403c05(( int64)dest)!= 1 )// game break; v8 = *((_QWORD *)dest + 2); } 每次会取 0x240 字节长度的内存进行解析, 我们简单看下这个内存区的结构, 从内存开始处解析如下 : 输入的坐标信息会保存到 X Y 这两项中, 经过如下运算 : 计算结果保存到 location_input 项中去 接着就是进行比较 : 如果对比成功, 就会显示出 nice shot 的提示, 当然完成所有关卡 之后, 会对 calc_sum 项进行校验, 如果答案正确, 那么就显示最后 的提示语 :Thanks for playing!
根据逆向的结果, 编写脚本, 直接获取所有坐标 : #trans hex_string to int def trans_value(v): ans = 0 length = len(v) for i in range(0,length): ans = ans + (ord(v[i])<<(8*i)) return ans def mapcor(v): value = trans_value(v) offset = -1 mcor = "" vcor = [] vcor.append([0,0,0,0,0,0,0,0]) vcor.append([0,0,0,0,0,0,0,0]) vcor.append([0,0,0,0,0,0,0,0]) vcor.append([0,0,0,0,0,0,0,0]) vcor.append([0,0,0,0,0,0,0,0]) vcor.append([0,0,0,0,0,0,0,0]) vcor.append([0,0,0,0,0,0,0,0]) vcor.append([0,0,0,0,0,0,0,0]) #print hex(value) while(value): offset = offset + 1 if(value % 2): vcor[offset>>3][offset%8] = 1 mcor = mcor + chr((offset>>3)+ord('a')) + chr((offset%8)+ord('1')) + " " value = value >> 1 for i in range(0,8): line = "" for j in range(0,8): if (vcor[i][j] == 1): line = line + "*" else: line = line + " " print line return mcor
map = [ ] "\x00\x78\x08\x08\x78\x08\x08\x00", "\x00\x88\x88\x88\xf8\x88\x88\x00", "\x7e\x81\x01\x01\xf1\x81\x81\x7e", "\x00\x00\x00\x90\x90\x90\x90\xf0", "\x00\xf8\x40\x20\x10\xf8\x00\x00", "\x07\x09\x07\x05\x09\x00\x00\x00", "\x00\x00\x00\x70\x10\x70\x10\x70", "\x00\x3e\x08\x08\x08\x09\x06\x00", "\x00\x00\x00\x44\x44\x44\x28\x10", "\x00\x00\x00\x0c\x12\x12\x12\x0c", "\x00\x00\x00\x00\x00\x00\x00\x00" cnt = len(map) for i in range(0,cnt): val = mapcor(map[i]) print "map[%d]:%s" %(i,val) 通关后, 拿到最后的提示字符, 去除中间无效字符 PEW, 得到最后的提示语句, 告诉我们需要重新排序获取的字符 排序后的字符是 :OHGJURERVFGUREHZ, 经过 ROT13 解密后, 得到 BUTWHEREISTHERUM, 字符长度是 17 字节 将 17 字节的结果作为如下红色字符函数的输入参数运行 if ( fgets(&s, 17, stdin) ) { v2 = (char)(s & 0xDF) - 65;
} v3 = v5-49; if ( v2 < 0 v2 > 7 v3 < 0 v3 > 7 ) { sub_403411(( int64)&s); } else { *(_QWORD *)(a1 + 8) = 1LL << (8 * (unsigned int8)v2 + v3); *(_BYTE *)(a1 + 28) = s & 0xDF; *(_BYTE *)(a1 + 29) = v5; } 得到的结果如下 :
突如其来 -payload.dll 序 : 从给出的名字可以知道出题者想考察一下参与者对于 Windows
动态库的掌握情况 查看导出表如下 : 这里有个提示, 需要根据格式进行调用 手动输入如下命令 >rundll32 payload.dll,entrypoint EntryPoint 提示如下 : 猜测可能在程序运行的过程中修改了导出函数名, 通过, #1 的方法调用 1 号导出函数, 命令如下 : >rundll32 payload.dll,#1 发现提示发生了变化
此时 dump 出内存中的 payload.dll 数据, 再次查看导出表, 如下 所示 : 1 号导出函数名和地址都被修改, 分析这个新的 1 导出函数 发现了用于弹出消息框的代码, 使用 x64dbg, 修改命令行为 C:\Windows\System32\rundll32.exe payload.dll,#1, 调试该 dll, 下断 点修改跳转, 使其执行 else 分支, 结果如下所示 :
分析 else 分支的代码可以得知, 函数会从带解密函数表中解密出 一段代码然后执行, 带解密函数表如下所示 : 可以分析出该函数表为 26 个待解密函数地址和一个用于解密函数的函数地址, 我们在前面获得的 key[24]=0x6f 即为, 解密出的函数表中下标为 24 的函数执行后弹出的消息框, 以此类推, 只要我们解密出所有其他的函数, 就可以得到完整的 flag
首先我们尝试通过修改解密函数时的偏移来修改要解密的函数地址, 结果发现出现无法运行, 即解密的 key 不是通过的, 每个函数解密时应该有自己的 key, 追踪这个 key 是怎么得到的, 动态调试发现 key 的值为修改后的导出函数名, 那么就要找导出函数是在哪里被修改的, 经过调试发现, 当调用 LoadLibrary 函数加载完 payload.dll 时, 函数名就已经被修改, 因此对 DllEntryPoint 函数进行调试, 发现了如下代码 : 通过 c++ 在初始化全局对象的时候, 调用执行 _CRT_INIT --> _initterm(unk_180010250, unk_180010260) ; 该函数会遍历 unk_180010250 和 unk_180010260 之间的地址, 发现有值则执行该地址的函数 我们发现在此之间有一个函数的地址 : 分析该函数会找到如下代码 :
该处是一个解密代码, 下断点动态调试, 发现这块会解密出导出 函数名, 并且要解密的数据地址由下标决定, 往回找该下标在哪获取 的 : 可以看到 v5 是函数 sub_180004710 的返回值, 直接下断点动态调试, 观察返回值为 0x18, 正好的之前得到的 key[24]=0x6f 下标, 直接将其修改为 0, 观察到解密出了不同的函数名, 继续执行, 注意在下面位置处 patch 程序 继续执行, 将弹出以下对话框 :
所以这次找到了正确的修改处, 一次将其修改为 0.1.2.3.4 19. 即 可得出 Flag: wuuut-exp0rts@flare-on.com
双龙取水 -zsud.exe 序 : 到达第七关, 依然是一个游戏关, 下载后打开程序, 界面如下图
所示 : 一个传统的文字解密类游戏, 是不是勾起了很多老程序员的回忆 敲击 help 查看所有命令, 可以看到游戏定义了一些内容 : 1 行走方向:North South East West Up Down; 2 动作 :get drop wear remove look inv( 检查背包 ) say put Take [something] off 从游戏中, 我们能直接获取到的目前是只有这么多信息, 我们分析下他的程序, 看看如何能获取到 key: 用 IDA 查看代码, 发现里面有个深调用, 隐藏了真实的函数实现 :
耐心跟下去, 第一个函数的目的是创建一个线程, 我们分析一下线程函数准备做什么 第一个深调用目的是获取 HTTP 系列相关的函数地址, 获取之后会直接调用, 在当前计算机中添加一个临时 HTTP 页面, 监听本地发来的 HTTP 请求 : 着重看下函数 sub_4010e1, 主要功能是接收数据并进行解析 : 将收到的内容根据 & 符号分割, 并且解析 k= 和 e= 后的内容, 处 理的 k= 后的内容时, 会将 k= 后的部分输入到以下算法中 :
之后和 e= 后的内容进行 hash 运算 : 得到结果后回复给客户端, 如果计算后 flag 标志为 0, 那么会给 客户端回复页面 : 至此, 第一个函数分析完毕 回到第二个函数, 第二个函数也是深调用, 里面内嵌了另外一个 深调用函数, 解到最后一层, 可以看到程序加载了
CorBindToRuntimeEx 函数 这个函数的功能是为非托管代码增加调用托管代码的功能, 也就是说一个正常的 C++ 程序, 可以通过这个函数加载相应的.Net 执行库, 来运行.Net 类的程序 接着程序向内存中加载了一个 DLL, 这个 DLL 是在本程序的数据段中存放的,dump 下来以后, 可以看到是一个 powershell 脚本 最后程序 Hook 了 rand 函数和 srand 函数 :
至此,EXE 已经分析完毕 接下来我们看一下 dump 出来的 Powershell 脚本文件 Powershell 脚本比较大, 但是注释写的非常清楚, 直奔主题, 我们直接查看控制人物移动的代码 从上至下看, 第一个框中有 key 字样, 可能我需要在地图中找一 个 key? 第二个框中调用了 rand() 函数, 我们知道 rand 这个函数已经
被 Hook 了,rand 函数的功能是什么? 第三个框中, 似乎 key 满足 @ 字符存在, 就会将当前人物移动到初始房间? 第四个框提示如果我们走的方向满足某种条件就会触发 key 的特效 带着这四个问题, 我们开始重新审视这个游戏, 首先我需要找到 key, 在脚本搜索一下 key 字段 : 可以通过提示字符看到 key 藏在桌子的抽屉里面 接下来, 当我们获取了 key, 输入一个方向, 就会和 rand 出来的字符进行对比, 如果相同的话, 就会对 key 进行一次运算, 似乎会得出什么内容, 那么 rand 是怎么实现的? 回到 rand 函数的实现部分 : 我们发现他有一个数组, 里面存放了 35 个字符, 这是不是对应的 方向的编号?
回到脚本中, 搜索一下找到 enum, 如此就可以与上面的表格内容 对应了 那么如何输入这些方向呢, 玩游戏可以发现, 房间的通向都是固定的, 也没有 up 和 down 方向, 仔细查看发现有个能够输入所有方向的工作间, 移动到工作间, 并且保证不会出现任何 key warm 的提示 接着顺次输入方向表中的所有字符, 得到最后的答案 : You can start to make out some words but you need to follow the RIGHT_PATH!@66696e646b6576696e6d616e6469610d0a 后面这串字符是 ASCII 字符, 翻译过来就是 f i n d k e v i n m a n d i a, 寻找 kevin 在迷宫中进行寻找, 可以找到 kevin, 跟 kevin 说话, 他仅仅说 hello, 并没有任何提示, 难道是方向错了么? 重新回到脚本中, 找到 say 控制函数, 分析一下 :
注意第一个框, 在这个房间中需要有 key 这个物品,key 在我们身上, 所以需要 drop 下来, 接着看第二个框, 我们身上需要带着 helmet, helmet 在进房间的时候在房间中放着, 我们需要 get 它, 接着 wear 它, 最后再和 kevin 对话,kevin 会告诉我们 key: 得到 key: '6D 75 64 64 31 6E 67 5F 62 79 5F 79 30 75 72 35 33 6C 70 68 40 66 6C 61 72 65 2D 6F 6E 2E 63 6F 6D'. mudd1ng_by_y0ur53lph@flare-on.com 附 : 如果并未发现 rand 函数被替换, 那么就需要对正确答案进行爆破求
解, 编写如下 ps1 脚本 :(encoded 就是我们直接复制原本的 key, 由 于过长, 这里就不完全显示了 ) $baseurl = 'http://127.0.0.1:9999/some/thing.asp' $encoded = 'BANKbEPxukZfP2EikF8jN04iqGJY0RjM3p++Rci2RiUFvS9RbjWYzbbJ3BSerzwGc9EZkKTvv1JbHOD6ldmehDxyJGa 60UJXsKQwr9bU3WsyNkNsVd/XtN9/6kesgmswA5Hvroc2NGYa91gCVvlaYg4U8RiyigMCKj598yfTMc1/koEDpZUhl9D y4zhxuufhbiyrxfariynsiqjaeynb0r93nsaqpnognrqg23oyd3rpp4thd8g6vkjuxltrgxv7px2cwdohuxkbvq6edum FSKpnNB7jAKo..EAwkG/jtoZzPtEVBhQ==' $array='n','s','e','w','d','u' $keytext='' $text='' $flag=0 $prekey='' for($i=0;$i -le 32; $i++) { for ($j=0;$j -le 5;$j++) { $prekey=$keytext $keytext+=$array[$j] $uri = "${baseurl}?k=${keytext}&e=${encoded}" $r = Invoke-WebRequest -UseBasicParsing "$uri" $decoded = $r.content if ($decoded.tolower() -NotContains "whale") { $split = $decoded.split() $text += $split[0..($split.length-2)] $text += ' ' $encoded = $split[-1] $flag=1 break } else { $keytext=$prekey } } if ( $flag -eq 0 ) { echo "ERROR!!!" break
} else { $flag = 0 } if (!#decoded) { echo "END!" echo "$text" } } echo "$keytext" echo "$text" 但是, 经过爆破, 你只能获取到以下提示字符 : You can start to make out some words but you need to follow the? 返回的 key 是 : ZipRg2+UxcDPJ8TiemKk7Z9bUOfPf7VOOalFAepISztHQNEpU4kza+I MPAh84PlNxwYEQ1IODlkrwNXbGXcx/Q== 接下来, 你只能去调试 powershell 脚本, 接着就会发现 rand 函数的问题
震惊百里 -flair.apk 序 : 这是一道 Android APK 逆向题, 总共 4 关, 每一关输入正确的 password 后才能进入下一关
以下是反编译后程序结构 : 第一关 :
使用 apktool 解包 apk 程序, 并在解压后的目录下搜索 who run it 字符串, 最后发现在 activity_michael.xml 文件中, 因此, 第 一关需要分析反编译后的 michael 文件
分析以上算法, 可以得到以下结论 : (1) Password 总长度为 12; (2) Pw[0] = M (3) Pw[1] = Y (4) Pw[2-4] = PRS (5) Pw[5] = H
(6) Pw[6] = E (7) Pw[7] == pw[8], 并且两者连接后计算 hashcode 得到的值为 3040 (8) Pw[9-10] = FT (9) Pw[11] = W 以上算法中, 唯一需要计算的是 pw[7] 和 pw[8] 的值, 由于 password 为可见字符, 因此在 0x20 到 0x7e 之间爆破, 获取到第 7 位和第 8 位的值 : 计算结果为下划线 _, 即 pw[7]=pw[8]= _. 因此, 第一关的 password 为 :MYPRSHE FTW 第二关 :
通过同样的方式搜索字符串 SOMETHING TO NIBBLE ON, 最后发现第二关为 Brian 从第二关开始, 代码做了混淆, 静态分析时可根据需要重命名函数名 静态分析 Brian 代码, 可以看到, 使用函数 teraljdknh 判断输入的值与函数 asdjfnhaxshcvhuw 的返回值是否相同, 相同时 password 验证通过 :
使用 Android Studio + Smalidea 动态调试, 在 teraljdknh 函数 处下断点, 参数 v 为输入的值, 参数 m 为正确的 password 最终获取到第二关的 password 为 :hashtag_covfefe_fajitas!
第三关 : 第三关中需要点亮 4 颗星, 才能继续下一步 搜索字符串 CARRY ON MY WAYWARD SON, 了解到第三关为 Milton 分析 Milton 校验算法, 输入值经过 Stapler.neapucx() 函数运算后, 与 nbsadf() 函数返回值做比较, 两者相同, 则验证通过
分析 Stapler.neapucx() 函数, 可以看到该函数功能为将十六进 制字符串转换为字节数组 : 使用 Android Studio 动态调试, 在函数 nbsadf 返回值处下断点, 最终获取到该函数返回值为字节数组 : {16, -82, -91, -108, -125, 30, 11, 66, -71, 86, -59, 120,
-17, -102, 109, 68, -18, 57, -109, -115}; 将该字节数组转换为十六进制字符串, 即为该题 password 最终结果为 :10aea594831e0b42b956c578ef9a6d44ee39938d
第四关 : 第四关为 Printer, 校验算法与第三关一样, 输入的 password 经 过 Stapler.neapucx() 转换为字节数组后, 与 Stapler.poserw 返回
值做比较, 两者相同, 验证通过 : 使用 Android Studio 动态调试, 在 Stapler.poserw 返回值处下断点, 获取到返回值为字节数组 : {95, 27, -29, -55, -80, -127, -60, 13, -33, -60, -96, 35, -127, 86, 0, -114, -25, 30, 36, -92}
使用第三关的程序, 将字节数组转换为十六进制字符串, 即为第 四关 password:5f1be3c9b081c40ddfc4a0238156008ee71e24a4 四关全部通过后, 获取到该题 flag:
时乘六龙 -remorse.ino.hex 序 :
该题目是一道 Arduino 平台逆向题, 题目提供了一个 Arduino 二 进制程序, 打开后, 看到内容如下 : 木有开发板? 不知道有啥好用的 Arduino 调试器? 这些都不重要, IDA 在手, 跟我走! 使用 IDA 打开程序, 注意选择处理器类型为 Atmel AVR [AVR] : 没错,IDA 反汇编后, 不需要动态调试, 纯静态分析, 获取 flag,
以下便是获取 flag 的代码 : ROM:0545 loc_545: ; CODE XREF: sub_536+11j ROM:0545 st X+, r1 ROM:0546 cpse r25, r26 ROM:0547 rjmp loc_545 ROM:0548 ldi r25, 0xB5 ; ' ROM:0549 std Y+1, r25 ROM:054A std Y+2, r25 ROM:054B ldi r25, 0x86 ; ' ROM:054C std Y+3, r25 ROM:054D ldi r25, 0xB4 ; ' ROM:054E std Y+4, r25 ROM:054F ldi r25, 0xF4 ; ' ROM:0550 std Y+5, r25 ROM:0551 ldi r25, 0xB3 ; ' ROM:0552 std Y+6, r25 ROM:0553 ldi r25, 0xF1 ; ' ROM:0554 std Y+7, r25 ROM:0555 ldi r18, 0xB0 ; ' ROM:0556 std Y+8, r18 ROM:0557 std Y+9, r18 ROM:0558 std Y+0xA, r25 ROM:0559 ldi r25, 0xED ; ' ROM:055A std Y+0xB, r25 ROM:055B ldi r25, 0x80 ; ' ' ROM:055C std Y+0xC, r25 ROM:055D ldi r25, 0xBB ; ' ROM:055E std Y+0xD, r25 ROM:055F ldi r25, 0x8F ; ' ROM:0560 std Y+0xE, r25 ROM:0561 ldi r25, 0xBF ; ' ROM:0562 std Y+0xF, r25 ROM:0563 ldi r25, 0x8D ; ' ROM:0564 std Y+0x10, r25 ROM:0565 ldi r25, 0xC6 ; ' ROM:0566 std Y+0x11, r25 ROM:0567 ldi r25, 0x85 ; ' ROM:0568 std Y+0x12, r25 ROM:0569 ldi r25, 0x87 ; ' ROM:056A std Y+0x13, r25 ROM:056B ldi r25, 0xC0 ; ' ROM:056C std Y+0x14, r25 ROM:056D ldi r25, 0x94 ; '
ROM:056E std Y+0x15, r25 ROM:056F ldi r25, 0x81 ; ' ROM:0570 std Y+0x16, r25 ROM:0571 ldi r25, 0x8C ; ' ROM:0572 std Y+0x17, r25 ROM:0573 ldi r26, 0x6C ; 'l' ROM:0574 ldi r27, 5 ROM:0575 ldi r18, 0 ;r18 为索引, 从 0 开始, 依次加 1 ROM:0576 ROM:0576 loc_576: ; CODE XREF: sub_536+46j ROM:0576 ld r25, Z+ ROM:0577 eor r25, r24 ;r25 与 r24 异或 ROM:0578 add r25, r18 ;r25 加上索引 r18 ROM:0579 st X+, r25 ROM:057A subi r18, -1 ; 索引 r18 加 1 ROM:057B cpi r18, 0x17 ROM:057C brne loc_576 ROM:057D lds r24, 0x576 ROM:057F cpi r24, 0x40 ; '@' ROM:0580 brne loc_595 ROM:0581 ldi r22, 0x2B ; '+' ROM:0582 ldi r23, 5 ROM:0583 ldi r24, 0x8F ; ' ROM:0584 ldi r25, 5 ROM:0585 call sub_332 分析上述代码逻辑 : (1) 向 r25 寄存器放了一堆数据 : B5 B5 86 B4 F4 B3 F1 B0 B0 F1 ED 80 BB 8F BF 8D C6 85 87 C0 94 81 8C (2) 设置索引从 0 开始, 依次遍历 r25 中的数, 将每个值与 r24 做异或操作, 然后再加上索引的值 r18; (3) 索引加 1, 取 r25 中下一个数, 重复 (2) 中算法 那么这个 r24 的值到底是多少呢? 管他呢, 爆破大法好, 对 r24 从 0x00 到 0xff 取值, 打印出所有计算后的结果 :
Python 爆破代码如下 : 最后在所有输出结果中去找 flag: 附 AVR 汇编指令参考地址 : http://www.atmel.com/webdoc/avrassembler/avrassembler.wb_in struction_list.html
龙战于野 -shell.php 序 :
这一题是一个 PHP 文件, 这个 PHP 程序接受 POST 的参数值, 主要通过 MD5 算法计算出一个长度可变的字符串 key, 用 key 字符串通过 XOR 算法解开前面一部分的字符串, 再将这一部分作为字符串去异或求出下一个分组的字符串, 分组长度和 key 长度相同 第一层密文解密 : 这题的 key 其实是很长的, 通过类似弱口令的方式并不能爆破出 POST 的参数值 不过因为算法是一个简单的分组异或, 而且知道加密前是一个合法的 PHP 代码, 可以通过加密后的密文反向推出明文 因为 key 长度是可变的, 准确来说是在 33 到 64 之间, 为了接下来的分析, 最好先求出 key 的长度 因为该算法是分组的, 所以直接拿分组的第一个元素去求解, 得出的值再去求解下一个分组的第一个元素, 以此类推 得出所有分组的第一个元素肯定是在 php 代码中合法的字符, 通过加入该约束条件即可求出 key 长度, 相关代码如下 : ''' calckeylen.py ''' import binascii import base64 import hexdump with open('in.txt','rb') as fd: str=base64.b64decode(fd.read())
outstr='' keyli='0123456789abcdef' # 所有可能的字符集合 keylen=31 def checkok(str): # 约束函数 for i in str: if ord(i)<0x20 and ord(i)!=0xa and ord(i)!=0xd: return False return True for keylen in range(33,65):#key 长度变化范围 for key in keyli: tkey=key for i in range(0,len(str)/keylen): res=chr((ord(tkey)^ord(str[keylen*i]))&0xff) outstr+=res tkey=res if checkok(outstr): print "keylen=%d"%keylen outstr='' 结果为 keylen=64 用相同的思路, 可以用字符 0-f 爆破前 64 字节的密文, 通过一小段的分批爆破, 手工查看是否是合法的 php 代码, 从而求出 key 字符串的值 ''' Keysol.py ''' import binascii import base64 import hexdump with open('in.txt','rb') as fd: str=base64.b64decode(fd.read()) outstr='' keyli='0123456789abcdef' key_dic={} g_str='' end=0 def checkok(str): # 约束函数 for i in str: if ord(i)<0x20 and ord(i)!=0xa and ord(i)!=0xd: return False return True
def decrypt(key): # 解密函数 global str ret='' for i in range(0,len(key)): ret+=chr((ord(str[i])^ord(key[i]))&0xff) return ret def crack(i): # 递归遍历输出可能的情况 global key_dic global g_str global end if i>=end: print hexdump.hexdump(decrypt(g_str)) return for c in key_dic[i]: g_str+=c crack(i+1) g_str=g_str[:-1] for k in range(0,64): for key in keyli: tkey=key for i in range(0,len(str)/(64)): res=chr((ord(tkey)^ord(str[64*i+k]))&0xff) outstr+=res tkey=res if checkok(outstr): if k not in key_dic.keys(): # 生成所有可能的情况 key_dic[k]=[] key_dic[k].append(key) outstr='' #print key_dic pre_key='d' # 预先设置的 key, 基于前面的猜测 for i in range(0,len(pre_key)): key_dic[i]=[pre_key[i]] end=len(pre_key)+3 #3 个一组去猜, 不要设置过大, 否则无法手动识别 crack(0) #print hexdump.hexdump(str) 值 通过手工识别正确的解密后的字符串, 并不断修改脚本中 pre_key
最后得出 : Key=db6952b84a49b934acb436418ad9d93d237df05769afc796d067 bccb379f2cac 第二层密文解密 : 解密出的代码如下 : 根据代码中的网址提示, 通过 google 即可查到提示代码 : site:www.p01.org Raytraced Checkboard
1,2,3 解出来的功能应该和提示的代码很类似, 对应关系如下 : Raytraced Checkboard: p01 256b Starfield: Wolfensteiny: 所以解密出来应该是 html+js 之类的代码, 可以通过爆破第一层的方法进行爆破 老方法, 先求正则之后的 key2 长度 : ''' Calckey2len.py ''' import binascii import base64 import hexdump with open('in.txt','rb') as fd: str=base64.b64decode(fd.read()) outstr='' keyli='0123456789abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz@_.-' key_dic={} def checkok(str): for i in str: # 约束函数
if ord(i)<0x20 and ord(i)!=0xa and ord(i)!=0xd: return False return True for lk in range(10,20): for k in range(0,lk): for key in keyli: tkey=key for i in range(0,len(str)/(lk+1)): res=chr((ord(tkey)^ord(str[(lk+1)*i+k]))&0xff) outstr+=res if checkok(outstr): if k not in key_dic.keys(): # 生成所有可能的情况 key_dic[k]=[] key_dic[k].append(key) outstr='' #print len(key_dic) if len(key_dic)==lk: print "keylen=%d"%(lk+1) key_dic={} 结果为 keylen=13 得出长度后继续用类似的方法去爆破内容 : 接下来可以得出 flag 正则之后的三部分 : Flag_part1=t_rsaat_4froc Flag_part2=hx ayowkleno
Flag_part3=3Oiwa_o3@a-.m Flag=th3_xOr_is_waaaay_too_w34k@flare-on.com
履霜冰至 -covfefe.exe 序 : 这一题是虚拟机代码分析的题目, 通过 IDA 简单的分析可以知道,
此代码写得非常简练, 用一个减法操作和一个条件跳转来模拟左移, 右移, 乘法之类的运算 同时由于虚拟机代码固定长度为 3 个 DWORD, 可以比较方便对虚拟机代码进行解析 定位关键比较处为了略去繁琐的虚拟机代码分析, 快速定位到关键比较地方, 可以通过简单的 hook judge_jmp() 函数, 来对所有执行过的代码进行分析或者日志输出 同时对 vmeip 向上跳的地方进行重点标记, 因为循环一般模拟非常重要的运算 class opcode{ public: DWORD vmeip; // 虚拟机的 eip DWORD op1; // 指令的第一个操作数 DWORD op2; // 指令的第二个操作数 DWORD addr; // 虚拟机的第三个操作数 DWORD op1_val; //op1 指向地址的值 DWORD op2_val; //op2 指向地址的值 }; vector<opcode> g_opcode;
map<dword, opcode>g_oplist; void write_log(vector<opcode>&data){ ofstream outfile, fout; char buf[100]; outfile.open("opcode.txt"); for (int i = 0; i < data.size(); i++){ if (i > 1){ if (data[i].vmeip < data[i - 1].vmeip){ sprintf(buf, "%s","re: "); // 可能是循环, 重点标记 } else{ sprintf(buf, "%s", "go: "); } } sprintf(buf + 4, "%d:%08x %08x %08x %08x========>[%08x] - [%08x] = [%08x]\n", i, data[i].vmeip, data[i].op1, data[i].op2, data[i].addr, data[i].op2_val, data[i].op1_val, data[i].op2_val - data[i].op1_val); outfile << buf << endl; } outfile.close(); } 通过分析日志文件, 最后一个循环跳出来的地方和最后开始输出的地方一定是存在关键比较的, 通过改变输入的值则可以发现关键对比的地方, 如下图日志所示, 当改变多次输入的时候, 在 vimeip=0x00000def :[00035e8a] - [0002d7ff] = [0000868b] 的地方发现只有第一个操作数指向的值 0x2d7ff 是改变的, 而第二个操作数指向的值 0x35e8a 是不变的, 也就是说它是正确的值 在改变输入的过程中还发现, 当输入一个字符, 对比的 vmeip 只执行一次, 输入 2 个字符时候还是一次, 而且要对比的值也改变, 输入 3 个字符时对比地方将会执行 2 次, 输入 4 个字符还是 2 次, 也就是说该程序将两个字符为一组进行运算并求得的值和正确的值对比 当输入非常长的字符串, 对比次数固定在 16, 也就是输入的字符串长度应该是
16*2=32 获取对比值因为他是一组组对比的, 搜索空间较小, 可以通过暴力破解的方式来得出正确的字符串 为了暴力破解, 首先得获取所有正确的对比值 : bool my_vm_exec(dword base,dword op1, DWORD op2, DWORD addr){ opcode op; static int g_cnt=0; DWORD eip = 0x12ff6c; op.op1 = op1; op.op2 = op2;
} op.addr = addr; op.op1_val = *(DWORD*)(base + op1*4); op.op2_val = *(DWORD*)(base + op2 * 4); DWORD _ebp; _asm{ mov _ebp, ebp } op.vmeip = *(DWORD*)(_ebp+0x1c); if (op.vmeip == 0xdef){ # 执行到关键对比 vmeip 处则输出正确对比值 printf("0x%08x,", *(DWORD*)(base + op2 * 4)); return _vm_exec(base,op1, op2, addr); 暴力破解获取到所有正确的值后, 通过 hook vm_loop 函数, 来对主函数进行不断循环执行尝试正确的字符组合 每循环一次, 通过 hook scanf 喂入尝试的字符组合 同时为了保证虚拟机状态都是最初的状态, 每次循环之前通过 memcpy 将虚拟机的空间 ( 数据和代码 ) 重置, 相关代码如下 : char g_buf[2]; DWORD key_arr[] = { 0x00035e8a, 0x0002df13, 0x0002f58e, 0x0002c89e, 0x0003391b, 0x0002c88d, 0x0002f59b, 0x00036d9c, 0x00036616, 0x000340a0, 0x0002d79b, 0x0002c89e, 0x0002df0c, 0x00036d8d, 0x0002ee0a, 0x000331ff }; // 模拟 scanf 的行为 int my_scanf(char*f, char*in){ static int g_call_scanf = 0; if (g_call_scanf == 0){ *in = g_buf[0]; } if (g_call_scanf == 1){ *in = g_buf[1]; }
if (g_call_scanf == 2){ *in = 0xa; } if ((++g_call_scanf) >= 3)g_call_scanf = 0; return 1; } // 不做任何操作 int my_printf(char*f,...){ return 1; } //hook 关键对比地方 bool my_vm_exec(dword base,dword op1, DWORD op2, DWORD addr){ opcode op; static int g_cnt=0; DWORD eip = 0x12ff6c; op.op1 = op1; op.op2 = op2; op.addr = addr; op.op1_val = *(DWORD*)(base + op1*4); op.op2_val = *(DWORD*)(base + op2 * 4); DWORD _ebp; _asm{ mov _ebp, ebp } op.vmeip = *(DWORD*)(_ebp+0x1c); if (op.vmeip == 0xdef){ // 关键对比地方 for (int k = 0; k < 16; k++){ if (op.op1_val == key_arr[k]){ // 破解成功则输出 _printf("%d: ", k + 1); _printf("%c", g_buf[0]); _printf("%c", g_buf[1]); _printf("\n"); } } } return _vm_exec(base,op1, op2, addr); } // 在此函数中进行字符组合尝试 int my_vmloop(dword base, DWORD end, DWORD start){ for (int i = 0; i < 0xff; i++){ for (int j = 0; j < 0xff; j++){ memcpy((void*)0x403000, init_opcode, 0x5000);
g_buf[0] = i; g_buf[1] = j; _vmloop((dword)base,end,start); } } return 1; } //hook 主要的函数 extern "C" declspec(dllexport) void hook(){ //dump 下虚拟机最初状态, 用于保证每次虚拟机运行都是最初状态 ifstream infile; ifstream in("opcode.bin", ios::in ios::binary ios::ate); int size = in.tellg(); in.seekg(0, ios::beg); init_opcode = new char[size]; in.read(init_opcode, size); in.close(); DWORD base = (DWORD)GetModuleHandle(NULL); base += 0x1000; MH_Initialize(); MH_CreateHook((void*)base, &my_vm_exec, reinterpret_cast<void**>(&_vm_exec)); MH_EnableHook((void*)base); HMODULE scanf_addr = GetModuleHandle("msvcrt.dll"); base=(dword)getprocaddress(scanf_addr, "scanf"); MH_CreateHook((void*)base, &my_scanf, reinterpret_cast<void**>(&_scanf)); MH_EnableHook((void*)base); HMODULE printf_addr = GetModuleHandle("msvcrt.dll"); base = (DWORD)GetProcAddress(printf_addr, "printf"); MH_CreateHook((void*)base, &my_printf, reinterpret_cast<void**>(&_printf)); base = 0x00401070; MH_CreateHook((void*)base, &my_vmloop, reinterpret_cast<void**>(&_vmloop)); MH_EnableHook((void*)base); } 运行结果如下 :
通过找出对应位置的字符串进行组合, 输入之后得到正确的 Flag:subleq_and_reductio_ad_absurdum@flare-on.com
亢龙有悔 - 一次 APT 攻击分析 -[missing] 实战没有章法, 需要审时度势, 综合运用所掌握内容, 夺取最终 目标 Layer 0: 抛砖引玉 ( 攻击场景介绍 ) 这里描述了一个 APT 攻击场景, 需要通过分析数据包及 PE 文件, 还原整个攻击过程, 获取最终的 flag;
Layer 1: 金蝉脱壳 ( 下载程序 ) 分析 coolprogram.exe 其功能为 downloader 程序, 使用 Delphi 语言编译生成的 PE 文件, 其代码相当比较简单, 从指定网址下载一 个加密文件, 并解密执行 ; 其解密算法相当比较简单, 下载文件的头 4 个字节为解密 key, 其后的数据为加密文件内容 ; 下图为解密代码 :
根据上面代码编写解密程序获取到 secondstage.exe Layer 2: 无中生有 ( 程序框架 ) 分析 secondstage.exe, 其为后门程序的 main 框架, 其它恶意功能均通过下载 plugin 加载执行 ; 其主要代码包含一个正向连接型后门和一个主动上线的反向连接型后门 ;( 后者在被控主机没有公网 IP 地址, 或者通过 NAT 方式上网等情况下使用 )
下图为程序 main 框架的功能类别, 以及各个功能对应的功能号 ; 通过代码分析可以知道 main 框架主要是从网络中获取 plugin, 并安装加载到 main 框架中, 之后通过框架代码解析 c&c 通信数据包, 根据 key 标识调用各个插件执行恶意功能 ; 因此每个插件都有唯一对应的 key 标识 (16 bytes), 用于识别调用对象 ;
Layer 3: 树上开花 ( 插件分析 ) 对代码进行分析后确认其使用 c&c 通信的数据包格式为 : key 用于标识调用的 plugin 模块 ; 而其提供的 pcap 数据包中包括大量 c&c 通信内容, 这里分别提取其网络通信过程中, 使用的插件 ; 插件相当比较多, 这里就不一一列举插件分析过程 ; 下图为各个插件对应名称 算法 对应功能 :CRPT 解密插件 8 个,
COMP 解压插件 3 个,CMD 功能插件 4 个, 上传文件 3 个 ; Layer 4: 顺手牵羊 ( 信息窃取 ) 通过重放数据包, 可以知道, 黑客入侵到 192.168.221.91 之后, 获取本机信息, 并且调用 CMD 插件 (m.dll) 功能获取了当前主机屏幕截图, 由于获取到的数据是没有头的 bmp 数据, 这里根据 bmp 的特性只需要知道图片的 width 参数, 之后通过调整 height, 可以查看到屏幕截图的信息, 通过暴力破解 bmp 的 width 可以获取到截屏内容
为 ; 其中包含一个 zip 文件的压缩密码 : infectedinfectedinfectedinfectedinfected919; 除此之外还收集到本地一些关于 lab10 的信息, 查看了 Challenge_10 目录下 TODO.txt 文件 ; Layer 5: 声东击西 ( 内网渗透 ) 通过调用 CMD 插件 (s.dll) 功能执行 ping larryjohnson-pc 命令获取该主机的 IP 地址 (192.168.221.105), 之后利用 CMD 插件 ( f.dll ) 功能下载 pse.exe 和 srv2.exe ( psexec.exe ) 到 192.168.211.91 主机上, 并利用 psexec 在内网中横向移动到了
192.168.221.105 主机上, 并运行 srv2.exe, 监听本地端口 16452 端 口 (srv2.exe 和 secondstage 功能相同, 一个是正向后门, 一个是反 向后门程序 ); 下一步下载了一个网络代理插件 (p.dll), 安装到 192.168.221.91 上, 通过这台代理服务器连接 192.168.221.105 主机上运行的后门程 序 ;
Layer 6: 暗度陈仓 ( 偷取重要文件 ) 这里简单理一下思路 ( 针对关键操作进行梳理 ), 1. 黑客入侵到 192.168.221.91 后, 先获取了屏幕截图 ( 内容包含了一个密码 ); 2. 查看 c:\work\flareon2017\challenge_10\todo.txt, 发现 larry 相关提示 ( 根据前期信息收集结果, 可以知道 larry.johnson 主机名 ); 3. 通过 ping 命令获取到内网 larry.johnson 主机 IP 地址 (192.168.221.105); 4. 使用 psexec 在 larry.johnson 的主机上安装后门 srv2.exe( 监听本地 16452 端口 ); 5. 之后通过内网代理连接该后门, 通过代理插件上传加密模块到了 larry.johnson 的主机上 c:\staging\cf.exe; 6. 利用加密程序 (cf.exe) 对 lab10 的文件进行加密, 之后将原始文件删除, 并且通过代理传到了黑客手里 ; 下面是 cf.exe 程序代码, 使用 AES 算法对数据进行了加密, 并将文件的 sha256, 路径信息, 文件大小, 加密使用的 iv 保存到文件头部, 并且在文件头部添加 cryp 标识加密文件 ;
由于已经分析清楚程序的执行流程, 这里直接解密输出流中, 定 位关键到上传数据包, 对其进行解密后内容为 :( 其中标记的部分为 传输 lab10.zip.cry 文件的数据流 )
Layer 7: 反客为主 ( 还原 lab10.zip.cry) 对发送的数据流进行组包后, 获取到 lab10.zip.cry 文件 ;
根据 cf.exe 编写解密程序, 对数据包进行解密后获取到 lab10.zip 文件 ; Layer 8: 釜底抽薪 (get flag!!!) 利用之前获取到的 zip 文件密码解压 lab10.zip 文件, 获取到 challenge10;
直接运行该程序 get flag:( see you next year :D)