forked from hackerschoice/zapper
-
Notifications
You must be signed in to change notification settings - Fork 0
/
zapper.c
992 lines (881 loc) · 33.6 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
/*
* 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
*
* Hide options:
* ./zapper nmap -sS 192.168.0.0/24
* Hide options and rename process 'nmap' to 'blah':
* ./zapper -a blah nmap -sS 192.168.0.0/24
*
* exec ./zapper -f -a BlahBlub bash -il
*/
/* 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.)
*/
// 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. At the moment, zapper starts first
// and is 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.
// * -p 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.
// * Use pid < 1024
// * 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.
#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 <linux/ptrace.h>
#include <syscall.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>
#if __WORDSIZE == 64
#define OAX(reg) (reg).orig_rax
#define AX(reg) (reg).rax
#define SP(reg) (reg).rsp
#define IP(reg) (reg).rip
#else
#define OAX(reg) (reg).orig_eax
#define AX(reg) (reg).eax
#define SP(reg) (reg).esp
#define IP(reg) (reg).eip
#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)
char *g_cur_prg_name;
pid_t g_pid;
int g_flags;
pid_t g_pid_master;
pid_t g_pid_zapper;
#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)
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;
#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 void
usage(void) {
printf("\
"CG"Hide command options and clear the environment of a command."CN"\n\
\n\
./zapper [-f] [-a name] command ...\n\
-a <name> Rename the process to 'name'. (Use -a \"\" for none).\n\
-f zap all child processes as well (follow).\n\
-E Do not zap the environment variables.\n\
\n\
Example - Start ssh but zap all options (only 'ssh' appears)\n\
$ "CC"./zapper "CM"ssh"CDM" [email protected]"CN"\n\
Example - Start 'nmap', zap all options & make nmap appear as 'harmless':\n\
$ "CC"./zapper "CDC"-a harmless "CM"nmap"CDM" -sCV -F -Pn scanme.nmap.org"CN"\n\
Example - Hide current shell and all child processes:\n\
$ "CC"exec ./zapper"CDC" -f -a- "CM"bash"CDM" -il"CN"\n\
Example - Hide current shell and all child processes as some kernel worker:\n\
$ "CC"exec ./zapper"CDC" -f -a'[kworker/1:0-rcu_gp]' "CM"bash"CDM" -il"CN"\n\
\n\
"CDY"Join us on Telegram: "CW"https://t.me/thcorg"CN"\n\
");
exit(0);
}
static int
do_getopts(int argc, char *argv[])
{
int c;
char buf[4096];
char dst[sizeof buf];
char *ptr;
while ( (c = getopt(argc, argv, "+a:fcEhD")) != -1) {
switch (c) {
case 'h':
usage();
break;
case 'D':
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 '?':
usage();
}
}
if (argv[optind] == NULL)
usage();
// When -f without -a is used then we still like to rename
// 'zapper' to the name of the first tracee:
if (! (g_flags & FL_PRGNAME) ) {
if ( (ptr = strrchr(argv[optind], '/')) )
g_cur_prg_name = ++ptr;
else
g_cur_prg_name = argv[optind];
}
if (strcmp(argv[0], g_cur_prg_name) != 0) {
// argv[0] is still 'zapper'. Execute ourself to fake our own argv[0]
argv[0] = g_cur_prg_name;
snprintf(buf, sizeof buf, "/proc/%d/exe", getpid());
if (realpath(buf, dst) == NULL)
ERREXIT(255, "realpath(%s): %s\n", buf, strerror(errno));
execv(dst, argv);
}
return optind;
}
// 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);
}
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 | PTRACE_O_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) != 0, "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, struct user_regs_struct *regsp, int *status) {
int signum;
siginfo_t sigi;
pid_t pid = *pidp;
void *data = NULL;
static int last_pid_stray_stop_signal;
*status = 0;
while(1) {
if ((pid > 0) && (ptrace(PTRACE_CONT, pid, NULL, data) != 0))
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);
// It can happen that SIGSTOP for this cpid arrived before the EVENT_FORK.
// In that case, by the time we get the FORK event we can no longer
// wait() for the SIGSTOP signal (because it has already been delivered).
// Thus we must use WNOHANG. (waitpid() returns 0 in this case and is already
// stopped.).
// On the other hand EVEN_FORK may be triggered before the cpid is stopped. Thus
// we need to waitpid normally.
if (cpid == last_pid_stray_stop_signal) {
waitpid(cpid, NULL, WNOHANG | WUNTRACED);
last_pid_stray_stop_signal = 0;
} else {
waitpid(cpid, NULL, WUNTRACED);
}
// 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).
// ==> Oops. this never happens???
// if (ptsetoptions(cpid) != 0)
// break; // Child has already exited.
ptsetoptions(cpid);
XFAIL(ptrace(PTRACE_CONT, cpid, NULL, NULL) == -1, "ptrace(%lu): %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);
last_pid_stray_stop_signal = pid;
pid = 0; // Do not continue cpid. Continue cpid after EVENT_FORK.
continue;
}
// Forward signal to offending process.
ptrace(PTRACE_GETSIGINFO, pid, NULL, &sigi);
// 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_GETREGS, pid, NULL, regsp) != 0)
GOTOERR("ptrace(GETREGS, %d): %s\n", pid, strerror(errno));
if (OAX(*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
struct ptrace_syscall_info si;
ret = ptrace(PTRACE_GET_SYSCALL_INFO, pid, sizeof si, &si);
if (ret == 0) {
DEBUGF("pid="CY"%d"CDY" OP #%d"CN" %d-%d\n", pid, si.op, (*status >> 8) & ~0x80, *status & 0xff);
if (si.op != PTRACE_EVENTMSG_SYSCALL_EXIT)
continue;
DEBUGF(" RET=%lld\n", si.exit.rval);
DEBUGF(" ISERR=%d\n", si.exit.is_error);
if (si.exit.is_error != 0)
continue;
ret = 0;
} else {
if (errno != EIO)
ERREXIT(255, "ptrac(): %s\n", strerror(errno));
// HERE: No PTRACE_GET_SYSCALL_INFO.
// This can happen if using the Linux >= 5.3 static binary on Linux < 5.3
}
#else
# warning "No PTRACE_GET_SYSCALL_INFO defined. Linux < 5.3? Using compat mode."
#endif
if (ret == 1) {
// PTRACE_GET_SYSCALL_INFO not available or call failed.
// FIXME: May need to use PTRACE_GETREGSET to better support Linux < 5.3
}
*pidp = pid;
return SYS_execve;
}
err:
return -1; // FATAL
}
static pid_t
start_trace_parent(const char *orig_prog, char *new_argv[], struct user_regs_struct *regsp) {
pid_t pid;
pid_t pid_tracee;
int ret;
int status;
// Try ZAPPER to be the CHILD (tracer) and trace the PARENT (tracee)
// See ptrace_scope
// https://www.kernel.org/doc/Documentation/security/Yama.txt
int up[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, up) != 0)
goto err;
pid_tracee = getpid();
g_pid_master = getpid();
pid = fork();
if (pid != 0) {
// PARENT (tracee)
// Wait for first child to exit. (See Note-#1)
waitpid(pid, &ret, WUNTRACED);
prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY);
// Cant use kill(getpid(), SIGSTOP); because of prctl().
close(up[0]);
ret = write(up[1], &ret, sizeof ret); // Signal to TRACER that we are ready for ATTACH.
ret = read(up[1], &ret, sizeof ret); // Wait for TRACER to be ready.
close(up[1]);
// Check if CHILD (tracer) successfully attached to us; PARENT (tracee)
if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) == -1) {
execvp(orig_prog, new_argv);
exit_emu_shell(127, orig_prog);
}
// TRACER failed to trace us.
kill(pid, SIGKILL); // Kill the TRACER (if still alive)
return -1;
}
// CHILD (tracer) (pid == 0)
// Note-#1: Fork again & let first child exit.
// This stops SIGCHLD to be send to the real parent when the TRACER exists
// without us messing with the sigmask.
pid = fork();
if (pid != 0)
exit(0);
g_pid = getpid();
// Wait for TRACEE to tell us when we can attach to TRACEE.
close(up[1]);
if (read(up[0], &ret, sizeof ret) < 0)
goto err;
if (ptrace(PTRACE_ATTACH, pid_tracee, NULL, NULL) != 0)
goto err;
// 5 == SIGTRAP
// 19 == SIGSTOP.
// 1029 (SIGTRAP | PTRACE_EVENT_EXEC << 8)
if (waitpid(pid_tracee, &ret, WUNTRACED) == -1)
GOTOERR("waitpid(%d): %s\n", pid_tracee, strerror(errno));
XFAIL(ptsetoptions(pid_tracee) != 0, "ptrace(%d): %s\n", pid_tracee, strerror(errno));
// Tell TRACEE that we are attached and TRACEE can call execve().
if (write(up[0], &ret, sizeof ret) != sizeof ret)
goto err;
close(up[0]);
DEBUGF("Tracing %d\n", pid_tracee);
ret = ptrace_until_execve(&pid_tracee, regsp, &status);
if (ret > 0)
return pid_tracee;
err:
close(up[0]);
close(up[1]);
return -1;
}
static void
fix_stack(pid_t pid, struct user_regs_struct *regsp)
{
size_t stack_sz;
char *stack;
unsigned long *stackp;
unsigned long *valp;
unsigned long spare_ofs = 0;
unsigned long argv0_ofs = 0;
unsigned long envv0_ofs = 0;
unsigned long stack_end;
int elft_idx;
int idx;
if (g_flags & FL_DRYRUN)
return;
// Find the end of the stack. We can not use /proc/<PID>/maps as this becomes
// inaccessible for the tracer if the tracee changes the EUID (is this a
// kernel bug? We are already tracing the tracee and have full control
// of the tracee anyway, so why deny access???)
for (errno = 0, idx = 0, stackp = NULL; errno == 0; idx++) {
stackp = realloc(stackp, (idx + 1) * sizeof (void *));
stackp[idx] = ptrace(PTRACE_PEEKDATA, pid, SP(*regsp) + idx * sizeof (void *), NULL);
}
stack_end = SP(*regsp) + (idx - 1) * sizeof (void *);
stack = (char *)stackp;
stack_sz = stack_end - SP(*regsp);
DEBUGF("=> SP 0x%lx-0x%lx (stack_sz=%zu)\n", (unsigned long)SP(*regsp), stack_end, stack_sz);
stack = calloc(1, stack_sz);
XFAIL(stack == NULL, "calloc(): %s\n", strerror(errno));
stackp = (unsigned long *)stack;
ptpeekcpy(stack, pid, (void *)SP(*regsp), stack_sz);
dumpfile("stack.dat", stack, stack_sz);
DEBUGF("argc = %lx\n", stackp[0]);
DEBUGF("&argv[0] = %lx\n", stackp[1]);
char *str = &stack[stackp[1] - SP(*regsp)];
DEBUGF("argv[0] = '"CDR"%s""'\n"CN, str);
// Make argv0 smaller (See Note #2)
char *end = str + strlen(str);
while ((end-- > str) && (*end == ' '))
*end = '\0';
DEBUGF("argv[0] = '"CDR"%s""'\n"CN, str);
#ifdef DEBUG
idx = 0;
fprintf(out, "ARGS=");
while ((void *)stackp[idx + 1] != NULL) {
// DEBUGF("%lx\n", stackp[idx +1 ]);
fprintf(out, "'%s' ", &stack[stackp[idx + 1] - SP(*regsp)]);
idx++;
}
fprintf(out, "\n");
#endif
// Find lowest address (which normally is ARGV[0] but dont have to be).
size_t len = 0;
valp = &stackp[1];
argv0_ofs = stackp[1] - SP(*regsp);
unsigned long arg_min = stack_end;
unsigned long arg_max = SP(*regsp);
for (; *valp != 0x00 /* NULL */; valp++) {
arg_min = MIN(arg_min, *valp);
arg_max = MAX(arg_max, *valp);
}
arg_max += strlen(&stack[arg_max - SP(*regsp)]) + 1;
valp++; // Skip NULL
// Skip through envp and find start of elf-table
// stack_envp = valp;
envv0_ofs = (valp - &stackp[0]) * sizeof (void *);
unsigned long env_min = stack_end;
unsigned long env_max = SP(*regsp);
for (; *valp != 0x00 /* NULL */; valp++) {
env_min = MIN(env_min, *valp);
env_max = MAX(env_max, *valp);
}
env_max += strlen(&stack[env_max - SP(*regsp)]) + 1;
valp++; // Skip NULL
unsigned long smin, smax;
smin = MIN(arg_min, env_min);
smax = MAX(arg_max, env_max);
DEBUGF("ARG from +%lu to +%llu (%lu bytes)\n", arg_min - (unsigned long)SP(*regsp), arg_max - SP(*regsp), arg_max - arg_min);
DEBUGF("ENV from +%lu to +%llu (%lu bytes)\n", env_min - (unsigned long)SP(*regsp), env_max - SP(*regsp), env_max - env_min);
// valp now points to start of ELF Table.
DEBUGF("stackp 0x%lx valp 0x%lx\n", (unsigned long)stackp, (unsigned long)valp);
elft_idx = valp - stackp; // this is INDEX, not offset.
DEBUGF("Elf Table start at idx=%d (ofs=%lu)\n", elft_idx, elft_idx * sizeof (void *));
unsigned long ofs;
// Find where Randomized Stack area starts and how long it is:
for (idx = elft_idx; stackp[idx] != 0; idx += 2) {
// After ELF-Table there is 0x00 + 16 bytes random + "x86_64\0"
// The specs dont define which comes first so we need to check and
// find the largest to determine where the randomized stack starts.
if (stackp[idx] == 0x0f) {
// pointer to AT_PLATFORM string.
ofs = stackp[idx + 1] - SP(*regsp);
ofs += strlen(&stack[ofs]) + 1;
if (ofs > spare_ofs)
spare_ofs = ofs;
}
if (stackp[idx] == 0x19) {
// pointer to AT_RANDOM (16 bytes).
ofs = stackp[idx + 1] - SP(*regsp) + 16;
if (ofs > spare_ofs)
spare_ofs = ofs;
}
}
// size_t elft_sz = (idx - elft_idx + 2) * sizeof (void *);
// DEBUGF("ELF Table size=%zd\n", elft_sz);
// Calculate the gap that should be added
// so that entire stack is still 16 bytes aligned.
len = (smax - smin);
unsigned long sz = stack_sz + len;
if (sz != (sz & ~15))
sz = (sz + 16) & ~15;
len = sz - stack_sz;
DEBUGF("Creating a gap of %zu bytes\n", len);
// FIXME: we could use spare space from randomized stack, if available.
stack = realloc(stack, stack_sz + len);
// Copy everything down to where kernel's pointers point to.
memcpy(stack + stack_sz, stack + stack_sz - len, len);
stackp = (unsigned long *)stack;
stack_sz += len;
// Adjust the elf table that we moved to a lower address by len.
// Find the start of the randomized (spare_ofs).
for (idx = elft_idx; stackp[idx] != 0; idx += 2) {
// See https://elixir.bootlin.com/linux/v5.19.17/source/include/uapi/linux/auxvec.h
switch (stackp[idx]) {
case 0x19: // AT_RANDOM
case 0x1f: // AT_EXECFN, static=outside, dynamic=inside
case 0x21: // ptr to "\177ELF\002\001\001\000", normally outside stack region.
case 0x0f: // AT_PLATFORM
case 0x18: // AT_BASE_PLATFORM
if (stackp[idx + 1] == 0)
break; // Value is NULL (not set)
if (stackp[idx + 1] - SP(*regsp) > stack_sz) {
DEBUGF("[0x%02x] Value outside of fake stack: 0x%lx\n", (unsigned int)stackp[idx], stackp[idx + 1]);
break; // Address is at higher address that wasnt moved.
}
// DEBUGF("Adjusting 0x%02x [%s]\n", (unsigned int)stackp[idx], &stack[stackp[idx + 1] - SP(*regsp)]);
stackp[idx + 1] -= len;
break;
}
}
// Adjust address off all argv-pointers
valp = (unsigned long *)&stackp[1]; // [0] is argc and argv starts at [1]
while (*valp != 0)
*valp++ -= len;
// ZAP argv
memset(&stack[(unsigned long)arg_min - SP(*regsp) + len], 0, arg_max - arg_min);
// Adjust address off all env-pointers
valp = (unsigned long *)&stack[envv0_ofs];
while (*valp != 0)
*valp++ -= len;
// ZAP env
if (g_flags & FL_ZAP_ENV)
memset(&stack[(unsigned long)env_min - SP(*regsp) + len], 0, env_max - env_min);
// Copy g_cur_prg_name (-a name) to argv[0] (whom's location the kernel
// references). This may overlap into ENV if g_cur_prg_name is longer
// than the original argv[*] but only when it's the tracee's tracee
// (a grand-tracee of the tracer). See also Note #2.
size_t max_sz = smax - smin;
size_t prglen = MIN(max_sz - 1, strlen(g_cur_prg_name));
memcpy(&stack[argv0_ofs + len], g_cur_prg_name, prglen);
stack[argv0_ofs + len + prglen] = '\0';
// dumpfile("stack2.dat", stack, stack_sz);
// Increase the stack size (by decreasing the stack pointer).
SP(*regsp) -= len;
DEBUGF("New stack 0x%llx-0x%lx (size=%llu == %zu)\n", SP(*regsp), stack_end, stack_end - SP(*regsp), stack_sz);
ptrace(PTRACE_SETREGS, pid, NULL, regsp);
ptpokecpy(pid, (void *)SP(*regsp), stack, stack_sz);
free(stack);
}
static void
follow_forever(pid_t pid)
{
int status;
struct user_regs_struct regs;
while (ptrace_until_execve(&pid, ®s, &status) > 0) {
fix_stack(pid, ®s);
}
exit(255);
}
// Trap at ELF's AT_ENTRY
static pid_t
start_trace(char *orig_prog, char *new_argv[], struct user_regs_struct *regsp) {
pid_t pid;
char *orig_argv0 = new_argv[0];
char *enlarged_argv0 = NULL;
char *ptr;
// Determine name of programm to show in 'ps'.
if (! (g_flags & FL_PRGNAME) ) {
if ( (ptr = strrchr(orig_prog, '/')))
g_cur_prg_name = ++ptr;
else
g_cur_prg_name = orig_prog;
}
// Note #2: ADM reported a bug when under special conditions the -a name
// may leak into the environment:
// $ ./zapper -a 12345678990abcdef cat
// $ xxd /proc/$(pidof 12345678990abcdef)/environ | head
// The solution is to make enough space for the tracer to put the fake
// -a name and remove the extra string later when tracing the execve().
size_t glen = strlen(g_cur_prg_name);
size_t alen = strlen(new_argv[0]);
if (glen > alen) {
enlarged_argv0 = malloc(glen + 1);
enlarged_argv0[glen] = '\0';
memcpy(enlarged_argv0, new_argv[0], alen);
memset(enlarged_argv0 + alen, ' ', glen - alen);
DEBUGF("Enlarging argv[0] by %zu spaces to make space for longer -a name\n", glen - alen);
new_argv[0] = enlarged_argv0;
}
if (!(g_flags & FL_FOLLOW)) {
// Detach after zapping if we are a background process.
if (fcntl(0, F_GETFD, 0) != 0) {
// STDIN is closed. Assume I'm a background process.
g_flags &= ~FL_STAY_ATTACHED;
} else {
// STDIN is open
if (getpid() != tcgetpgrp(0))
g_flags &= ~FL_STAY_ATTACHED; // Got started as background process by shell
}
}
// Try for the CHILD to be the TRACER and trace this process.
if (!(g_flags & FL_FORCE_TRACER_IS_PARENT)) {
pid = start_trace_parent(orig_prog, new_argv, regsp);
if (pid > 0) {
DEBUGF("Trapped PARENT pid %d\n", pid);
if (!(g_flags & FL_FOLLOW))
g_flags &= ~FL_STAY_ATTACHED;
goto done; // We are now the CHILD.
}
DEBUGF("ERROR: TRACER failed to be the PARENT\n");
fprintf(stderr, "ERROR: Try with -c option\n");
exit(255);
}
// ### This PARENT is the TRACER and tracing the CHILD (TRACEE)
// Must always stay attached even if background process in case caller checks $!
g_flags |= FL_STAY_ATTACHED;
pid = start_trace_child(orig_prog, new_argv);
DEBUGF("[%d] Tracing child %d\n", getpid(), pid);
XFAIL(ptrace(PTRACE_GETREGS, pid, NULL, regsp) != 0, "ptrace(%d): %s\n", pid, strerror(errno));
done:
if (enlarged_argv0) {
free(enlarged_argv0);
new_argv[0] = orig_argv0;
DEBUGF("restored to %p\n", new_argv[0]);
}
return pid;
}
int
main(int argc, char *argv[], char *envp[]) {
pid_t pid;
struct user_regs_struct regs;
int i;
init_vars();
do_getopts(argc, argv);
pid = start_trace(argv[optind], &argv[optind], ®s);
fix_stack(pid, ®s);
// TRACEE is a background process _OR_ TRACER is the child process
if (!(g_flags & FL_STAY_ATTACHED)) {
ptrace(PTRACE_DETACH, pid, NULL, NULL);
DEBUGF("All done. Tracer exiting....\n");
exit(0);
}
// Destroy my own argv
for (i = 1; i < argc; i++) {
DEBUGF("SelfZAP '%s' %zu\n", argv[i], strlen(argv[i]));
memset(argv[i], 0, strlen(argv[i]));
}
// Destroy my own envp
if (g_flags & FL_ZAP_ENV) {
for (i = 0; envp[i] != NULL; i++)
memset(envp[i], 0, strlen(envp[i]));
}
if (g_flags & FL_FOLLOW) {
DEBUGF("FOLLOWING...\n");
follow_forever(pid);
exit(255); // NOT REACHED.
}
ptrace(PTRACE_DETACH, pid, NULL, NULL);
// Wait for child to terminate
waitpid(pid, &i, 0);
if (WIFEXITED(i))
exit(WEXITSTATUS(i));
exit(255); // NOT REACHED
}