通过netlinker和svc获取mac地址解析

通过netlinker和svc获取mac地址解析

参考🔗:参考:https://bbs.kanxue.com/thread-271698.htm

Unix/Linux编程:Netlink机制_unix netlink.h-CSDN博客

杂谈

太久没更新博客了,上年纪了(bushi)感觉又想多记录下自己平时的学习过程,最近也找到实习了,感觉自己在业务方面还有很多要进步的地方(他们怎么都那么厉害TT),菜就多多多多练,以此共勉,希望能持续更下去

免责声明:主要是记录下自己学习的思路,很乱很杂()仅自我记录(别喷我别喷我,疯狂叠甲)

导语以及系统相关API函数介绍

在获取指纹时,我们传统的获取方法是直接使用与安卓系统的属性系统交互的API,例如system_property_get , system_property_find , system_property_read。这些函数用于获取、查找和读取设备的系统属性值。
Android 系统属性系统是一个键值对存储,允许开发者在应用或系统级别获取和设置设备的一些配置信息,如设备的 ID、版本号、型号等。这个系统属性数据库类似于 Linux 系统的 /proc 文件系统,但它更加高效并且适用于 Android 的需求。

system_property_get:

system_property_get 是最常用的一个函数,它用于获取一个系统属性的值

1
int system_property_get(const char *name, char *value);
  • **name**:要获取的系统属性的名称。
  • **value**:系统属性的值,将会存储在这个字符数组中。
  • 返回值:返回属性的长度(包括终止符)。

函数原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>
#include <cutils/properties.h>

int main() {
char value[256];
// 获取系统属性 "ro.product.model"(设备型号)
if (system_property_get("ro.product.model", value)) {
printf("Device model: %s\n", value);
} else {
printf("Failed to get system property.\n");
}
return 0;
}

这段代码使用 system_property_get 获取设备的型号(ro.product.model),并打印出该值

system_property_fin

system_property_find 用于查找一个系统属性是否存在。与 system_property_get 不同的是,system_property_find 不会直接获取值,它只是检查属性是否存在。

函数原型:

1
2

const PropInfo* system_property_find(const char *name);
  • **name**:要查找的系统属性的名称。
  • 返回值:返回 PropInfo 结构体指针,如果属性存在,返回指向该属性的指针;如果不存在,返回 NULL

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

#include <stdio.h>
#include <stdlib.h>
#include <cutils/properties.h>

int main() {
const PropInfo *prop_info;

// 查找系统属性 "ro.build.version.release"(Android版本)
prop_info = system_property_find("ro.build.version.release");
if (prop_info) {
printf("Property exists!\n");
} else {
printf("Property not found.\n");
}
return 0;
}

  • 解释:这段代码通过 system_property_find 检查是否存在系统属性 ro.build.version.release,该属性存储了设备的 Android 版本。

system_property_rea

system_property_read 是另一个获取系统属性的函数,它与 system_property_get 类似,但是在返回时,它提供了更详细的错误信息。它读取属性的值,并且返回成功与否。

函数原型:

1
2

int system_property_read(const char *name, char *value)
  • **name**:要读取的系统属性名称。
  • **value**:属性的值将存储在此字符数组中。
  • 返回值:返回值为 0 表示失败,非 0 表示成功。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

#include <stdio.h>
#include <stdlib.h>
#include <cutils/properties.h>

int main() {
char value[256];

// 读取系统属性 "ro.product.manufacturer"(厂商)
if (system_property_read("ro.product.manufacturer", value)) {
printf("Manufacturer: %s\n", value);
} else {
printf("Failed to read system property.\n");
}
return 0;
}

  • 解释:这段代码读取 ro.product.manufacturer 属性,这个属性表示设备的制造商,并打印出它的值。

这三个函数都是与 Android 系统属性相关的 API,用于获取、查找和读取系统属性值。

  • **system_property_get**:用来获取指定系统属性的值。
  • **system_property_find**:用来查找指定的系统属性是否存在。
  • **system_property_read**:读取指定系统属性的值,返回成功或失败。

