0%

HackBrowserDataManual开发日记

HackBrowserDataManual开发日记

最近写的玩具,实际上感觉实战基本上用不到。。。我打的攻防还没有这么前沿,但是因为最近好久都没更新博客了。闲来无事水一篇

项目地址HackBrowserDataManual

首先full credit to HackBrowserData,主体框架都是从这抄的,也是从这个的代码学的大概的还原浏览器数据的逻辑。

该项目是为了解决HackerBrowserData在目标机器上存在高强度edr防护时无法利用的情况。其实绕过的想法也很简单,并且已经到了高级对抗,实现的方法也还可以有很多,也可以针对特定规则搓出特定的方案,这里只是给出一个可能且稍微具有一点泛用性的一个例子。

顺便练习了一下用go的interface写面向对象的操作(非常坐牢)。

该程序仅在win10和win11上测试通过。(理论上可以运行在linux和mac上,但实际应用感觉基本上都是抓windows密码加上没有环境就没测)

下文均默认以在windows平台上运行进行叙述,本文均使用chrome称呼浏览器,内容可扩展到chromium内核实现的浏览器上

原理

chrome浏览器,或者chromium内核的浏览器,使用sqlite对数据进行存储。这里仅挑选出常用的cookie,password和history三个数据进行提取。

数据存储位置

默认情况下,用户数据目录为homeDir + "/AppData/Local/Google/Chrome/User Data/",用户的默认profile文件为Default,该目录下存储着浏览器的各种数据库文件。

其中主密钥文件位于数据目录下的Local State文件中,password和cookie两个数据库被该密钥加密,history无需解密可明文读取,各文件具体位置可以在本项目的item文件夹下查看。

加解密还原

windows平台下密码和cookie的加密并非直接的aes或rsa等加密,而是使用了DPAPI,DPAPI实现了用户层面的加密,即只有同一个用户调用该api时可以恢复对应的数据。而chrome这里的主密钥是经过dpapi加密的,因此只能在目标机器以对应用户的身份还原。将文件外带后本地还原是行不通的。

这也是部分情况下攻击者获取了system权限后,HackBrowserData无法还原数据的原因(另一个原因是其默认情况下是根据用户家目录搜寻数据的,system的家目录下自然没有东西)

当然,dpapi的加密依赖于用户口令,如果攻击者直接是administrator及以上层级,能够mimikatz一把梭恢复出用户口令hash的话,也可以进行本地还原。如下就是一个类似的例子
通过Dpapi获取Windows身份凭证

devtools protocol绕过edr

如果只有前面这两部分,显然是无法作为一个新的东西拿出来的。因为edr只允许浏览器访问自己的数据,那我就用浏览器去做。chrome的devtools协议长期被用来操纵浏览器进行自动化任务,比较有名的项目有puppeteer等。控制浏览器访问file协议,以及进行文件下载自然也在其中。而edr只会检测位于对应位置的数据文件,控制chrome对其进行读取和转存即可绕过这一限制。

可以先看看谷歌的完整文档Chrome DevTools Protocol
在这里面就已经可以看到一个有意思的功能了,一键获取所有的cookie。不过仔细翻了几遍也没翻到其他内容,如password自动填充和历史记录之类的。有一个database模块,瞬间联想到所有数据都是sqlite形式存储的,但是官方文档对这个模块的描述过于抽象,并且也没有提供列举数据库的函数,所以暂时放弃。
Storage.getCookies

可以很轻松的找到谷歌官方在go方面的devtools支持chromedp,并给出了简单的例子,接下来只需要一顿缝合实现下载功能即可。具体代码自行参看项目内实现

此处有一个需要注意的地方,若使用devtools protocol直接获取用户cookie,则需要指定用户数据目录,否则chrome会使用一个临时目录启动实例,类似于匿名模式启动。并且chrome在实现时做出了奇怪的限制,若存在chrome实例,cookie文件就无法访问,甚至不支持只读打开。故无论是devtool协议还是下载文件对cookie进行窃取均仅能在当前用户未启动chrome实例时使用,或是强行关闭对应进程后使用。

为此提供了–kill选项,遍历进程关闭当前用户的chrome实例。关闭实例有两种办法,使用terminalProcess强杀,和发送信号。(go似乎没实现windows下的发送信号,但是用任务管理器杀似乎是发送信号类型的杀进程)两者的区别在于,前者会导致chrome重启时显示浏览器异常关闭,而后者则不会。关闭后均可使用--restore-last-session选项恢复之前的页面,可以有效地防止用户发现浏览器被关了。

下载数据文件则需要使用两种方法,当浏览器使用file协议访问本地文件时,存在两种情况:1. 文件为文本文件,此时chrome不会将文件进行下载,而是直接呈现,2. 二进制文件,此时chrome会将文件下载到指定目录中去。chrome的主密钥文件是json格式的文本文件,而数据库均为sqlite的二进制文件,故对应这两个情况需要分别处理

