Skip to content

Commit ac36f83

Browse files
committed
Log state of g_array in libc
See JingMatrix/NeoZygisk#50 for details of this detection point.
1 parent f036a0e commit ac36f83

File tree

4 files changed

+305
-1
lines changed

4 files changed

+305
-1
lines changed

app/src/main/cpp/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ set(CMAKE_CXX_STANDARD 20)
2929
# used in the AndroidManifest.xml file.
3030
add_library(${CMAKE_PROJECT_NAME} SHARED
3131
# List C/C++ source files with relative paths to this CMakeLists.txt.
32-
elf_util.cpp native-lib.cpp smap.cpp solist.cpp vmap.cpp)
32+
atexit.cpp elf_util.cpp native-lib.cpp smap.cpp solist.cpp vmap.cpp)
3333

3434
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC include)
3535
# Specifies libraries CMake should link to your target library. You

app/src/main/cpp/atexit.cpp

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
#include "atexit.hpp"
2+
3+
#include "elf_util.h"
4+
#include "logging.h"
5+
6+
template <typename T>
7+
inline T *getExportedFieldPointer(const SandHook::ElfImg &libc,
8+
const char *name) {
9+
auto *addr = reinterpret_cast<T *>(libc.getSymbAddress(name));
10+
11+
return addr == nullptr ? nullptr : addr;
12+
}
13+
14+
namespace Atexit {
15+
16+
bool AtexitArray::append_entry(const AtexitEntry &entry) {
17+
if (size_ >= capacity_ && !expand_capacity())
18+
return false;
19+
20+
size_t idx = size_++;
21+
22+
set_writable(true, idx, 1);
23+
array_[idx] = entry;
24+
++total_appends_;
25+
set_writable(false, idx, 1);
26+
27+
return true;
28+
}
29+
// Extract an entry and return it.
30+
AtexitEntry AtexitArray::extract_entry(size_t idx) {
31+
AtexitEntry result = array_[idx];
32+
33+
set_writable(true, idx, 1);
34+
array_[idx] = {};
35+
++extracted_count_;
36+
set_writable(false, idx, 1);
37+
38+
return result;
39+
}
40+
41+
void AtexitArray::recompact() {
42+
if (!needs_recompaction()) {
43+
LOGD("needs_recompaction returns false");
44+
// return;
45+
}
46+
47+
set_writable(true, 0, size_);
48+
49+
// Optimization: quickly skip over the initial non-null entries.
50+
size_t src = 0, dst = 0;
51+
while (src < size_ && array_[src].fn != nullptr) {
52+
++src;
53+
++dst;
54+
}
55+
56+
// Shift the non-null entries forward, and zero out the removed entries at the
57+
// end of the array.
58+
for (; src < size_; ++src) {
59+
const AtexitEntry entry = array_[src];
60+
array_[src] = {};
61+
if (entry.fn != nullptr) {
62+
array_[dst++] = entry;
63+
}
64+
}
65+
66+
// If the table uses fewer pages, clean the pages at the end.
67+
size_t old_bytes = page_end_of_index(size_);
68+
size_t new_bytes = page_end_of_index(dst);
69+
if (new_bytes < old_bytes) {
70+
madvise(reinterpret_cast<char *>(array_) + new_bytes, old_bytes - new_bytes,
71+
MADV_DONTNEED);
72+
}
73+
74+
set_writable(false, 0, size_);
75+
76+
size_ = dst;
77+
extracted_count_ = 0;
78+
}
79+
80+
// Use mprotect to make the array writable or read-only. Returns true on
81+
// success. Making the array read-only could protect against either
82+
// unintentional or malicious corruption of the array.
83+
void AtexitArray::set_writable(bool writable, size_t start_idx,
84+
size_t num_entries) {
85+
if (array_ == nullptr)
86+
return;
87+
88+
const size_t start_byte = page_start_of_index(start_idx);
89+
const size_t stop_byte = page_end_of_index(start_idx + num_entries);
90+
const size_t byte_len = stop_byte - start_byte;
91+
92+
const int prot = PROT_READ | (writable ? PROT_WRITE : 0);
93+
if (mprotect(reinterpret_cast<char *>(array_) + start_byte, byte_len, prot) !=
94+
0) {
95+
PLOGE("mprotect failed on atexit array: %m");
96+
}
97+
}
98+
99+
// Approximately double the capacity. Returns true if successful (no overflow).
100+
// AtexitEntry is smaller than a page, but this function should still be correct
101+
// even if AtexitEntry were larger than one.
102+
bool AtexitArray::next_capacity(size_t capacity, size_t *result) {
103+
if (capacity == 0) {
104+
*result = page_end(sizeof(AtexitEntry)) / sizeof(AtexitEntry);
105+
return true;
106+
}
107+
size_t num_bytes;
108+
if (__builtin_mul_overflow(page_end_of_index(capacity), 2, &num_bytes)) {
109+
PLOGE("__cxa_atexit: capacity calculation overflow");
110+
return false;
111+
}
112+
*result = num_bytes / sizeof(AtexitEntry);
113+
return true;
114+
}
115+
116+
bool AtexitArray::expand_capacity() {
117+
size_t new_capacity;
118+
if (!next_capacity(capacity_, &new_capacity))
119+
return false;
120+
const size_t new_capacity_bytes = page_end_of_index(new_capacity);
121+
122+
set_writable(true, 0, capacity_);
123+
124+
bool result = false;
125+
void *new_pages;
126+
if (array_ == nullptr) {
127+
new_pages = mmap(nullptr, new_capacity_bytes, PROT_READ | PROT_WRITE,
128+
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
129+
} else {
130+
// mremap fails if the source buffer crosses a boundary between two VMAs.
131+
// When a single array element is modified, the kernel should split then
132+
// rejoin the buffer's VMA.
133+
new_pages = mremap(array_, page_end_of_index(capacity_), new_capacity_bytes,
134+
MREMAP_MAYMOVE);
135+
}
136+
if (new_pages == MAP_FAILED) {
137+
PLOGE("__cxa_atexit: mmap/mremap failed to allocate %zu bytes: %m",
138+
new_capacity_bytes);
139+
} else {
140+
result = true;
141+
prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, new_pages, new_capacity_bytes,
142+
"atexit handlers");
143+
array_ = static_cast<AtexitEntry *>(new_pages);
144+
capacity_ = new_capacity;
145+
}
146+
set_writable(false, 0, capacity_);
147+
return result;
148+
}
149+
150+
AtexitArray *findAtexitArray() {
151+
SandHook::ElfImg libc("libc.so");
152+
auto p_array = getExportedFieldPointer<AtexitEntry *>(libc, "_ZL7g_array.0");
153+
auto p_size = getExportedFieldPointer<size_t>(libc, "_ZL7g_array.1");
154+
auto p_extracted_count =
155+
getExportedFieldPointer<size_t>(libc, "_ZL7g_array.2");
156+
auto p_capacity = getExportedFieldPointer<size_t>(libc, "_ZL7g_array.3");
157+
auto p_total_appends =
158+
getExportedFieldPointer<uint64_t>(libc, "_ZL7g_array.4");
159+
160+
if (p_array == nullptr || p_size == nullptr || p_extracted_count == nullptr ||
161+
p_capacity == nullptr || p_total_appends == nullptr) {
162+
LOGD("failed to find exported g_array fields in memory");
163+
return nullptr;
164+
}
165+
166+
return reinterpret_cast<AtexitArray *>(p_array);
167+
}
168+
169+
} // namespace Atexit
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#pragma once
2+
3+
#include <pthread.h>
4+
#include <stddef.h>
5+
#include <stdint.h>
6+
#include <stdlib.h>
7+
#include <string.h>
8+
#include <sys/auxv.h>
9+
#include <sys/cdefs.h>
10+
#include <sys/mman.h>
11+
#include <sys/param.h>
12+
#include <sys/prctl.h>
13+
#include <sys/user.h>
14+
15+
#include <memory>
16+
#include <sstream>
17+
18+
namespace Atexit {
19+
20+
inline size_t page_size() {
21+
#if defined(PAGE_SIZE)
22+
return PAGE_SIZE;
23+
#else
24+
static const size_t page_size = getauxval(AT_PAGESZ);
25+
return page_size;
26+
#endif
27+
}
28+
29+
// The maximum page size supported on any Android device. As
30+
// of API level 35, this is limited by ART.
31+
constexpr size_t max_android_page_size() {
32+
#if defined(PAGE_SIZE)
33+
return PAGE_SIZE;
34+
#else
35+
return 16384;
36+
#endif
37+
}
38+
39+
// Returns the address of the page containing address 'x'.
40+
inline uintptr_t page_start(uintptr_t x) { return x & ~(page_size() - 1); }
41+
42+
// Returns the offset of address 'x' in its page.
43+
inline uintptr_t page_offset(uintptr_t x) { return x & (page_size() - 1); }
44+
45+
// Returns the address of the next page after address 'x', unless 'x' is
46+
// itself at the start of a page.
47+
inline uintptr_t page_end(uintptr_t x) {
48+
return page_start(x + page_size() - 1);
49+
}
50+
51+
struct AtexitEntry {
52+
void (*fn)(void *); // the __cxa_atexit callback
53+
void *arg; // argument for `fn` callback
54+
void *dso; // shared module handle
55+
};
56+
57+
class AtexitArray {
58+
public:
59+
AtexitArray(AtexitEntry *existing_array, size_t current_size,
60+
size_t current_capacity, size_t initial_extracted_count,
61+
uint64_t initial_total_appends)
62+
: array_(existing_array), size_(current_size),
63+
extracted_count_(initial_extracted_count), capacity_(current_capacity),
64+
total_appends_(initial_total_appends) {}
65+
66+
std::string format_state_string() const {
67+
std::stringstream ss;
68+
69+
// Use the << operator to stream text and variables into the stringstream.
70+
// The '\n' and '\t' characters provide the desired logcat formatting.
71+
ss << "\n--- Live AtexitArray Snapshot State ---\n";
72+
ss << "\t(this pointer): " << static_cast<const void *>(this) << "\n";
73+
ss << "\tarray_: " << static_cast<const void *>(array_) << "\n";
74+
75+
// For numeric values, show both decimal and hexadecimal for easier
76+
// debugging.
77+
ss << "\tsize_: " << size_ << " (0x" << std::hex << size_
78+
<< std::dec << ")\n";
79+
ss << "\textracted_count_: " << extracted_count_ << " (0x" << std::hex
80+
<< extracted_count_ << std::dec << ")\n";
81+
ss << "\tcapacity_: " << capacity_ << " (0x" << std::hex << capacity_
82+
<< std::dec << ")\n";
83+
ss << "\ttotal_appends_: " << total_appends_ << " (0x" << std::hex
84+
<< total_appends_ << std::dec << ")\n";
85+
86+
ss << "---------------------------------------";
87+
88+
// The .str() method returns the complete, concatenated string.
89+
return ss.str();
90+
}
91+
92+
size_t size() const { return size_; }
93+
uint64_t total_appends() const { return total_appends_; }
94+
const AtexitEntry &operator[](size_t idx) const { return array_[idx]; }
95+
96+
bool append_entry(const AtexitEntry &entry);
97+
AtexitEntry extract_entry(size_t idx);
98+
void recompact();
99+
100+
private:
101+
AtexitEntry *array_;
102+
size_t size_;
103+
size_t extracted_count_;
104+
size_t capacity_;
105+
106+
// An entry can be appended by a __cxa_finalize callback. Track the number of
107+
// appends so we restart concurrent __cxa_finalize passes.
108+
uint64_t total_appends_;
109+
110+
static size_t page_start_of_index(size_t idx) {
111+
return page_start(idx * sizeof(AtexitEntry));
112+
}
113+
static size_t page_end_of_index(size_t idx) {
114+
return page_end(idx * sizeof(AtexitEntry));
115+
}
116+
117+
// Recompact the array if it will save at least one page of memory at the end.
118+
bool needs_recompaction() const {
119+
return page_end_of_index(size_ - extracted_count_) <
120+
page_end_of_index(size_);
121+
}
122+
123+
void set_writable(bool writable, size_t start_idx, size_t num_entries);
124+
static bool next_capacity(size_t capacity, size_t *result);
125+
bool expand_capacity();
126+
};
127+
128+
AtexitArray *findAtexitArray();
129+
130+
} // namespace Atexit

app/src/main/cpp/native-lib.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#include "atexit.hpp"
12
#include "logging.h"
23
#include "smap.h"
34
#include "solist.hpp"
@@ -17,6 +18,10 @@ Java_org_matrix_demo_MainActivity_stringFromJNI(JNIEnv *env,
1718
VirtualMap::MapInfo *abnormal_vmap = VirtualMap::DetectInjection();
1819
size_t module_injected = SoList::DetectModules();
1920
VirtualMap::DumpStackStrings();
21+
auto g_array = Atexit::findAtexitArray();
22+
if (g_array != nullptr) {
23+
LOGD("g_array status: %s", g_array->format_state_string().c_str());
24+
}
2025

2126
if (abnormal_soinfo != nullptr) {
2227
solist_detection =

0 commit comments

Comments
 (0)