然而除了刚刚上面的传统的直接读取系统属性的方法(存在弊端,高版本Android限制普通App访问敏感属性),我们还有

  • **读取/proc/sys/kernel/random/boot_id**:通过文件I/O获取设备标识。
  • 反射mValues映射:反射修改SystemProperties的私有变量,需突破反射限制。

但是这样的方法也存在弊端,比如需声明权限(如READ_PHONE_STATE ),易被Xposed/Frida等框架Hook拦截,改机软件可伪造返回值等

而本文则通过netlinker(内核级别)来演示如何通过与内核交互并通过套接字发送请求和接收响应。相比于Android应用层(如Java层或Native层)获取信息,Netlink通信的过程是更加底层和透明的,绕过了Android的框架层。内核数据访问不依赖于用户权限:Netlink允许直接从内核获取硬件信息(如MAC地址),而无需通过Android权限系统的层级保护。也就是说,改机软件无法像在应用层通过修改Android权限或模拟权限来伪造或篡改信息,因为Netlink通信不依赖于这些权限。

Netlink是一种Linux内核与用户空间通信的机制,允许用户空间程序与内核交互。

通过Netlink方式与Linux内核通信,可以直接查询网卡硬件信息,包括设备的MAC地址

改机软件通常是通过在用户空间(应用程序层)进行数据篡改的,或者通过动态库修改来影响系统调用(如recvrecvfrom等)。这些修改会依赖于在应用层或库层的访问权限进行实现

即使APP未被授予网络权限或其他系统权限,也可直接通过Netlink通信访问内核网络接口信息。这种方式访问的是设备底层真实信息,绕过了Android框架层面可能做的伪装或干扰,几乎无法被Mock或模拟伪造,因此非常适合大厂APP做设备唯一标识(设备指纹)的用途。

设备指纹的唯一性与不可伪造性

  • 获取MAC地址作为设备唯一标识的一部分,具有很强的不可伪造性。即使设备发生了修改(如刷机、虚拟化、模拟等),通常很难改变网卡硬件的MAC地址。而通过Netlink直接获取的MAC地址来自硬件层,改机软件通常难以模拟真实硬件的MAC地址。
  • 改机软件的目标是通过修改Android系统或应用层的标识(如Android ID、IMEI等)来伪装设备。但是,Netlink方式直接从内核获取MAC地址,这种方法直接触及硬件信息,几乎不可能通过普通的软件手段进行修改或伪造。

这里复习一下进程间通信方式:

https://www.bilibili.com/video/BV1tv411p7WX/?spm_id_from=333.337.search-card.all.click&vd_source=d7f903c8e55e49011126ea9ac27a3d31

进程间通信方式

用户空间进程间通信通常有更常用、更简单的机制 (如管道、消息队列、共享内存、Unix Domain Socket、TCP/UDP Socket 等)。因此,Netlink 一般不被用于普通的进程间通信。只有当需要利用 Netlink 特有的 广播 (Broadcast) 或 组播 (Multicast) 特性时 (一个进程发送消息,多个监听该 Netlink 协议/组的进程都能收到),才会考虑在用户态进程间使用 Netlink。

Netlink机制是一种特殊的socket:

  • 解释: Netlink 的实现基础是 BSD Socket API。这意味着用户态程序使用 Netlink 的方式与使用 TCP 或 UDP Socket 非常相似:**socket()**, bind()sendmsg()recvmsg()close() 等系统调用。
  • 关键区别: 它是“特殊”的,因为它使用的不是 AF_INET 或 AF_UNIX 这样的地址族,而是专门为内核通信设计的 AF_NETLINK 地址族。

因为 Linux 把 Netlink 当作一种特殊的「协议族」(Protocol Family)来实现,就像 AF_INET(IPv4)、AF_INET6(IPv6)一样,对应的就是 AF_NETLINK;而在内核角度,又把它映射到标准的套接字层(Socket Layer)。

