-
Notifications
You must be signed in to change notification settings - Fork 52
Expand file tree
/
Copy pathproc_kallsyms.c
More file actions
149 lines (136 loc) · 5.36 KB
/
Copy pathproc_kallsyms.c
File metadata and controls
149 lines (136 loc) · 5.36 KB
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// This file is part of KASLD - https://github.com/bcoles/kasld
//
// Retrieve kernel _text (image base) and _stext symbols from /proc/kallsyms
//
// Based on original code by spender:
// https://grsecurity.net/~spender/exploits/exploit.txt
//
// Requires:
// - kernel.kptr_restrict = 0 (Default on Debian <= 9 systems)
//
// On modern kernels, kptr_restrict = 0 alone is insufficient.
// /proc/kallsyms uses kallsyms_show_value() (evaluated at open time)
// to gate address visibility. This requires CAP_SYSLOG, or
// perf_event_paranoid <= 1 (with kptr_restrict = 0), to reveal
// addresses. Without these, all addresses appear as zero.
//
// Leak primitive:
// Data leaked: kernel symbol virtual addresses (_stext, etc.)
// Kernel subsystem: kernel/kallsyms — /proc/kallsyms
// Data structure: kernel symbol table (struct kallsym_iter)
// Address type: virtual (kernel text / data)
// Method: parsed (symbol table read)
// Status: gated by design (kptr_restrict)
// Access check: kallsyms_show_value() checks kptr_restrict + CAP_SYSLOG
// Source: https://elixir.bootlin.com/linux/v6.12/source/kernel/kallsyms.c
//
// Mitigations:
// kernel.kptr_restrict >= 1 masks addresses (mainline defaults to 0;
// Debian/Ubuntu ship 1).
// Bypass requires CAP_SYSLOG or (kptr_restrict=0 + perf_event_paranoid<=1).
// On modern kernels, kallsyms_show_value() checks at open() time.
// ---
// <bcoles@gmail.com>
#include "include/kasld/api.h"
#include "include/kasld/cli.h"
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
KASLD_EXPLAIN(
"Reads kernel symbol virtual addresses from /proc/kallsyms. When "
"kernel.kptr_restrict is 0 (or the reader has CAP_SYSLOG), symbol "
"addresses are printed in full. The _stext symbol gives the kernel "
"text base directly. Distributions such as Debian and Ubuntu set "
"kptr_restrict to 1 (mainline defaults to 0), hiding addresses from "
"unprivileged users.");
KASLD_META("method:parsed\n"
"phase:inference\n"
"discloses:virtual\n"
"sysctl:kptr_restrict>=1\n"
"bypass:CAP_SYSLOG\n");
int main(void) {
/* Pre-check: is /proc/kallsyms readable? */
FILE *f = kasld_fopen("/proc/kallsyms", "r");
if (!f)
return (errno == EACCES || errno == EPERM) ? KASLD_EXIT_NOPERM
: KASLD_EXIT_UNAVAILABLE;
/* Detect kptr_restrict: when restricted, ALL addresses are 0.
* Some symbols (e.g. __per_cpu_start) are legitimately at address 0,
* so check several lines — if every address is 0, the read is restricted. */
char buf[64];
int all_zero = 1;
for (int i = 0; i < 16 && fgets(buf, sizeof(buf), f); i++) {
unsigned long test;
const char *e;
int ok = kasld_addr_parse(buf, 16, &test, &e);
if (ok && test != 0) {
all_zero = 0;
break;
}
/* A refusal that consumed digits means the address was too wide for this
* build, not that the kernel masked it — a masked line parses cleanly as
* zero and must keep the scan going. */
if (!ok && e != buf) {
all_zero = 0;
break;
}
}
fclose(f);
if (all_zero)
return KASLD_EXIT_NOPERM;
unsigned long text = 0, stext = 0, etext = 0;
FILE *ks = kasld_fopen("/proc/kallsyms", "r");
if (ks) {
unsigned long a;
char line[512], type, sym[256];
kasld_info("scanning /proc/kallsyms for _text, _stext and _etext ...");
/* Read line-wise rather than with a "%lx" field: a symbol address wider
* than this build's word must be refused, and scanf would hand back a
* truncated one that looks like a valid base. */
while (fgets(line, sizeof(line), ks)) {
const char *e;
if (!kasld_addr_parse(line, 16, &a, &e))
continue;
if (sscanf(e, " %c %255s", &type, sym) != 2)
continue;
if (!text && strcmp(sym, "_text") == 0)
text = a;
else if (!stext && strcmp(sym, "_stext") == 0)
stext = a;
else if (!etext && strcmp(sym, "_etext") == 0)
etext = a;
if (text && stext && etext)
break;
}
fclose(ks);
}
if (!text && !stext) {
kasld_err("neither _text nor _stext found in /proc/kallsyms");
return 0;
}
/* Each symbol is reported on its own. They are read independently and either
* can be absent -- older mips exports _stext but no _text -- so neither may
* gate the other: a symbol that was read is a fact already in hand, and
* discarding it because its neighbour is missing loses the strongest witness
* there is.
*
* _text IS the kernel image base — emit it directly (KERNEL_IMAGE) so the
* engine anchors the image base on the real symbol, with no reliance on the
* compile-time head gap. _stext is also reported (KERNEL_TEXT) for provenance
* and as the source for arches/sources that only expose _stext. */
if (text) {
kasld_info("kernel image base (_text): 0x%lx", text);
kasld_result_base(KASLD_TYPE_VIRT, REGION_KERNEL_IMAGE, text, "_text",
CONF_PARSED);
}
if (stext) {
kasld_info("kernel text start (_stext): 0x%lx", stext);
kasld_info("possible kernel base: 0x%lx", kasld_floor_text_base(stext));
kasld_result_base(KASLD_TYPE_VIRT, REGION_KERNEL_TEXT, stext, "_stext",
CONF_PARSED);
}
if (etext)
kasld_info("kernel text end (_etext): 0x%lx", etext);
return 0;
}