一份条理清晰、结构完善的文档,帮助你入门 chrome 漏洞利用 & v8 漏洞利用
本文档的组织方式
浏览器是当今最常用的技术之一。在每一台现成的计算机上,只要我们即插即用,就会看到已安装的浏览器。因此,从攻击者和威胁模型的角度来看,如果攻击者能够通过恶意页面攻破浏览器,那将是非常有回报的。基于上述理由,我选择研究 Google 的 JavaScript 引擎,尤其是 v8。
鉴于 v8 项目规模庞大,我选择以解释器(即 d8)作为起点。尽管针对 d8 已经进行了大量研究,但我们仍希望至少能找到一个 bug;即使找不到,也能继续推进浏览器漏洞利用研究,因为 v8 为浏览器漏洞利用中常用的基础利用开发策略提供了切入点。
我之所以选择 v8 作为目标,另一个原因是它被多个浏览器使用。
如果我们深入研究,就会发现该引擎也用于 MicrosoftEdge,因此有机会获得多个漏洞赏金计划的奖励。对于底层操作系统,研究者将混合使用 Windows 和 Linux,因为在获取 shell 方面没有限制,v8 漏洞允许通过 wasm 页面执行代码,并且不绑定任何特定平台。
遗憾的是,虽然 v8 中被利用的漏洞会导致代码执行,但由于沙箱的存在,我们无法执行任何代码;也就是说,我们只能在渲染器上下文中获得代码执行,而无法在机器上执行代码。为此,我们需要另一个沙箱漏洞利用,也就是需要一条完整的攻击链才能攻破系统。
因此,为了能够开始浏览器安全研究,我们定义了以下目标
在项目的第一阶段,有必要尽可能多地收集有关 Chrome 架构以及各个组件之间如何交互的知识。为了更好地理解这一点,我们需要将 Chromium 项目拆分为多个子组件,以便能够隔离各部分并正确分析。更确切地说,下列每个组件会拆分成多少个子组件:
那么,对于第一步来说,最合乎逻辑的步骤是理解 Chromium 架构。 好的,我们想要利用浏览器,但当我们第一次启动浏览器时会发生什么?嗯,点击 Chromium 可执行文件之后,该可执行文件会启动一些进程。
它们的启动顺序和名称如下:
第一个叫作 content 进程。这个进程是做什么的?
既然我们大致了解了它的作用,现在是时候更深入地探讨它了:
至少从 Windows 上看,Chromium 的工作方式是将文件编译成一个 dll,然后将其加载到内存中。因此,Chromium 浏览器(Chroium 浏览器)的核心逻辑就在 chromium.dll 中。
代码也证实了这一点
.
这段代码取自 chrome_exe_main_win.cc,如果你好奇想阅读完整代码,它位于 chromium/src/chrome/app。好,顺着执行流程往下走,我们可以看到它调用 MakeMainDllLoader() 来调用 dll 加载类;之后它启动“loader”(加载器),也就是加载 chrome.dll,如果有必要,还会使用必要的命令行重启它。为了进一步分析 Loader,我们需要理解它的代码,这些代码位于同一目录下的 mail_dll_loader_win.cc 文件中。一直滚动到文件底部,我们可以看到对 MakeMainDllLoader 的调用,它会根据你所拥有的版本调用 ChromeDllLoader 或 ChromiumDllLoader。

我们可以看到,ChromiumDllLoader 是一个继承自 MainDllLoader 的类。从定义中可以看出,这个类所做的只是根据传递给 cmdline 和进程类型的参数来加载 dll。
.
分析 Launch 方法,我们可以了解 chrome.dll 启动之前发生的一些事情:我们可以从这条注释中理解:“// Launching is a matter of loading the right dll and calling the entry point. // Derived classes can add custom code in the OnBeforeLaunch callback.” 启动 chrome 实际上就是加载一组真正完成工作的 dll。其次,它获取传递给命令行的参数,并进一步初始化沙箱服务。
.
首先,它检查是否由浏览器来调用沙箱初始化;然后检查调用沙箱初始化的进程是否以云打印服务的方式被调用。它还检查二进制文件是否传入了 参数,这基本上就是告诉二进制文件不要在沙箱中运行。它检查这些条件中是否有任何一个被设置,如果任一为真,它就会使用相应的选项调用沙箱。最后我们来到
这正是我们感兴趣的,它表示对调用 的封装。
现在让我们看看它做了什么。我们用 Binja 打开 chromium.dll。
根据来自 https://blogs.igalia.com/jaragunde/files/2019/03/chrome-init-sequence.png 的这张图片,我们对在 Binja 中应该寻找什么有了大致的想法。
下图就是它在 Binja 中的图显示效果
.
为了更容易追踪,请寻找一个名为 ChromeMain 的函数。这就是 chrome 的主要(也就是核心)逻辑。在它内部,我们可以看到前面提到的 chrome.dll 启动时运行的各个阶段:
这里,我们首先看到它调用了一些函数,如 sub_180001420、sub_180017020、sub_180017020,它们会进行一些检查,以了解 chrome 是如何安装/编译的细节。通过源代码,我们可以追踪它们的属性。第一个函数 sub_180001420 对应 UmaHistogramEnumeration;接下来是 sub_180017020,即 InitializeFromPrimaryModule;第三个也是最后一个 sub_180017020 是 chrome_main_delegate。
我们一直在重复 chrome_main_delegate,但从未定义它的用途。ChromeMainDelegate 是一个继承自 ContentMainDelegate 的类,主要提供与启动相关的函数以及进程调用处理。!如果你愿意,你可以实现一个自定义的 ContentMainDelegate 接口来改变 Content 模块的默认行为,并在 Chromium 中使用自定义的 ChromeMainDelegate 类来定制启动进程的行为。