因此,从用户态的视角看,Netlink 就和 TCP/UDP 之类的「套接字协议」没什么区别:

  1. socket(AF_NETLINK, SOCK_DGRAM, protocol)
    ↳ 创建一个 Netlink 套接字,protocol 指定子协议(路由表、链接状态、SELinux 通知等)。

  2. bind()
    ↳ 绑定到某个 Netlink 组(group)或进程 ID,以接收内核发来的消息。

  3. sendmsg() / recvmsg()
    ↳ 发送/接收 Netlink 消息。你把自定义的 nlmsghdr + payload 塞进去,内核那头就能正确解析,反过来内核发出的消息也会执行相同的封包/解包流程。

  4. close()
    ↳ 关闭套接字,内核释放相关资源。

异步通信机制:

  • 核心特性: 这是 Netlink 区别于 ioctl 和 procfs/sysfs 读写的关键点。
  • 运作方式: 当消息通过 Netlink Socket 发送时,它首先被放入接收端的 Socket 接收缓冲区 中排队。接收端 (无论是内核还是用户态进程) 不会立即被阻塞或中断 来处理这条消息。接收端会在它下一次主动去检查 (例如,调用 recvmsg() 系统调用) 其 Socket 接收缓冲区时,才会获取并处理这些消息。
  • 优势:
    • 发送方非阻塞: 发送方发送消息后通常可以继续执行,无需等待接收方处理完毕 (除非缓冲区满导致阻塞)。
    • 接收方控制节奏: 接收方可以以自己的节奏处理消息,不会被突如其来的消息强制打断当前工作。
    • 解耦: 生产者和消费者在时间上解耦。
  • 对比同步机制 (如 ioctl / 系统调用):
    • 同步: 调用 ioctl() 或进行 procfs/sysfs 文件读写 (read()/write()) 时,**用户进程会一直阻塞 (等待)**,直到内核完成该操作并将结果返回。内核处理是立即发生的、是调用的一部分。用户进程必须等待结果。
  1. 用户空间和内核空间的通信方式有三种:proc, ioctl, Netlink:
    • 解释: 这句话列举了 Linux 中用户态与内核态进行数据交换的三种主要传统机制 (现代还有 sysfs, configfs, debugfs, perf_event, bpf_map 等更多方式):
      • proc (/proc 文件系统): 一个虚拟文件系统,内核通过文件的形式暴露信息 (如 /proc/cpuinfo/proc/meminfo/proc/net/route)。用户进程通过标准文件 I/O (open(), read(), write(), close()) 访问。主要用于内核向用户空间输出信息,部分文件可写用于配置。本质是同步的 (read/write 调用会阻塞)。
      • ioctl: 一个系统调用 (int ioctl(int fd, unsigned long request, ...);),用于对已打开的文件描述符所代表的设备或对象进行特定于该设备的控制操作。这是驱动程序与用户空间交互的常用方式 (如设置串口波特率、控制网卡模式)。它是同步的、命令-响应模式的。
      • Netlink: 如前所述,基于 Socket 的异步双向通信机制。
    • 注意: 虽然 proc 和 ioctl 在主要用途上常常表现为单向 (proc 侧重读,**ioctl** 侧重写/控制),但技术上:
      • **proc**: 很多文件是只读的 (内核->用户),但部分文件可写 (用户->内核)。
      • ioctl: 主要用途是用户向设备发送命令 (request) 和可能的参数 (->内核),同时也能通过其参数或返回值从内核获取信息 (<-内核)。所以 ioctl 本身也是双向的,但它是同步的、命令驱动的。Netlink 的“双工”更强调的是基于消息流的、非阻塞的双向通信能力。
  2. Netlink可以实现双工通信:
    • 解释: “双工通信”意味着数据可以在两个方向上独立地、同时地流动。
    • 在 Netlink 中的体现: 建立一个 Netlink Socket 连接后:
      • 用户空间进程可以主动发送消息给内核模块。
      • 内核模块也可以主动发送消息给用户空间进程 (例如,异步通知事件的发生,如网卡状态变化、路由表更新、热插拔事件)。
      • 这种消息的发送和接收是独立的,不需要像 ioctl 那样严格遵循“请求-响应”模式。内核可以主动推送消息给监听者。这是 Netlink 相对于 proc (主要读) 和 ioctl (主要是命令/请求) 在通信模式上的重大优势。

