C/C++程序中内存被非法改写的一个检测方法
本文所讨论的“内存”主要指(静态)数据区、堆区和栈区空间(详细的布局和描述参考《Linux虚拟地址空间布局》一文)。数据区内存在程序编译时分配,该内存的生存期为程序的整个运行期间,如全局变量和static关键字所声明的静态变量。函数执行时在栈上开辟局部自动变量的储存空间,执行结束时自动释放栈区内存。堆区内存亦称动态内存,由程序在运行时调用malloc/calloc/realloc等库函数申请,并由使用者显式地调用free库函数释放。堆内存比栈内存分配容量更大,生存期由使用者决定,故非常灵活。然而,堆内存使用时很容易出现内存泄露、内存越界和重复释放等严重问题。
数据区的内存访问越界可以分为读越界和写越界,数据区内存越界主要指读写某一数据区内存(如全局或静态变量、数组或结构体等)时,超出该内存区域的合法范围。读越界表示读取不属于自己的数据,如读取的字节数多于分配给目标变量的字节数。若所读的内存地址无效,则程序立即崩溃;若所读的内存地址有效,则可读到随机的数据,导致不可预料的后果。写越界亦称“缓冲区溢出”,所写入的数据对目标地址而言也是随机的,因此同样导致不可预料的后果。
内存越界访问会严重影响程序的稳定性,其危险在于后果和症状的随机性。这种随机性使得故障现象和本源看似无关,给排障带来极大的困难。你永远也不知道是不是有其他线程操作时候偷偷改动了你的数据。如果是一般的业务数据,唔,一个bug。但是是如果该内存块指向一个对象,然后就呵呵了——你持有了一个无效的内存地址,一般来说会crash,无止境的debug在等待你。
写越界的主要原因有两种:1) memset/memcpy/memmove等内存覆写调用;2) 数组下标超出范围。
#include <string.h>
#include <stdio.h>
#define NAME_SIZE 8
#define NAME_LEN 9
char name1[NAME_SIZE] = "ABCDEFGH";
char name2[NAME_LEN] = "123456789";
int main() {
strncpy(name1, name2, NAME_LEN);
printf("name2: %s\n", name2);
return 0;
}
输出结果显然是name2: 923456789。常见的所谓数组越界方法实现起来比较繁琐。用工具(VALGRIND等)可以发现,但是对于生产系统(采用了全局数组+多线程之类的高级技巧……),一般来说是难以查找到的,特别是如果其他线程由其他团队成员开发,你对其代码缺少相关知识的时候。
对于这个问题,gdb提供了一种可能的方法:观察点(watch命令)。用法如下:watch name2[0]。这样当该变量被改写的时候进程将会停下来。当然你也可以watch某个地址:watch *(data type*)addr。如果你怀疑是特定线程改写了该变量的时候,可以使用watch expr thread threadnum,在某个线程改写的时候让进程停止。使用这个方法,在绝大多数情况下可以发现未知的变量改写问题。
(gdb) watch name2[0]
Hardware watchpoint 1: name2[0]
(gdb) r
Starting program: /home/afreet/sourcecodes/memdemo/build/bin/memdemo
Hardware watchpoint 1: name2[0] Old value = 49 '1'
New value = 57 '9'
__strncpy_ssse3 () at ../sysdeps/x86_64/multiarch/strcpy-ssse3.S:2443
2443 ../sysdeps/x86_64/multiarch/strcpy-ssse3.S: No such file or directory.
(gdb) bt
#0 __strncpy_ssse3 () at ../sysdeps/x86_64/multiarch/strcpy-ssse3.S:2443
#1 0x000000000040080e in main () at /home/afreet/sourcecodes/memdemo/memdemo.c:11
如果在调试状态下运行仍然没有发现问题或者是嵌入式环境根本无法调试,那么是不是就只能去烧香?或者拜基督(取决于你的宗教信仰,但是财神我相信大多数现代中国人是不会拒绝去拜拜的)。Linux还提供了一个杀手锏级的API:mprotect。
mprotect函数的原型如下:
int mprotect(const void *addr, size_t len, int prot);
其中addr是待保护的内存首地址,必须按页对齐;len是待保护内存的大小,必须是页的整数倍,prot代表模式,可能的取值有PROT_READ(表示可读)、PROT_WRITE(可写)等。
不同体系结构和操作系统,一页的大小不尽相同。如何获得页大小呢?通过PAGE_SIZE宏或者getpagesize()系统调用即可。下面是另一个简单的例子:
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <assert.h> #include <thread> #define BUF_LEN 4096 using namespace std; int buf[BUF_LEN] = {0};
int* p = &buf[2048]; void func1() {
char* q = reinterpret_cast<char*>(p);
*q = 0xFF;
} void func2() {
sleep(5); for ( auto x: buf) {
assert(x == 0);
}
} int main() {
std::thread t1(func1);
std::thread t2(func2);
t1.join();
t2.join(); return 0;
}
由于buf[2048]在func1中被改写,所以断言会失败。因此引入mprotect函数,对问题所在进行检测。改进后的版本如下:
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <sys/mman.h> #include <thread> #define BUF_LEN 1024 using namespace std; int buf[BUF_LEN] = {0};
int* p = &buf[512]; void func2() {
char* q = reinterpret_cast<char*>(p);
*q = 0xFF;
} void func1() {
long pageSize = sysconf(_SC_PAGESIZE);
void *pageStart = (void*)((long)p - (long)p % pageSize); int rst = mprotect(pageStart, pageSize, PROT_READ);
if ( rst == -1 )
printf("mprotect failed: %s", strerror(errno));
sleep(10);
} int main() {
std::thread t1(func1);
std::thread t2(func2);
t1.join();
t2.join(); return 0;
}
然后再来测试一下:
(gdb) r
Starting program: /home/afreet/sourcecodes/memdemo/build/bin/memdemo
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff6ff1700 (LWP 2794)]
[New Thread 0x7ffff67f0700 (LWP 2795)]
[Thread 0x7ffff67f0700 (LWP 2795) exited] Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7ffff6ff1700 (LWP 2794)]
_dl_fixup (l=<optimized out>, reloc_arg=<optimized out>) at ../elf/dl-runtime.c:148
148 ../elf/dl-runtime.c: No such file or directory.
(gdb) bt
#0 _dl_fixup (l=<optimized out>, reloc_arg=<optimized out>) at ../elf/dl-runtime.c:148
#1 0x00007ffff7df02e5 in _dl_runtime_resolve () at ../sysdeps/x86_64/dl-trampoline.S:45
#2 0x000000000040740b in func1 () at /home/afreet/sourcecodes/memdemo/memdemo.cpp:33
#3 0x0000000000408aac in void std::_Bind_simple<void (*())()>::_M_invoke<>(std::_Index_tuple<>) (this=0x60eda8)
at /usr/include/c++/4.9/functional:1700
#4 0x00000000004089d2 in std::_Bind_simple<void (*())()>::operator()() (this=0x60eda8)
at /usr/include/c++/4.9/functional:1688
#5 0x0000000000408939 in std::thread::_Impl<std::_Bind_simple<void (*())()> >::_M_run() (this=0x60ed90)
at /usr/include/c++/4.9/thread:115
#6 0x00007ffff796a970 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#7 0x00007ffff7bc70a4 in start_thread (arg=0x7ffff6ff1700) at pthread_create.c:309
#8 0x00007ffff70da87d in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:111
注意调用栈#2,明确的指出了试图改写buf[256]的函数名。这样就可以轻松的找到犯罪分子早点下班吃饭了。
但是还是没有彻底解决问题:如果被测代码无法使用调试器运行怎么办?另一个问题,mprotect需要保护整个页面,那么很多时候被保护的数据会和其他全局数据共存在一个页面上。如果其他线程访问了这个页面,一样会发生segment fault。显然这不是我们需要的结果。如果在被保护的数据之前人为加padding,让被改写的数组后退到某个页面起始处,那么越界访问往往就不会发生了——因为访问到了padding上,这样也无法重现错误。办法还是有的:利用信号处理函数,判定发生页面访问错误的地址是否是我们期望的某个元素所在,如果不是,那么什么都不做就可以了;如果是,那就打印调用栈到指定文件。用一个包装类来实现这个目的:
class MemoryDetector
{
public:
typedef void (*segv_handler) (int sig, siginfo_t *si, void *unused); static void init(const char *path)
{
register_handler(handler);
fd_ = open(path, O_RDWR|O_CREAT, 777);
} static int protect(void *ptr, int len)
{
address_ = reinterpret_cast<uint64_t>(ptr);
len_ = len;
uint64_t start_address = (address_ >> PAGE_SHIFT) << PAGE_SHIFT;
return mprotect(reinterpret_cast<void *>(start_address), PAGE_SIZE, PROT_READ);
} static int umprotect(void *ptr, int len)
{
uint64_t addr = reinterpret_cast<uint64_t>(ptr);
uint64_t start_address = (addr >> PAGE_SHIFT) << PAGE_SHIFT;
return mprotect(reinterpret_cast<void *>(start_address), PAGE_SIZE, PROT_READ | PROT_WRITE);
} static int umprotect()
{
uint64_t start_address = (address_ >> PAGE_SHIFT) << PAGE_SHIFT;
return mprotect(reinterpret_cast<void *>(start_address), PAGE_SIZE, PROT_READ | PROT_WRITE);
} static void finish()
{
close(fd_);
}
private:
static void register_handler(segv_handler sh)
{
struct sigaction act;
act.sa_sigaction = sh;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_SIGINFO;
if(sigaction(SIGSEGV, &act, NULL) == -1){
perror("Register hanlder failed");
exit(EXIT_FAILURE);
}
} static void handler(int sig, siginfo_t *si, void *unused)
{
uint64_t address = reinterpret_cast<uint64_t>(si->si_addr);
if (address >= address_ && address < address_ + len_) {
umprotect(si->si_addr, PAGE_SIZE);
my_backtrace();
}
} static void my_backtrace()
{
const int N = 100;
void* array[100];
int size = backtrace(array, N);
backtrace_symbols_fd(array, size, fd_);
} static uint64_t address_;
static int len_;
static int fd_;
}; uint64_t MemoryDetector::address_;
int MemoryDetector::len_;
int MemoryDetector::fd_;
随后我们把测试程序改成这个样子:
void func() {
char* q = reinterpret_cast<char*>(p);
*q = static_cast<char>(0xFF); //Line 101
} int main() {
MemoryDetector::init("memdemo.rst");
MemoryDetector::protect(p, 4); std::thread t(func);
t.join();
sleep(5);
MemoryDetector::finish(); return 0;
}
再运行一把,得到了memdemo.rst文件,内容如下:
./memdemo(_ZN14MemoryDetector12my_backtraceEv+0x2b)[0x407b79]
./memdemo(_ZN14MemoryDetector7handlerEiP9siginfo_tPv+0x64)[0x407b4c]
/lib/x86_64-linux-gnu/libpthread.so.0(+0xf8d0)[0x7fb039e928d0]
./memdemo(_Z4funcv+0x1c)[0x4076fc]
./memdemo(_ZNSt12_Bind_simpleIFPFvvEvEE9_M_invokeIIEEEvSt12_Index_tupleIIXspT_EEE+0x2a)[0x408fa4]
./memdemo(_ZNSt12_Bind_simpleIFPFvvEvEEclEv+0x22)[0x408eca]
./memdemo(_ZNSt6thread5_ImplISt12_Bind_simpleIFPFvvEvEEE6_M_runEv+0x21)[0x408e31]
/usr/lib/x86_64-linux-gnu/libstdc++.so.6(+0xb6970)[0x7fb039c2e970]
/lib/x86_64-linux-gnu/libpthread.so.0(+0x80a4)[0x7fb039e8b0a4]
/lib/x86_64-linux-gnu/libc.so.6(clone+0x6d)[0x7fb03939e87d]
接着addr2line命令看看:
addr2line -e memdemo 0x4076fc
/home/afreet/sourcecodes/memdemo/memdemo.cpp:101
也很轻松的找到了肇事者所在。
C/C++程序中内存被非法改写的一个检测方法的更多相关文章
- c/c++程序中内存区划分
转自:http://wenzongliang.iteye.com/blog/1866629 操作系统启动程序时会加载程序代码到内存(叫程序的代码区),然后创建进程PCB为进程分配内存资源(数据区,32 ...
- 在Java Web程序中使用监听器可以通过以下两种方法
之前学习了很多涉及servlet的内容,本小结我们说一下监听器,说起监听器,编过桌面程序和手机App的都不陌生,常见的套路都是拖一个控件,然后给它绑定一个监听器,即可以对该对象的事件进行监听以便发生响 ...
- 把一下程序中的print()函数改写成
源代码: #include <iostream> using namespace std; void print( int w ) { ; i <= w ; i++ ) { ; j ...
- 记录:50多行程序中找出多写的一个字母e
小霍同学调程序,做的是第11周的项目1 - 存储班长信息的学生类,可是她写的程序(就在以下),呃,请读者自己执行一下吧.(下午在机房调试时用的是Code::Blocks10.05.输出的是非常长的莫名 ...
- 在eclipse程序中设置的断点上有一个斜杠,正常启动debug不能够跳转到debug页面,怎么解决
在run菜单里面,把skip all breakpoints 选项勾去即可,这个选项可能是你无意间选上的.
- SpringMVC中Controller跳转到另一个Controller方法
1.直接Redirect后加 Controller/Action Response.Redirect("/User/Edit"); return Redirect("/U ...
- 如何检查mysql中建立的索引是否生效的检测方法及相关参数说明
所使用的mysql函数explain语法:explain < table_name >例如: explain select * from t3 where id=3952602;expla ...
- 判定你的java应用是否正常(是否内存、线程泄漏)的一个简单方法
给大家推荐一个最简单的判定你的java应用是否正常的方法: step1:部署你的应用,让它跑起来: step2:打开jdk下bin目录下的jconsole.exe工具,连接到你的应用——以监测线程和内 ...
- LR中错误代号为27796的一个解决方法
问题: 曾经遇到过一个问题,在一次性能测试过程中,使用http协议的多用户向服务器发送请求.设置了持续时间,出现错误为:27796, Failed to connect to server 'ho ...
随机推荐
- 学习C语言,在软件测试中如何用?
1)为什么学? 掌握基础: 编写测试脚本: 自动化: 性能测试: 看懂代码,定位问题(白盒测试). C语言如何开发有界面的程序? 首先给大家扫盲:1 什么叫做界面程序: 归根到底就是设置LCD上的 ...
- 1. 做node项目 (第二个月)
工作栈: Node + Express + Mongoose + Mongodb + Vuejs 主要做了 mongodb的 curd , 因为以前做 PHP + MySql 所以基本大同小异. n ...
- 在java中浅谈Math类中的常用方法
通过最近的学习,学到了一些的Math类中的常见方法 package org.stm.demo; public class Test { public static void main(String[] ...
- 【leetcode】482. License Key Formatting
problem 482. License Key Formatting solution1: 倒着处理,注意第一个字符为分隔符的情况要进行删除,注意字符的顺序是否正序. class Solution ...
- HQL知识点一
Hive创表语法 create [external] table [if not exists] [db_name.]table_name (col1_name data_type,col2_name ...
- android 判断横竖屏的方法(转)
public boolean isScreenChange() { Configuration mConfiguration = this.getResources().getConfiguratio ...
- CrawlSpider模板
crawlSpider 创建CrawlSpider模板 scrapy genspider -t crawl <爬虫名字> <域名> 模板代码示例: # -*- coding: ...
- Ubuntu使用总结一
一.安装 Ubuntu桌面版与服务器版的不同之处桌面版面向个人电脑使用者,可以进行文字处理.网页浏览.多媒体播放和玩游戏.本质上说,这是一 个为普通用户所定制的多用途操作系统.另一方面,服务器版旨在充 ...
- php 服务端允许跨域访问
加上需要允许跨域访问,配置如下(一下配置内容前不允许有其他任何输出操作): //设置允许跨域的 请求源地址//方式一:header("Access-Control-Allow-Origin: ...
- 2019嘉韦思杯线上初赛writeup
1 土肥原贤二 看到页面怀疑是sql注入,写了个4'进去就发生报错.could not to the database You have an error in your SQL syntax; ch ...