-
Notifications
You must be signed in to change notification settings - Fork 52
Expand file tree
/
Copy pathksymoff
More file actions
executable file
·392 lines (375 loc) · 16.7 KB
/
Copy pathksymoff
File metadata and controls
executable file
·392 lines (375 loc) · 16.7 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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
#!/usr/bin/env bash
# This file is part of KASLD - https://github.com/bcoles/kasld
#
# Apply a KASLR slide to kernel symbols, derive the slide and text base
# from a known runtime symbol address, or translate physical <-> virtual
# addresses through the direct map. Works against System.map,
# `/proc/kallsyms` snapshots, and vmlinux ELF (debug-symbols enabled).
#
# Synopsis:
# ksymoff [-b BASE | --from NAME=ADDR] -s SOURCE [--slide] [SYMBOL ...]
# ksymoff --page-offset BASE { --phys2virt ADDR | --virt2phys ADDR }
# ksymoff --vmemmap BASE [--page-size N] [--page-shift N] --phys2page ADDR
#
# Modes
# -----
# Forward Given the runtime text base, print runtime addresses for
# one or more symbols, or for every symbol in the source
# when no symbols are listed.
#
# Inverse Given a known runtime symbol address (--from NAME=ADDR),
# derive the runtime text base. Combine with positional
# symbol names to chain into forward lookup — one leaked
# address yields many.
#
# --slide Print only the KASLR slide (signed hex). Combines with
# either mode.
#
# Physical Translate physical <-> virtual through the direct map, for
# data-only pivots (Dirty Pagetable, cross-cache) that work in
# physical addresses. Needs a base, not a symbol source:
# --phys2virt ADDR virt = phys + page_offset_base
# --virt2phys ADDR phys = virt − page_offset_base
# --phys2page ADDR struct page = vmemmap_base + pfn * page-size
#
# Supply the runtime text base one of two mutually exclusive ways, or let it
# fall back to stdin:
# --from NAME=ADDR derived from any known runtime symbol address
# -b BASE a hex address (with or without 0x prefix)
# (--from and -b are alternatives; giving both is an error)
# stdin fallback: a `kasld -1` line piped in (uses `text=` field)
#
# Usage
# -----
# # Forward: look up specific symbols
# ksymoff -b 0xffffffff82200000 -s System.map commit_creds
# ksymoff -b 0xffffffff82200000 -s vmlinux commit_creds prepare_kernel_cred
# kasld -1 | ksymoff -s System.map commit_creds
#
# # Forward: dump every symbol with the slide applied
# ksymoff -b 0xffffffff82200000 -s System.map
#
# # Slide only
# ksymoff -b 0xffffffff82200000 -s System.map --slide
# ksymoff --from commit_creds=0xffffffff82345678 -s System.map --slide
#
# # Inverse: derive the runtime text base from one known symbol
# ksymoff --from commit_creds=0xffffffff82345678 -s System.map
#
# # Inverse + forward: one leak yields many symbols
# ksymoff --from commit_creds=0xffffffff82345678 -s vmlinux \
# prepare_kernel_cred init_cred modprobe_path
#
# # Physical <-> virtual through the direct map (data-only pivots)
# ksymoff --page-offset 0xffff9e8000000000 --phys2virt 0x34600000
# ksymoff --page-offset 0xffff9e8000000000 --virt2phys 0xffff9e8034600000
# kasld -1 | ksymoff --phys2virt 0x34600000 # reads the `dmap=` field
#
# # Physical address -> its struct page (page-table attacks)
# ksymoff --vmemmap 0xffffea0000000000 --phys2page 0x34600000
#
# Physical translation assumes 4 KiB pages and a 64-byte struct page; override
# with --page-shift / --page-size (KASLD's `page_size` and `btf_struct_page_size`
# facts report both for the target).
#
# Caveat: assumes a SINGLE KASLR slide — i.e. canonical (source/link-order)
# function layout, where every symbol sits at a fixed offset from the text base.
# This is FALSE on reordered-text kernels: under FG-KASLR each function is moved
# independently per boot (no single slide — one leak pins only that one symbol),
# and under LTO / AutoFDO / Propeller the offsets come from that exact build only
# (a generic version-level System.map mis-resolves). Check first with
# `kasld -H` ("Function layout" / "symbol resolution") or the `text_order` field
# in `kasld -H -j`: proceed only for `canonical`; for a static reorder use the
# build_id-matched map; for per-boot do not use ksymoff for chaining at all.
#
# Options
# -------
# -b, --base BASE Runtime text base (hex, with or without 0x).
# -s, --symbols FILE Symbol source: System.map, kallsyms dump, or
# vmlinux ELF. Required.
# --from NAME=ADDR Known runtime symbol address; derives base.
# Mutually exclusive with -b.
# --slide Print only the slide.
# --page-offset BASE Direct-map base (page_offset_base) for --phys2virt /
# --virt2phys; or piped from `kasld -1` (`dmap=` field).
# --phys2virt ADDR Print the direct-map virtual address of a phys addr.
# --virt2phys ADDR Print the physical address of a direct-map virt addr.
# --vmemmap BASE vmemmap base (struct page array); for --phys2page.
# --page-size N sizeof(struct page) (default 64).
# --page-shift N Page shift, log2(page size) (default 12 = 4 KiB).
# --phys2page ADDR Print the `struct page *` for a physical address.
# -h, --help Show this help and exit.
#
# Symbol file
# -----------
# `-s` accepts System.map, a kallsyms dump, or a vmlinux ELF. The
# file must correspond to the running kernel; a mismatch produces
# wrong addresses without error.
#
# The KASLD `kernel_notes_buildid` component prints the GNU build_id
# from /sys/kernel/notes. `readelf -n vmlinux` prints the build_id
# embedded in a vmlinux. Equal values mean the vmlinux matches the
# running kernel.
#
# Sources for a matching vmlinux include: an offline build of the
# running kernel's source tree; a captured System.map from install
# time; the distro debug-symbols package when the target runs a
# stock distro kernel and the analysis host can reach that distro's
# archive (linux-image-$(uname -r)-dbgsym on Debian/Ubuntu,
# kernel-debuginfo-$(uname -r) on Fedora/RHEL); `debuginfod-find
# debuginfo <build_id>` when the kernel falls within a debuginfod
# server's coverage window.
#
# Requires bash for 64-bit hex arithmetic (dash clamps to LONG_MAX).
# Requires nm(1) or readelf(1) only when the symbol source is ELF.
# ---
# <bcoles@gmail.com>
set -e
die() { printf 'ksymoff: %s\n' "$1" >&2; exit 1; }
have() { command -v "$1" >/dev/null 2>&1; }
# Unsigned less-than for up-to-64-bit hex (bash arithmetic is signed, so kernel
# addresses with the top bit set miscompare). Zero-pad to 16 and compare
# lexically — byte order on equal-width lowercase hex is unsigned order.
hex_ult() { # a b (hex, no 0x) -> true if a < b unsigned
local a b
a=$(printf '%016s' "${1,,}"); a=${a// /0}
b=$(printf '%016s' "${2,,}"); b=${b// /0}
[[ "$a" < "$b" ]] && return 0
return 1
}
# SECURITY: any value interpolated into `$(( 0x$x ))` must be pure hex/decimal.
# bash arithmetic evaluates embedded command substitutions and array subscripts,
# so a value like `a[$(cmd)]` from an untrusted symbol source would execute as a
# command. Every arithmetic input is validated by one of these first. (The same
# guard is duplicated in extra/check-results — deliberately no shared library:
# ksymoff installs to $PREFIX/bin and check-results is copied to a target, so
# neither can source one. A correction here does NOT reach numeric()/hex16()
# there -- review all four together. The accept sets differ on purpose (is_hex
# refuses an 0x prefix, its callers having stripped one; is_dec is decimal-only
# for --page-shift and --page-size); what none may do is accept a shell
# metacharacter, which tests/check-validators asserts against a shared
# adversarial corpus.
#
# @arith-validator
is_hex() { case "$1" in ''|*[!0-9A-Fa-f]*) return 1 ;; *) return 0 ;; esac; }
# @arith-validator
is_dec() { case "$1" in ''|*[!0-9]*) return 1 ;; *) return 0 ;; esac; }
# -- Argument parsing -----------------------------------------------------
base="" symfile="" slide_only=0
from_sym="" from_addr=""
symbols=()
page_offset="" vmemmap="" page_size=64 page_shift=12
phys2virt="" virt2phys="" phys2page=""
while [[ $# -gt 0 ]]; do
case "$1" in
-b|--base)
[[ $# -ge 2 ]] || die "$1 requires an argument"
base="$2"; shift 2 ;;
-s|--symbols)
[[ $# -ge 2 ]] || die "$1 requires an argument"
symfile="$2"; shift 2 ;;
--slide)
slide_only=1; shift ;;
--from)
[[ $# -ge 2 ]] || die "$1 requires NAME=ADDR"
[[ "$2" == *=* ]] || die "bad --from value: expected NAME=ADDR"
from_sym="${2%%=*}"
from_addr="${2#*=}"
from_addr="${from_addr#0[xX]}"
is_hex "$from_addr" \
|| die "bad --from address: ${2#*=}"
[[ "$from_sym" =~ ^[A-Za-z_][A-Za-z0-9_.]*$ ]] \
|| die "bad --from symbol name: $from_sym"
shift 2 ;;
--page-offset)
[[ $# -ge 2 ]] || die "$1 requires BASE"
page_offset="$2"; shift 2 ;;
--vmemmap)
[[ $# -ge 2 ]] || die "$1 requires BASE"
vmemmap="$2"; shift 2 ;;
--page-size)
[[ $# -ge 2 ]] || die "$1 requires N"
page_size="$2"; shift 2 ;;
--page-shift)
[[ $# -ge 2 ]] || die "$1 requires N"
page_shift="$2"; shift 2 ;;
--phys2virt)
[[ $# -ge 2 ]] || die "$1 requires ADDR"
phys2virt="$2"; shift 2 ;;
--virt2phys)
[[ $# -ge 2 ]] || die "$1 requires ADDR"
virt2phys="$2"; shift 2 ;;
--phys2page)
[[ $# -ge 2 ]] || die "$1 requires ADDR"
phys2page="$2"; shift 2 ;;
-h|--help)
sed -n '2,/^[^#]/{ /^#/s/^# \?//p; }' "$0"
exit 0 ;;
--)
shift
while [[ $# -gt 0 ]]; do symbols+=("$1"); shift; done ;;
-*)
die "unknown option: $1" ;;
*)
symbols+=("$1"); shift ;;
esac
done
# -- Physical-address translation (data-only pivots) ----------------------
# Independent of the symbol modes: needs a base, not a symbol source. For
# data-only exploits that work in physical addresses (Dirty Pagetable,
# cross-cache):
# virt = phys + page_offset_base (--phys2virt)
# phys = virt − page_offset_base (--virt2phys)
# struct page = vmemmap_base + pfn * page-size (--phys2page)
n_phys=0
for v in "$phys2virt" "$virt2phys" "$phys2page"; do
[[ -n "$v" ]] && n_phys=$((n_phys + 1))
done
if (( n_phys > 0 )); then
(( n_phys == 1 )) || die "use one of --phys2virt / --virt2phys / --phys2page"
[[ -z "$symfile" ]] || die "physical translation takes no symbol source (-s)"
[[ -z "$base" && -z "$from_sym" ]] \
|| die "physical translation does not combine with -b/--from"
# struct page needs only the vmemmap base and the page geometry.
if [[ -n "$phys2page" ]]; then
[[ -n "$vmemmap" ]] || die "--phys2page needs --vmemmap BASE (struct page array base)"
vm=${vmemmap#0[xX]}
is_hex "$vm" || die "bad --vmemmap: $vmemmap"
{ is_dec "$page_size" && (( page_size > 0 )); } || die "bad --page-size: $page_size"
{ is_dec "$page_shift" && (( page_shift > 0 )); } || die "bad --page-shift: $page_shift"
pa=${phys2page#0[xX]}
is_hex "$pa" || die "bad --phys2page address: $phys2page"
pfn=$(( 0x$pa >> page_shift ))
printf '%016x\n' $(( 0x$vm + pfn * page_size ))
exit 0
fi
# phys<->virt need the direct-map base: --page-offset, else stdin `dmap=`.
po=""
if [[ -n "$page_offset" ]]; then
po=${page_offset#0[xX]}
is_hex "$po" || die "bad --page-offset: $page_offset"
elif [[ ! -t 0 ]]; then
IFS= read -r line || die "stdin: no input (expected \`kasld -1\`)"
if [[ "$line" =~ [[:space:]]dmap=0x([0-9a-fA-F]+) ]]; then
po="${BASH_REMATCH[1]}"
else
die "no \`dmap=0x...\` field in stdin (direct-map base unresolved? \`dmap=na\`)"
fi
else
die "no direct-map base — use --page-offset BASE or pipe \`kasld -1\`"
fi
if [[ -n "$phys2virt" ]]; then
pa=${phys2virt#0[xX]}
is_hex "$pa" || die "bad --phys2virt address: $phys2virt"
printf '%016x\n' $(( 0x$pa + 0x$po ))
else
va=${virt2phys#0[xX]}
is_hex "$va" || die "bad --virt2phys address: $virt2phys"
hex_ult "$va" "$po" && die "virtual address is below the direct-map base"
printf '0x%x\n' $(( 0x$va - 0x$po ))
fi
exit 0
fi
[[ -n "$symfile" ]] || die "no symbol source (use -s SOURCE)"
[[ -f "$symfile" ]] || die "symbol file not found: $symfile"
[[ -n "$from_sym" && -n "$base" ]] \
&& die "cannot combine -b/--base with --from"
# -- ELF symbol extraction ------------------------------------------------
# Normalise an ELF symbol source into `addr type name` format. Tries
# nm (.symtab), nm -D (.dynsym), then readelf -sW. Fails if all three
# yield empty output (a stripped binary without a usable symbol table).
if file "$symfile" 2>/dev/null | grep -q ELF; then
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
if have nm && nm "$symfile" 2>/dev/null > "$tmp" && [[ -s "$tmp" ]]; then :
elif have nm && nm -D "$symfile" 2>/dev/null > "$tmp" && [[ -s "$tmp" ]]; then :
elif have readelf && readelf -sW "$symfile" 2>/dev/null \
| awk 'NF>=8 && $2 ~ /^[0-9a-fA-F]+$/ { printf "%s %s %s\n", $2, substr($4,1,1), $8 }' \
> "$tmp" && [[ -s "$tmp" ]]; then :
else
die "no usable symbols in ELF (stripped binary? install matching debug symbols)"
fi
symfile="$tmp"
fi
# -- Locate the default text base ----------------------------------------
# _text is the canonical entry symbol on every architecture; _stext and
# startup_64 are fallbacks for unusual sources (older kallsyms with
# section symbols stripped, x86_64-only dumps, etc.).
default_hex=""
for sym in _text _stext startup_64; do
default_hex=$(awk -v s="$sym" '$2 ~ /^[Tt]$/ && $3 == s { print $1; exit }' "$symfile")
[[ -n "$default_hex" ]] && break
done
[[ -n "$default_hex" ]] || die "no _text / _stext / startup_64 in symbol source"
# Security: this value is interpolated into `$(( 0x$default_hex ... ))`. A
# non-hex value from a hostile symbol source (e.g. `a[$(cmd)]`) would be
# executed by bash arithmetic evaluation, so require strict hex before use.
is_hex "$default_hex" \
|| die "malformed _text address in symbol source (expected hex)"
# -- Resolve runtime text base and KASLR slide ---------------------------
if [[ -n "$from_sym" ]]; then
# Inverse: slide = runtime_addr − default_addr_of(NAME)
sym_default=$(awk -v s="$from_sym" '$3 == s { print $1; exit }' "$symfile")
[[ -n "$sym_default" ]] || die "symbol not in source: $from_sym"
# Security: validate before arithmetic — see the _text check above.
is_hex "$sym_default" \
|| die "malformed address for $from_sym in symbol source (expected hex)"
slide=$(( 0x$from_addr - 0x$sym_default ))
base_hex=$(printf '%x' $(( 0x$default_hex + slide )))
else
# Forward: base from -b or stdin; slide = base − default _text
if [[ -z "$base" ]]; then
[[ ! -t 0 ]] || die "no runtime base supplied — use -b BASE, --from NAME=ADDR, or pipe \`kasld -1\` on stdin"
if ! IFS= read -r line; then
die "stdin: no input (expected \`kasld -1\` output)"
fi
# Anchor on whitespace so we match the virtual `text=` field and not
# the substring inside `ptext=` (the physical base — wrong slide).
if [[ "$line" =~ [[:space:]]text=0x([0-9a-fA-F]+) ]]; then
base="0x${BASH_REMATCH[1]}"
else
die "no virtual \`text=0x...\` field in stdin (ptext alone cannot derive the virt slide)"
fi
fi
base_hex="${base#0[xX]}"
is_hex "$base_hex" || die "bad runtime base: $base"
slide=$(( 0x$base_hex - 0x$default_hex ))
fi
# -- Output ---------------------------------------------------------------
if (( slide_only )); then
(( slide >= 0 )) && printf '+0x%x\n' "$slide" \
|| printf -- '-0x%x\n' $(( -slide ))
exit 0
fi
# --from with no positional symbols: print just the derived base.
if [[ -n "$from_sym" && ${#symbols[@]} -eq 0 ]]; then
printf '0x%s\n' "$base_hex"
exit 0
fi
# Look up specific symbols, or dump every symbol with the slide applied.
if (( ${#symbols[@]} > 0 )); then
for sym in "${symbols[@]}"; do
line=$(awk -v s="$sym" '$3 == s { print; exit }' "$symfile")
if [[ -z "$line" ]]; then
printf '# not found: %s\n' "$sym" >&2
continue
fi
read -r addr_hex sym_type _ <<< "$line"
# Security: validate before arithmetic — a non-hex address from a hostile
# symbol source would otherwise be executed by bash arithmetic evaluation.
if ! is_hex "$addr_hex"; then
printf '# malformed address for: %s\n' "$sym" >&2
continue
fi
# Strip control characters from the file-derived type: a hostile symbol
# source must not inject terminal escape sequences into the output (CWE-150).
printf '%016x %s %s\n' $(( 0x$addr_hex + slide )) "${sym_type//[[:cntrl:]]/}" "$sym"
done
else
while IFS=' ' read -r addr sym_type name _; do
is_hex "$addr" || continue
[[ -n "$name" ]] || continue
# Strip control characters from file-derived fields (see the per-symbol path).
printf '%016x %s %s\n' $(( 0x$addr + slide )) "${sym_type//[[:cntrl:]]/}" "${name//[[:cntrl:]]/}"
done < "$symfile"
fi