异步通信机制:

  • 核心特性: 这是 Netlink 区别于 ioctl 和 procfs/sysfs 读写的关键点。
  • 运作方式: 当消息通过 Netlink Socket 发送时,它首先被放入接收端的 Socket 接收缓冲区 中排队。接收端 (无论是内核还是用户态进程) 不会立即被阻塞或中断 来处理这条消息。接收端会在它下一次主动去检查 (例如,调用 recvmsg() 系统调用) 其 Socket 接收缓冲区时,才会获取并处理这些消息。
  • 优势:
    • 发送方非阻塞: 发送方发送消息后通常可以继续执行,无需等待接收方处理完毕 (除非缓冲区满导致阻塞)。
    • 接收方控制节奏: 接收方可以以自己的节奏处理消息,不会被突如其来的消息强制打断当前工作。
    • 解耦: 生产者和消费者在时间上解耦。
  1. Netlink协议基于BSD socket和AF_NETLINK地址簇(address family):
    • BSD Socket: 指符合 Berkeley Software Distribution (BSD) 操作系统定义的 Socket 编程接口标准。Linux 实现了这套广泛使用的标准 API。Netlink 复用这套成熟的 API,降低了用户态开发者的学习成本。
    • AF_NETLINK地址簇: 这是定义 Netlink 类型的关键。创建 Netlink Socket 时,必须指定 domain/address family 参数为 AF_NETLINK (或等价常量 PF_NETLINK)。这告诉内核:“我要创建的是一个用于 Netlink IPC 的 Socket,而不是用于网络 (AF_INET/AF_INET6) 或本地 (AF_UNIX) 通信的 Socket”。
  2. 使用32位的端口号寻址(以前称为PID):
    • 寻址机制: 为了标识通信的端点,Netlink 使用一个 32 位的无符号整数作为**端口号 (Port ID)**。
    • 历史名称 (PID): 在早期实现中,内核通常将发送消息的用户态进程的进程 ID (PID) 自动用作其 Netlink 消息的源端口号。因此,这个标识符常被称为 PID。现在更准确的术语是 Port ID 或 nl_pid。
    • 当前用法:
      • 用户态: 用户态进程在 bind() 其 Netlink Socket 时,需要指定一个 nl_pid (端口号)。这通常是该进程自己选择的唯一标识符 (常用进程 PID,但不是必须的)。它标识了这个用户态 Socket 端点。
      • 内核态: 内核端的 Netlink 端点通常固定使用 Port ID **0**。
    • 作用: Port ID 用于确保消息被路由到正确的目的地 Socket。内核在发送消息给用户态时,会指定目标 Port ID。用户态在发送消息给内核时,内核知道来源 Port ID,以便回复。
  3. 每个Netlink协议(或称作总线,man手册中则称之为netlink family):
    • 概念: AF_NETLINK 是一个大的地址族,其下细分为多个具体的 Netlink 协议族 或 Netlink 总线。你可以把它们想象成不同的“频道”或“线路”。
    • 标识: 创建 Socket (socket()) 时,除了指定 **AF_NETLINK**,还需要指定具体的协议族 protocol (如 NETLINK_ROUTENETLINK_KOBJECT_UEVENT)。
    • 作用: 不同的协议族用于隔离不同的通信主题和目的。内核中不同的子系统会注册处理特定 Netlink 协议族上的消息。这避免了所有内核通信都挤在一个通道上,提高了效率和安全性。
  4. 通常与一个或者一组内核服务/组件相关联:
    • 解释: 每个预定义的 Netlink 协议族 (NETLINK_XXX) 都被设计用来服务于特定的内核功能模块或子系统。
    • 例子:
      • NETLINK_ROUTE: 关联内核网络子系统。 用于获取和设置 路由表、网络接口 (链路) 信息 (IP 地址、状态、MTU 等)、邻居表 (ARP/NDISC)、流量控制 (QoS)、网络命名空间 等。iproute2 工具包 (ipsstc 命令) 主要使用此协议族与内核通信。
      • **NETLINK_KOBJECT_UEVENT**: 关联内核设备模型和热插拔机制。 内核通过此协议族向用户空间的 udev 守护进程发送 设备事件通知 (如 USB 设备插入/拔出、磁盘添加/移除、设备状态变化)。udev 监听此总线并根据事件动态管理 /dev 下的设备节点和规则。
    • 其他常见协议族举例:
      • NETLINK_GENERIC: 一个更通用的框架,允许内核模块动态定义自己的子协议 (属性),无需申请新的顶层协议号。非常灵活,许多较新的内核功能通过它暴露 (genetlink)。
      • NETLINK_SOCK_DIAG: 用于查询套接字诊断信息 (ss 命令使用)。
      • **NETLINK_NETFILTER**: 用于与 Netfilter 子系统 (iptables/nftables 底层) 通信,配置防火墙规则、获取连接跟踪信息等。
      • **NETLINK_AUDIT**: 用于内核审计子系统向用户空间 auditd 守护进程发送审计记录。
      • **NETLINK_SELINUX**: 用于 SELinux 安全模块的通信。
      • **NETLINK_CRYPTO**: 用于配置内核加密算法和接口。
      • **NETLINK_SCSITRANSPORT**: 用于 SCSI 传输层管理。
      • **NETLINK_RDMA**: 用于 RDMA (远程直接内存访问) 子系统。
      • **NETLINK_FIB_LOOKUP**: 用于进行 FIB (转发信息库) 查找 (较旧/特定场景)。

