-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathzapper.c
1444 lines (1282 loc) · 47.5 KB
/
zapper.c
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
/*
* https://www.thc.org
*
* Destroy all options and environment (/proc/<PID>/environ) and make the process
* appear as a different process in the process list (ps -eF f).
*
* This tool does _NOT_ use LD_PRELOAD but ptrace() instead, allowing its
* magic working on static binaries (like those generated by GoLang).
*
* It's library agnostic and directly screws with the Kernel's
* elf-table (located on the stack) after each return from SYS_execve().
*
* Compile:
* gcc -o zapper zapper.c
*/
/* Security:
* - The process name and options may show for a few milliseconds before
* the Kernel schedules zapper to zap them. (the only way around this is
* a trampoline app and passing the options via env and then recontructing
* the argv during EVENT_EXEC.)
* - Some apps will show the process name as well as /proc/PID/exe (the
* executeable filename) - which is not hidden (see "Full Privacy" below).
* - Does not work on +s binaries (EUID,EGID).
*/
// See also:
// - https://github.com/strace/strace/blob/master/doc/README-linux-ptrace
// - https://manpages.debian.org/bookworm/manpages-dev/ptrace.2.en.html
// - https://elixir.bootlin.com/linux/v5.19.17/source/include/uapi/linux/ptrace.h
// - https://elixir.bootlin.com/linux/v5.19.17/source/include/uapi/asm-generic/siginfo.h
// TODO:
// * Follow (-f) from a separate process. Current '-f' makes zapper
// the parent to all tracees (like strace does):
// ptrace_scope > 0 prevents the tracer (zapper) to be a separate process
// that is not a parent of the tracees.
// We could set prctl(, PR_SET_PTRACER_ANY) in zapper before execve() of the
// tracee but that flag is not inherited if the tracee forks another process.
// The way around this to either inject 'prctl(, PR_SET_PTRACER_ANY)' into
// the tracee or hook SYS_execve() and execute any new process via a
// trampoline program (zapper) to set prctl before calling execve on the
// original program.
// * -x to zap argv/env from an existing process: Search through .stack
// and .heap and modify any pointer that points inside argv[] region.
// Copy old argv[] to unused stack region that Linux creates to randomize
// its stack.
// * Use spare stack space that Linux creates to randomize .stack
// * Start from /dev/shm and unlink() the binary afterwards to hide binary.
// Needs trampoline program to also do this for all childs.
// * Periodically rename argv[0]
// * pick argv[0] at random
// * PPID=1: Make all tracee's PPID's to be 1 and proxy the SIGCHLD to correct
// pid: Double-fork via trampoline app.
// * Full Privacy: use a shell function "zap(){ ...; }" that embeds $@ into the
// environment (Z0=argv[0], Z1=argv[1],...Zn=argv[n]) and then calls zapper.
// Zapper then unpacks the Z0..Zn and puts the on the 'new stack'. This way
// the options wont show up in 'ps' at all (not even for a few milliseconds
// between the execve() and ptrace() call.
#ifndef ZVERSION
# define ZVERSION "1.1"
#endif
#define _GNU_SOURCE
#include <sched.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/mman.h>
#include <linux/ptrace.h>
#include <elf.h>
#include <syscall.h>
#include <stdio.h>
#include <stdarg.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>
// Dump all Pre-processor defines: echo | gcc -dM -E - | grep -i arm
#if defined(__ARM_ARCH)
# if __ARM_ARCH==6
# error "ARM6 not supported. Convince me to add support :>" // FIXME
// struct user_regs_struct {
// unsigned long int uregs[18];
// };
// # define SP(reg) (reg).uregs[13]
# else
# define SP(reg) (reg).sp
# endif
#else // __ARM_ARCH
# if __WORDSIZE == 64
# define SP(reg) (reg).rsp
// #define SYSNO(reg) (reg).orig_rax
// #define AX(reg) (reg).rax
// #define IP(reg) (reg).rip
# else
# define SP(reg) (reg).esp
// #define SYSNO(reg) (reg).orig_eax
// #define AX(reg) (reg).eax
// #define IP(reg) (reg).eip
# endif
#endif
// https://elixir.bootlin.com/linux/latest/source/kernel/pid.c#L64
#ifndef RESERVED_PIDS
# define RESERVED_PIDS 300
#endif
static union u {
long val;
char c[sizeof (long)];
} data;
// ANSI color codes.
#define CDR "\033[0;31m"
#define CDG "\033[0;32m"
#define CDY "\033[0;33m"
#define CDB "\033[0;34m"
#define CDM "\033[0;35m"
#define CDC "\033[0;36m"
#define CR "\033[1;31m"
#define CG "\033[1;32m"
#define CY "\033[1;33m"
#define CN "\033[0m"
#define CB "\033[1;34m"
#define CM "\033[1;35m"
#define CC "\033[1;36m"
#define CW "\033[1;37m"
#define ERREXIT(code, a...) do{fprintf(stderr, a); exit(code);}while(0)
#define XFAIL(expr, fmt, ...) do { \
if (expr) { \
fprintf(stderr, "%s:%d:%s() ASSERT(%s): " fmt, __FILE__, __LINE__, __func__, #expr, ##__VA_ARGS__); \
exit(255); \
} \
} while (0)
#ifdef DEBUG
FILE *out;
# define DEBUGF(fmt, ...) do{if (!out) out=stderr; fprintf(out, "[DEBUG %s:%d] " fmt, __FILE__, __LINE__, ##__VA_ARGS__); fflush(out);}while(0)
#else
# define DEBUGF(fmt, ...)
#endif
#ifndef MAX
# define MAX(X, Y) (((X) < (Y)) ? (Y) : (X))
#endif
#ifndef MIN
# define MIN(X, Y) (((X) < (Y)) ? (X) : (Y))
#endif
#define GOTOERR(a...) do { \
DEBUGF(a); \
goto err; \
} while (0)
#define FL_FOLLOW (0x01)
#define FL_STAY_ATTACHED (0x02)
#define FL_FORCE_TRACER_IS_PARENT (0x04)
#define IS_TRACER_IS_PARENT (0x08)
#define FL_ZAP_ENV (0x10)
#define IS_SIGNAL_PROXY (0x20)
#define FL_PRGNAME (0x40)
#define FL_DRYRUN (0x80)
#define FL_IS_CURSOR_OFF (0x200)
#define FL_IS_QUIET (0x400)
char *g_cur_prg_name;
pid_t g_pid;
int g_flags;
pid_t g_pid_master;
pid_t g_pid_zapper;
struct iovec g_iov; // PT registers
struct user_regs_struct g_regs;
// Fast Forwarding pids:
#ifdef DEBUG
# define MAX_WORKERS (3)
#else
# define MAX_WORKERS (8)
#endif
pid_t g_max_pid;
pid_t *g_ff_pidp; // mmap() between all workers. Atomic.
pid_t g_ff_next_pid;
int g_ff_opt_workers;
char stack[1024];
static int fast_forward_pid(pid_t target);
static void
dumpfile(const char *file, void *data, size_t n)
{
#ifdef DEBUG
FILE *fp;
fp = fopen(file, "wb");
fwrite(data, n, 1, fp);
fclose(fp);
#endif
}
static void
init_vars()
{
g_pid_zapper = getpid();
g_flags |= FL_STAY_ATTACHED;
g_flags |= FL_ZAP_ENV;
g_ff_opt_workers = MAX_WORKERS;
g_iov.iov_base = &g_regs;
g_iov.iov_len = sizeof g_regs;
#ifdef DEBUG
if (getenv("DEBUG_LOG")) {
out = fopen(getenv("DEBUG_LOG"), "wb");
if (!out)
DEBUGF("fopen(): %s\n", strerror(errno));
}
#endif
}
static void
cb_signal(int sig) {
if (g_pid_master <= 0)
return;
kill(g_pid_master, sig);
}
static void
set_proxy_signals(void) {
g_flags |= IS_SIGNAL_PROXY;
signal(SIGHUP, cb_signal);
signal(SIGINT, cb_signal);
signal(SIGQUIT, cb_signal);
signal(SIGUSR1, cb_signal);
signal(SIGUSR2, cb_signal);
signal(SIGPIPE, cb_signal);
signal(SIGTERM, cb_signal);
signal(SIGURG, cb_signal);
signal(SIGWINCH, cb_signal);
}
static pid_t
read_max_pid(void) {
char buf[1024];
size_t sz;
pid_t max = 4194304;
FILE *fp = fopen("/proc/sys/kernel/pid_max", "rb");
if (!fp)
return 4194304; // Educated guess.
sz = fread(buf, 1, sizeof buf, fp);
fclose(fp);
if (sz <= 0)
return 4194304;
max = atoi(buf);
if (max <= 300) // Cant be true.
return 4194304;
return max;
}
// It is faster to demove the \x1b...m after if no tty (no color)
static void
cprintf(FILE *fp, const char *fmt, ...) {
va_list ap;
char *buf;
char *ptr;
char *dst;
char *end;
static int tty_mode;
int ret;
// Check once if STDOUT is a TTY (e.g. color output)
if (tty_mode == 0) {
tty_mode = -1; // NO COLOR
if (isatty(STDOUT_FILENO))
tty_mode = 1; // COLOR
}
va_start(ap, fmt);
if (tty_mode == 1) {
// Color output & return.
vfprintf(fp, fmt, ap);
goto end;
}
// HERE: Not a TTY => Remove color ANSI codes from string.
buf = malloc(4096);
ptr = buf;
dst = buf;
ret = vsnprintf(buf, 4096, fmt, ap);
if (ret >= 4096) {
buf = realloc(buf, ret + 1);
ptr = buf;
ret = vsnprintf(buf, ret + 1, fmt, ap);
}
dst = buf;
ptr = buf;
end = buf + ret + 1;
// Filter out the colors
while (ptr < end) {
if (*ptr == '\x1b') {
ptr++;
while (ptr < end) {
if (*ptr == 'm') {
ptr++;
break;
}
ptr++;
}
}
*dst = *ptr;
if (*ptr == '\0')
break;
ptr++;
dst++;
}
fprintf(fp, "%s", buf);
free(buf);
end:
va_end(ap);
}
static void
usage(void) {
#ifndef STEALTH
cprintf(stderr, "Version v%s [%s]\n\
"CG"Hide command options and clear the environment of a command."CN"\n\
\n\
./zapper [-fE] [-n pid] [-a name] command ...\n\
-a <name> Rename the process to 'name'. (Use -a- for empty string).\n\
-f Zap all child processes as well (follow).\n\
-E Do not zap the environment variables.\n\
-n <pid> Fast forward to this pid (-n 300 is often the smallest possible)\n\
\n\
Example - Start ssh but zap all options (only 'ssh' shows)\n\
"CDR"$ "CC"./zapper "CM"ssh"CDM" [email protected]"CN"\n\
Example - Start 'nmap', zap all options & make nmap appear as 'harmless':\n\
"CDR"$ "CC"./zapper "CDC"-a harmless "CM"nmap"CDM" -sCV -F -Pn scanme.nmap.org"CN"\n\
Example - Same but also with the lowest possibl process id:\n\
"CDR"$ "CC"./zapper "CDC"-a harmless -n0 "CM"nmap"CDM" -sCV -F -Pn scanme.nmap.org"CN"\n\
Example - Start a PHP script as a background daemon. Hidden as 'apache2 -k...'\n\
"CDR"$ "CDC"("CC"./zapper "CDC"-f -a '/usr/sbin/apache2 -k start' "CM"php"CDM" tool.php "CDC"&>/dev/null &)"CN"\n\
Example - Hide tmux and all child processes as some kernel process:\n\
"CDR"$ "CC"./zapper"CDC" -f -a '[kworker/1:0-rcu_gp]' "CM"tmux"CN"\n\
Example - Use 'exec' to replace the parent shell as well:\n\
"CDR"$ "CC"exec ./zapper"CDC" -f -a '[kworker/1:0-rcu_gp]' "CM"tmux"CN"\n\
Example - Hide bash and all child processes (as empty string):\n\
"CDR"$ "CC"./zapper"CDC" -f -a- "CM"bash"CDM" -il"CN"\n\
Example - Use 'exec' to replace the parent shell as well:\n\
"CDR"$ "CC"exec ./zapper"CDC" -f -a- "CM"bash"CDM" -il"CN"\n\
\n\
Check it is working: "CDC"ps -eF f"CN"\n\
"CDY"Join us on Telegram: "CW"https://t.me/thcorg"CN"\n\
", ZVERSION, __DATE__);
#endif
exit(0);
}
static int
do_getopts(int argc, char *argv[])
{
int c;
char buf[4096];
char dst[sizeof buf];
char *ptr;
pid_t ff_opt_pid = -1;
char *ff_optarg = NULL;
while ( (c = getopt(argc, argv, "+a:n:t:fcEhD")) != -1) {
switch (c) {
case 'h':
usage();
break;
case 'D':
DEBUGF("DRYRUN is set\n");
g_flags |= FL_DRYRUN;
break;
case 'E':
g_flags &= ~FL_ZAP_ENV;
break;
case 'a':
// ps shows '?' if name is empty. Help user and default to " ".
if (*optarg == '\0')
g_cur_prg_name = " ";
else if ((optarg[0] == '-') && (optarg[1] == '\0'))
g_cur_prg_name = " ";
else
g_cur_prg_name = strdup(optarg);
g_flags |= FL_PRGNAME;
break;
case 'f':
g_flags |= (FL_FOLLOW | FL_STAY_ATTACHED | FL_FORCE_TRACER_IS_PARENT);
break;
case 'c':
// Force the child to be the TRACEE.
// e.g. shell -> zapper -> orig
g_flags |= FL_FORCE_TRACER_IS_PARENT;
break;
case 'n':
if (*optarg == '-')
break; // See Note #3, '-' is used internally
ff_opt_pid = atoi(optarg);
ff_optarg = optarg;
break;
case 't':
g_ff_opt_workers = atoi(optarg);
break;
case '?':
usage();
}
}
if ((argv[optind] == NULL) && (ff_opt_pid < 0))
usage();
// Bail if trying to set the PID of a process that is started as
// a foreground process (attached to the shell). This is not
// possible: Zapper can only claim the new PID by calling fork()
// and allowing the parent-zapper to exit. This would detach the
// process from the shell's process group (because parent pid would
// become 1) and put zapper and/or the target process into the background
// and detach itself from the terminal - that's likely not what the user
// wants.
int is_background = 0;
// Method 1: Mostly, when user's start a process with & they like it to
// disconnect from the shell's job control. We do this for them:
// if (getpgrp() != tcgetpgrp(STDOUT_FILENO))
// is_background = 1;
// Method 2: Check if it's disconnected from the job control:
if (!isatty(STDIN_FILENO))
is_background = 1;
if ((ff_opt_pid >= 0) && (!is_background) && (argv[optind] != NULL)) {
ptr = buf;
char *str;
char *end = buf + sizeof buf;
// Construct all argv except "-n1234" or "-n 1234"
for (c = 1; c < argc; c++) {
str = argv[c];
if (strncmp(str, "-n", 2) == 0) {
if (strlen(str) == 2)
c++; // "-n" "1000" variant.
continue;
}
if (strncmp(str, "-t", 2) == 0) {
if (strlen(str) == 2)
c++;
continue;
}
ptr += snprintf(ptr, end - ptr, "%s ", str);
}
// Lying: More precise: Can not set the PID of a process that
// is part of the job-control of the shell...
#ifndef STEALTH
cprintf(stderr, "\n\
"CDY"Can not set the PID of a process that is started in the foreground"CN".\n\
Instead, execute:\n\
"CC"./zapper "CDC"-n%s; "CC"./zapper "CDC"%s"CN"\n\
or start the process in the background:\n\
"CDC"("CC"./zapper "CDC"-n%s %s &>/dev/null &)"CN"\n", ff_optarg, buf, ff_optarg, buf);
#endif
exit(255);
}
// When -f without -a is used then we still like to rename
// 'zapper' to the name of the first tracee:
if (argv[optind] != NULL) {
if (! (g_flags & FL_PRGNAME) ) {
if ( (ptr = strrchr(argv[optind], '/')) )
g_cur_prg_name = ++ptr;
else
g_cur_prg_name = argv[optind];
}
}
if ((g_cur_prg_name) && (strcmp(argv[0], g_cur_prg_name) != 0)) {
// argv[0] is still 'zapper'. Execute ourself to fake our own argv[0]
snprintf(buf, sizeof buf, "/proc/%d/exe", getpid());
if (realpath(buf, dst) == NULL)
ERREXIT(255, "realpath(%s -> %s): %s\n", argv[0], buf, strerror(errno));
argv[0] = g_cur_prg_name;
execv(dst, argv);
}
if (ff_opt_pid >= 0) {
fast_forward_pid(ff_opt_pid);
if (argv[optind] == NULL) {
if (! (g_flags & FL_IS_QUIET)) {
buf[0] = '\0';
if (ff_opt_pid < RESERVED_PIDS)
snprintf(buf, sizeof buf, " ("CDR"PIDs < %d are reserverd and inaccessible"CN")", RESERVED_PIDS);
g_ff_next_pid = MAX(RESERVED_PIDS, g_ff_next_pid);
cprintf(stdout, CDG"SUCCESS"CN". Next process will start with PID "CDY"%d"CN"%s.\n", g_ff_next_pid, buf);
}
exit(0);
}
if (g_flags & FL_STAY_ATTACHED) {
// Note #3:
// _THIS_ process will stay attached (wont exit) and thus needs
// to claim the new pid by forking and executing itself.
// Destroy the -n option (with '-') to prevent
// executing this part twice.
*ff_optarg = '-';
pid_t pid;
pid = fork();
if (pid < 0)
ERREXIT(255, "fork(): %s\n", strerror(errno));
if (pid > 0)
exit(0); // Parent.
execv(dst, argv);
}
}
return optind;
}
// Worker 1..MAX_WORKERS end at target-MAX_WORKERS and only the first
// worker will fork-forward the last MAX_WORKERS pids to the target.
static void
fast_forward_pid_worker(int worker, pid_t stop) {
pid_t p = getpid();
pid_t old_p = 0;
int need_wraparound = 0;
if (stop < 0)
stop = g_max_pid + stop;
if (p > stop)
need_wraparound = 1;
if (p == stop)
exit(0);
signal(SIGCHLD, SIG_IGN);
while (1) {
if (!need_wraparound) {
if (*g_ff_pidp >= stop)
exit(0);
}
old_p = p;
p = clone((int (*)(void *))exit, stack + sizeof stack, CLONE_VFORK | CLONE_VM | SIGCHLD, NULL);
if (p <= 0)
break;
// FIXME: Should use MUTEX for access but it's so much faster to
// only have 1 worker to do the 'last mile' and have all other workers
// stop 8 * MAX_WORKERS before the target pid is hit.
*g_ff_pidp = p; // Copy to shared memory
if (p < old_p)
need_wraparound = 0;
}
fprintf(stderr, "#%d clone(): %s (try -t 1)\n", worker, strerror(errno));
exit(0);
}
pid_t g_ff_opt_target; // For display purposes.
pid_t g_ff_target;
pid_t g_ff_total_distance;
static void
cb_alarm(int sig) {
pid_t left;
left = g_ff_target - *g_ff_pidp;
if (left < 0)
left += g_max_pid;
fprintf(stderr, "\rFast forwarding to PID %d: %02.02f%%...", g_ff_opt_target, 100 - (double)(left * 100) / g_ff_total_distance);
alarm(1);
}
static void
cb_reset(int sig) {
if (g_flags & FL_IS_CURSOR_OFF)
fprintf(stderr, "\e[?25h");
g_flags &= ~FL_IS_CURSOR_OFF;
signal(sig, SIG_DFL);
kill(0, sig);
}
// Return 0 when next pid is the target pid or close to it.
static int
fast_forward_pid(pid_t opt_target) {
pid_t pid;
pid_t target;
int n_workers = 0;
int i;
int need_wraparound = 0;
g_ff_opt_target = opt_target;
if (g_max_pid <= 0)
g_max_pid = read_max_pid();
// Normalize target is specified to large by user
target = MIN(opt_target, g_max_pid - 1);
// On some Docker it is possible to get PID < 300
g_ff_target = target;
pid = getpid();
if (target == (pid + 1) % g_max_pid)
return 0; // Next pid is already our target pid.
if (! (g_flags & FL_IS_QUIET)) {
// Statistics every 1 second.
g_ff_pidp = mmap(NULL, sizeof *g_ff_pidp, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
*g_ff_pidp = pid;
g_ff_total_distance = target - pid;
if (g_ff_total_distance < 0)
g_ff_total_distance += g_max_pid;
}
for (i = 0; i < g_ff_opt_workers; i++) {
// Calculate where the worker should stop. Only worker #0
// does the last few pids (e.g. the last mile).
pid_t stop;
if (i == 0) {
// Worker #0
// If the user precisly wishes for 300 then stop at 300. Otherwise try to
// hit the target (even if within reserved range) anyway (which is possible
// on some versions of Docker)
if (target == RESERVED_PIDS)
stop = g_max_pid - 1;
else
stop = target - 1;
} else {
// Other workers
if (target <= RESERVED_PIDS + g_ff_opt_workers * 8)
// Target 0..300 + 16 * 8 => Stop way before MAX_PID
stop = g_max_pid - 1 - g_ff_opt_workers * 8;
else
stop = target - g_ff_opt_workers*8;
}
if (stop <= pid)
need_wraparound = 1;
DEBUGF("#%d STOPPING at %d (target=%d) (cur=%d, need_wraparound=%d)\n", i, stop, target, pid, need_wraparound);
pid = fork();
if (pid < 0) {
fprintf(stderr, "#%d fork(): %s\n", i, strerror(errno));
break;
}
*g_ff_pidp = pid;
n_workers++;
if (pid == 0) {
// CHILD
fast_forward_pid_worker(i, stop);
exit(0); // CHILD exit
}
if (i == 0) {
// Worker #0
if (pid == stop)
break;
} else {
if (need_wraparound) {
if (pid == stop)
break;
}
}
}
// Kick start all workers
DEBUGF("Waiting for %d workers to finish\n", n_workers);
// Set signal and atexit() after spawning childs (!).
if (isatty(STDOUT_FILENO) && (! (g_flags & FL_IS_QUIET))) {
signal(SIGINT, cb_reset);
signal(SIGTERM, cb_reset);
g_flags |= FL_IS_CURSOR_OFF;
fprintf(stderr, "\033[?25l"); // Hide cursor
signal(SIGALRM, cb_alarm);
cb_alarm(0);
}
// Wait for all workers to complete
while (n_workers > 0) {
waitpid(-1, NULL, 0);
n_workers--;
}
if (g_flags & FL_IS_CURSOR_OFF) {
// Output statistics
fprintf(stderr, "\r\e[?25h\e[K\r");
signal(SIGALRM, SIG_DFL);
signal(SIGINT, SIG_DFL);
signal(SIGTERM, SIG_DFL);
if (g_ff_pidp) {
g_ff_next_pid = (*g_ff_pidp + 1) % g_max_pid;
munmap(g_ff_pidp, sizeof *g_ff_pidp);
g_ff_pidp = NULL;
}
}
return 0;
}
// Read data from pid@src to dest.
// static void
// ptpeekcpy(void *dst, pid_t pid, void *src, size_t n)
// {
// void *src_end = src + n;
// while (src_end - src >= sizeof (long)) {
// data.val = ptrace(PTRACE_PEEKDATA, pid, src, NULL);
// memcpy(dst, data.c, sizeof (long));
// dst += sizeof (long);
// src += sizeof (long);
// }
// if (src >= src_end)
// return;
// // Partial copy
// data.val = ptrace(PTRACE_PEEKDATA, pid, src, NULL);
// memcpy(dst, data.c, src_end - src);
// }
static void
ptpokecpy(pid_t pid, void *dst, void *src, size_t n)
{
void *src_end = src + n;
while (src_end - src >= sizeof (long)) {
memcpy(data.c, src, sizeof (long));
ptrace(PTRACE_POKEDATA, pid, dst, data.val);
dst += sizeof (long);
src += sizeof (long);
}
if (src >= src_end)
return;
data.val = ptrace(PTRACE_PEEKDATA, pid, src, NULL);
memcpy(data.c, src, src_end - src);
ptrace(PTRACE_POKEDATA, pid, dst, data.val);
}
int ptopts;
#ifdef PTRACE_O_EXITKILL
#define PTOPT_X_EXITKILL PTRACE_O_EXITKILL
#else
# warning "PTRACE_O_EXITKILL not found. Super old linux kernel?"
# define PTOPT_X_EXITKILL (0)
#endif
static int
ptsetoptions(pid_t pid) {
// execve() delivers an extra TRAP, ignore it:
// https://manpages.debian.org/bookworm/manpages-dev/ptrace.2.en.html
return ptrace(PTRACE_SETOPTIONS, pid, NULL, PTRACE_O_TRACESYSGOOD | PTOPT_X_EXITKILL | PTRACE_O_TRACEFORK | PTRACE_O_TRACEVFORK | PTRACE_O_TRACEEXEC);
}
// Emulate shell's exit string
static void
exit_emu_shell(int code, const char *prog) {
char *str;
char buf[1024];
char *ptr = &buf[0];
char *end = ptr + sizeof buf;
int is_path = 0;
if (strchr(prog, '/'))
is_path = 1;
// Emulate shell's exit string.
str = getenv("SHELL");
while (str) {
str = strrchr(str, '/');
if ((!str) || (*str == '\0'))
break;
str++;
// Zsh always prefixes with $SHELL.
// Bash only when 'No such file or directory'.
if ((!is_path) && (strcmp(str, "zsh")) != 0)
break;
ptr += MAX(0, snprintf(ptr, end - ptr, "%.64s: ", str));
break;
}
if (is_path)
snprintf(ptr, end - ptr, "%s: %s\n", prog, strerror(errno));
else
snprintf(ptr, end - ptr, "%s: command not found\n", prog);
fprintf(stderr, "%s", buf);
exit(code);
}
static pid_t
start_trace_child(const char *orig_prog, char *new_argv[]) {
int status;
XFAIL((g_pid = fork()) < 0, "fork(): %s\n", strerror(errno));
if (g_pid == 0) {
// CHILD
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execvp(orig_prog, new_argv);
exit_emu_shell(127, orig_prog);
}
// PARENT
g_pid_master = g_pid;
close(0); // Dont consume any input. Input should reach forked child (orig_prog).
if (waitpid(g_pid, &status, 0) == -1)
goto err;
if (WIFEXITED(status))
exit(WEXITSTATUS(status));
XFAIL(ptsetoptions(g_pid) == -1, "ptrace(%d): %s\n", g_pid, strerror(errno));
set_proxy_signals();
g_flags |= IS_TRACER_IS_PARENT;
return g_pid;
err:
if (g_pid > 0)
kill(SIGKILL, g_pid);
return -1;
}
/*
* Return SYS_execve on success.
* Return -1 on error
* Return -2 if g_pid_master exited.
*/
static int
ptrace_until_execve(pid_t *pidp, int *status) {
int signum;
siginfo_t sigi;
pid_t pid = *pidp;
void *data = NULL;
static pid_t ss_pids[16 * 1024]; // Stray Stops
static int ss_idx;
*status = 0;
while(1) {
if ((pid > 0) && (ptrace(PTRACE_CONT, pid, NULL, data) == -1))
GOTOERR("ptrace(%d): %s\n", pid, strerror(errno));
data = NULL;
if ( (pid = waitpid(-1, status, WUNTRACED)) == -1)
GOTOERR("waitpid()=%d: %s\n", pid, strerror(errno));
*pidp = pid;
if (WIFEXITED(*status)) {
DEBUGF("pid="CY"%d "CG"exited"CN".\n", pid);
if (pid == g_pid_master)
exit(WEXITSTATUS(*status)); // tracee exited. Exit with same error code.
pid = 0;
continue;
}
if (WIFSIGNALED(*status)) {
// Tracee was termianted with a signal
signum = WTERMSIG(*status);
DEBUGF(CY"%d "CDY"terminated"CN" by SIG-%d\n", pid, signum);
if (pid == g_pid_master) {
if (signum == SIGSEGV)
exit(128 + signum); // Do not generate core dump of zapper.
// Tracer to commit suicide with same signal as tracee died.
if (g_flags & IS_SIGNAL_PROXY)
signal(signum, SIG_DFL);
DEBUGF(CR"SUICIDE\n"CN);
kill(getpid(), signum);
}
pid = 0;
continue;
}
if (!WIFSTOPPED(*status)) {
ERREXIT(255, "SHOULD NOT HAPEN?\n");
// SHOULD NOT HAPPEN
pid = 0;
continue;
}
// 5 = SIGTRAP
// 17 = SIGCHLD
// 19 = SIGSTOP
signum = WSTOPSIG(*status);
if (! (signum & 0x80)) {
// Signal was for TRACEE (not tracer)
if (signum == SIGTRAP) {
DEBUGF("Event for "CY"%d"CN" ("CDG"event=%d"CN")\n", pid, (*status >> 16) & 0xffff);
// NOTE: Stop occures in parent, not the newly created thread.
switch ((*status >> 16) & 0xffff) {
case PTRACE_EVENT_CLONE: // 3
DEBUGF(CDR"CLONE()"CN" not implemented\n");
break;
case PTRACE_EVENT_EXIT: // 6
// EVENT_EXIT should never trigger before EVENT_FORK (?). See Note #3.
break;
case PTRACE_EVENT_FORK: // 1
case PTRACE_EVENT_VFORK: ; // 2
unsigned long cpid;
XFAIL(ptrace(PTRACE_GETEVENTMSG, pid, NULL, &cpid) == -1, "ptrace(%d): %s\n", pid, strerror(errno));
DEBUGF(CDY"FORK "CY"%d"CDY" to cpid="CY"%lu\n"CN, pid, cpid);
// Wait for the child to be stopped:
// It can happen that SIGSTOP for this cpid arrived before the EVENT_FORK.
// In that case, by the time the EVENT_FORK happens, we can no longer
// wait() for the SIGSTOP signal (because it has already been delivered).
// Thus we must use WNOHANG and waitpid() will return 0 indicating that
// the child has already been stopped.
// On the other hand, EVEN_FORK may be triggered before the cpid is stopped. Thus
// we need to waitpid normally.
// waitpid(,,WUNTRACED) => Will hang forever if SIGSTOP got delivered before EVENT_FORK
// waitpid(,,WNOHANG) => May return 0 (child exists) but PTRACE_SETOPTIONS will
// fail with -ENOENT (No such process))
// => The only way around this is to track all stray SIGSTOPs. A good test is to
// use './zapper -n 500' within a zapped tmux.
int opt = WUNTRACED;
int i;
for (i = 0; i < sizeof ss_pids / sizeof *ss_pids; i++) {
if (ss_pids[i] != cpid)
continue;
ss_pids[i] = 0;
opt |= WNOHANG;
break;
}
waitpid(cpid, NULL, opt);
// Note #3: cpid may have already exited (and before we received its EVENT_EXIT).
// The only thing we can hope for is trying to call ptsetoptions() and dont
// fail hard if ptsetoptions() fails (e.g. when client has already exited).
ptsetoptions(cpid);
if (ptrace(PTRACE_CONT, cpid, NULL, NULL) == -1) {
DEBUGF("ERROR zapper: ptrace(%ld): %s\n", cpid, strerror(errno));
// Child may have exited already
// if (errno != EIO)
// fprintf(stderr, "ERROR zapper: ptrace(%ld): %s\n", cpid, strerror(errno));
}
break;
case PTRACE_EVENT_EXEC: // 4
// Catch execve() after returning from syscall.
// Trap when the SYSCALL exit occurs...
ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
pid = 0; // ...and to not call PTRACE_CONT.
break;
}
continue;
}
// SIGSTOP may arrive _before_ we receive the fork() event (above).
// [DEBUG zapper.c:455] Stray SIGSTOP for (untracked?) pid=42056 (status=4991)
// [DEBUG zapper.c:426] Event for 42054 (event=1)
// [DEBUG zapper.c:436] FORK 42054 to cpid=42056
if (signum == SIGSTOP) {
DEBUGF(CR"Stray SIGSTOP for (untracked?) pid=%d (status=%d)\n"CN, pid, *status);
ss_pids[ss_idx] = pid;
ss_idx = (ss_idx + 1) % (sizeof ss_pids / sizeof *ss_pids);
pid = 0; // Do not continue cpid. Continue cpid after EVENT_FORK.
continue;
}
// Forward signal to offending process.
if (ptrace(PTRACE_GETSIGINFO, pid, NULL, &sigi) == -1) {
DEBUGF("SHOULD NOT HAPPEN\n");
continue;
}
// Do not forward SIGCHLD to report when child has stopped (by trap).
if (signum == SIGCHLD) {
switch (sigi.si_code) {
case CLD_DUMPED:
case CLD_TRAPPED:
case CLD_STOPPED:
case CLD_CONTINUED:
DEBUGF("NOT forwarding signal [%d, %d, %d]\n", sigi.si_signo, sigi.si_code, sigi.si_errno);
continue;
}
}
DEBUGF("Forwarding "CDY"SIG_%d"CN" to pid "CY"%d"CN" [%d, %d, %d]\n", signum, pid, sigi.si_signo, sigi.si_code, sigi.si_errno);
data = (void *)((long)signum);
continue;
}
if (ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &g_iov) == -1)
GOTOERR("ptrace(GETREGSET, %d): %s\n", pid, strerror(errno));
// if (SYSNO(*regsp) != SYS_execve)
// ERREXIT(255, "Not SYS_execve()\n"); // CAN NOT HAPPEN. We only trap execve().
// Linux prior 5.3 does not have GET_SYSCALL_INFO.
int ret = 1;
#ifdef PTRACE_GET_SYSCALL_INFO