How does WebADB work (part 1) - Preparation

Following the overview, in this post I want to talk about:

  1. Tools I used
  2. Some research I did before starting coding

Tools

Hack ADB

After a quick scan of the code, I discovered some features that can make my work easier.

ADB_TRACE environment variable

ADB can dump all messages to the terminal, so I can gather real data to test against my implementation.

Set ADB_TRACE environment variable to all enables all debug output, including message dumps.

Run ADB server manually

Set ADB_TRACE alone is not enough. ADB has three parts: ADB daemon, ADB server and ADB client.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+-----------------------------------+
| |
| +----------+ +--------+ |
| | <---------+ | | +--------------+
| | Client 1 | | | | | |
| | +---------> | | | +--------+ |
| +----------+ | +-------------> | |
| | Server | | | | Daemon | |
| +----------+ | <-------------+ | |
| | +---------> | | | +--------+ |
| | Client 2 | | | | | |
| | <---------+ | | | Device |
| +----------+ +--------+ | | |
| | +--------------+
| Computer |
| |
+-----------------------------------+

Normally, running an adb command will start a ADB client. The client will try to connect to a server running locally, and if there isn’t one, spawn a new one in background.

The problem is that what I want to do is dumping messages between server and daemon, and since the server was started by client in background, there is no way to access its output.

So we need a way to start a server manually. --help message isn’t always that helpful, however I found the answer from source code:

1
adb server nodaemon

ADB_EMU environment variable

Upon launching, ADB server will scan through TCP ports 5554 to 5570 on localhost to detect running Android emulators.

With ADB_TRACE=all set, this process results in dozens of connection error messages.

Set ADB_EMU=0 disables emulator detection entirely. While set ADB_EMU={port} let ADB to only search for emulators at {port}.

Logs are … truncated …

After setting these environment variables, ADB now starts outputting messages to the terminal.

1
adb.exe D 09-28 13:34:34 15484  2084 transport.cpp:687] 5541494e4c583398: from remote: [AUTH] arg0=1 arg1=0 (len=20) 8f402a7b9b59006202c4049f8066f041 .@*{.Y.b.....f.A [truncated]

However, ADB only prints first 16 bytes, that’s not very useful if we want to see how the protocol work.

Search for code

A quick search leads to adb_utils.cpp#L162

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
std::string dump_hex(const void* data, size_t byte_count) {
size_t truncate_len = 16;
bool truncated = false;
if (byte_count > truncate_len) {
byte_count = truncate_len;
truncated = true;
}

const uint8_t* p = reinterpret_cast<const uint8_t*>(data);

std::string line;
for (size_t i = 0; i < byte_count; ++i) {
android::base::StringAppendF(&line, "%02x", p[i]);
}
line.push_back(' ');

for (size_t i = 0; i < byte_count; ++i) {
int ch = p[i];
line.push_back(isprint(ch) ? ch : '.');
}

if (truncated) {
line += " [truncated]";
}

return line;
}

Sadly the limit is hard-coded and I can’t modify the source code (that requires a rebuild).

Evil method

So I got my Ghidra out. It’s one of the most powerful disassembly tools.

Search for some keywords.

Search -> Program Text -> " [truncated]"

Search result

Double click on XREF to jump to usage.

The “Decompile” view shows the reconstructed C code from assembly (I added some blank lines and comments).

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
// std::string dump_hex(uint8_t* data, uint32_t data_length)
// `param1` is the return value pointer.
// I guess the compiler use this pattern to "move" the result instead of copying.
undefined4 * FUN_00428410(undefined4 *param_1,int param_2,uint param_3)
{
int iVar1;
byte bVar2;
uint uVar3;
uint uVar4;

// std::string line;
param_1[1] = 0;
*param_1 = 0;
param_1[2] = 0;

// uint32_t byte_count = 16;
uVar3 = 0x10;
// if (data_length < 17)
if (param_3 < 0x11) {
// byte_count = data_length;
uVar3 = param_3;
}

// if (byte_count == 0)
if (uVar3 == 0) {
// line.push_back(' ');
FUN_004ad5b0(0x20);
}
else {
// uint32_t i = 0;
uVar4 = 0;
do {
// android::base::StringAppendF(&line, "%02x", data[i]);
FUN_0045ab70(param_1,&DAT_005d173c,(uint)*(byte *)(param_2 + uVar4));
// i++;
uVar4 = uVar4 + 1;
// while (i < byte_count);
} while (uVar4 < uVar3);

// line.push_back(' ');
FUN_004ad5b0(0x20);

// i = 0;
uVar4 = 0;
do {
// uint8_t ch = data[i];
bVar2 = *(byte *)(param_2 + uVar4);
// auto flag = isprint(ch);
iVar1 = isprint((uint)bVar2);
// if (!flag)
if (iVar1 == 0) {
// ch = '.';
bVar2 = 0x2e;
}
// line.push_back(ch);
FUN_004ad5b0((int)(char)bVar2);
// i++;
uVar4 = uVar4 + 1;
// while (i < byte_count);
} while (uVar4 < uVar3);
}

// if (data_length > 16)
if (0x10 < param_3) {
// line += " [truncated]";
FUN_004ad4c0(" [truncated]");
}

// return line;
return param_1;
}

Clearly the target is removing checks at line 19 and 64.

Line 19 is a CMOVBE (Conditional MOVe if Below or Equal), change to a MOV (pad with a NOP).

line 19

line 19 after

Line 64 is a JBE (Jump if Below or Equal), change to a JMP.

line 64

line 64 after

All done. Use File -> Export Program to save a copy, name it adb.2.exe.

Done

Execute, now it correctly dumps the entire message, so I can observe the compare my implementation with these real messages.

Output of adb.2.exe

What’s next

Next time I will start talking about the ADB protocol, and my implementation.