总结关键点:

  • 本质: 基于 Socket (AF_NETLINK) 的异步 IPC。
  • 主场景: 用户态 <-> 内核态 双向通信。
  • 次场景: 用户态进程间通信 (需利用广播/组播特性时)。
  • 核心优势:
    • 异步: 非阻塞,消息缓冲。
    • 双向: 内核可主动推送消息。
    • 广播/组播: 支持一对多通信。
    • 结构化消息: 使用基于属性的消息格式 (如 nlmsghdr + 属性 nlattr),比 ioctl 魔数和 procfs 文本解析更健壮灵活。
    • 标准化: 复用 Socket API。
  • 寻址: 32 位 Port ID (nl_pid)。
  • 组织: 划分为多个协议族 (NETLINK_XXX),每个服务于特定的内核子系统 (NETLINK_ROUTE for 网络, NETLINK_KOBJECT_UEVENT for 设备事件 等)。

正文

源码参考:https://cs.android.com/android/platform/superproject/main/+/main:bionic/libc/bionic/ifaddrs.cpp?hl=zh-cn

(之前作者给的连接的帖子无了)

302行:

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
int getifaddrs(ifaddrs** out) {
// We construct the result directly into `out`, so terminate the list.
*out = nullptr;

// Open the netlink socket and ask for all the links and addresses.
NetlinkConnection nc;
// SELinux policy only allows RTM_GETLINK messages to be sent by system apps.
bool getlink_success = false;
if (getuid() < FIRST_APPLICATION_UID) {
getlink_success = nc.SendRequest(RTM_GETLINK) && nc.ReadResponses(__getifaddrs_callback, out);
}
bool getaddr_success =
nc.SendRequest(RTM_GETADDR) && nc.ReadResponses(__getifaddrs_callback, out);

if (!getaddr_success) {
freeifaddrs(*out);
// Ensure that callers crash if they forget to check for success.
*out = nullptr;
return -1;
}

if (!getlink_success) {
// If we weren't able to depend on GETLINK messages, it's possible some
// interfaces never got their name set. Resolve them using if_indextoname or remove them.
resolve_or_remove_nameless_interfaces(out);
// Similarly, without GETLINK messages, interfaces will not have their flags set.
// Resolve them using the SIOCGIFFLAGS ioctl call.
get_interface_flags_via_ioctl(out);
}

return 0;
}

