去年年底,我受邀参加了Facebook的Bountycon活动,这是一个受邀者才能参加的应用安全会议,其中有一个现场黑客环节。虽然参与者可以提交任何Facebook产品内的漏洞,但Facebook邀请我们关注Facebook游戏。由于之前测试过Facebook的产品,所以我知道这将是一个高难度的挑战。这些年来,他们的安全控制已经越来越严格——即使像跨站脚本攻击这样的简单漏洞也很难找到,这也是为什么他们肯为这些漏洞付出非常高的赏金的原因。因此,顶级白帽黑客往往会从第三方软件的角度来挖掘Facebook的安全漏洞,比如Orange Tsai著名的MobileIron MDM漏洞。
鉴于我的时间有限(由于某些原因,我迟到了),所以,我决定不进行全面的漏洞研究,而专注于对Facebook Gaming的访问控制进行简单的安全审计。然而,正如人们所期望的那样,移动和网页应用的安全性都很好。经过一番折腾,我发现了Facebook Gameroom程序,它是一个用于玩Facebook游戏的Windows本地客户端。于是,我就踏上了将攻势攻击性逆向分析应用于本地桌面应用程序的启蒙之旅。
关于Facebook Gameroom
您是不是还没听说过Facebook Gameroom——这很正常,我之前也没听说过。实际上,Gameroom发布于2016年11月,被誉为Steam的强劲对手,支持Unity、Flash和最近的HTML5游戏。然而,近年来,随着流媒体的崛起,Facebook已经将注意力转向其移动和网络平台。事实上,Gameroom计划在今年6月退役。幸运的是,在我挖到其中的安全漏洞时,它还没有退出市场。
我注意到的第一件事是,安装Gameroom时,无需提升任何的权限。它似乎是一个分步安装的程序:首先,通过一个小型安装程序从网络上下载额外的文件,也就是说,它的安装程序不是一体式的。事实上,我很快就找到了安装目录,即C:\Users\< USERNAME >\AppData\Local\Facebook\Games由于大多数用户级应用程序都位于该C:\Users\< USERNAME >\AppData文件夹中,因此我很快在处找到了安装目录。该文件夹包含许多.dll文件以及几个可执行文件。一些事情对我很重要:
1. Gameroom自带的7zip可执行文件(7z.exe和7z.dll),可能已经过时,而且很容易受到攻击。
2. Gameroom将用户会话数据存储在Cookies SQLite数据库中,这对攻击者来说是一个非常有吸引力的目标。
3. Gameroom包含了CefSharp库(CefSharp.dll),经过进一步研究,发现这是一个用于C#的嵌入式Chromium浏览器。
第三点表明,Gameroom是用.NET框架编写的。我们知道,.NET框架可以将程序被编译成通用中间语言(CIL)代码,而不是机器代码,也就是说,这些代码可以在通用语言运行时应用程序虚拟机中运行。这样做有许多好处,比如可以提高.NET应用程序的互操作性和可移植性。然而,由于这些应用程序被编译为CIL而不是纯机器代码,因此,将这些应用程序反编译回接近源码的过程也要容易得多。
对于.NET程序集,DNSpy是事实上的标准。逆向工程师可以用DNSpy轻松地调试和分析.NET应用程序,包括对它们进行实时修改。于是,我把FacebookGameroom.exe拖拽到DNSpy中,开始进行分析。
挖掘含有漏洞的函数
首先,我们开始查找易受攻击的函数,如不安全的反序列化函数等。限于篇幅,这里就不对反序列化攻击进行详细介绍了,简单来说,这涉及到将数据类型转换为易于传输的格式,然后再转换回来,如果处理不当,这可能会导致严重的漏洞。例如,微软就曾警告不要使用BinaryFormatter,因为BinaryFormatter是不安全的,也不能使其安全。
不幸的是,在我搜索“Deserialize”字符串时,BinaryFormatter竟然冒了出来。
然而,我还需要找到易受攻击的代码路径。为此,我右击搜索结果,选择“Analyze”,然后沿着“Used By”链找到了Gameroom使用BinaryFormatter.Deserialize的地方。
最终,这让我找到了System.Configuration.ApplicationSettingsBase.GetPreviousVersion(string)和System.Configuration.ApplicationSettingsBase.GetPropertyValue(string)函数。Gameroom在启动时使用了反序列化函数来检索其应用程序的相关设置——但这些设置是从哪里来的呢?通过考察安装文件夹,我发现了fbgames.settings,这是一个序列化的blob。因此,如果我在这个文件中注入一个恶意的反序列化payload,就可以获得代码执行权限。不过,在此之前,我还需要找到一个反序列化的gadget。根据已知的反序列化gadget列表,我展开了进一步的搜索,发现Gameroom使用的是WindowsIdentity类。
在此基础上,我研究出了一个代码执行漏洞的概念验证过程:
1. 使用ysoserial反序列化攻击工具,通过命令ysoserial.exe -f BinaryFormatter -g WindowsIdentity -o raw -c "calc" -t > fbgames.settings生成了相应的代码执行payload。
2. 接下来,我将fbgames.settings复制到C:\Users\
3. 最后,打开Facebook Gameroom,计算器就弹了出来!
虽然获得代码执行权限是件非常令人兴奋的事情,但在与Facebook团队进一步讨论后,我们一致认为这不符合他们的威胁模型。因为Gameroom是作为用户级应用执行的,所以没有机会升级权限。此外,由于覆盖文件需要某种程度的访问权限(例如,需要通过审批才能被公开列出的恶意Facebook游戏),所以没有可行的远程攻击载体。
在本地应用程序构成的不同威胁环境中,我学到了一个重要的教训——在深入研究代码级漏洞之前,最好先寻找可行的远程攻击载体。
规划探索路径
您是否有点击邮件中的链接就神奇地启动了Zoom的经验?这背后到底发生了什么?其实很简单,这只是使用了一个自定义的URI方案,它允许你像网络上的其他链接一样打开应用程序。例如,Zoom注册了zoommtg: URI方案,来解析像下面这样的链接: zoommtg:zoom.us/join?confno=123456789&pwd=xxxx&zc=0&browser=chrome&uname=Betty。
同样,我注意到Gameroom使用了自定义URI方案,以便可以在单击Web浏览器中的链接后自动打开Gameroom。搜索完代码后,我发现Gameroom会通过FacebookGames\Program.cs来检查fbgames:URI方案:
- private static void OnInstanceAlreadyRunning()
- {
- Uri uri = ArgumentHelper.GetLaunchScheme() ?? new Uri("fbgames://");
- if (SchemeHelper.GetSchemeType(uri) == SchemeHelper.SchemeType.WindowsStartup)
- {
- return;
- }
- NativeHelpers.BroadcastArcadeScheme(uri);
- }
即使Gameroom已经用fbgames://URI打开,它还是会继续通过SchemeHelper类对其进行解析:
- public static SchemeHelper.SchemeType GetSchemeType(Uri uri)
- {
- if (uri == (Uri) null)
- return SchemeHelper.SchemeType.None;
- string host = uri.Host;
- if (host == "gameid")
- return SchemeHelper.SchemeType.Game;
- if (host == "launch_local")
- return SchemeHelper.SchemeType.LaunchLocal;
- return host == "windows_startup" ? SchemeHelper.SchemeType.WindowsStartup : SchemeHelper.SchemeType.None;
- }
- public static string GetGameSchemeId(Uri uri)
- {
- if (SchemeHelper.GetSchemeType(uri) != SchemeHelper.SchemeType.Game)
- return (string) null;
- string str = uri.AbsolutePath.Substring(1);
- int num = str.IndexOf('/');
- int length = num == -1 ? str.Length : num;
- return str.Substring(0, length);
- }
如果URI含有gameid主机,它将用SchemeHelper.SchemeType.Game.Game来进行解析。如果它使用的是launch_local主机,则会用 SchemeHelper.SchemeType.LaunchLocal来进行解析。于是,我从最有希望的launch_local路径开始,追踪到了FacebookGames.SchemeHelper.GenLocalLaunchFile(Uri):
- public static async Task
- {
- string result;
- if (SchemeHelper.GetSchemeType(uri) != SchemeHelper.SchemeType.LaunchLocal || uri.LocalPath.Length <= 1)
- {
- result = null;
- }
- else if (!(await new XGameroomCanUserUseLocalLaunchController().GenResponse()).CanUse)
- {
- result = null;
- }
- else
- {
- string text = uri.LocalPath.Substring(1);
- result = ((MessageBox.Show(string.Format("Are you sure you want to run file\n\"{0}\"?", text), "Confirm File Launch", MessageBoxButtons.YesNo) == DialogResult.Yes) ? text : null);
- }
- return result;
- }
遗憾的是,即使我可以通过像fbgames://launch_local/C:/evilapp.exe这样的URI来启动系统中的任何任意文件(正如Facebook所记录的那样),但仍然会被确认对话框所阻止。我试图用格式字符串和非标准输入绕过这个对话框,但并没有得手。
于是,我将目光转向gameid路径,该路径会根据URI中的游戏ID打开Facebook URL。例如,如果您想在Gameroom中启动Words With Friends,可以在浏览器中访问fbgame://gameid/168378113211268,这时Gameroom就会在本地应用窗口中打开https://apps.facebook.com/168378113211268。
然而,我发现GetGameSchemeId会从URI中提取ID,并将其添加到app.facebook.com URL中,但是,它并没有验证slug是否为有效的ID。因此,攻击者可以将本地应用程序窗口重定向到Facebook上的任何其他页面。
- public static string GetGameSchemeId(Uri uri)
- {
- if (SchemeHelper.GetSchemeType(uri) != SchemeHelper.SchemeType.Game)
- return (string) null;
- string str = uri.AbsolutePath.Substring(1);
- int num = str.IndexOf('/');
- int length = num == -1 ? str.Length : num;
- return str.Substring(0, length);
- }
例如,fbgame://gameid/evilPage会把Gameroom窗口重定向到https://apps.facebook.com/evilPage。
但是,我怎么才能在Gameroom中重定向到攻击者控制的代码呢?这里有多种方法,其中包括滥用app.facebook.com上的开放式重定向漏洞。不幸的是,当时我手头一个这样的漏洞也没有。另一种方法是重定向到允许将自定义代码嵌入iframe的Facebook页面或广告。
这时,我遇到了一个障碍。我需要重新修改GetGameSchemeId的代码,因为它只取URI路径中的第一个slug,所以fbgame://gameid/evilPage/app/123456会将原生应用窗口引导到https://apps.facebook.com/evilPage,并会丢弃/app/123456。
幸运的是,我还可以借助于其他的代码gadget。另外,在Gameroom中使用的Chrome浏览器版的确有点陈旧:63.0.3239.132——而当时的版本是86.0.4240.75。因此,它并不支持新版本的Facebook Pages。经典的Facebook Pages版本会接受一个sk参数,这样,https://apps.facebook.com/evilPage?sk=app_123456就会生成一个自定义标签页,其中攻击者控制的代码位于https://apps.facebook.com/evilPage/app/123456!
但是,怎样才能在我们的自定义方案中注入额外的查询参数呢?别忘了,Gameroom会丢弃第一个URL slug之后的所有内容,包括查询参数。果真是这样吗?回顾FacebookGames/SchemeHelper.cs,我发现了GetCanvasParamsFromQuery:
- public static IDictionary
- {
- if (uri == (Uri) null)
- return (IDictionary
- string stringToUnescape;
- if (!UriHelper.GetUrlParamsFromQuery(uri.ToString()).TryGetValue("canvas_params", out stringToUnescape))
- return (IDictionary
- string str = Uri.UnescapeDataString(stringToUnescape);
- try
- {
- return JsonConvert.DeserializeObject<IDictionary
- }
- catch
- {
- return (IDictionary
- }
- }
在传递自定义URI之前,GetCanvasParamsFromQuery会寻找查询参数canvas_params,并将其序列化为JSON字典,并将其转换为新的URL,并用作查询参数。
这让我想到了最终的payload方案——fbgames://gameid/evilPage?canvas_params={"sk": "app_123456"}将被Gameroom解析成本地应用浏览器窗口中的https://apps.facebook.com/evilPage/app/123456,然后执行我的自定义的JavaScript代码。
如前所述,原生应用的威胁环境与Web应用有很大区别。通过将嵌入式Chrome原生窗口重定向到攻击者控制的Javascript代码,攻击者可以继续在3年前的嵌入式Chromium浏览器上执行已知的漏洞。虽然完整的漏洞利用代码尚未公开发布,但我仍然可以利用CVE-2018-6056的概念验证代码,通过类型混淆漏洞使Chrome引擎崩溃。
另外,攻击者可以创建本质上是合法的原生MessageBoxes的弹出框来执行钓鱼攻击,或者尝试读取缓存的凭证文件。幸运的是,与集成Node.JS APIs的Electron应用程序不同,CefSharp限制了API的访问。然而,它仍然容易受到Chromium和第三方库漏洞的影响。
小结
Facebook将本文介绍的安全漏洞评为高危,并随即修复了该漏洞。同时,我藉此荣升Bountycon排行榜的前十名。虽然Gameroom即将退出市场,但它仍然给我留下了攻击性逆向分析的美好回忆(和实践机会)。对于应用逆向工程的新手来说,Electron、CefSharp和其他基于浏览器的框架是一个很好的练手工具,可以用于测试跨站脚本攻击和开放式重定向等类Web漏洞,同时,还可以利用桌面应用特有的代码执行手段。