Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Android Address Sanitizer (ASan) 原理简介

Android Address Sanitizer (ASan) 原理简介

作者头像
字节流动
发布于 2021-06-09 06:37:58
发布于 2021-06-09 06:37:58
5.6K00
代码可运行
举报
文章被收录于专栏:字节流动字节流动
运行总次数:0
代码可运行

前面介绍了 NDK 开发中快速上手使用 ASan 检测内存越界等内存错误的方法,现分享一篇关于 ASan 原理介绍的文章。


本文介绍Android上定位native代码野指针/数组越界/栈踩踏的终极武器—-Address Sanitizer(asan) 和 Hardware assisted Address Sanitizer (hwasan) 的基本实现原理。

Address Sanitizer

基本原理

程序申请的每 8bytes 内存映射到 1byte 的 shadown 内存上。

因为 malloc 返回的地址都是基于8字节对齐的,所以每8个字节实际可能有以下几个状态:

case 1:8 个字节全部可以访问,例如char* p = new char[8]; 将0写入到这8个字节对应的1个字节的shadow内存。

case 2:前 1<=n<8 个字节可以访问, 例如char* p = new char[n], 将数值n写入到相应的1字节的shadow内存,尽管这个对象实际只占用5bytes,malloc的实现里[p+5, p+7]这尾部的3个字节的内存也不会再用于分配其他对象,所以通过指针p来越界访问最后3个字节的内存也是被允许的。

asan还会在程序申请的内存的前后,各增加一个redzone区域(n * 8bytes),用来解决overflow/underflow类问题。

free对象时,asan不会立即把这个对象的内存释放掉,而是写入1个负数到该对象的shadown内存中,即将该对象成不可读写的状态, 并将它记录放到一个隔离区(book keeping)中, 这样当有野指针或use-after-free的情况时,就能跟进shadow内存的状态,发现程序的异常;一段时间后如果程序没有异常,就会再释放隔离区中的对象。

编译器在对每个变量的load/store操作指令前都插入检查代码,确认是否有overflow、underflow、use-after-free等问题。

检测堆上变量的非法操作的基本实现方式

asan在运行时会替换系统默认的malloc实现,当执行以下代码时,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void foo() {
  char* ptr = new char[10];
  ptr[1] = 'a';
  ptr[10] = '\n'
}

我们知道 new 关键字实际最终调用还是 malloc 函数,而 asan 提供的 malloc 实现基本就如下代码片段所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// asan提供的malloc函数
void* asan_malloc(size_t requested_size) {
    size_t actual_size = RED_ZONE_SIZE /*前redzone*/ + align8(requested_size) + RED_ZONE_SIZE/*后redzone*/;
    // 调用libc的malloc去真正的分配内存
    char* p = (char*)libc_malloc(acutal_size);
    // 标记前后redzone区不可读写
    poison(p, requested_size);

    return p + RED_ZONE_SIZE; // 返回偏移后的地址
}

void foo() {
  // 运行时实际执行的代码
  char* ptr = asan_malloc(10);

  // 编译器插入的代码
  if (isPoisoned(ptr+1)) {
    abort();
  }
  ptr[1] = 'a';

  // 编译器插入的代码
  if (isPoisoned(ptr+10)) {
    abort(); // crash:访问到了redzone区域
  }
  ptr[10] = '\n'
}

asan_malloc 会额外多申请 2 个 redzone 大小的内存, 实际的内存布局如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
----------------------------------------------------------------   
|    redzone(前)    |    用户申请的内存      |    redzone()    |   
----------------------------------------------------------------

用户申请的内存对应的shadow内存会被标记成可读写的,而redzone区域内存对应的shadow内存则会被标记成不可读写的,

这样就可以检测对堆上变量的越界访问类问题了。

检测栈上对象的非法操作的基本实现方式