这个 getifaddrs 实现流程:

  1. 初始化输出指针。
  2. 使用 Netlink 请求接口信息(RTM_GETLINKRTM_GETADDR)。
  3. 对于权限不足的情况,使用替代手段补全信息。
  4. 构建 ifaddrs 链表并返回。

它兼顾了系统级服务和普通应用之间的权限差异,具有较强的容错和兼容性。

  • RTM_GETLINKNetlink 路由协议(NETLINK_ROUTE) 中的一个消息类型

  • 它用于请求内核返回系统中所有网络接口(network links)的信息

第 1 行

1
2
3

bool getlink_success = false;

  • 声明并初始化一个布尔变量 getlink_success,表示是否成功通过 netlink 获取到网络接口的基本信息。

  • 初始设为 false,后面会尝试赋值为真实状态。


    🔍 第 2 行:权限判断

    1
    2
    3

    if (getuid() < FIRST_APPLICATION_UID)

  • getuid() 返回当前进程的 实际用户 ID

  • FIRST_APPLICATION_UID 是一个系统常量,通常在 Android 中定义为 应用用户 UID 的起始值,一般是 10000

    • 系统进程 UID 通常小于 10000(如 system: 1000,root: 0)。
  • 因此,getuid() < FIRST_APPLICATION_UID 用于判断:当前进程是否是系统级别的进程(非第三方 App)

    ✅ 背景解释:

  • 为什么要判断 UID?

    • Android 的 SELinux 策略限制普通应用不能发送 RTM_GETLINK 类型的 netlink 请求。
    • 如果当前进程是普通 App,发送这个请求会被系统拒绝或返回错误。
    • 所以为了安全,只有系统 UID 才去尝试发这个请求。

    🔍 第 3 行:发送请求并读取相应

    1
    2
    3

    getlink_success = nc.SendRequest(RTM_GETLINK) && nc.ReadResponses(__getifaddrs_callback, out);

    这行代码包含两部分:

  1. nc.SendRequest(RTM_GETLINK)
    • 使用 NetlinkConnection 实例 nc 向内核发送一个 RTM_GETLINK 请求。
    • 该请求用于获取 所有网络接口的基本信息,例如接口名称、MTU、MAC 地址、设备状态(up/down)等。
  2. nc.ReadResponses(__getifaddrs_callback, out)
    • 从 netlink socket 中读取内核返回的响应消息。
    • 读取到的每个消息都交给回调函数 __getifaddrs_callback 处理。
    • 回调函数负责解析消息,并将解析出的信息写入到 out 指向的 ifaddrs 链表中。
  3. 两个操作用 && 连接,表示两个操作都成功才算成功,否则结果为 false
部分 含义
getuid() 获取当前用户 ID
< FIRST_APPLICATION_UID 判断是否为系统进程
RTM_GETLINK 请求网络接口基础信息(如接口名、状态)
NetlinkConnection::SendRequest() 向内核发送 Netlink 请求
NetlinkConnection::ReadResponses() 读取并处理内核返回
__getifaddrs_callback 回调函数,构建 ifaddrs 链表
getlink_success 表示是否成功获取接口基础信息

bionic/libc/bionic/bionic_netlink.cpp:

第52行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool NetlinkConnection::SendRequest(int type) {
// Rather than force all callers to check for the unlikely event of being
// unable to allocate 8KiB, check here.
if (data_ == nullptr) return false;

// Did we open a netlink socket yet?
if (fd_.get() == -1) {
fd_.reset(socket(PF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE));
if (fd_.get() == -1) return false;
}

// Construct and send the message.
struct NetlinkMessage {
nlmsghdr hdr;
rtgenmsg msg;
} request;
memset(&request, 0, sizeof(request));
request.hdr.nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST;
request.hdr.nlmsg_type = type;
request.hdr.nlmsg_len = sizeof(request);
request.msg.rtgen_family = AF_UNSPEC; // All families.
return (TEMP_FAILURE_RETRY(send(fd_.get(), &request, sizeof(request), 0)) == sizeof(request));
}