文本文件可以直接navigate到对应url下,执行document.body.innerText获取内容,而二进制文件则需要设置下载路径等行为。可以直接抄chromedp项目中的example魔改实现。

碎碎念

着实被go的垃圾面向对象支持恶心了。。。下文中的类均指代实现了对应方法的结构体,按照面向对象的习惯使用类对其进行描述

一开始的设计思路是写两个类,一个browser类处理各种chromium内核的浏览器,一个data类负责写入各种数据。经典的继承与多态

data类写的比较轻松,先写一个接口,然后写一个父类实现写入等公有方法,并用一个any类型存储数据,各子类实现其解析方法即可。能够写出类似于多态和继承的操作

然而,browser类写的就很坐牢,因为data中write是统一的,而parse方法则是各写各的,父类函数中不会调用子类的函数。而browser类中如果想封装的好一点,那么必然会在父类函数中调用子类的方法,以browser.initPath为例

func (b *Browser) InitPath() {
    if b.MasterKeyFile == "" && b.Action != item.History {
        b.MasterKeyFile = b.getUserDir() + item.ChromiumKey
    }
    ......
}

该函数的流程是固定的,但是对应不同的浏览器,其文件路径不同,需要调用子类的方法来解析对应的路径。对于成熟的面向对象语言,父类中getUserDir()可以留空,然后子类进行实现,子类无需重复实现InitPath,而是继承父类。这种情况下子类对象调用InitPath方法时会调用父类的方法,但getUserDir()会使用子类的实现,非常合理的多态。但是go不支持。。。
使用如下的伪代码进行操作,会调用Browser类中留空的getUserDir()方法导致程序原地爆炸

var browser IBrowser
browser = &Chrome{}
browser.InitPath()

如果还是要强行写类思路的话,有两个方案,第一个是完全放弃继承,直接这个函数每个子类里面复制一遍,显然顶级不优雅。方案2是进行传参,即在InitPath()中添加一个参数IBrowser,将调用形式变为browser.InitPath(browser),然后在函数体中使用传入的参数调用getUserDir()。是一个略微可行的方法,但仍然不够优雅,并且由于传入的参数是IBrowser类型,导致一些本来可以藏在类内部的方法必须暴露在接口中,还得反向去给接口加方法。(其实还有第三个方案,反射,但我觉得在开发流程中,能不使用反射就应当尽量去避免使用)

说到底,go的这个结构体写出来的类,并不是继承关系,而是组合关系,可以在Browser类和Chrome类中各自实现接口的部分函数,再拼到一起就变成了实现该接口。这里的Chrome类”继承”了Browser类,实际上只是将Browser类组合了进来,然后再实现自己的方法去覆盖掉Browser类的方法。如果一开始的调用入口是Browser类的话,后续的处理也就是按照Browser类去做的,无法做到真正的面向对象的多态与继承。

网上搜了一大串都没有更加优秀的解决方案,但是看到了类似的需求,以及下面的答复:

考虑用接口实现,别总想着继承。

最后我直接不写继承和多态了,我直接将Browser类作为一个实例我,也不用基础接口,而是在里面添加一个接口去实现各个浏览器下的具体功能,原本应当是子类浏览器实例继承父类浏览器的方法,并实现其具体逻辑,而我现在反过来,浏览器直接实现主体框架,而其中添加一个浏览器实例的接口,应对不同浏览器情况下的特殊情形。

应当使用接口去实现不同功能,即browser.ChromeInterface.func,而不是Chrome继承Browser后调用Chrome.func

后记

我有一个朋友的电脑上上了顶级火绒规则防御,这里需要解决的只允许chrome访问其配置文件就是其中一条,我写完这个之后让他试试,自然,chrome访问配置文件是可以通过的。然而他这里存在着第二条规则,只允许特定进程调用chrome程序。那么该程序自然无法绕过这条规则。而他的规则顶级严格,能够访问chrome数据文件的只有chrome进程,也就是说他自己去访问也会触发报警,需要手动放行,感觉有点无敌防御了

一个可能的绕过方案是进程注入chrome,或者注入explorer去拉起chrome,然而进程注入也会有相应的防护规则,到了这个地步只能是针对每一种特殊情况去具体解决了。

不过我看他的防护规则中是对二进制的名字进行检测,那么我自己直接改名成chrome.exe也许有绕过可能?不过这样子应该还是会触发其他程序调用chrome.exe的报警,不过要是我有3389的话上去就可以。或者说有命令行调用explorer启动其他程序的方法?(搞不好又有对explorer的防护规则,开始无限套娃)