对于以下代码片段

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void foo() {  char a[8];  a[1] = '\0';  a[8] = '\0'; // 越界  return;}

编译器则直接在 a 数组的前后都插入1个 redzone,最终的代码会变成下面的方式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void foo() {  char redzone1[32];  // 编译器插入的代码, 32字节对齐  char a[8];  char redzone2[24];  // 编译器插入的代码, 与用于申请的数组a一起做32字节对齐  char redzone3[32];  // 编译器插入的代码, 32字节对齐  // 编译器插入的代码  int  *shadow_base = MemToShadow(redzone1);  shadow_base[0] = 0xffffffff;  // 标记redzone1的32个字节都不可读写  shadow_base[1] = 0xffffff00;  // 标记数组a的8个字节为可读写的,而redzone2的24个字节均不可读写  shadow_base[2] = 0xffffffff;  // 标记redzone3的32个字节都不可读写  // 编译器插入的代码  if (isPoisoned(a+1)) {      abort();  }  a[1] = '0';  // 编译器插入的代码  if (isPoisoned(a+8)) {      abort(); // crash: 因为a[8]访问到了redzone区  }  a[8] = '\0';  // 整个栈帧都要被回收了,所以要将redzone和数组a对应的内存都标记成可读可写的  shadow_base[0] = shadow_base[1] = shadow_base[2] = 0;  return;}

程序申请的对象内存和它的shadow内存映射关系

因为 asan 对每 8bytes 程序内存会保留1byte 的 shadow 内存,所以在进程初始化时,asan得预留(mmap)1/8的虚拟内存。

而对于64bit的Linux,实际最大可用虚拟地址是pow(2, 47), 另外要保证预留的地址不会被程序启动时就占用掉,所以实际预留的地址要再加上一个适当的偏移, 这样就不会与app的申请内存区域重叠,于是有: ShadowByteAddr = (AppMemAddr >> 3) + Offset


Hardware assisted address sanitizer 原理简介

依赖 AArch64的 address tagging,也叫 top byte ignore 特性,允许程序将自定义数据存到虚拟地址的最高8位(bit),cpu在操作这个虚拟地址的时候,会自动忽略高8位。

基本原理

内存对齐:不论是在堆上,栈上分配的对象,还是全局对象,他们的内存起始地址都会做16bytes对齐(malloc或者编译器来保证)

标记内存:在分配这些对象时,hwasan挑选一个随机数值tag(<=255),对这个对象做标记,并将它保存到这个对象的对应shadow内存中

标记指针:hwasan提供的malloc函数返回的对象虚拟地址的最高8bits也被设置成同样的tag值,栈上对象的标记工作由编译器完成

映射关系:每16 bytes程序内存映射到1 byte的shadow内存,用于保存tag值

回收对象:重新分配一个随机值,保存对象地址关联的shadow内存中,如果有人使用一个指向一个已经被释放了的对象指针去访问数据,由于tag已经发生了变化,就会被及时检测到

检验tag:跟asan类似,在对每个指针的store/load指令前,编译器都插入相应的检查指令,用于确认正在被读或写的指针的高8位上的tag值与指针所指向对象对应的shaow内存里的tag值是一致的,如果出现不一致就会终止当前进程。

另外,当分配的对象的内存实际小于16字节时,例如我们通过 char* p = new char[10] 分配一个长度是10byte的char数组,因为要保证每16个字节对应1个字节的shadow byte,所以[p+10, p+15]这6个字节的内存也不会再用于分配其他对象。而这部分预留的闲置内存的最后一个字节就可以用来存放数组的实际大小,这样的话,当检测到指针上的tag与shadow内存里的tag是一致时,还要再校验指针所指向对象的实际大小来检测是否有数组越界问题。

原理图解

hwasan的漏检率

对一个指针上的保存的tag值,它实际指向的对象所对应的shadow内存里的tag值可能有256(2^8)种可能。

那么2个不同的对象就会有1/256,即大约 0.4% 的概率拥有相同tag的情况,这样的野指针/越界方位就不能及时的被检测到,但我们还是可以通过长时间的测试和多次测试来提高检测率。

hwasan相比asan的优势

  • 相比 asan,hwsan 的 shadow memory 占用更少(10% ~ 35%) hwsan也要对分配的栈/堆上的变量做16字节对齐,还有每16个字节会占用1个字节的shadow内存用于保存tag值,但它不再要像asan的实现里那样,在分配的对象前后添加redzone,来检查越界访问,所以内存占用会降低不少。
  • 定位对于野指针类问题的概率更高 asan 只能检测到一个野指针恰好访问的是某个对象之前或之后的 redzone 内存的情况,理论上 redzone 越大,能检测到野指针的概率也就越高,不过随之也会带来更大的内存开销(overload); hwsan上,因为两个不同对象的tag值一般是不同的,所以只要是有野指针就能够被及时检测到。

参考

  • AddressSanitizer: A Fast Address Sanity Checker
  • Detecting Memory Corruption Bugs With HWASan
  • google/sanitizers
  • Hardware-assisted AddressSanitizer Design Documentation

作者:wwm 来源:https://wwm0609.github.io/2020/04/17/hwasan/

-- END --

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-05-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 字节流动 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
C++关于main函数的几点说明
main函数是C++程序的入口函数,C++标准规定main()函数的返回值类型为int,返回值用于表示程序的退出状态,如果返回0则表示程序正常退出,如果返回非0,则表示出现异常。C++标准规定,main()函数原型有两种:
恋喵大鲤鱼
2019/02/22
7.2K0
干货 | 深度剖析C语言的main函数
main函数的返回值用于说明程序的退出状态。如果返回0,则代表程序正常退出。返回其它数字的含义则由系统决定。通常,返回非零代表程序异常退出。
C语言与CPP编程
2020/12/10
2.3K1
干货 | 深度剖析C语言的main函数
【编程基础】你是否真的了解main()函数?
最近看到很多人、甚至市面上的一些书籍,都使用了void main() ,其实这是错误的。C/C++中从来没有定义过void main() 。C++之父 Bjarne Stroustrup在他的主页上的 FAQ 中明确地写着 The definition void main() { /* …… */ } is not and never has been C++, nor has it even been C。( void main() 从来就不存在于 C++ 或者 C )。下面我分别说一下 C 和 C++
程序员互动联盟
2018/03/13
6940
【编程基础】你是否真的了解main()函数?
C/C++代码调试的几点建议
代码调试在程序开发阶段占有举足轻重的地位,可见代码调试的重要性。但是有一点必须强调:程序是设计出来的,而不是调试出来的。这是所有程序员必须牢记在心的一条准则。一个没有设计或者这几得很糟糕的程序,无论怎样调试,也不会成为一个合格的程序。
恋喵大鲤鱼
2018/08/03
6910
C语言的main函数到底该怎么写
main函数是程序执行自定义的第一个函数。从开始学习C语言到现在,我们似乎看到了很多个版本的main函数,那么哪一种才是正确的呢?我们先来看看目前有哪些版本。
编程珠玑
2019/09/03
1.5K0
C语言的main函数到底该怎么写
C语言return函数
说到return,有必要提及主函数的定义。很多人甚至市面上的一些书籍,都使用了void main( )这一形式 ,其实这是错误的。
Java架构师必看
2021/03/22
3.3K0
关于main函数返回值
其他还有写成main( )、 void main( )和 int main(void),这些有什么区别对错呢。
用户6755376
2019/11/19
3.3K0
C语言 函数指针和指针函数及Main()函数
指针函数 定义 指针函数,简单的来说,就是一个返回指针的函数,其本质是一个函数,而该函数的返回值是一个指针。 声明格式为:类型标识符 *函数名(参数表)
全栈程序员站长
2022/06/25
8050
C语言 函数指针和指针函数及Main()函数
4.9 C++ Boost 命令行解析库
命令行解析库是一种用于简化处理命令行参数的工具,它可以帮助开发者更方便地解析命令行参数并提供适当的帮助信息。C++语言中,常用的命令行解析库有许多,通过本文的学习,读者可以了解不同的命令行解析库和它们在C++项目中的应用,从而更加灵活和高效地处理命令行参数。
王 瑞
2023/08/22
5250
再探函数
main:处理命令行 //main函数的两种定义形式 int main(int argc,char **argv[]) int main(int argc,char *argv[]) argc:指的是
Cloud-Cloudys
2020/07/07
4000
【寻找Linux的奥秘】第九章:自定义SHELL
Shell 是一种用于与操作系统交互的命令行界面程序。它充当用户和操作系统内核之间的中介,通过用户输入的命令来执行操作,提供与操作系统的互动。
code_monnkey_
2025/06/02
650
【寻找Linux的奥秘】第九章:自定义SHELL
【答疑释惑】main函数的参数是哪里传递来的
int main(int argc, char *argv[]) 这个参数是哪里传递来的? C/C++语言中的main函数,经常带有参数argc,argv,如下: int main(int argc, char** argv) int main(int argc, char* argv[]) 这两个参数的作用: argc 是指命令行输入参数的个数(以空白符分隔) argv存储了所有的命令行参数。 1.通过命令行运行 假如你的程序是Helloworld.exe,在cmd命令行运行时,增加参数即可: Hello
程序员互动联盟
2018/03/14
1.2K0
【答疑释惑】main函数的参数是哪里传递来的
嵌入式Linux:main函数的使用方法
和单片机开发一样,在Linux中,C语言程序的执行通常从main函数开始。main函数是程序的入口点,当程序启动时,操作系统会调用main函数来执行程序的主要逻辑。
不脱发的程序猿
2024/05/26
2390
c++获取命令行参数
第一个形参必须是int(c语言规定),第二个形参必须是指向字符串的指针数组,而且main函数自身的参数值是从操作系统命令行获取的。
高久峰
2023/06/23
6400
VC 在调用main函数之前的操作
title: VC 在调用main函数之前的操作 tags: [VC++, 反汇编, C++实现原理] date: 2018-09-16 10:36:23 categories: VC++反汇编分析 keywords: VC++, 反汇编, C++实现原理, main函数调用, VC 运行环境初始化 --- 在C/C++语言中规定,程序是从main函数开始,也就是C/C++语言中以main函数作为程序的入口,但是操作系统是如何加载这个main函数的呢,程序真正的入口是否是main函数呢?本文主要围绕这个主题,通过逆向的方式来探讨这个问题。本文的所有环境都是在xp上的,IDE主要使用IDA 与 VC++ 6.0。为何不选更高版本的编译器,为何不在Windows 7或者更高版本的Windows上实验呢?我觉得主要是VC6更能体现程序的原始行为,想一些更高版本的VS 它可能会做一些优化与检查,从而造成反汇编生成的代码过于复杂不利于学习,当逆向的功力更深之后肯定得去分析新版本VS 生成的代码,至于现在,我的水平不够只能看看VC6 生成的代码 首先通过VC 6编写这么一个简单的程序
Masimaro
2018/10/10
2.5K0
VC 在调用main函数之前的操作
C语言 main 函数到底怎么写是对的?
各位,C语言中的main函数大家都再熟悉不过了,这是你学习C语言首先就要学习的东西,但是我看过很多人写的代码包括我们的一些读者在main函数的写法方面版本很多,今天就跟大家聊一聊main函数到底应该怎么写的问题。
Power7089
2020/07/26
1.5K0
c语言main函数里的参数argv和argc解析
一般我们平时写main函数的话,一般都是写不带参数的比较多,而且也习惯了这样写;其实标准的形式写法,main函数是带两个参数的,这两个参数分别是:argc和argv,那么这两个参数是表示什么意思,怎么用呢?今天就给大家分享main函数里面这两个参数的使用,下面看到这样的写法,要明白这样写的意思哦!
用户6280468
2022/03/21
3.5K0
c语言main函数里的参数argv和argc解析
main函数与命令行参数
执行程序时,可以从命令行传值给C程序。这些值被称为命令行参数,特别是想从外部控制程序,不是通过在代码内对这些值进行硬编码时,而是通过参数来控制代码部分逻辑。 int main(int argc,char *argv[]) argc:命令行参数个数(不给main()函数传递参数时默认值为1,即至少有一个参数为该可执行文件的文件名(含目录)) argv:命令行参数数组(分别指向各个字符串参数的首地址,其中argv[0]存储的是可执行文件的文件名的首地址。)
用户7272142
2023/10/11
4560
main函数与命令行参数
OpenvSwitch系列之浅析main函数
通过前面几篇解析OpenvSwitch内部主要数据结构和流程,对OpenvSwitch有了相对简单的了解,由于本人不是专业搞OpenvSwitch的,纯属业余爱好,今天可能是OpenvSwitch最后一篇了,我们要做到有始有终嘛,所以我们来分析一下main函数。然而main函数里面涉及内容比较多,而且比较深入,所以这篇文章只是浅析,不能算深入剖析,希望以后能有哪位大神能够做一个深入剖析。 自己在学习开源软件总是喜欢看一下main函数,认为不把main函数搞明白了,就不算一个好程序员!!其实把main函数搞明
SDNLAB
2018/04/03
1.8K0
OpenvSwitch系列之浅析main函数
指针数组做main函数的形参
SarPro
2024/02/20
1770
相关推荐
C++关于main函数的几点说明
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验