这里要构造并发送一个最小的 Netlink 报文请求给内核:

1
2
3
4
5
6

struct NetlinkMessage {
nlmsghdr hdr;
rtgenmsg msg;
} request;

  • 定义了一个局部结构体 NetlinkMessage,包含两部分:
    1. nlmsghdr hdr; —— Netlink 报文头,用来描述消息的类型、长度和标志;
    2. rtgenmsg msg; —— 路由生成消息体,最小的负载,用于指定要查询的地址族。
  • request 是该结构的一个实例,后续会填充字段并发送。
1
2
3

memset(&request, 0, sizeof(request));

  • request 整块内存清零,保证所有字段初始为 0,避免垃圾值。
1
2
3

request.hdr.nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST;

  • nlmsg_flags 是 Netlink 报文头的标志位:
    • NLM_F_REQUEST 表示这是一个请求报文。
    • NLM_F_DUMP 通常配合请求使用,告诉内核“请把对应表(如路由表、接口表)全量导出给我”。
1
2
3

request.hdr.nlmsg_type = type;

  • nlmsg_type 指定要请求的动作/资源类型,type 由调用者传入,如:
    • RTM_GETLINK 获取网络接口列表
    • RTM_GETADDR 获取接口地址
    • RTM_GETROUTE 获取路由表 等。
1
2
3

request.hdr.nlmsg_len = sizeof(request);

  • nlmsg_len 是整个 Netlink 消息(包括 hdr + msg)的字节总长度,用于内核解析时知道读多少。
1
2
3
4

// All families
request.msg.rtgen_family = AF_UNSPEC;

  • rtgenmsg 里的 rtgen_family 字段指定查询哪个地址族:
    • AF_UNSPEC 表示不过滤,IPv4、IPv6、甚至本地(AF_PACKET)都一并返回。
1
2
3
4
5
6

//使用socket数据发送
return (TEMP_FAILURE_RETRY(
send(fd_, &request, sizeof(request), 0)
) == sizeof(request));

  • send(fd_, &request, sizeof(request), 0):将构造好的 request 原封不动地写入 Netlink socket。
  • TEMP_FAILURE_RETRY(...) 宏会在调用因信号中断(EINTR)时自动重试。
  • 最后比较实际发送的字节数是否等于 sizeof(request)
    • 相等则发送完整成功,返回 true
    • 否则(失败或只发出部分字节)返回 false

整体流程

  1. 清零构造一个最简 Netlink 请求报文。
  2. 标记为「请求 + 全量导出」,指定消息类型和长度。
  3. 将地址族设为 AF_UNSPEC(查询所有)。
  4. 通过 send() 发给内核,返回是否成功。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool NetlinkConnection::ReadResponses(void callback(void*, nlmsghdr*), void* context) {
// Read through all the responses, handing interesting ones to the callback.
ssize_t bytes_read;
while ((bytes_read = TEMP_FAILURE_RETRY(recv(fd_.get(), data_, size_, 0))) > 0) {
nlmsghdr* hdr = reinterpret_cast<nlmsghdr*>(data_);
for (; NLMSG_OK(hdr, static_cast<size_t>(bytes_read)); hdr = NLMSG_NEXT(hdr, bytes_read)) {
if (hdr->nlmsg_type == NLMSG_DONE) return true;
if (hdr->nlmsg_type == NLMSG_ERROR) {
nlmsgerr* err = reinterpret_cast<nlmsgerr*>(NLMSG_DATA(hdr));
errno = (hdr->nlmsg_len >= NLMSG_LENGTH(sizeof(nlmsgerr))) ? -err->error : EIO;
return false;
}
callback(context, hdr);
}
}

// We only get here if recv fails before we see a NLMSG_DONE.
return false;
}

