从棉花糖开始,Android的蓝牙native代码就开始发生变化,更多的用面向对象的C++来设计。到了奥利奥,代码中还使用了一些相对新一些的技术,同时提供了一些基本的工具。今天先从内存分配开始,学习其中用到的一些技术。
内存分配与释放的接口
在整个蓝牙的native代码中,动态的内存分配使用了osi_malloc、osi_calloc接口,释放则是osi_free。以osi_malloc为例,它的实现如下:
void* osi_malloc(size_t size) {
size_t real_size = allocation_tracker_resize_for_canary(size);
void* ptr = malloc(real_size);
CHECK(ptr);
return allocation_tracker_notify_alloc(alloc_allocator_id, ptr, size);
}
在函数内部,最终的内存分配还是用到了malloc函数,只是对内存的size和返回值作了额外处理。第一眼看到allocation_tracker_resize_for_canary这个函数的时候,你会发现它的名字取得不错,应该包含了对buffer的resize和track功能。那么canary这个词代表什么呢?
矿井中的金丝雀
在win10的translator中输入canary这个单词,你得到的答案就只有“金丝雀”,看起来和内存分配没半毛钱关系。不甘心,翻了墙问了谷歌,最终在维基百科的英文页面找到了答案1。
金丝雀除了有一个好嗓子,它还对瓦斯等有毒气体特别敏感。少量的一点瓦斯气体即可让其产生反应,晕倒甚至是死亡。在科技还不发达的17世纪,这样的特性对于采矿业工人来说可谓救命稻草。在矿井中放置一只金丝雀,工人们便可以预判是否有瓦斯泄漏,保证生命安全。
而到了编程世界,金丝雀就是内存溢出保护的一种方法。
Buffer Overflow Protection
内存溢出本身是一种黑客的攻击手段,可以用来破坏内存数据。而如果是stack中的数据出现了内存溢出,主机的控制权可能就会被交出去了。不要以为内存溢出的攻击有多么复杂,简易的实现网上都有2。基于stack的内存溢出攻击,其基本原理就是,通过传递超过已知buffer长度的数据,覆盖stack中保存函数返回地址的那个内存栏位,使其跳转至指定的地址,执行恶意代码。
为了能正确的处理内存溢出问题,金丝雀、边界检查和打标签等方案被提出。金丝雀方案的基本思路是,在已分配内存和其他控制数据之间提供一段内容已知的“保护数据”。就像瓦斯泄漏时,最先受到影响的是金丝雀一样,一旦出现内存溢出,最先被“污染”的内存将是这段保护数据。当代码检测到这种情况时,便可以选择安全的方式进行异常处理。
Random Canaries
今天要分析的蓝牙代码,它使用的内存保护机制被称为“random canaries”。基本思路就是,在分配的内存块的头尾各分配一段空间,用随机的数据将其填充;同时,这段随机的数据会被记录下来。在释放内存块前,代码对内存块头尾的随机数据进行检查。如果发现这段数据和最初分配时设置的不一样,则判定为出现内存溢出,进而引入断言错误,终止程序的运行。分配内存的代码如下:
void* allocation_tracker_notify_alloc(uint8_t allocator_id, void* ptr,
size_t requested_size) {
char* return_ptr;
{
std::unique_lock<std::mutex> lock(tracker_lock);
if (!enabled || !ptr) return ptr;
// Keep statistics
alloc_counter++;
alloc_total_size += allocation_tracker_resize_for_canary(requested_size);
return_ptr = ((char*)ptr) + canary_size;
auto map_entry = allocations.find(return_ptr);
allocation_t* allocation;
if (map_entry != allocations.end()) {
allocation = map_entry->second;
CHECK(allocation->freed); // Must have been freed before
} else {
allocation = (allocation_t*)calloc(1, sizeof(allocation_t));
allocations[return_ptr] = allocation;
}
allocation->allocator_id = allocator_id;
allocation->freed = false;
allocation->size = requested_size;
allocation->ptr = return_ptr;
}
// Add the canary on both sides
memcpy(return_ptr - canary_size, canary, canary_size);
memcpy(return_ptr + requested_size, canary, canary_size);
return return_ptr;
}
经过上面的分配,实际使用的内存布局如下:
random canaries | buffer can be used | random canaries |
---|---|---|
8bytes | user requested buffer size | 8bytes |
除了基本的random canaries,这里还使用了一个用于保存引用记录的结构体,定义如下:
typedef struct {
uint8_t allocator_id;
void* ptr;
size_t size;
bool freed;
} allocation_t;
与其他工具相同的是,这个结构体对外部也是隐藏的,因为它定义在源文件中,头文件中只有声明,外部只能使用指针的方式来使用它。每一个使用allocation_tracker_notify_alloc分配的内存,都会有一个对应的allocation_t结构体,保存用户实际可以使用的内存起始地址、可使用的内存块大小等信息。一方面,这样做可以追踪内存的分配与释放;另一方面,如果错误地将经过malloc、calloc直接分配的空间传递给allocation_tracker来释放,由于没有对应的allocation_t的记录,可以判定其为非法操作,避免内存泄漏。除此,代码中还有一个全局的unordered_map,保存实际可使用内存地址与allocation_t的键值对。
内存释放的代码如下,其实现思路就是前面提到的“random canaries”。注意,它返回的是实际分配的内存的起始地址,而不是用户可使用的内存首地址。
void* allocation_tracker_notify_free(UNUSED_ATTR uint8_t allocator_id,
void* ptr) {
std::unique_lock<std::mutex> lock(tracker_lock);
if (!enabled || !ptr) return ptr;
auto map_entry = allocations.find(ptr);
CHECK(map_entry != allocations.end());
allocation_t* allocation = map_entry->second;
CHECK(allocation); // Must have been tracked before
CHECK(!allocation->freed); // Must not be a double free
CHECK(allocation->allocator_id ==
allocator_id); // Must be from the same allocator
// Keep statistics
free_counter++;
free_total_size += allocation_tracker_resize_for_canary(allocation->size);
allocation->freed = true;
UNUSED_ATTR const char* beginning_canary = ((char*)ptr) - canary_size;
UNUSED_ATTR const char* end_canary = ((char*)ptr) + allocation->size;
for (size_t i = 0; i < canary_size; i++) {
CHECK(beginning_canary[i] == canary[i]);
CHECK(end_canary[i] == canary[i]);
}
// Free the hash map entry to avoid unlimited memory usage growth.
// Double-free of memory is detected with "assert(allocation)" above
// as the allocation entry will not be present.
allocations.erase(ptr);
free(allocation);
return ((char*)ptr) - canary_size;
}
随机数的生成
最后是“random canaries”的来源——随机数生成器。它用到了系统的/dev/urandom节点,每次从其中read出一个int值,取1byte作为canaries的数组的一个成员。关于/dev/urandomh和/dev/random,网上有很多资料3和4,看起来其实现还是挺复杂的,这里不做展开。