在它内部有一些函数调用,它们对某些数据区域进行异或(xor)操作,并将其与某些注册表项拼接,以获取有关 chrome 启动的一些选项
.
另请注意,函数 sub_180001510 正在创建一个带有两个回调的 scoped 指针引用。它所做的这件事并不是特别重要,但了解什么是 BindStateBase 很有意思。我们会在 chrome 的 basecode 中经常遇到它。
现在你可能会好奇最后三行到底做了什么,确切地说
做什么。所以第一行基本上为 Closures 创建了一个 。闭包(closure)是什么鬼?!好吧,如果引用 Mozilla 开发者文档的话:“闭包是一个函数与其周围状态(词法环境)的引用捆绑在一起(封装)的组合。换句话说,闭包让你可以从内部函数访问外部函数的作用域。在 JavaScript 中,每次创建函数时都会创建闭包,即在函数创建的时候。”如果用通俗的语言来说,它就是一个函数内部的函数,可以访问外部函数内部的变量。例如 。
所以本质上,它确保闭包会执行。另外两行只是在 chrome 时设置了一些特定的转储行为,(此处粘贴)。
继续深入,它会在运行时检查版本,如果不匹配就会崩溃,接着我们就来到了命令行解析
进入 sub_184171800 内部,我们可以看到它并不是很大 首先,它获取传递给当前进程的参数,然后调用一个函数,该函数接收一个 StringPiece(本质上是一个 std::string 的类包装器,但稍微酷一点)。这个函数基本上会检查二进制文件是否以 headless 方式被调用,比较被运行的二进制文件名是否为 chrome,检查是否设置了 USE_HEADLESS_CHROME,然后继续进入下一步,即 content main。
根据所提供的参数,content main 的职责是启动相应的 shell。
如果我们不向 shell 提供任何参数,执行流程就会转到 。
为了理解它做了什么,我们需要查看它的源代码。我们可以在 找到它。
我们需要一直滚动到底部,在那里会找到 ContentMain 函数的定义。在那里,我们看到它调用了两个函数。一个初始化 ContentMainRunner,该类负责处理创建浏览器 ipc、sqli、net 等所有“进程创建”相关的事务;另一个基本上检查子进程的类型,并将其提供给 ContentMainRunner 类。
现在让我们退一步,以理解代码流程。首先,让我们检查 RunContentProcess,因为 ContentMainRunner 相当复杂。它做的第一步是创建一个 GlobalActivityTracker。天哪!!!你猜对了,Google 会追踪你 :))) 哈哈,开玩笑的,请不要起诉我,Google。但说正经的,它会跟踪每个线程(本质上是一个线程跟踪器),并为了更好的调试做了一些贴心的事情,例如: 这本质上意味着每个线程都会有一个唯一的整数作为标识符,以便更好地理解是什么导致了进程崩溃。
例如:```
kTypeIdActivityTracker = 0x5D7381AF + 4, // SHA1(ActivityTracker) v4
kTypeIdUserDataRecord = 0x615EDDD7 + 3, // SHA1(UserDataRecord) v3
kTypeIdGlobalLogMessage = 0x4CF434F9 + 1, // SHA1(GlobalLogMessage) v1
kTypeIdProcessDataRecord = kTypeIdUserDataRecord + 0x100,
跟踪进程生命周期的一个阶段,该阶段被定义为```
// The phases are generic and may have meaning to the tracker.
PROCESS_PHASE_UNKNOWN = 0,
PROCESS_LAUNCHED = 1,
PROCESS_LAUNCH_FAILED = 2,
PROCESS_EXITED_CLEANLY = 10,
PROCESS_EXITED_WITH_CODE = 11,
// Add here whatever is useful for analysis.
PROCESS_SHUTDOWN_STARTED = 100,
PROCESS_MAIN_LOOP_STARTED = 101,
在进程退出时尽快记录日志,保存有关模块的信息,也就是当某个模块(即 chromium 的一个组件)被加载时的信息;基本上功能相同,但针对不同的线程和类有所区别。如果你感兴趣,可以在 chromium/src/base/debug/activity_tracker.h 中找到它。
之后,它检查之前是否调用了“Main()”。这里我认为他们指的是之前是否调用了 ChromeMain()。他们基本上检查是否有任何命令被传递给内容进程,以及内容进程是否以任何参数被调用。如果有,他们就确保浏览器进程也会获得相同的参数。然后他们检查平台特定的内容,如果发现是 Windows,则通过执行 CreateATLModuleIfNeeded 来初始化他们自己的处理程序。接着又是同样的流程,检查平台特定内容,然后使用 SetupCRT 传递参数。接下来就到了我们初始化 IPC 的部分,以便在我们生成其他进程后能与它们通信。以下是负责此功能的代码。我们不会深入讨论它,因为稍后我们会回来更深入地讨论 chrome 的 IPC 机制。目前只需要知道,这是促进 chrome 多架构进程之间通信的机制。如果你不知道 IPC 代表什么,它就是进程间通信(inter process communication)。
.
然后,它所做的事情更多是“转发”参数,而不是为 UI 设置参数:使用 ui::RegisterPathProvider,调用跟踪器,并通过 content_main_runner->Initialize(std::move(params)) 获得结果,创建一个父控制台(我猜他们的意思是创建一个父进程),进行更多检查,然后我们进入重要部分,即
.
这里我们看到了对名为 IsSubprocess 的函数的调用。
其作用基本上是为了避免旧的样板代码:它在一个函数中检查从命令行接收到的进程类型,如果传入了某个选项,它会从以下选项中选择,并创建相应的进程。```
return type == switches::kGpuProcess ||
type == switches::kPpapiPluginProcess ||
type == switches::kRendererProcess ||
type == switches::kUtilityProcess || type == switches::kZygoteProcess;
从那里我们进入 content_main_runner->Run(); 这一步本质上完成了所有神奇的操作。让我们深入了解一下它的内部机制。显然,content_main_runner 是 ContentMainRunner 类的一个实例,可以在 `content\app\content_main_runner_impl.cc` 中找到。
为了真正理解这里的代码流程,我们必须从下往上看。话虽如此,我们再次往下滚动,发现实际上 `ContentMainRunner::Create()` 调用的是 `ContentMainRunnerImpl::Create()`;这让我们意识到,我们实际上必须查找 ContentMainRunnerImpl 的定义,而不是 ContentMainRunner,我们在上面提到的同一个文件夹中的 `content_main_runner_impl.h` 中找到了它。

我们看到它继承自 ContentMainRunner,并且可以看到它的核心方法。实际上这里内容不多,因为它的行为大部分在 `content_main_runner_impl.cc` 中被重写。
在 `content_main_runner_impl.cc` 内部,在 run 方法中,它首先使用 DCHECK(这到底是什么函数?(```The CHECK() macro will cause an immediate crash if its condition is not met. DCHECK() is like CHECK() but is only compiled in when DCHECK_IS_ON is true (debug builds and some bot configurations, but not end-user builds).``` 引自谷歌文档 :))进行一些检查,以查看 is_initialized、content_main_params_、is_shutdown_ 是否已设置。

之后,它获取参数并确定我前面提到的类型。
然后,如果找不到该类型,我们就调用 InitializeFieldTrialAndFeatureList() 和 delegate_->PostFieldTrialInitialization(); 并使用 mojo 将结果作为消息发布。
 。
接下来,它为 UI 设置一些东西。
同样基于命令行传入的内容,它调用 RegisterMainThreadFactories,后者是 RegisterUtilityMainThreadFactory 的包装器,而 RegisterUtilityMainThreadFactory 只设置 g_utility_main_thread_factory;从名字来看,我认为它会创建主线程,该线程就像是其他线程的监视者,并最终启动浏览器进程。
如果你不信任我,这里是 chromium 在 binja 中反汇编的结果,我们还会看到一些针对它的内存分析。
幸运的是,我们有 chome.exe.pdb,它让我们拥有调试符号,这样在逆向该二进制时就不会有任何痛苦。
由于我们研究的是 Windows 上的 chrome,程序的主入口是 wWinMain:
以下是函数流程图的样貌
。
这个函数本身非常庞大,因此我只展示其中必要的部分。在一堆初始化之后,我们到达调用 `MakeMainDllLoader()` 的地方
,关于这个函数我们之前已经解释过它的作用了。那么,我们如何动态调试 Chrome 并证明我上面提到的所有内容呢?首先在 windbg 中加载它,然后执行 lm 并查找可执行文件的名称。之后我们寻找名为 chrome!MakeMainDllLoader 的函数,设置断点,然后让执行继续。。我们进入其中 ,让它运行直到遇到 ret,退出该函数后,我们进入 chrome!wWinMain+0x764 。然后我们让它运行到 chrome!MainDllLoader::Launch,并单步进入。从那里,我们在 chrome!MainDllLoader::Load 处设置断点,以便观察 chrome.dll 是如何被加载到内存中的。正如我们所见,最先加载的内容之一就是 chrome.dll  从这里开始,分析流程是相同的,完整的内存分析将作为练习留给读者。出于特殊目的,我将把分析做到 content 进程几乎完成、浏览器进程即将开始的那个点,然后在那里停下来。这样做的目的只是为了展示 IPC 是如何开始相互通信的。现在,在加载 dll 之后,我们需要寻找一条 call rax 指令。我所做的是在加载 dll 后,从当前 eip 开始列出 0x40 条指令。。那个地址实际上正是我们正在寻找的目标,也就是 chrome!ChromeMain。
从那里,我们想在  处下断点,这基本上是在跳转到 content!content::ContentMain 之前的一个大检查。从那里,我们想越过几条指令并到达 content!content::ContentMain。我们想单步进入,并在 content!content::RunContentProcess 处下断点。我们想单步进入,并在 content!content::ContentMainRunnerImpl::Run+0x430 处设置断点;一旦我们单步越过 ,就会看到 mojo IPC 启动,于是我们得出结论:下一个进程,也就是浏览器进程,负责实际的浏览器实现以及对所有其他进程的处理。
===============================================================================================
2.
上次我们在理解了 content 进程之后停了下来。今天我们将研究 browser 进程。
它是在 content 进程之后启动的进程,也是 chrome 启动的进程中的第二个。
让我们简要描述一下它:
* 我们可以把它视为主进程,因为 content 进程更像是一个初始化例程和一些依赖检查,而且它是由 content 进程启动的。
* 它在浏览器的整个生命周期内保持存活。
* 它是所有进程的中央协调器,并以浏览器可用的最高权限级别运行。
* 由于它以最高权限级别运行,如果其他进程需要执行更高权限的操作,该请求将由 browser 进程处理。
* 它控制着地址栏、书签以及后退/前进/刷新按钮等功能。由于这是权限最高的进程,它不信任任何其他进程提供的数据。
* 它还会在必要时为其他进程处理特权操作,例如 UI、网络或文件系统存储。
现在我们简要了解了它的作用,是时候更深入地研究它了:
我们的旅程从 `src\content\app` 目录下名为 content_main_runner_impl.cc 的文件开始,具体在函数 int ContentMainRunnerImpl::RunBrowser(MainFunctionParams main_params,bool start_minimal_browser) 中,它执行的第一项操作,我们看到是 TRACE_EVENT_INSTANT0 。
那是什么?在此之前,trace 函数又到底是什么?如果我们访问 https://lwn.net/Articles/379903/ ,就会看到他们把它的定义展示为 。稍微抽象一下,我们可以得出一个结论:trace 函数是一种在栈帧中特定位置记录数据的函数。它不仅记录执行函数的调用栈,还能记录最近执行函数的局部变量。既然我们知道了 trace 函数是什么,让我们看看我们的这个函数做了什么。
如果我们访问 https://chromium.googlesource.com/chromium/src/base/trace_event/common/+/refs/heads/main/trace_event_common.h ,就会看到他们将其定义为一个唯一目的是“跟踪应用程序性能和资源使用情况”的函数。为了更深入地理解它的作用,我们看到它是一个宏,定义如下 。在宏上方我们可以看到一段简短的说明,它说该宏会立即记录一个名为“name”的单一事件,带有 0、1 或 2 个参数。如果所属事件的类别未启用,它就不会做任何事。我们可以看到,参数中“startup”是主类别,子系统是“ContentMainRunnerImpl::RunBrowser(begin)”,第三个参数是 TRACE_EVENT_SCOPE_THREAD,其定义为 "#define TRACE_EVENT_SCOPE_THREAD (static_cast<unsigned char>(2 << 2))",我认为基于这个值,它是相应瞬时事件的一个 id。所以本质上,它所做的事情仅仅是跟踪(记录)我们的执行已经进入 RunBrowser 函数这一事实。然后我们进行一次检查,看看浏览器主循环是否已经启动;如果已经启动,我们就退出该函数  。然后我们设置一个标志,之后到达代码中一个相当有趣的部分。我们检查是否支持 mojo_ipc 机制;如果支持,就使用 ShouldCreateFeatureList,它会为不同进程创建一个特性列表并尝试初始化它们,最后尝试初始化 mojo 特性。。然后我们创建一个线程池。 线程池到底是什么?!引用维基百科:“一种在计算机程序中实现并发执行的软件设计模式”。更准确地说,“线程池会维护多个线程,等待监督程序分配任务以进行并发执行”(这句也引自维基百科)。更形象地解释,想象工厂里有两条工人流水线。我们把它们称为 A 线和 B 线。它们都由一个老板监督,我们把他称为 C 线。B 线必须等待 A 线完成工作,并由 C 线通知后才能开始工作。A 线也是如此。这就是线程池。这里还有一个小型 C 示例 。这个示例直接取自 https://stackoverflow.com/questions/15752659/thread-pooling-in-c11 。接下来我们调用 PreBrowserMain(); 它执行一些特定平台的初始化;之后我们到达调用 `BrowserTaskExecutor::Create()` 的地方。
。让我们深入看看它到底做了什么,因为这个名字相当有趣,并且基于有根据的猜测,我们意识到这可能会很有趣。我们的绕行从 content/browser/scheduler/browser_task_executor.h 内部开始,在那里我们发现 BrowserTaskExecutor 是一个类,它的作用是“将 base::TaskTraits 映射到浏览器进程的实际任务队列”。沿着文件往下看一点,我们首先看到 ,它继承自 BaseBrowserTaskExecutor。因此,为了理解 BrowserTaskExecutor,我们需要理解 BaseBrowserTaskExecutor。我们可以看到它继承自 TaskExecutor。幸运的是,我们看到它用可覆盖属性重写了 TaskExecutor 的方法,这意味着它们稍后会被其他方法调用覆盖。但为了满足那些好奇的人,我们可以在 base/task/task_executor.h 中找到它;如果我们检查一下,就会发现 TaskExecutor 是一个“可以执行具有特定 TaskTraits 扩展 id 的任务”的类  。
什么是任务,什么是 TaskTraits?我们已经提到过什么是任务,但简单回顾一下:任务是 chrome 各进程中的一个执行单元。那么 TaskTraits 又是什么?它们位于 base/task/task_traits.h 中,定义如下:“封装有关任务的信息,帮助线程池做出更好的调度决策”。回到我们的 BrowserTaskExecutor。我们调用的方法是 Create(),它的分析如下 
首先检查当前任务是否需要从 SingleThreadTaskRunner 运行,也就是说,我们是否需要使用一个独立的线程来运行此任务。我们通过获取 tls 的指针来完成这一点。然后我们初始化一个 UI 和一个线程调度器。基本上,这里我们为未来与 UI 相关的事件初始化一个调度器。。
这个特性大概就是这样。继续分析 content_main_runner_impl.cc,我们来到了这里。
搜索 variations ids provider 类的文件,我们在 `components/variations/variations_ids_provider.h` 中找到了它,在那里我们看到了相当有趣的东西。它包含一个 `.mojom.h` 文件。我们可以在 `Debug/gen/components/variations/` variations.mojom.h 中找到它的定义,这意味着它与 ipc 机制有关。现在查看该类的实际定义,我们看到一条注释,内容为“一个辅助类,用于维护通过自定义 HTTP 请求头传输的客户端实验和指标状态”。查看它的行为和源码定义,我们得出结论:它只是用作一个函数的标记,该标记表示“提供给 GetClientDataHeaders() 的已登录参数”。这意味着在栈执行过程中的某个地方调用了 GetClientDataHeaders,稍后它将被带参数调用。然后我们执行 delegate_->PostEarlyInitialization(!!main_params.ui_task); 它只是向 mojo 的 ipc 发布消息,表明必须启动 ui 任务。我们做更多的初始化  ,然后调用 RunBrowserProcessMain。 。如果你和我一样希望在这里看到 GUI 启动,那你就错了。引用 master oogwgay 的一句话 。然后我们继续做更多检查,到达 BrowserMain。浏览一下它,它做了一些跟踪,然后我们到达 ,这才是我们真正启动 GUI 的地方。如果你不信任我,那你就要再耐心一点,直到我们进行动态分析。然后我们到达 Run 方法,这才是我们真正感兴趣的内容;接下来我们将进入理解 browser 进程的下一个要点。但现在,让我们再短暂绕行一下,研究 Initialize 方法是如何创建的。我们立刻看到它跟踪 init 方法的执行,在此之前它创建了一个直方图 。然后我们检查 initialization_started_ 标志,看看是否已经进入初始化阶段;如果没有,我们初始化 skia(chrome 使用的图形库),启动一个“计时器”来统计运行此方法所经过的秒数,以便稍后在直方图中使用,检查是否向二进制传递了等待调试器附加到进程的参数,最后启动一个 notification_service_;根据有根据的猜测,它会通知线程池的主监视者何时启动服务。。然后我们初始化 chrome 所需的字体,并创建主浏览器循环(mainbrowserloop),它是所有进程的协调器;然后我们跳过三个对我们来说无关紧要的方法: main_loop_->CreateStartupTasks();
int result_code = main_loop_->GetResultCode(); 。从这里开始,我们感兴趣的是进入 CreateStartupTasks。从那里我们查看 content\browser\browser_main_loop.cc,我们感兴趣的是 startup_task_runner_->RunAllTasksNow(); 它位于 createstartuptask 方法内部。starup_task_runner_ 是一个 StartupTaskRunner,它位于同一目录下的 startup_task_runner.cc 文件中。如果我们检查 RunAllTasksNow 方法,就会看到它所做的只是遍历所有任务并运行它们 
==========================================================================================
Time for sone dynamic analysis现在,为了捕获渲染器(renderer),我们需要在 windbg 中启动该二进制文件。这可以通过 File->Open Executable 并传入 --renderer-startup-dialog --no-sandbox --wait-for-debugger-children=renderer --renderer-process-limit=1 作为参数来实现。。然后我们在 content!content::StartupTaskRunner::RunAllTasksNow+0x88 处设置断点,这样我们就能捕获 IPC 消息,之后是渲染器。作为一个参考点,这是你运行一次后 dbg 应该看起来的样子  。你应该会看到 chrome is running in full browser mode 的消息。从那里开始,你从 1 数到 5,也就是再运行四次。然后你应该会看到类似这样的东西 。我建议使用 procmon 来监控渲染器进程的启动。之后你把它挂接到另一个调试器上,如果你使用了上面的参数,你应该会看到一个消息框弹出,告诉你渲染器的 pid 。从那里,你会想在 base!base::RunLoop::Run 处设置断点。不幸的是,由于某些原因,你无法在 content!content::RendererMain 处断下。也许是因为我们在进程刚退出 RendererMain 函数后就挂接了它,而且它是由一个线程运行的。不管怎样,这是四次运行后的 IPC 样子。。这表示 GUI 已启动。这是渲染器进程启动后的 IPC 样子。。然后我们在
content!content::StartupTaskRunner::RunAllTasksNow+0x88 和 content!content::RunOtherNamedProcessTypeMain 处设置断点,让它运行。
============================================================================================================
3. 现在,对于浏览器进程分析的第二部分,我们已经到了能够理解和调试渲染器的地步,但我们暂时还不会深入那里。我特意留下了一个在 RunBrowserProcessMain 之后需要分析的函数,严格来说它是在 RunBrowser 之后,但你可能还记得 RunBrowser 是 RunBrowserProcessMain 的包装器。我留出来的是:在 src/content/app 文件夹中的 content_main_runner_impl.cc 文件里,还有一个在我们生成渲染器之后被调用的函数,名为 RunOtherNamedProcessTypeMain。这个进程负责运行所有其他进程。。现在让我们理解一下代码中发生了什么。我们看到它的原型是 ,这表示它将接收传递给命令行的参数、期望的进程类型以及一个 chrome delegate。然后我们来到函数的开头,这里有一个宏定义,用于检查平台特性,并检查要实例化哪个进程的事件处理程序,也就是要查看是为控制台进程运行某些函数,还是为浏览器进程运行函数。 。然后我们遍历传入的进程,与已知进程列表进行比较,并运行相应的进程。  如果我们没有匹配到相应的进程,那么它就是一个由某人实现的自定义进程。
这里有一个实现自定义进程的链接,我们将在动态分析的第二部分使用这个示例。 https://bitbucket.org/chromiumembedded/cef/wiki/Tutorial 。
=====================================================================
动态分析部分
动态分析的第一部分如下:首先,在具有管理员权限的 cmd.exe 中运行以下命令:windbg.exe chrome.exe -G -o --renderer-startup-dialog --no-sandbox --wait-for-debugger-children=renderer --renderer-process-limit=1 --allow-pre-commit-input --allow-sandbox-debugging 。之后设置 .childdbg 1,这样我们就可以调试新生成的子进程。本质上,上面的命令所做的事情是“确保你附加到所有子进程”。感谢 @spoofyroot 和 @_coreDump 为我指出了多进程调试的正确方向。设置 .childdbg 1 之后,它应该看起来像这样  由于我无法用其他方法捕获接下来发生的事情,我做了一个视频,我会在其中解释接下来会发生什么。 https://streamable.com/9t4iof 。基本上,在我们最后一次运行 content!content::StartupTaskRunner::RunAllTasksNow+0x88 之后,我们会生成一些处理 IPC 相关事务的新进程,并且我们必须继续在 content!content::RunContentProcess 处设置断点,直到它命中。这就是我所说的有根据的试错:)) 。
=====================================================================
4. 渲染器分析
!免责声明
在我们对渲染器代码进行分析的同时,我们也会稍微深入一下 blink 代码,以便正确理解渲染器中发生的事情。
在编写这部分课程时,我意识到我忘了简要说明这是做什么的,以及我们为什么要研究它。我们知道这是 Chromium 启动的第三个进程,它被称为渲染器。但为什么叫“渲染器”呢?之所以这样叫,是因为它的工作是渲染(绘制)我们在网站上看到的一切。基本上,这就是为什么当你访问一个网站时,你的表格看起来像表格,或者你的 CSS 能够自定义一段文本,又或者你的 JS 能够施展黑魔法*。我们分析它的另一个重要原因是,大多数 bug 都来自这里。无论是 HTML、CSS、JS 还是任何其他组件,你都会在 chrome bugs.chromium.org 上看到它们被归在 blink 下。
* 渲染器进程不止一个。浏览器当前打开的每个标签页都有一个完全独立的进程。
* 此进程控制实际网站标签页内的所有内容
* 从 2018 年开始,iframe 获得了一项升级,它们都可以拥有标签页。因此,iframe 的每个标签页都有独立的渲染器进程。这被称为站点隔离(Site-Isolation)。
* 它的职责是解析网站,并在屏幕上绘制网站内部的内容,例如表格、图像,以及执行 JavaScript。
* 它被沙箱化(sandboxed)。
* 其核心使用名为 Blink 的渲染引擎。
* 创建并处理诸如 chrome://、devtools://、chrome-error:// 之类的 URL 方案。
* 初始化 Blink 引擎,该引擎将实际完成渲染器的所有解析和繁重工作。
上次我们讲到完成了浏览器进程分析,现在是我们一直等待的时刻:进入渲染器分析部分。对了,我在摆弄 Chromium 时曾让它崩溃,并得到了以下堆栈跟踪。  基于这个,我们知道我们的旅程从 content::RendererMain 开始,它位于 `\content\renderer\renderer_main.cc` 中。你猜对了,起点就是 RendererMain。让我们从分析它的定义以及其中的内容开始  。我们可以看到它接受一个 MainFunctionParams 类型的参数,这意味着这个函数将接受传递给二进制文件的参数。接下来,我们添加一个跟踪点,以便知道我们已经调用了 RendererMain,然后解引用 parameters 的值并将其保存到 command_line 中。然后我们有一些用于检查特定平台架构的宏,这些可以忽略 ,我们检查传递给 kTimeZoneForTesting 的值,它是一个用于测试的时区,然后我们遇到了一个名为 icu 的新类和数据类型。

现在,为了寻找它的定义来源,我们来到了 https://unicode-org.github.io/icu-docs 。如果我们查看文件开头包含指令的位置,可以看到它是一个第三方库。所以到目前为止,我们知道它是一个处理 Unicode 国际化组件的第三方库,也就是我们用它来支持 Unicode。好吧,但那个函数是做什么的?查看 https://unicode-org.github.io/icu-docs 我们看到了 ,也就是根据我们设置的参数来设置默认时区。然后我们初始化 skia 库,并处理 --renderer-startup-dialog
。然后我们又遇到了一个新类 RendererMainPlatformDelegate。
这是做什么的?首先我们必须说明,这是一个抽象类,并且是平台相关的。在我们的案例中,它位于 /content/renderer 目录下的 renderer_main_platform_delegate_win.cc 文件中。它看起来像这样
。因此我们可以得出结论,它只是一个辅助函数,在我们传入 --no-sandbox 时启用沙箱并执行必要的操作,仅此而已。然后我们将线程的名称设置为 CrRendererMain。接着我们又遇到了另一个新类,名为 RenderThread。 
再次强调,由于我们基本不知道它是做什么的,让我们稍微探索一下它。我们理解 RendererThread 面貌的探索之旅始于 content/public/renderer/render_thread.h。查看 render_thread.h 文件内部,我们看到它的定义是 。在整个文件中,我们感兴趣的是 IsMainThread,它定义在 rendere_thread.cc 文件中,看起来像这样  (待补充:关于此机制的详细描述)。现在继续对渲染器代码进行分析,我们看到了期待已久的时刻:我们终于看到了一些 blink 代码。这是第一次出现,它的作用是初始化库。。在幕后,这个函数看起来像这样 。现在自然会出现的问题是:这些类到底是什么?WTF 和 Platform 是什么?幸运的是,blink 的文档实际上告诉了我们这些是什么。
https://docs.google.com/document/d/1aitSOucL0VHZa9Z2vbRJSyAIsAz24kX8LFByQ5xQnUg/edit 。查看“Directory structure and dependencies”,我们可以推断出 platform 是一个帮助处理几何和图形的类。现在来看 WTF 和 Partitions 类。Blink 文档还这样描述 WTF:它来自 Web Template Framework,在很大程度上是一个 STL 库的“包装器”,正如“是 Blink 的基础库,提供各种基本功能,如容器、字符串库、引用计数机制、函数对象、线程原语等”(引自 blink 文档(https://chromium.googlesource.com/chromium/src/+/refs/heads/main/third_party/blink/renderer/platform/wtf/README.md))。再补充一些关于 blink 的细节。
好的,现在继续分析 renderer_main.cc,我们又看到了一些不知道其作用的东西,那就是  (明天再补充细节)。然后我们调用 。接着我们检查编译时是否启用了插件,如果启用了,就加载它们。
 然后我们还会进行一些其他检查,我决定跳过这些不解释,因为解释起来会很长,而且这个阶段没有必要。但用 TL;DR 版本来概括,就是决定在 RenderProcess 初始化之前是否启用沙箱。然后我们终于到达 。(待补充:关于其余函数的其余细节)。虽然这还不是渲染器分析的终点,但我们可以将其视为实际渲染器分析的起点,因为正如我们稍后将看到的,这里发生了大多数有趣的事情,所以我们可以将它视为渲染器的核心代码。我们感兴趣的是 RenderThreadImpl,它看起来像 。在所有这些代码中,我们感兴趣的是 Init() 函数,它看起来像这样:
,但它要长得多。:) 不幸的是,我们无法在一张图片中捕获它,所以我们只捕获了它的开头。除此之外,从 InitializeWebKit() 之后发生的其他事情并不是我们真正感兴趣的,因为其中大部分只是与 GPU 进程的 IPC 通信,而 GPU 进程目前不在我们的关注范围内。幸运的是,在那个函数中,我们也捕获到了我们感兴趣的函数之一,正是 InitializeWebKit(),它再次看起来像 。为了更好理解 InitializeWebKit 内部发生的事情,我们稍微偏离一下解释 renderer_main.cc 的主线。我知道这需要理解很多东西,但请耐心听我讲,我们试图理清这一团乱麻。所以我们看到 InitializeWebKit() 首先获取传递给该进程的任何参数,然后我们检查编译时是否启用了 -dENABLE_VTUNE_JIT_INTERFACE,如果启用了,我们还会检查命令行中是否传入了 enable-vtune-support 选项。这到底是什么?我们在谷歌上搜索,找到了一个链接 https://www.intel.com/content/www/us/en/develop/documentation/vtune-help/top.html ,该页面上说它是:“用于串行和多线程应用程序的性能分析工具”。所以简而言之,就是能提升 Chrome 性能的东西。然后我们初始化 blink 。为了理解 blink 初始化的过程,我们简要解释一下,因为我们将在下一章中深入探讨 blink 的细节。我们又遇到了一个陌生的类,那就是 RendererBlinkPlatformImpl,它看起来像
 ,可以在
content/renderer/renderer_blink_platform_impl.cc 找到。根据名称,我们可以推断它是一个基于平台实现的抽象类,我们还可以看到它继承自 。查看 RendererBlinkPlatformImpl 实际做的事情,我们看到它检查基于平台的特性,然后根据检查结果标记一个标志,,接着通过获取 TLS 指针来检查当前线程是否为 RendererThread。
等等等等,看看是否还要补充一些关于这些类的内容。然后我们来到了一个我们可能都在等待的东西:第一段 V8 代码。它是 。这是做什么的?首先,我们从文档中知道,v8::isolate 是 V8 引擎的一个实例。也就是(V8 运行时的一个独立副本,包括堆管理器、垃圾回收器等),但不足以运行脚本。我们看到 blink::MainThreadIsolate() 的定义是
 ,它位于文件 blink/renderer/platform/bindings/v8_per_isolate_data.cc 中,而 V8PerIsolateData::MainThreadIsolate() 看起来像
 ,V8PerIsolateData 呢,你猜对了,它也是一个类,位于 bindings/core/v8/V8PerIsolateData.h 中,大致看起来像  等等等等,待补充细节。然后我们检查是否向命令行传递了 kDisableThreadedCompositing(明天再补充这个标志到底长什么样)。如果没有传递,我们就启动一个合成器(compositor)线程。这到底是什么?引用自 https://frontendmasters.com/courses/web-performance/the-compositor-thread/ ,它是一个“唯一的任务是绘制位图、获取位图、将其发送到 GPU、再放到屏幕上”的线程。然后我们注册所谓的 scheme。 基本上,你还记得在查看源代码时,URL 前面会有一些东西,比如:“view-source:website” 。是的,那实际上就是由渲染器处理的。而且还有更多这样的 scheme。他们说的“注册”是什么意思?我还不确定,但我觉得他们的意思是:嘿,当用户来做这件事时,我希望你来处理它。总之,看起来就是这样。 待补充更多细节。好的,接下来我要详细说明最后一段代码 
. 巴拉巴拉巴拉,最后我们来到了 renderer_main 的最终部分,待更新细节

=====================================================================
动态分析部分现在,为了能够动态调试它,如果你和我一样是个菜鸟,你会想从 cmd.exe 运行 windbg,如下所示:windbg.exe chrome.exe -G -o --renderer-startup-dialog --no-sandbox --wait-for-debugger-children=renderer --renderer-process-limit=1 --allow-pre-commit-input --allow-sandbox-debugging --time-zone-for-testing="US/Pacific",并设置 .childdbg 1 以便能够调试生成的子进程。接下来,你需要对 content!content::RendererMain 下断点,并让它运行大约 5 或 6 次。(要修改一下,对视频做第 4 点的引用,因为这样更清楚)之后我们就到了 。






--no-sandbox
chrome_mainstd::unique_ptr<>
crashes.InstallDetails::Get().VersionMismatch()


/src/content/app/content_main.cc