注释版

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
bool NetlinkConnection::ReadResponses(void callback(void*, nlmsghdr*), void* context) {
// 循环接收内核通过 Netlink socket 返回的数据
ssize_t bytes_read;
while ((bytes_read = TEMP_FAILURE_RETRY(
recv(fd_.get(), // 从已打开的 netlink socket 读取
data_, // 存放到构造时分配的缓冲区
size_, // 缓冲区大小,一般 8KiB
0 // 默认标志
)))
> 0) { // 只要读取到字节就继续处理

// 将缓冲区数据解释为第一个 nlmsghdr 报文头
nlmsghdr* hdr = reinterpret_cast<nlmsghdr*>(data_);

// 遍历缓冲区中可能包含的多条 Netlink 消息
for (; NLMSG_OK(hdr, static_cast<size_t>(bytes_read));
hdr = NLMSG_NEXT(hdr, bytes_read)) {
// 如果遇到 NLMSG_DONE,表示所有数据已结束,返回成功
if (hdr->nlmsg_type == NLMSG_DONE)
return true;

// 如果遇到错误消息 NLMSG_ERROR,提取错误码并返回失败
if (hdr->nlmsg_type == NLMSG_ERROR) {
// 获取错误结构体指针
nlmsgerr* err = reinterpret_cast<nlmsgerr*>(NLMSG_DATA(hdr));
// 如果报文长度足够,使用内核返回的负错误码;否则设为 I/O 错误
errno = (hdr->nlmsg_len >= NLMSG_LENGTH(sizeof(nlmsgerr)))
? -err->error
: EIO;
return false;
}

// 对于其他类型的消息,调用用户传入的回调处理
callback(context, hdr);
}
// 循环继续,直到 recv 返回 <= 0 或者遇到 NLMSG_DONE/NLMSG_ERROR
}

// 如果在看到 NLMSG_DONE 之前,recv 出错或返回 0,就当作失败
return false;
}

作者认为原来的使用netlinker通信使用recv容易被直接hook,故给出了两种方法

在不直接使用系统提供的recv 以后

  • 方法1:直接调用syscall函数,通过syscall函数进行切入到recv

  • 方法2:我们直接把recv换成svc内联汇编代码如下
    相当于自己实现syscall (代码摘抄自libc syscall)

但是方法2存在弊端

安卓8内核上使用了seccomop 过滤掉了svc 直接调用 recv

对此,解决办法是直接通过libc的syscall()进行调用

下面是AI给我生成的流程图

修改前 (被拦截路径):

deepseek_mermaid_20250530_d8094a

修改后:

deepseek_mermaid_20250530_136373

最后代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bool NetlinkConnection::ReadResponses(void callback(void*, nlmsghdr*), void* out) {
// Read through all the responses, handing interesting ones to the callback.
ssize_t bytes_read;

while ((bytes_read = TEMP_FAILURE_RETRY(raw_syscall(__NR_recvfrom,fd_, data_, size_, 0, NULL,0))) > 0) {
auto* hdr = reinterpret_cast<nlmsghdr*>(data_);
for (; NLMSG_OK(hdr, static_cast<size_t>(bytes_read)); hdr = NLMSG_NEXT(hdr, bytes_read)) {

if (hdr->nlmsg_type == NLMSG_DONE) return true;
if (hdr->nlmsg_type == NLMSG_ERROR) {
auto* err = reinterpret_cast<nlmsgerr*>(NLMSG_DATA(hdr));
errno = (hdr->nlmsg_len >= NLMSG_LENGTH(sizeof(nlmsgerr))) ? -err->error : EIO;
return false;
}
//处理具体逻辑
callback(out, hdr);
}
}

// We only get here if recv fails before we see a NLMSG_DONE.
return false;
}

通过netlinker和svc获取mac地址解析
http://example.com/2025/05/30/netlinker/
作者
John Doe
发布于
2025年5月30日
许可协议