9月2日凌晨,Flare-on开通了第四届的逆向挑战赛,网址为https://2017.flare-on.com。
见龙在田-注册
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:
outstr += ch
return outstr
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
序:
拿到程序后,首先在命令行上运行一下,看有什么提示。
根据提示可以知道如果输入了正确的flag,那么程序应该会有不同的提示。
将程序载入到IDA,看一下验证流程。
函数sub_401050的返回值来决定在终端显示成功还是失败。
验证的算法为从输入的末尾开始读取每一个字节与V4(初始值位0x04)进行异或,将结果传送给指定的数组,并将输入的当前位置字节赋值给V4。
参与比较的结果如下:
编写解密代码如下:
运行后结果如下:
潜龙勿用-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对这些数据进行了操作。
所以我们可以将对应文件对应位置处的数据读取出来,编写程序对这些数据进行运算,同样可以得到答案。
参考脚本:
结果如下:
密云不雨-pewpewboat.exe
序:
穿过弯弯曲曲的盘山路,来到了河边,旁边有块牌子提示,“请放松一下,来做个游戏,只有通关了,才会有通往彼岸的吊桥”。
使用file命令查看文档格式,发现是一个64位的ELF程序。
运行后界面如下:
这是一个射击游戏,通过猜测目标位置来进行射击,成功打到所有目标即可过关。如此以来,需要逆向看看靶子上目标的坐标是如何生成的了。
先来看看他的限制条件:
- 尝试的次数。当射击达到一定次数以后,提示弹药使用完毕。
- 相互关联。无法通过修改内存直接得到坐标,必须通过计算完成。
- 输入限制。只能输入一个坐标,多个坐标同时输入会产生错误的结果。
注意到以上条件,则需要看的是如何生成坐标的了。首先要确定,坐标生成所需要内容是什么,下图中展示的靶和目标坐标生成的代码段。
每次会取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字节的结果作为如下红色字符函数的输入参数运行。
得到的结果如下:
突如其来-payload.dll
序:
从给出的名字可以知道出题者想考察一下参与者对于Windows动态库的掌握情况。
查看导出表如下:
这里有个提示,需要根据格式进行调用。手动输入如下命令。
提示如下:
猜测可能在程序运行的过程中修改了导出函数名,通过 “, #1”的方法调用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++Rci2RiUFvS9RbjWYzbbJ3BSerzwGc9EZkKTvv1JbHOD6ldmehDxyJGa60UJXsKQwr9bU3WsyNkNsVd/XtN9/6kesgmswA5Hvroc2NGYa91gCVvlaYg4U8RiyigMCKj598yfTMc1/koEDpZUhl9Dy4ZhxUUFHbiYRXFARiYNSiqJAEYNB0r93nsAQPNogNrqg23OYd3RPp4THd8G6vkJUXltRgXv7px2CWdOHuxKBVq6EduMFSKpnNB7jAKo…………………..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+IMPAh84PlNxwYEQ1IODlkrwNXbGXcx/Q==
接下来,你只能去调试powershell脚本,接着就会发现rand函数的问题。
震惊百里-flair.apk
序:
这是一道Android APK逆向题,总共4关,每一关输入正确的password后才能进入下一关。
以下是反编译后程序结构:
第一关:
使用apktool解包apk程序,并在解压后的目录下搜索“who run it”字符串,最后发现在activity_michael.xml文件中,因此,第一关需要分析反编译后的michael文件。
分析以上算法,可以得到以下结论:
- Password总长度为12;
- Pw[0] = “M”
- Pw[1] = “Y”
- Pw[2-4] = “PRS”
- Pw[5] = “H”
- Pw[6] = “E”
- Pw[7] == pw[8],并且两者连接后计算hashcode得到的值为3040
- Pw[9-10] = “FT”
- 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
分析上述代码逻辑:
- 向r25寄存器放了一堆数据:
B5 B5 86 B4 F4 B3 F1 B0 B0 F1 ED 80 BB 8F BF 8D C6 85 87 C0 94 81 8C
- 设置索引从0开始,依次遍历r25中的数,将每个值与r24做异或操作,然后再加上索引的值r18;
- 索引加1,取r25中下一个数,重复(2)中算法。
那么这个r24的值到底是多少呢?管他呢,爆破大法好,对r24从0x00到0xff取值,打印出所有计算后的结果:
Python爆破代码如下:
最后在所有输出结果中去找flag:
附AVR汇编指令参考地址:
http://www.atmel.com/webdoc/avrassembler/avrassembler.wb_instruction_list.html
龙战于野-shell.php
序:
这一题是一个PHP文件,这个PHP程序接受POST的参数值,主要通过MD5算法计算出一个长度可变的字符串key,用key字符串通过XOR算法解开前面一部分的字符串,再将这一部分作为字符串去异或求出下一个分组的字符串,分组长度和key长度相同。
第一层密文解密:
这题的key其实是很长的,通过类似弱口令的方式并不能爆破出POST的参数值。不过因为算法是一个简单的分组异或,而且知道加密前是一个合法的PHP代码,可以通过加密后的密文反向推出明文。
因为key长度是可变的,准确来说是在33到64之间,为了接下来的分析,最好先求出key的长度。因为该算法是分组的,所以直接拿分组的第一个元素去求解,得出的值再去求解下一个分组的第一个元素,以此类推。得出所有分组的第一个元素肯定是在php代码中合法的字符,通过加入该约束条件即可求出key长度,相关代码如下:
结果为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=db6952b84a49b934acb436418ad9d93d237df05769afc796d067bccb379f2cac
第二层密文解密:
解密出的代码如下:
根据代码中的网址提示,通过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:暗度陈仓(偷取重要文件)
这里简单理一下思路(针对关键操作进行梳理),
- 黑客入侵到168.221.91后,先获取了屏幕截图(内容包含了一个密码);
- 查看c:\work\FlareOn2017\Challenge_10\TODO.txt,发现larry相关提示(根据前期信息收集结果,可以知道larry.johnson主机名);
- 通过ping命令获取到内网larry.johnson主机IP地址(192.168.221.105);
- 使用psexec在larry.johnson的主机上安装后门srv2.exe(监听本地16452端口);
- 之后通过内网代理连接该后门,通过代理插件上传加密模块到了larry.johnson的主机上c:\staging\cf.exe;
- 利用加密程序(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)