Refactoring to clean up decisions about argument types, centralized processing of...
[signduterre.git] / signduterre.py
blobe55019fa14ca0cfd599be2667b5116ddf969caed
1 #!/usr/bin/python3
3 # ToC
4 # 1. DOCUMENTATION
5 # 2. IMPORT & INITIALIZATION
6 # 3. OPTION HANDLING
7 # 4. ARGUMENT PROCESSING
8 # 5. SIGNATURE CREATION AND CHECKING
10 #############################################################################
11 # #
12 # DOCUMENTATION #
13 # #
14 #############################################################################
16 # The full manual can be printed as:
17 # - HTML: Replace '[[[' by < and ']]]' by >, protect <, > brackets in text
18 # - plain text long: Remove everything between '[[[' and ']]]'
19 # - plain text short: as long, but remove text between [[[LONG]]] and [[[/LONG]]]
20 # - makefile: Print only the text between [[[pre make=<label>]]] and [[[/pre]]],
21 # remove '\\\n' and replace '^\$' by "\t". Add label and grouped labels
22 # and a 'clean' action to complete functional makefile
24 manual = """
25 [[[!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"]]][[[html]]][[[header]]][[[title]]]Signature-du-Terroir[[[/title]]][[[/header]]][[[body]]][[[h1]]]Signature-du-Terroir[[[/h1]]][[[p]]]
26 Construct a signature of the installed software state or check the integrity of the installation
27 using a previously made signature.
28 [[[/p]]][[[p]]]
29 Usage: signduterre.py [options] FILE1 FILE2 ...
30 [[[/p]]][[[p]]]
31 Options:[[[/p]]][[[pre]]]
32 -h, --help show this help message and exit
33 -s HEX, --salt=HEX Enter salt in cleartext. If not given, a hexadecimal
34 salt will be suggested. The SUGGESTED[=N] keyword will
35 cause the selection of the suggested string. N is the
36 number of salts generated (default N=1). If N>1, all
37 will be printed and a random one will be used to
38 generate the signature (selection printed to STDERR).
39 -a, --all-salts-pattern
40 Use all salts in sequence, randomly replace salts with
41 incorrect ones in the output to create a pattern of
42 failing hashes indicated by a corresponding integer
43 number. Depends on '--salt SUGGESTED=N'. Implies
44 --total-only.
45 -p TEXT, --passphrase=TEXT
46 Enter passphrase in cleartext, the keyword
47 SUGGESTED[=N] will cause the suggested passphrase to
48 be used. If N>1, N passphrases will be printed to
49 STDERR and a random one will be used (selection
50 printed to STDERR). Entering the name of an existing
51 file (or '-' for STDIN) will cause it to be read and a
52 random passphrase found in the file will be used
53 (creating a signature), or they will all be used in
54 sequence (--check-file).
55 -c FILE, --check-file=FILE
56 Check contents with the output of a previous run from
57 file or STDIN ('-'). Except when the --quiet option is
58 given, the previous output will contain all
59 information needed for the program, but not the
60 passphrase and the --execute option.
61 -i FILE, --input-file=FILE
62 Use names from FILE or STDIN ('-'), use one filename
63 per line.
64 -o FILE, --output-file=FILE
65 Print to FILE instead of STDOUT.
66 --file-source=PATH Read all files from PATH. The PATH-string is prepended
67 to every plain file-path that is read for a signature.
68 Remote files can be checked with
69 'ssh://<user>@<host>[/path]'. The option overrules any
70 File source specification in the --check-file.
71 -P FILE, --Private-file=FILE
72 Print private information (passwords etc.) to FILE
73 instead of STDERR.
74 -u USER, --user=USER Execute $(cmd) as USER, default 'nobody' (root/sudo
75 only)
76 -S, --Status For each file, add a line with unvarying file status
77 information: st_mode, st_ino, st_dev, st_uid, st_gid,
78 and st_size (like the '?' prefix, default False)
79 --Status-values=MODE Status values to print for --Status, default MODE is
80 'fmidugs' (file, mode, inode, device, uid, gid, size).
81 Also available (n)l(inks) a(time), (m)t(ime), and
82 c(time).
83 -t, --total-only Only print the total hash, unsets --detailed-view
84 (default True)
85 -d, --detailed-view Print hashes of individual files, is unset by --total-
86 only (default False)
87 -e, --execute Interpret $(cmd) (default False)
88 --execute-args=ARGS Arguments for the $(cmd) commands ($1 ....)
89 -n, --no-execute Explicitely do NOT Interpret $(cmd)
90 --import=FILE Import python modules (comma separated list without extension)
91 --print-textdump Print printable character+hexadecimal dump of input
92 bytes to STDERR for debugging purposes
93 --message=TEXT Add a comment message about the test
94 -m, --manual Print a short version of the manual and exit
95 --manual-long Print the long version of the manual and exit
96 --manual-html Print the manual in HTML format and exit
97 --manual-make Print the examples in the manual as a makefile and
98 exit
99 -r, --release-notes Print the release notes and exit
100 -l, --license Print license text and exit
101 -v, --verbose Print more information on output
102 -q, --quiet Print minimal information (hide filenames). If the
103 output is used with --check-file, the command line
104 options and arguments must be repeated.
105 [[[/pre]]][[[p]]]
106 FILE1 FILE2 ...
107 Names and paths of one or more files to be checked. All file arguments in SdT accept '-' as the STDIN file
108 (ie, piped data). Can use ssh://<user>@<host>/path pseudo-URLs for checking files at remote sites.
109 [[[/p]]][[[p]]]
110 Any name starting with a '$', eg, $PATH, will be interpreted as an environmental variable or a command
111 according to the bash conventions: '$ENV' and '${ENV}' as variables, '$(cmd;cmd...)' as system commands
112 (bash --restricted -c 'cmd;cmd...' PID). Where PID the current Process ID is (available as positional
113 parameter $0). Other parameters can be entered with the --execute-args option ($1 etc). Do not forget to
114 enclose the arguments in single ''-quotes! The commands are scanned for unwanted characters and these
115 are removed (eg, ' and \\, however, escaping $ is allowed, eg, '\\$1'). The use of '$(cmd;cmd...)'
116 requires explicit use of the -e or --execute option.
117 [[[/p]]][[[p]]]
118 Any string '@(python code)' will be evaluated as python 3 code. The '--execute' option is obligatory.
119 Note that the outer ()-brackets are removed. You can extend the program by importing modules with the
120 '--import <module>,<module>,....' option. The python code will be interpreted as a function body,
121 complete with obligatory return statement(s), and wrapped in a function definition. This function will
122 be executed in a separate namespace and the 'return'ed value will be exported and hashed. The current PID
123 is available as 'argv[0]' and the --execute-args argument values are available as list elements 'argv[1]',
124 'argv[2]', etc. @() statements are executed inside the running signduterre program and cannot be used to
125 querry a remote system with ssh:// pseudo-URL constructs.
126 [[[/p]]][[[p]]]
127 If executed as root or sudo, $(cmd;cmd...) will be executed as 'sudo -H -u <user>' which defaults to
128 --user nobody ('--user root' is at your own risk). This will obviously not work when invoked as non-root/sudo.
129 --user root is necessary when you need to check privileged information, eg, you want to check the MBR with
130 '$(dd if=/dev/hda bs=512 count=1 | od -X)'
131 However, as you might use --check-file with files you did not create yourself, it is important to
132 be warned if commands are to be executed.
133 [[[/p]]][[[p]]]
134 Interpretation of $() ONLY works if the -e or --execute options are entered. signduterre.py can easily
135 be adapted to automatically use the setting in the check-file. However, this is deemed insecure and
136 commented out in the distribution version.
137 [[[/p]]][[[p]]]
138 The -n or --no-execute option explicitely supress the interpretation of $(cmd) arguments.
139 [[[/p]]][[[p]]]
140 Meta information from stat() on files is signed when the filename is preceded by a '?'. '?./signduterre.py' will
141 extract (st_mode, st_ino, st_dev, st_nlinks, st_uid, st_gid, st_size) and hash a line of these data (visible
142 with --verbose). The --Status option will automatically add such a line in front of every file. Note that '?'
143 is implied for directories. Both '/' and '?/' produce a hash of, eg,:
144 [[[/p]]][[[pre]]]
145 stat(/) = [st_mode=041775, st_ino=2, st_dev=234881026, st_uid=0, st_gid=80, st_size=1360]
146 [[[/pre]]][[[p]]]
147 The --Status-values=<mode> option selects which status values will be used: f(ile), m(ode), i(node),
148 d(evice), u(id), g(id), s(ize), (n)l(inks), a(time), (m)t(ime), and c(time). Default is
149 --Status-values='fmidugs'. Note that nlinks of a directory include every file in the directory, so this
150 option can check whether files have been added to a directory.
151 [[[/p]]][[[p]]]
152 Arguments enclosed in []-brackets will be hidden in the output. That is, '[/proc/self/exe]' will show up as
153 '[1]' in the output (or '[n]' with n the number of the hidden argument), equivalent to the use of the
154 --quiet option. This means the hidden arguments must be entered again when using the --check-file (-c)
155 option.
156 [[[/p]]][[[p]]]
157 Signature-du-Terroir
158 [[[/p]]][[[p]]]
159 A very simple tool to generate a signature that can be used to test the integrity of files and "states" in
160 a running installation. signduterre.py constructs a signature of the current system state and checks
161 installation state with a previously made signature. The files are hashed with a passphrase to allow detection
162 of compromised systems while running on the same system. The signature checking can be subverted, but the
163 flexibillity of signduterre.py and the fact that the output of any command can be tested should hamper
164 automated root-kit attacks.
165 [[[/p]]][[[p]]]
166 signduterre.py writes a total SHA-256 hash to STDOUT of all the files and commands entered as arguments. It
167 can also write a hash for each individual file (insecure). The output of a signature can be send to a file and
168 later used to check with --check-file. Hashes are calculated with a hashed salt + passphrase sequence
169 pre-pended to create unpredictable hashes. This procedure ensures that an attacker does not know whether or
170 not the correct passphrase has been entered. An attacker can only know when to supply the requested hash
171 values if she knows the passphrase or has copies available of all the tested files and output of commands to
172 calculate the hashes on the fly.
173 [[[/p]]][[[LONG]]][[[p]]]
174 The Problem
175 [[[/p]]][[[p]]]
176 The problem SdT tries to solve is how to test whether your system has been compromised when you can only use
177 the potentially compromised system? The solution is to store a password encrypted signature (or fingerprint)
178 of your system when you are sure it is in a good state. Then you check whether the system can still
179 distinguish between correct and incorrect passwords when it regenerates the signature. The trick is to use
180 the right data (ie, questions) to generate the signature.
181 [[[/p]]][[[p]]]
182 The underlying idea is that some bits have to be changed to compromise a system. That is, program
183 files have been altered, settings and accounts changed, new processes are running or existing processes
184 altered. The most common situation is that some system programs have been changed to hide the traces of
185 the attack. For instance, the [[[i]]]ls[[[/i]]], [[[i]]]find[[[/i]]], and [[[i]]]stat[[[/i]]] commands are altered to hide the existence of new files
186 and programs, and the [[[i]]]netstat[[[/i]]] and [[[i]]]ps[[[/i]]] commands or the [[[i]]]/proc[[[/i]]] pseudo file system are changed to hide the
187 malicious processes that are running. Such wholescale adaptations of running systems can be executed
188 using standard, off-the-shelf application suits, so called rootkits. There are applications that can
189 detect common (known) rootkits and other malicious programs, eg, [[[i]]]chkrootkit[[[/i]]] ([[[a
190 href="http://www.chkrootkit.org/"]]]www.chkrootkit.org[[[/a]]]) and
191 [[[i]]]rootkit hunter[[[/i]]] ([[[a
192 href="http://www.rootkit.nl"]]]www.rootkit.nl[[[/a]]]). However, these rootkit detectors also use existing commands on the
193 potentially compromised system, so a rootkit can hide from them too.
194 [[[/p]]][[[p]]]
195 There are two obvious directions to guard against rootkits. One is to continuously run a process that
196 looks for attempts to install a rootkit and other malicious activities. The other is to take a snapshot
197 of the system in a known good state, and then flag changes in relevant areas, eg, like [[[i]]]Tripwire[[[/i]]]
198 ([[[a href="http://sourceforge.net/projects/tripwire/"
199 ]]]http://sourceforge.net/projects/tripwire/[[[/a]]]) and [[[i]]]Radmind[[[/i]]] ([[[a
200 href="http://rsug.itd.umich.edu/software/radmind/"
201 ]]]http://rsug.itd.umich.edu/software/radmind/[[[/a]]]).
202 Signature-du-Terroir takes the second route, it creates a signature of a set of relevant files and
203 command output, and checks later whether these have not been changed. However, when running such a test
204 on a compromised system, the attacker can theoretically "fool" any (automated) test. In practise, time
205 and other precious resources will limit what an attacker can accomplish. The idea is to raise the bar
206 for rootkits high enough to make them not worthwhile. SdT tries to make using signatures easy (cheap)
207 and subverting it difficult (expensive).
208 [[[/p]]][[[p]]]
209 As an illustration of the problem SdT treis to solve, take the [[[i]]]sha256sum[[[/i]]] command which generates file
210 hashes (signatures) using the SHA256 algorithm. Hashes can be generated and checked with this command:
211 [[[/p]]][[[pre]]]
212 # Use of sha256sum to check integrity of ps and ls commands
213 $ sha256sum /bin/ps /bin/ls > ps-ls.sh256
214 $ sha256sum -c ps-ls.sh256
215 [[[/pre]]][[[p]]]
216 A compromised file will show up as FAILED. This is ok for unintentional changes to the files. However, a
217 malicious attacker could easily replace [[[i]]]/usr/bin/sha256sum[[[/i]]] with a program that would replace the hash of
218 malicious replacements of these files with the hash sums of the original files. There are three easy ways
219 of doing that. Either simply say 'ok' when checking the file, print out the stored old hash value whenever
220 an altered file is requested by name, or look for the hash of the new, malicious replacement and print out
221 the old hash sum instead. The former two are easy to circumvent, the last one is somewhat less easy.
222 [[[/p]]][[[p]]]
223 The first solution to these avoidance strategies is to generate the signatures with a passphrase and random
224 string (salt). As long as the attacker does not know the passphrase, the only way to subvert SdT is to store
225 the original bits in the files and calculate the signature the moment SdT is called. As the attacker does
226 not know when the correct password or salt is entered, it is not possible to simply answer OK or repeat the
227 stored earlier results instead of calculating them de-novo.
228 [[[/p]]][[[p]]]
229 To be able to serve up the original bits, instead of the bits used on the compromised system, when asked
230 for the hashes, the attacker must divert attempts to read the files by SdT, but not at other moments.
231 There are many ways to do this, eg, running python in a chroot-jail, changing python itself, changing other
232 programs. To accommodate these diversion strategies, SdT allows to read data from each and every command
233 that can supply it. So, a binary file can be entered by name, with eg, cat, dd, perl, python, ruby, or read
234 from the [[[i]]]/proc[[[/i]]] system (if it is a running process), or from STDIN or shell subprocesses. For instance,
235 to protect against running in a chroot-jail, the inode number and device of the root directory can be read
236 from [[[i]]]/proc/self/root[[[/i]]], or [[[i]]]/proc/<PID>/root[[[/i]]], or simply from [[[i]]]/[[[/i]]].
237 [[[/p]]][[[/LONG]]][[[p]]]
238 Signature creation: Passphrases, salts, and hashes
239 [[[/p]]][[[p]]]
240 Good passphrases are difficult to remember, so their plaintext form should be protected. To protect the
241 passphrase against rainbow and brute force attacks, the passphrase is concatenated to a salt phrase and
242 hashed before use (SHA-256).
243 [[[/p]]][[[p]]]
244 The salt phrase is requested when constructing a signature. In interactive use, an 8 byte hexadecimal
245 (= 16 character) salt from [[[i]]]/dev/urandom[[[/i]]] is suggested. If '--salt SUGGESTED' is entered on the command line
246 as the salt, the suggested value will be used. The salt is printed in plaintext to the output. The salt will
247 make it more difficult to determine whether the same passphrase has been used to create different signatures.
248 [[[/p]]][[[p]]]
249 At the bottom, a 'TOTAL HASH' line will be printed that hashes all the lines printed for the files. This
250 includes the file names as printed on the hash lines. It is not inconceivable that existing signature files
251 could have been compromised in ways that might be missed when checking the signature. The total hash will
252 point out such changes.
253 [[[/p]]][[[p]]]
254 SECURITY
255 [[[/p]]][[[LONG]]][[[p]]]
256 When run on a compromised system, signduterre.py can be subverted if the attacker keeps a copy of all the
257 files and command outputs, and reroutes the open() and stat() functions, or simply delegating signduterre.py
258 to a chroot jail with the original system. In principle, signduterre.py only checks whether the computer
259 responds identically to when the signature file was made. There is no theoretic barrier against a compromised
260 computer perfectly simulating the original system when tested, but behaving adversely at other times. Except
261 for running from clean boot media (USB?), I know of no theoretical sound solution to this problem.
262 [[[/p]]][[[p]]]
263 However, this scenario assumes the use of unlimited resources and time. Inside a limited, real computer system,
264 the attacker must make compromises on what can and what cannot be simulated with the available time and
265 hardware. The idea behind signduterre.py is to "ask difficult questions" that increase the cost of simulating
266 the original system high enough to make detection of successful attacks likely.signduterre.py simply intends
267 to raise the bar high enoug. One point is to store the times needed to create the original hashes. This timing
268 can later be used to see whether the new timings are reasonable. If the same hardware takes considerably
269 longer to perform the same calculations, or needs a much longer delay before it starts, the tester might want
270 to see where this time is spent.
271 [[[/p]]][[[/LONG]]][[[p]]]
272 Signature-du-Terroir works on the assumption that any attacker in control of a compromised system cannot
273 predict whether the passphrase entered is correct or not. An attacker can always intercept the in- and output
274 of signduterre. When running with --check-file, this means the program can be made to print out OK
275 irrespective of the tests. A safe use of signduterre.py is to start with a random number of incorrect
276 passphrases and see whether they fail. Alternatively, and easier, is to add a number of unused salts
277 to the check-file and let the attacker guess which one is correct.
278 [[[/p]]][[[p]]]
279 THE CORRECT USE OF signduterre.py IS TO ENTER A RANDOM NUMBER OF INCORRECT PASSPHRASES OR SALTS FOR EACH
280 TEST AND SEE WHETHER IT FAILS AT THE CORRECT INSTANCES!
281 [[[/p]]][[[p]]]
282 On a compromised system, signduterre.py's detailed file testing (--detailed-view) is easily subverted. With a
283 matched file hash, the attacker will know that the correct passphrase has been entered and can print out the
284 stored hashes or 'ok's for the rest of the checks. So if the attacker keeps any entry in the signature file
285 uncompromised, she can intercept the output, test the password on the unchanged entry and substitute the
286 requested hashes for the output if the hash of that entry matches.
287 [[[/p]]][[[LONG]]][[[p]]]
288 When checking for root-kits and other malware, it is safest to compare the signature files from a different,
289 clean, system. But then you would not need signduterre.py anyway. If you have to work on the system itself,
290 only use the -t or --total-only options to create signatures with a total hash and without individual file
291 hashes. Such a signature can be used to check whether the system is unchanged. Another signature file WITH A
292 DIFFERENT PASSPHRASE can then be used to identify the individual files that have changed. If a detailed
293 signature file has the same passphrase, an attacker could use that other file to read the individual file
294 hashes to check whether the correct passphrase was entered.
295 [[[/p]]][[[/LONG]]][[[p]]]
296 Using the --check-file option in itself is UNsafe. An attacker simply has to print out 'OK' to defeat the
297 check. This attack can be foiled by making it unpredictable when signduterre.py should return 'OK'. This can
298 be done by using a list of salts or passphrases where only one of them (or none!) is correct. Any attacker
299 will have to guess when to return 'OK'.
300 [[[/p]]][[[LONG]]][[[p]]]
301 As generating and entering wrong passphrases and salts is tedious, users have to be supported in correct use
302 of SdT. To assist users, the '--salt SUGGESTED=<N>' option will generate a number N of salts. When
303 checking, each of these salts is tried in turn. An attacker that is unable to simulate the uncompromised
304 system will have to guess which one of the salts is the correct one, and whether or not the passphrase
305 is correct. This increases the chances of detecting compromised systems. If this is not enough guess
306 work, the '-a', '--all-salts-pattern' option will use all salts in sequence to generate total hashes,
307 but random salts will be changed in the output. This generates a pattern of failed salt tests. This pattern
308 is translated into a bit pattern and printed as an integer ([Fail, Fail, OK, Fail, OK, OK, Fail, OK]
309 = 00101101 (least significant first) = 10110100 (unsigned bin) = 180). On creation of a signature, this
310 number is printed to STDERR, on checking (--check-file) it is printed to STDOUT (note that the number
311 will never become 0 or all Fail). So for '--salt SUGGESTED=<N> --all-salts-pattern' the probability of
312 guessing the correct output goes from 1/N to 1/(2^N - 1). Note that '--all-salts-pattern' will work,
313 but is pointless, without '--salt SUGGESTED=<N>' with N>1.
314 [[[/p]]][[[p]]]
315 The '--passphrase SUGGESTED=N' option will generate and print N passphrases. One of these is chosen at
316 random for the signature. The number of the chosen passphrase is printed on STDERR with the passwords.
317 When checking a file, the stored passphrases can be read in again, either by entering the passphrase
318 file after the --passphrase option ('--passphrase <passphrase file>'), or directly from the --check-file.
319 signduterre.py will print out the result for each of the passphrases.
320 [[[/p]]][[[p]]]
321 Note, that storing passphrases in a file and feeding it to signduterre.py is MUCH less secure than just
322 typing them in. Moreover, it might completely defeat the purpose of signduterre.py. If future experiences
323 cast any more doubt on the security of this option, it will be removed.
324 [[[/p]]][[[p]]]
325 For those who want to know more about what an "ideal attacker" can do, see:[[[br]]]
326 Ken Thompson "Reflections on Trusting Trust"[[[br]]]
327 [[[a href="http://cm.bell-labs.com/who/ken/trust.html"]]]http://cm.bell-labs.com/who/ken/trust.html[[[/a]]][[[br]]]
328 [[[a href="http://www.ece.cmu.edu/~ganger/712.fall02/papers/p761-thompson.pdf"]]]http://www.ece.cmu.edu/~ganger/712.fall02/papers/p761-thompson.pdf[[[/a]]]
329 [[[/p]]][[[p]]]
330 David A Wheeler "Countering Trusting Trust through Diverse Double-Compiling"[[[br]]]
331 [[[a href="http://www.acsa-admin.org/2005/abstracts/47.html"]]]http://www.acsa-admin.org/2005/abstracts/47.html[[[/a]]]
332 [[[/p]]][[[p]]]
333 and the discussion of these at Bruce Schneier's 'Countering "Trusting Trust"'[[[br]]]
334 [[[a href="http://www.schneier.com/blog/archives/2006/01/countering_trus.html"]]]http://www.schneier.com/blog/archives/2006/01/countering_trus.html[[[/a]]]
335 [[[/p]]][[[/LONG]]][[[p]]]
336 Manual
337 [[[/p]]][[[p]]]
338 The intent of signduterre.py is to ensure that the signature cannot be subverted even if the system has been
339 compromised by an attacker that has obtained root control over the computer and any existing signature files.
340 [[[/p]]][[[p]]]
341 signduterre.py asks for a passphrase which is PRE-pended to every file before the hash is constructed (unless
342 the passphrase is entered with an option). As long as the passphrase is not compromised, the hashes cannot
343 be reconstructed. A randomly generated, unpadded base-64 encoded 16 Byte password (ie, ~22 characters) is
344 suggested in interactive use. If '--passphrase SUGGESTED' is entered on the command line or no passphrase is
345 enetered when asked, the suggested value will be used. This value is printed to STDERR (the screen or 2) for
346 safe keeping. Please, make sure you store the printed passphrase. For instance:
347 [[[/p]]][[[pre make=example1]]]
348 # make: example1
349 # Simple system sanity test using the 'which' command to establish the paths
350 $ python3 signduterre.py --passphrase SUGGESTED --salt SUGGESTED --detailed-view \\
351 `which python3 bash ps ls find stat` 2> test-20090630_11-14-03.pwd > test-20090630_11-14-03.sdt
352 $ python3 signduterre.py --passphrase test-20090630_11-14-03.pwd --check-file test-20090630_11-14-03.sdt
353 [[[/pre]]][[[p]]]
354 The first command will store the passphrase (and all error messages) in a file 'Signature_20090630_11-14-03.pwd'
355 and the check-file in 'Signature_20090630_11-14-03.sdt'. The second line will test the signature.
356 The signature will be made of the files used for the commands python3, bash, ps, ls, find, and stat.
357 These files are found using the 'which' command.
358 [[[/p]]][[[p]]]
359 Working with remote systems
360 [[[/p]]][[[p]]]
361 It is not secure to store files with the passphrase on the system you want to check. However, you could
362 pipe STDERR to some safe site.
363 [[[/p]]][[[pre]]]
364 # Send passphrase over ssh tunnel to safe site
365 $ python3 signduterre.py --passphrase SUGGESTED --salt SUGGESTED `which bash python3` \\
366 -o test-safe-store.sdt 2>&1 | ssh user@safe.host.site 'dd of=/home/user/safe/test-safe-store.pwd'
367 [[[/pre]]][[[p]]]
368 As the security of the passphrases is important and off-site storrage of files is often prudent or convenient,
369 this tunneling construct has been automated in all in- and output as a pseudo-URL: 'ssh://<user>@<host></path>',
370 eg, 'ssh://user@safe.host.site/home/user/safe/test-safe-store.pwd'. It is not possible to enter a
371 password in such a pseudo-URL, so the automatical login into the host system must be configured in SSH.[[[br /]]]
372 [[[em]]]Note: There are severe security risks involved when using SSH to login into another system if the
373 originating system is compromised[[[/em]]].
374 [[[/p]]][[[p]]]
375 The pseudo-url can be used with the [[[i]]]--output-file, --Private-file, --input-file, --check-file, --passphrase[[[/i]]]
376 options as well as for the actual file, ${ENV}, and $(cmd) arguments used to determine the signatures. The latter
377 allows to check files on remote systems, or to repeat a check from a remote system using the [[[i]]]--file-source[[[/i]]]
378 option (only works with plain files, ${ENV}, and $(cmd), not for @(python code), directories, or --Status arguments).
379 For instance:
380 [[[/p]]][[[pre]]]
381 # Use ssh:// pseudo-url to send passphrase to safe.host.site
382 $ python3 signduterre.py --passphrase SUGGESTED --salt SUGGESTED `which bash python3` \\
383 -o ssh://user@safe.host.site/home/user/safe/test-safe-store.sdt \\
384 -P ssh://user@safe.host.site/home/user/safe/test-safe-store.pwd
385 # Check files on remote compromised.host.site while running test program on safe.host.site
386 $ python3 signduterre.py --passphrase test-safe-store.pwd --check-file test-safe-store.sdt \\
387 --file-source ssh://user@compromised.host.site
388 [[[/pre]]][[[p]]]
389 To execute a remote $(cmd) argument, write $(ssh://<user>@<host>/cmd). Be aware that nested "-quotes might cause
390 problems. ${ENV} can be written as ${ssh://<user>@<host>/ENV}. When using a --file-source argument that starts
391 with 'ssh://', the $(cmd) and ${ENV} commands are internally rewritten into the above form. In both forms,
392 as well as the arguments entered with --execute-args, any '$' and '"' symbols are protected by '\$' and '\"' to
393 be evaluated at the host system, as they would be evaluated locally by the ssh command line. This might not
394 always work out as planned, so take care when using these pseudo-URLS. Note that no <path> argument will be used.
395 [[[/p]]][[[p]]]
396 The next example uses the ssh:// pseudo-URL to read the data in an alternative way on [[[i]]]localhost[[[/i]]]. Obviously, storing the
397 plain text passphrase on the same system makes it a rather pointless excersize. The example only works if your have
398 (open)SSH server and clients installed and appended the '~/.ssh/id_dsa.pub' or '~/.ssh/id_rsa.pub' file to '~/.ssh/authorized_keys',
399 and you used ssh-add or another application to open the key.
400 [[[/p]]][[[pre make=ssh1]]]
401 # make: ssh1
402 # Use ssh:// pseudo-url to read data in an alternative way
403 $ python3 signduterre.py --passphrase SUGGESTED --salt SUGGESTED -v -d -e `which dd` '$(cat `which dd`)' \\
404 -o test-safe-store.sdt \\
405 -P ssh://`whoami`@localhost${PWD}/test-safe-store.pwd
406 # check files the standard way
407 $ python3 signduterre.py -e --passphrase ssh://`whoami`@localhost${PWD}/test-safe-store.pwd --check-file test-safe-store.sdt
408 # Check files using ssh on localhost
409 $ python3 signduterre.py -e --passphrase ssh://`whoami`@localhost${PWD}/test-safe-store.pwd --check-file test-safe-store.sdt \\
410 --file-source ssh://`whoami`@localhost
411 [[[/pre]]][[[p]]]
412 Examples:[[[/p]]][[[pre make=example2]]]
413 # make: example2
414 # Self test of root directory, python, and signduterre.py using the 'which' command to establish the paths
415 $ python3 signduterre.py --detailed-view --salt 436a73e3 --passphrase liauwefa3251EWC -o test-self.sdt \\
416 / `which python3 signduterre.py`
417 $ python3 signduterre.py --passphrase liauwefa3251EWC -c test-self.sdt
418 [[[/pre]]][[[LONG]]][[[p]]]
419 Write a signature to the file test-self.sdt and test it with the --check-file option. The signature contains
420 the SHA-256 hashes of the files, [[[i]]]/usr/bin/python3[[[/i]]], [[[i]]]signduterre.py[[[/i]]], and the status information on the root
421 directory. The salt '436a73e3' and passphrase 'liauwefa3251EWC' are used.
422 [[[/p]]][[[/LONG]]][[[pre make=procfs1]]]
423 # make: procfs1
424 # Self test of root directory, python, and signduterre.py using the the /proc file system
425 $ python3 signduterre.py --detailed-view --salt SUGGESTED --passphrase liauwefa3251EWC -o test-self_proc.sdt \\
426 /proc/self/root /proc/self/exe `which signduterre.py`
427 $ python3 signduterre.py --passphrase liauwefa3251EWC --check-file test-self_proc.sdt
428 [[[/pre]]][[[LONG]]][[[p]]]
429 Write a signature to the file test-self_proc.sdt and test it with the --check-file option. The signature
430 contains the SHA-256 hashes of the same files as above, [[[i]]]/usr/bin/python3[[[/i]]], [[[i]]]signduterre.py[[[/i]]], and the status
431 information on the root directory. However, the python executable and the root directory are now accessed
432 through the [[[i]]]/proc[[[/i]]] file system. The suggested salt is used (written to test-self_proc.sdt) and the passphrase
433 is (again) 'liauwefa3251EWC'.
434 [[[/p]]][[[/LONG]]][[[pre make=example3]]]
435 # make: example3
436 # Test of supporting commands for chkrootkit
437 $ python3 signduterre.py --execute --total-only --salt SUGGESTED=8 --passphrase SUGGESTED --Status \\
438 --output-file=test-chkrootkit.sdt --Private-file=test-chkrootkit.pwd \\
439 signduterre.py `which bash awk cut egrep find head id ls netstat ps strings sed uname`
440 $ python3 signduterre.py --execute --passphrase test-chkrootkit.pwd --check-file test-chkrootkit.sdt
441 [[[/pre]]][[[LONG]]][[[p]]]
442 Writes a signature of the requested files to test-chkrootkit.sdt (signature) and private information to
443 test-chkrootkit.pwd (password and selected salt) and checks it in the next line. The files are those of
444 commands required by the [[[i]]]chkrootkit[[[/i]]] program (http://www.chkrootkit.org/), with bash added. The 'which'
445 command will give the paths for the commands. Eight salts are generated, of which only 1 is actually
446 used. When checking, the correct salt should match. This prevents a compromised program from simply
447 printing out OK tot he check. A more comprehensive evation of guessing the correct salt can be obtained
448 by using the '--all-salts-pattern' option.
449 [[[/p]]][[[/LONG]]][[[pre make=procfs2]]]
450 # make: procfs2
451 # Simply lump all "system" files, the PATH environment variable and the first 2 columns of the output of lsmod
452 $ python3 signduterre.py --execute --detail --salt SUGGESTED --passphrase liauwefa3251EWC --Status --total-only \\
453 signduterre.py /sbin/* /bin/* /usr/bin/find /usr/bin/stat /usr/bin/python3 '${PATH}' \\
454 '$(lsmod | awk "{print \$1, \$2}")' > test-20090625_14-31-54.sdt
456 # Failing check due to missing --execute option
457 $ python3 signduterre.py --passphrase liauwefa3251EWC -c test-20090625_14-31-54.sdt
458 $ python3 signduterre.py --passphrase liauwefa3251EWC -c test-20090625_14-31-54.sdt --no-execute
460 # Successful check
461 $ python3 signduterre.py --execute --passphrase liauwefa3251EWC --check-file test-20090625_14-31-54.sdt
462 [[[/pre]]][[[LONG]]][[[p]]]
463 Prints a signature to the system test-20090625_14-31-54.sdt and the automatically generated password to
464 test-20090625_14-31-54.pwd. The salt will be automatically determined. The signature contains the SHA-256
465 hashes of the file status and file contents of [[[i]]]signduterre.py, /sbin/*, /bin/*, /usr/bin/find,
466 /usr/bin/file, /usr/bin/python*[[[/i]]] on separate lines, and a hash of the PATH environment variable. Do not
467 display the hash of every single file, which could be insecure, but only the total hash.
468 The first two checks will both fail if test-20090625_14-31-54.sdt contains a $(cmd) entry.
469 The --no-execute option is default and prevents the execute option (if reading the execute option from the
470 signature file has been activated). The last check will succeed (if the files have not been changed).
471 [[[/p]]][[[/LONG]]][[[pre make=example4]]]
472 # make: example4
473 # Use a list of generated passphrases
474 $ python3 signduterre.py --salt SUGGESTED --passphrase SUGGESTED=20 signduterre.py \\
475 2> test-20090630_16-44-34.pwd > test-20090630_16-44-34.sdt
476 $ python3 signduterre.py -p test-20090630_16-44-34.pwd -c test-20090630_16-44-34.sdt
477 [[[/pre]]][[[LONG]]][[[p]]]
478 Will generate and print 20 passphrases and print a signature using one randomly chosen passphrase from these
479 20. Everything is written to the files 'test-20090630_16-44-34.pwd' and 'test-20090630_16-44-34.sdt'.
480 Such file names can easily be generated with 'test-`date "+%Y%m%d_%H-%M-%S"`.sdt'.
481 The next command will check all 20 passphrases generated before from the Signature file and print the results.
482 [[[/p]]][[[/LONG]]][[[pre make=example5]]]
483 # make: example5
484 # Use a list of generated salts with a pattern of correct salts
485 $ python3 signduterre.py --salt SUGGESTED=16 --passphrase SUGGESTED --all-salts-pattern \\
486 -P test-salt-pattern.pwd -o test-salt-pattern.sdt `which bash stat find ls ps id uname awk gawk perl`
487 $ python3 signduterre.py -p test-salt-pattern.pwd -c test-salt-pattern.sdt
488 # Compare to salt pattern number to the one from the check-file
489 $ cat test-salt-pattern.pwd
490 [[[/pre]]][[[LONG]]][[[p]]]
491 As the previous, but with a pattern of random correct and incorrect salts. The salt pattern number
492 indicates which salts were and were not correct.
493 [[[/p]]][[[/LONG]]][[[pre make=sudo1]]]
494 # make: sudo1
495 # Check MBR and current root directory (sudo and root user)
496 $ sudo python3 signduterre.py -u root -s SUGGESTED -p SUGGESTED --Status-values='i' -v -e -t \\
497 --output-file test-boot-sector.sdt --Private-file test-boot-sector.pwd --execute-args=sda \\
498 '?/proc/self/root' `which dd` '$(dd if=/dev/$1 bs=512 count=1 | od -X)'
499 $ sudo python3 signduterre.py -u root -e -p test-boot-sector.pwd -c test-boot-sector.sdt
500 [[[/pre]]][[[LONG]]][[[p]]]
501 Will hash the inode numbers of the effective root directory (eg, chroot) and the executable (python)
502 together with the contents of the MBR (Master Boot Record) on [[[i]]]/dev/sda[[[/i]]] in Hex. It uses suggested salt and
503 passphrase. Accessing [[[i]]]/dev/sda[[[/i]]] is only possible when [[[i]]]root[[[/i]]], so the command is entered with [[[i]]]sudo[[[/i]]] and
504 '--user root'. Use the '--print-execute' option if you want to check the output of the [[[i]]]dd[[[/i]]] command.
505 [[[/p]]][[[p]]]
506 The main problem with intrusion detection by comparing file contents is the ability of an attacker
507 to redirect attempts to read a compromised file to a stored copy of the original. So, [[[i]]]sha256sum[[[/i]]] or
508 python could be changed to read [[[i]]]'/home/attacker/old/ps'[[[/i]]] when the argument was [[[i]]]'/bin/ps'[[[/i]]]. This would
509 foil any scheme that depends on entering file names in programs. An answer to this threat is to
510 read the bytes in files in as many ways as possible. Therefor, forcing an attacker to change many
511 files which itself would increase the probability of detection of the attack. The following command
512 will read the same (test) file, and generate identical hashes, in many different ways.
513 [[[/p]]][[[/LONG]]][[[pre make=example6]]]
514 # make: example6
515 # Example generating identical signatures of the same text file in different ways
516 $ dd if=signduterre.py 2>/dev/null | \\
517 python3 signduterre.py -v -d -s 1234567890abcdef -p poiuytrewq \\
518 --execute --execute-args='signduterre.py' \\
519 signduterre.py - \\
520 '$(cat $1)' \\
521 '$(grep "" $1)' \\
522 '$(awk "{print}" $1)' \\
523 '$(cut -f 1-100 $1)' \\
524 '$(perl -ane "{print \$_}" $1)' \\
525 '$(python3 -c "import sys;f=open(sys.argv[1]);sys.stdout.buffer.write(f.buffer.read())" $1;)' \\
526 '$(ruby -e "f=open(ARGV[0]);print f.read();" $1;)'
527 [[[/pre]]][[[LONG]]][[[p]]]
528 These "commands" do not always return the same bytes (awk), or any bytes at all (grep), from a text
529 file as when used with a binary file. However, if the commands can print the bytes unaltered, the
530 signatures will be identical. That is, the following arguments will work on a binary file:
531 [[[/p]]][[[/LONG]]][[[pre make=example6]]]
532 # make: example6
533 # Example generating identical signatures of the same file in different ways, now for binary files
534 $ dd if=/bin/bash 2>/dev/null | \\
535 python3 signduterre.py -v -d -s 1234567890abcdef -p poiuytrewq \\
536 --execute --execute-args='/bin/bash' \\
537 /bin/bash - \\
538 '$(cat $1)' \\
539 '$(perl -ane "{print \$_}" $1)' \\
540 '$(python3 -c "import sys;f=open(sys.argv[1]);sys.stdout.buffer.write(f.buffer.read())" $1;)' \\
541 '$(ruby -e "f=open(ARGV[0]);print f.read();" $1;)'
542 [[[/pre]]][[[LONG]]][[[p]]]
543 Will generate the same identical signatures for [[[i]]]/bin/bash[[[/i]]], [[[i]]]STDIN[[[/i]]], [[[i]]]'$(cat /bin/bash)'[[[/i]]] etc.
544 There are obviously many more ways to read out the bytes from the disk or memory. The main point
545 being that it should be difficult to predict for an attacker which commands must be compromised
546 to hide changes in the system.
547 [[[/p]]][[[ /long]]][[[p]]]
548 The examples can be run as a makefile using make. Use one of the following commands:
549 [[[/p]]][[[pre]]]
550 # General examples, use them all
551 python3 signduterre.py --manual-make |make -f - example
552 # Linux specific examples using the second procfs example
553 python3 signduterre.py --manual-make |make -f - procfs2
554 # Examples requiring sudo, using first
555 python3 signduterre.py --manual-make | sudo make -f - sudo1
556 [[[/pre]]][[[p]]]
557 Known Bugs:
558 [[[/p]]][[[p]]]
559 - Reading files from STDIN (-) does not work if ssh:// has been used before as input for,
560 eg, file arguments, --check-file or --passphrase
561 [[[/p]]][[[pre make=sshbug1]]]
562 # make: sshbug1
563 # '-' stdin before ssh:// is fine
564 $ dd if=/bin/ps 2>/dev/null | python3.0 signduterre.py -edv -p SUGGESTED -s SUGGESTED \
565 /bin/ps - ssh://`whoami`@localhost/bin/ps
566 # '-' stdin after ssh:// FAILs
567 $ dd if=/bin/ps 2>/dev/null | python3.0 signduterre.py -edv -p SUGGESTED -s SUGGESTED \
568 /bin/ps ssh://`whoami`@localhost/bin/ps -
569 [[[/pre]]][[[p]]]
570 - Reading URLs as file arguments should work when Python treats URLs identical
571 to file descriptors. For the technically inclined:
572 when [[[br /]]][[[tt]]]with urllib.request.urlopen(url) as f:[[[/tt]]] [[[br /]]]works, URLs can be
573 entered where ever file paths can be entered..
574 [[[/p]]][[[/body]]][[[/html]]]
577 license = """
578 Signature-du-Terroir
579 Construct a signature of the installed software state or check a previously made signature.
581 copyright 2009, R.J.J.H. van Son
583 This program is free software: you can redistribute it and/or modify
584 it under the terms of the GNU General Public License as published by
585 the Free Software Foundation, either version 3 of the License, or
586 (at your option) any later version.
588 This program is distributed in the hope that it will be useful,
589 but WITHOUT ANY WARRANTY; without even the implied warranty of
590 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
591 GNU General Public License for more details.
593 You should have received a copy of the GNU General Public License
594 along with this program. If not, see <http://www.gnu.org/licenses/>.
595 """;
597 # Note that only release notes are put here
598 # See git repository for detailed change comments:
599 # git clone git://repo.or.cz/signduterre.git
600 # http://repo.or.cz/w/signduterre.git
601 releasenotes = """
602 20090820 - Release v0.6RC
603 20090820 - Added extensibility, or plugins, with functional @(python code) execution
604 20090817 - Replaced --print-hexdump by --print-textdump
605 20090817 - Implemented ssh:// with ${ENV}
606 20090814 - Release v0.5RC
607 20090811 - Implemented ssh tunnel for commands
608 20090810 - Added --file-source=PATH option
609 20090810 - Added ssh tunnel for all file i/o (ssh://...)
610 20090807 - DIFFERENT became FAIL in --check-file
611 20090730 - Release v0.4
612 20090724 - Added '--all-salts-pattern' and HTML formatting in manual
613 20090723 - Added URL support for all files. Does not yet work due to bug in Python 3.0
614 20090723 - Added '-' for STDIN
615 20090717 - Added --execute-args
616 20090716 - Release v0.3
617 20090713 - Added --quiet option
618 20090712 - moved from /dev/random to /dev/urandom
619 20090702 - Replaced -g with -p SUGGESTED[=N]
620 20090702 - Generating and testing lists of random salts
621 20090701 - Release v0.2
622 20090630 - Generating and testing random passphrases
623 20090630 - --execute works on $(cmd) only, nlinks in ?path and ? implied for directories
624 20090630 - Ported to Python 3.0
626 20090628 - Release v0.1b
627 20090628 - Added release-notes
629 20090626 - Release v0.1a
630 20090626 - Initial commit to Git
631 """;
633 #############################################################################
635 # IMPORT & INITIALIZATION #
637 #############################################################################
639 import sys;
640 import os;
641 import subprocess;
642 import stat;
643 import subprocess;
644 # if sys.stdout.isatty(): import readline;
645 import binascii;
646 import hashlib;
647 import re;
648 import time;
649 from optparse import OptionParser;
650 import base64;
651 import random;
652 import struct;
653 import urllib.request;
654 import urllib.error;
656 # Limit the characters that can be used in $(cmd) commands
657 # Only allow the escape of '$'
658 not_allowed_chars = re.compile('([^\w\ \.\/\"\|\;\:\,\-\$\[\]\{\}\(\)\@\`\!\*\=\\\\]|([\\\\]+([^\$\"\\\\]|$)))');
660 programname = "Signature-du-Terroir";
661 version = "0.6RC";
664 # Open files or pipes for in/output, use mode = 'b' if binary is needed
665 def open_infile(filename, mode):
666 if filename == '-':
667 return sys.stdin;
668 elif filename.lower().find('ssh://') > -1:
669 match = re.search('(?i)ssh://([^/]+)/(.*)$', filename);
670 tunnel_command = 'ssh '+match.group(1)+' "dd if=/'+match.group(2)+' "';
671 if 'b' in mode:
672 pipe = subprocess.Popen(tunnel_command, shell=True, stdout=subprocess.PIPE);
673 else:
674 pipe = subprocess.Popen(tunnel_command, shell=True, stdout=subprocess.PIPE, universal_newlines=True);
675 return pipe.stdout;
676 elif filename.find('://') > -1:
677 print("URL:", filename, file=current_private);
678 return urllib.request.urlopen(filename);
679 else:
680 if not os.path.isfile(filename):
681 print(filename, "does not exist", file=sys.stderr)
682 quit();
683 return open(filename, mode);
685 def open_outfile(filename, mode):
686 if filename == '-':
687 return sys.stdout;
688 elif filename.lower().find('ssh://') > -1:
689 match = re.search('(?i)ssh://([^/]+)/(.*)$', filename);
690 tunnel_command = 'ssh '+match.group(1)+' "dd of=/'+match.group(2)+' "';
691 if 'b' in mode:
692 pipe = subprocess.Popen(tunnel_command, shell=True, stdin=subprocess.PIPE);
693 else:
694 pipe = subprocess.Popen(tunnel_command, shell=True, stdin=subprocess.PIPE, universal_newlines=True);
695 return pipe.stdin;
696 elif filename.find('://') > -1:
697 print("URL:", filename, file=current_private);
698 return urllib.request.urlopen(filename);
699 else :
700 return open(filename, mode);
702 current_outfile = sys.stdout;
703 current_private = sys.stderr;
705 # Determine which kind of argument you have
706 def arg_is_shell_command (argument):
707 return_value = False;
708 if argument.startswith('$(') and argument.endswith(')'):
709 return_value = argument[2:-1];
710 return return_value;
712 def arg_is_env (argument):
713 return_value = False;
714 if arg_is_shell_command(argument): return return_value;
715 match = re.match(r'\$\{?[^\}]+\}?$', argument);
716 if match != None:
717 return_value = match.group(1);
718 return return_value;
720 def arg_is_python_script (argument):
721 return_value = False;
722 if argument.startswith('@(') and argument.endswith(')'):
723 return_value = argument[2:-1];
724 return return_value;
726 def arg_is_hidden (argument):
727 return_value = False;
728 if argument.startswith('[') and argument.endswith(']'):
729 return_value = argument[1:-1];
730 return return_value;
732 def arg_is_tunnel (argument):
733 return argument.startswith('ssh://');
735 def arg_is_stat (argument):
736 return_value = False;
737 if argument.startswith('?'):
738 return_value = argument[1:];
739 return return_value;
741 def arg_is_stdin (argument):
742 return argument == '-';
744 def arg_is_URL (argument):
745 return re.search(r'\://', argument) and not arg_is_tunnel(argument);
747 def arg_is_dir (argument):
748 return os.path.isdir(argument);
750 # Plain files are defined as NOT something else
751 def arg_is_plain_file (argument):
752 not_file = True;
753 not_file = not_file or arg_is_env(argument)
754 not_file = not_file or arg_is_shell_command (argument);
755 not_file = not_file or arg_is_python_script (argument);
756 not_file = not_file or arg_is_tunnel (argument);
757 not_file = not_file or arg_is_stdin (argument);
758 not_file = not_file or arg_is_URL (argument);
759 not_file = not_file or arg_is_dir (argument);
760 return not not_file;
762 #############################################################################
764 # OPTION HANDLING #
766 #############################################################################
768 parser = OptionParser()
769 parser.add_option("-s", "--salt", metavar="HEX",
770 dest="salt", default=False,
771 help="Enter salt in cleartext. If not given, a hexadecimal salt will be suggested. The SUGGESTED[=N] keyword will cause the selection of the suggested string. N is the number of salts generated (default N=1). If N>1, all will be printed and a random one will be used to generate the signature (selection printed to STDERR).")
772 parser.add_option("-a", "--all-salts-pattern",
773 dest="allsalts", default=False, action="store_true",
774 help="Use all salts in sequence, randomly replace salts with incorrect ones in the output to create a pattern of failing hashes indicated by a corresponding integer number. Depends on '--salt SUGGESTED=N'. Implies --total-only.")
775 parser.add_option("-p", "--passphrase", metavar="TEXT",
776 dest="passphrase", default=False,
777 help="Enter passphrase in cleartext, the keyword SUGGESTED[=N] will cause the suggested passphrase to be used. If N>1, N passphrases will be printed to STDERR and a random one will be used (selection printed to STDERR). Entering the name of an existing file (or '-' for STDIN) will cause it to be read and a random passphrase found in the file will be used (creating a signature), or they will all be used in sequence (--check-file).")
778 parser.add_option("-c", "--check-file",
779 dest="check", default=False, metavar="FILE",
780 help="Check contents with the output of a previous run from file or STDIN ('-'). Except when the --quiet option is given, the previous output will contain all information needed for the program, but not the passphrase and the --execute option.")
781 parser.add_option("-i", "--input-file",
782 dest="input", default=False, metavar="FILE",
783 help="Use names from FILE or STDIN ('-'), use one filename per line.")
784 parser.add_option("-o", "--output-file",
785 dest="output", default=False, metavar="FILE",
786 help="Print to FILE instead of STDOUT.")
787 parser.add_option("--file-source",
788 dest="filesource", default=False, metavar="PATH",
789 help="Read all files from PATH. The PATH-string is prepended to every plain file-path that is read for a signature. Remote files can be checked with 'ssh://<user>@<host>[/path]'. The option overrules any File source specification in the --check-file.")
790 parser.add_option("-P", "--Private-file",
791 dest="private", default=False, metavar="FILE",
792 help="Print private information (passwords etc.) to FILE instead of STDERR.")
793 parser.add_option("-u", "--user",
794 dest="user", default="nobody", metavar="USER",
795 help="Execute $(cmd) as USER, default 'nobody' (root/sudo only)")
796 parser.add_option("-S", "--Status",
797 dest="status", default=False, action="store_true",
798 help="For each file, add a line with unvarying file status information: st_mode, st_ino, st_dev, st_uid, st_gid, and st_size (like the '?' prefix, default False)")
799 parser.add_option("--Status-values",
800 dest="statusvalues", default="fmidugs", metavar="MODE",
801 help="Status values to print for --Status, default MODE is 'fmidugs' (file, mode, inode, device, uid, gid, size). Also available (n)l(inks) a(time), (m)t(ime), and c(time).")
802 parser.add_option("-t", "--total-only",
803 dest="total", default=False, action="store_true",
804 help="Only print the total hash, unsets --detailed-view (default True)")
805 parser.add_option("-d", "--detailed-view",
806 dest="detail", default=False, action="store_true",
807 help="Print hashes of individual files, is unset by --total-only (default False)")
808 parser.add_option("-e", "--execute",
809 dest="execute", default=False, action="store_true",
810 help="Interpret $(cmd) (default False)")
811 parser.add_option("--execute-args",
812 dest="executeargs", default='', metavar="ARGS",
813 help="Arguments for the $(cmd) commands ($1 ....)")
814 parser.add_option("-n", "--no-execute",
815 dest="noexecute", default=False, action="store_true",
816 help="Explicitely do NOT Interpret $(cmd)")
817 parser.add_option("--import",
818 dest="importfile", default='', metavar="FILE",
819 help="Import python modules (comma separated list)")
820 parser.add_option("--print-textdump",
821 dest="printtextdump", default=False, action="store_true",
822 help="Print printable character+hexadecimal dump of input bytes to STDERR for debugging purposes")
823 parser.add_option("--message",
824 dest="message", default='', metavar="TEXT",
825 help="Add a comment message about the test")
826 parser.add_option("-m", "--manual",
827 dest="manual", default=False, action="store_true",
828 help="Print a short version of the manual and exit")
829 parser.add_option("--manual-long",
830 dest="manuallong", default=False, action="store_true",
831 help="Print the long version of the manual and exit")
832 parser.add_option("--manual-html",
833 dest="manualhtml", default=False, action="store_true",
834 help="Print the manual in HTML format and exit")
835 parser.add_option("--manual-make",
836 dest="manualmake", default=False, action="store_true",
837 help="Print the examples in the manual as a makefile and exit")
838 parser.add_option("-r", "--release-notes",
839 dest="releasenotes", default=False, action="store_true",
840 help="Print the release notes and exit")
841 parser.add_option("-l", "--license",
842 dest="license", default=False, action="store_true",
843 help="Print license text and exit")
844 parser.add_option("-v", "--verbose",
845 dest="verbose", default=False, action="store_true",
846 help="Print more information on output")
847 parser.add_option("-q", "--quiet",
848 dest="quiet", default=False, action="store_true",
849 help="Print minimal information (hide filenames). If the output is used with --check-file, the command line options and arguments must be repeated.")
851 (options, check_filenames) = parser.parse_args();
854 # Start with opening any non-default output files
855 my_output = False;
856 if options.output:
857 current_outfile = open_outfile(options.output, 'w');
858 my_output = options.output;
860 my_private = False;
861 if options.private:
862 current_private = open_outfile(options.private, 'w');
863 my_private = options.private;
865 print("# Program: "+programname + " version " + version, file=current_outfile);
866 print("#", time.strftime("%Y/%m/%d %H:%M:%S", time.localtime()), "("+time.tzname[0]+")\n", file=current_outfile);
868 # Print license
869 if options.license:
870 print (license, file=sys.stderr);
871 exit(0);
872 # Print manual
873 if options.manual or options.manuallong:
874 cleartext_manual = re.sub(r"(?i)\[\[\[\s*(/?)\s*LONG\s*\]\]\]", r'[[[\1LONG]]]', manual);
875 if not options.manuallong:
876 currentstart = cleartext_manual.find('[[[LONG]]]');
877 while currentstart > -1:
878 currentend = cleartext_manual.find('[[[/LONG]]]', currentstart)+len('[[[/LONG]]]');
879 (firstpart, secondpart) = cleartext_manual.split(cleartext_manual[currentstart:currentend]);
880 cleartext_manual = firstpart+secondpart;
881 currentstart = cleartext_manual.find('[[[LONG]]]');
882 htmltags = re.compile('\[\[\[[^\]]*\]\]\]');
883 cleartext_manual = htmltags.sub('', cleartext_manual);
884 print (cleartext_manual, file=sys.stdout);
885 exit(0);
886 # Print HTML manual
887 if options.manualhtml:
888 protleftanglesbracks = re.compile('\<');
889 protrightanglesbracks = re.compile('\>');
890 leftanglesbracks = re.compile('\[\[\[');
891 rightanglesbracks = re.compile('\]\]\]');
892 html_manual = re.sub(r"(?i)\[\[\[\s*(/?)\s*LONG\s*\]\]\]", '', manual);
893 html_manual = protleftanglesbracks.sub('&lt;', html_manual);
894 html_manual = protrightanglesbracks.sub('&gt;', html_manual);
895 html_manual = leftanglesbracks.sub('<', html_manual);
896 html_manual = rightanglesbracks.sub('>', html_manual);
897 print (html_manual, file=sys.stdout);
898 exit(0);
899 # Print manual examples as makefile
900 if options.manualmake:
901 make_manual = re.sub("\$ ", "\t", manual);
902 make_manual = re.sub("\#", "\t#", make_manual);
903 make_manual = re.sub(r"\\\s*\n", '', make_manual);
904 make_manual = re.sub(r"\$", r'$$', make_manual);
905 # Protect "single" [ brackets
906 make_manual = re.sub(r"(^|[^\[])\[([^\[]|$)", r"\1&#91;\2", make_manual);
907 extrexamples = re.compile(r"\[\[\[pre\s+make\=?([a-zA-Z]*)([0-9]*)\s*\]\]\]\n([^\[]*)\n\[\[\[/pre\s*\]\]\]", re.IGNORECASE|re.MULTILINE|re.DOTALL);
908 exampleiter = extrexamples.finditer(make_manual);
909 makefile_list = [];
910 group_list={};
911 group_list['all'] = "all: ";
912 for match in exampleiter:
913 # We had to convert any '[' in the command. Now convert them back.
914 command_text = re.sub(r"\&\#91\;", '[', match.group(3));
915 makefile_list.append(match.group(1)+match.group(2)+":\n"+command_text);
916 if len(match.group(2)) > 0:
917 if not match.group(1) in group_list.keys():
918 group_list[match.group(1)] = match.group(1)+": ";
919 group_list['all'] += match.group(1)+" ";
920 group_list[match.group(1)] += match.group(1)+match.group(2)+" ";
921 else:
922 group_list['all'] += match.group(1)+" ";
923 print("help: \n\t@echo 'Use \"make -f - "+re.sub(':', '', group_list['all'])+"\"'", file=sys.stdout);
924 for group in group_list:
925 print(group_list[group]+"\n", file=sys.stdout);
926 makefile_list.sort()
927 previous_cat = 'NOT A VALUE';
928 for line in makefile_list:
929 (category, commands) = line.split(':\n');
930 if category != previous_cat:
931 previous_cat = category;
932 print("\n"+previous_cat+":", file=sys.stdout);
933 print(commands, file=sys.stdout);
934 # Clean option
935 print("\nclean:\n\trm test-*.sdt test-*.pwd", file=sys.stdout);
936 exit(0);
937 # Print release notes
938 if options.releasenotes:
939 print ("Version: "+version, file=sys.stderr);
940 print (releasenotes, file=sys.stderr);
941 exit(0);
943 my_salt = options.salt;
944 my_allsalts = options.allsalts;
945 my_passphrase = options.passphrase;
946 my_check = options.check;
947 my_status = options.status;
948 my_statusvalues = options.statusvalues;
949 my_verbose = options.verbose and not options.quiet;
950 my_quiet = options.quiet;
951 execute = options.execute;
952 execute_args = options.executeargs;
953 if options.noexecute: execute = False;
954 input_file = options.input;
955 my_filesource = options.filesource;
956 if my_filesource: print("File source: '"+my_filesource+"'\n", file=current_outfile);
957 my_message = options.message;
958 my_importfiles = options.importfile;
959 if my_importfiles:
960 print("Import: '"+my_importfiles+"'\n", file=current_outfile);
962 # Set total-only with the correct default
963 total_only = True;
964 total_only = not options.detail;
965 if options.total: total_only = options.total;
966 if my_allsalts: total_only = my_allsalts; # All alts pattern only sensible with total-only
967 if my_check: total_only = False;
969 my_user = options.user;
970 # Things might be executed as another user
971 user_change = '';
972 if os.getuid() == 0:
973 user_change = 'sudo -H -u '+my_user+' ';
974 if not my_quiet: print("User: "+my_user, file=current_outfile);
976 # Execute option
977 if execute:
978 text_execute = "True";
979 else:
980 text_execute = "False";
982 if execute and not my_quiet:
983 print("Execute system commands: "+text_execute+"\n", file=current_outfile);
984 if execute_args != '': print("Execute arguments: '"+execute_args+"'\n", file=current_outfile);
986 # --quiet option
987 if my_quiet: print("Quiet: True\n", file=current_outfile);
989 # --Status-values option
990 if my_statusvalues != 'fmidugs': print("Status-values: '"+my_statusvalues+"'\n", file=current_outfile);
992 # --message option
993 if len(my_message) > 0: print("Message: '''"+my_message+"'''\n", file=current_outfile);
995 #############################################################################
997 # ARGUMENT PROCESSING #
999 #############################################################################
1001 # Measure time intervals
1002 start_time = time.time();
1004 dev_random = open("/dev/urandom", 'rb');
1006 # Read the check file
1007 passphrase_list = [];
1008 salt_list = [];
1009 check_hashes = {};
1010 total_hash_list = [];
1011 if my_check:
1012 highest_arg_used = 0;
1013 print("# Checking: "+my_check+"\n", file=current_outfile);
1014 arg_list = check_filenames;
1015 check_filenames = [];
1016 with open_infile(my_check, 'r') as c:
1017 for line in c:
1018 match = re.search("Execute system commands:\s+(True|False)", line);
1019 if match != None:
1020 # Uncomment the next line if you want automatic --execute from the check-file (DANGEROUS)
1021 # execute = match.group(1).upper() == 'TRUE';
1022 continue;
1024 match = re.search("Execute arguments:\s+\'([\w\$\s\-\+\/]*)\'", line);
1025 if match != None:
1026 execute_args = match.group(1);
1027 continue;
1029 match = re.search("Quiet:\s+(True|False)", line);
1030 if match != None:
1031 my_quiet = match.group(1).upper() == 'TRUE';
1032 if my_quiet: my_verbose = False;
1033 continue;
1035 match = re.search("File source:\s+\'([\w\-\./\@\:]+)\'", line);
1036 if not my_filesource and match != None:
1037 my_filesource = match.group(1);
1038 continue;
1040 match = re.search("Salt\s*\+\s*TOTAL HASH\s*\:\s+\'([\w]*)\'\s+\'([\w]*)\'", line);
1041 if match != None:
1042 salt_list.append(match.group(1));
1043 total_hash_list.append(match.group(2));
1044 my_allsalts = True; # Salt+TOTAL HASH imples all-salts-pattern
1045 continue;
1047 match = re.search("Salt\:\s+\'([\w]*)\'", line);
1048 if match != None:
1049 salt_list.append(match.group(1));
1050 continue;
1052 match = re.search("Salt\s*\+\s*TOTAL HASH\s*\:\s+\'([\w]+)\'\s+\'([a-f0-9]+)\'", line);
1053 if match != None:
1054 salt_list.append(match.group(1));
1055 total_hash_list.append(match.group(2));
1056 continue;
1058 match = re.search("User\:\s+\'([\w]*)\'", line);
1059 if match != None:
1060 # Uncomment the next line if you want automatic --user from the check-file (DANGEROUS)
1061 # my_user = match.group(1);
1062 continue;
1064 match = re.search("Passphrase\:\s+\'([^\']*)\'", line);
1065 if match != None:
1066 passphrase_list.append(match.group(1));
1067 continue;
1069 match = re.search("Status-values\:\s+\'([\w]*)\'", line);
1070 if match != None:
1071 my_statusvalues = match.group(1);
1072 continue;
1074 match = re.search(r"Import\:\s+\'([^\']*)\'", line);
1075 if match != None:
1076 my_importfiles = match.group(1);
1077 continue;
1079 match = re.search("Message\:\s+\'\'\'(.*)$", line);
1080 if match != None:
1081 my_message = match.group(1);
1082 my_message = my_message[0:my_message.find("'''")];
1083 print("Message: '''"+my_message+"'''\n", file=current_outfile);
1084 continue;
1086 match = re.search("^\s*([a-f0-9]+)\s+\*(TOTAL HASH)\s*$", line)
1087 if match != None:
1088 total_hash_list.append(match.group(1));
1089 continue;
1091 match = re.search("^\s*([a-f0-9\-]+)\s+\*\[([0-9]+)\]\s*$", line)
1092 if match != None:
1093 filenumber = int(match.group(2));
1094 if filenumber > highest_arg_used: highest_arg_used = filenumber;
1095 # Watch out, arguments count from 0
1096 check_filenames.append(arg_list[filenumber - 1]);
1097 check_hashes['['+match.group(2)+']'] = match.group(1);
1098 continue;
1100 match = re.search("^\s*([a-f0-9\-]+)\s+\*(.*)\s*$", line)
1101 if match != None:
1102 check_filenames.append(match.group(2));
1103 # Catch --execute error as early as possible
1104 if match.group(2).startswith('$(') and not execute:
1105 error_message = "Executable argument \'"+match.group(2)+"\' only allowed with the --execute flag";
1106 print (error_message, file=sys.stderr);
1107 if not sys.stdout.isatty(): print(error_message, file=current_outfile);
1108 exit(0);
1109 check_hashes[match.group(2)] = match.group(1);
1110 continue;
1111 for i in range(highest_arg_used, len(arg_list)):
1112 check_filenames.append(arg_list[i]);
1113 check_hashes['['+str(i+1)+']'] = (64*'-');
1115 # Read input-file
1116 if input_file:
1117 with open_infile(input_file, 'r') as i:
1118 for line in i:
1119 # Clean up filename
1120 current_filename = re.sub('[^\w\-\.\/\$\{\(\)\}\?\[\]]', '', line);
1121 check_filenames.append(current_filename);
1122 if my_check: check_hashes['['+str(i+1)+']'] = (64*'-');
1124 stat_list = [];
1125 for x in check_filenames:
1126 if os.path.isdir(x):
1127 x = '?'+x;
1128 if my_status and not x.startswith(('?', '$')):
1129 stat_list.append('?'+x);
1130 stat_list.append(x);
1131 check_filenames = stat_list;
1133 # Seed Pseudo Random Number Generator
1134 seed = dev_random.read(16);
1135 random.seed(seed);
1137 # Read suggested salts from /dev/(u)random if needed
1138 if my_salt:
1139 if my_salt.startswith('SUGGESTED'):
1140 N=1;
1141 match = re.search("([0-9][0-9]*)$", my_salt);
1142 if match != None:
1143 N = int(match.group(1));
1144 for i in range(0,N):
1145 salt = dev_random.read(8);
1146 salt_list.append(str(binascii.hexlify(salt), 'ascii'));
1147 else:
1148 salt_list.append(my_salt);
1149 elif len(salt_list) == 0:
1150 salt = dev_random.read(8);
1151 sys.stderr.write("Enter salt (suggest \'"+str(binascii.hexlify(salt), 'ascii')+"\'): ");
1152 new_salt = input();
1153 if not new_salt: new_salt = str(binascii.hexlify(salt), 'ascii');
1154 salt_list.append(new_salt);
1156 # If not combining salts with TOTAL HASH, print salts now
1157 if not my_allsalts:
1158 for my_salt in salt_list:
1159 print("Salt: \'"+my_salt+"\'", file=current_outfile);
1161 # Get passphrase
1162 if my_passphrase and(my_passphrase == '-' or my_passphrase.find("://") > -1 or os.path.isfile(my_passphrase)):
1163 with open_infile(my_passphrase, 'r') as file:
1164 for line in file:
1165 match = re.search("Passphrase\:\s+\'([^\']*)\'", line);
1166 if match != None:
1167 passphrase_list.append(match.group(1));
1168 elif not my_passphrase and len(passphrase_list) == 0:
1169 suggest_passphrase = dev_random.read(16);
1170 suggest_string = "";
1171 if not my_check:
1172 suggest_string = "(suggest \'"+str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=')+"\')";
1173 sys.stderr.write("Enter passphrase"+suggest_string+": ");
1174 # How kan we make this unreadable on input?
1175 current_passphrase = input();
1176 if not current_passphrase:
1177 current_passphrase = str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=');
1178 print("Passphrase: \'"+current_passphrase+"\'", file=current_private);
1179 passphrase_list.append(current_passphrase);
1180 elif my_passphrase.startswith('SUGGESTED'):
1181 N = 1;
1182 match = re.search("([0-9][0-9]*)$", my_passphrase);
1183 if match != None:
1184 N = int(match.group(1));
1185 j = int(random.random()*N);
1186 for i in range(0, N):
1187 suggest_passphrase = dev_random.read(16);
1188 current_passphrase = str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=');
1189 print("Passphrase: \'"+current_passphrase+"\'", file=current_private);
1190 passphrase_list.append(current_passphrase);
1191 else:
1192 passphrase_list.append(my_passphrase);
1194 selected_salt = 1;
1195 fail_fraction = 0.5;
1196 if not my_check:
1197 if len(passphrase_list) > 1:
1198 j = int(random.random()*len(passphrase_list));
1199 passphrase_list = [passphrase_list[j]];
1200 print("# Selected passphrase:", j+1, file=current_private);
1201 if len(salt_list) > 1:
1202 j = int(random.random()*len(salt_list));
1203 # Make sure at least 1 salt will match and print the selection if only one is used
1204 selected_salt = j+1;
1205 if not my_allsalts:
1206 salt_list = [salt_list[selected_salt-1]];
1207 print("# Selected salt:", selected_salt, file=current_private);
1208 else:
1209 salt_N = len(salt_list);
1210 fail_fraction = (salt_N/2.0)/(salt_N - 1);
1211 else:
1212 fail_fraction = 0;
1214 # Close /dev/(u)random
1215 dev_random.close;
1217 #############################################################################
1219 # SIGNATURE CREATION AND CHECKING #
1221 #############################################################################
1223 end_time = time.time();
1224 print("# Preparation time:", end_time - start_time, "seconds\n", file=current_outfile);
1226 pnum = 1;
1227 snum = 1;
1228 corrpnum = 0;
1229 corrsnum = 0;
1230 matched_salt_pattern = -1;
1231 salt_pattern_number = -1;
1233 for my_passphrase in passphrase_list:
1234 snum = 1;
1235 # Initialize salt pattern
1236 if my_allsalts:
1237 salt_pattern_number = 0;
1238 current_salt_power = 1;
1240 for my_salt in salt_list:
1241 print("# Start signature: ", end='', file=current_outfile);
1242 if len(passphrase_list) > 1: print("passphrase -", pnum, end='', file=current_outfile);
1243 if len(salt_list) > 1: print(" salt -", snum, end='', file=current_outfile);
1244 print("", file=current_outfile);
1246 # Should everything be printed?
1247 print_verbose = my_verbose and not (my_allsalts and snum > 1);
1249 file_argnum = 0;
1250 start_time = time.time();
1251 # Construct the passphrase hash
1252 passphrase = hashlib.sha256();
1254 passphrase.update(bytes(my_salt, encoding='ascii'));
1255 passphrase.update(bytes(my_passphrase, encoding='ascii'));
1257 # Create prefix which is a hash of the salt+passphrase
1258 prefix = passphrase.hexdigest();
1260 ##########################################
1262 # Create signature and write output #
1264 ##########################################
1266 totalhash = hashlib.sha256();
1267 totalhash.update(bytes(prefix, encoding='ascii'));
1268 for org_filename in check_filenames:
1269 # Create file hash object
1270 filehash = hashlib.sha256();
1271 filehash.update(bytes(prefix, encoding='ascii'));
1272 # Remove []
1273 filename = org_filename.strip('[').rstrip(']');
1274 # Preprocessing filename to include file sources
1275 # Insert remote ssh:// if necessary
1276 if my_filesource:
1277 if arg_is_tunnel(my_filesource) and not arg_is_tunnel(filename):
1278 if arg_is_env(filename) :
1279 match = re.search(r"(?i)ssh://([^/]+)", my_filesource);
1280 if match != None: filename = "${ssh://"+match.group(1)+"/"+arg_is_env(filename)+"}";
1281 elif arg_is_shell_command(filename):
1282 match = re.search(r"(?i)ssh://([^/]+)", my_filesource);
1283 if match != None: filename = "$(ssh://"+match.group(1)+"/"+arg_is_shell_command(filename)+")";
1284 elif arg_is_plain_file(filename):
1285 filename = my_filesource+filename;
1287 # Use python @() constructs
1288 if arg_is_python_script(filename):
1289 if not execute:
1290 error_message = "Executable argument \'"+filename+"\' only allowed with the --execute flag";
1291 print (error_message, file=sys.stderr);
1292 exit(1);
1293 # Check for dangerous constructs
1294 if re.search(r'\bimport\b', filename):
1295 print("Error: Import statement not allowed, please use --import option", file=sys.stderr);
1296 exit(1);
1297 # Construct a code wrapper around the user supplied code
1298 statement_string = "";
1299 if my_importfiles: statement_string += "import "+re.sub(',', ', ', my_importfiles)+';\n';
1300 statement_string += "def sdt_exec_code():\n\t"+re.sub(r'\\n', "\n\t", re.sub(r'\\t', '\t', filename[2:(len(filename)-1)]));
1301 # Compile and execute code in a limited namespace
1302 user_code = compile(statement_string+"\nsdt_export_result = sdt_exec_code()\n", '<user code>', 'exec');
1303 userdict = {'sdt_export_result' : None};
1304 argvlist = [];
1305 argvlist.append(os.getpid());
1306 for value in execute_args.split():
1307 argvlist.append(value);
1308 userdict['argv'] = argvlist;
1309 # Execute code
1310 exec(user_code, userdict);
1311 # Use result
1312 b = None;
1313 if userdict['sdt_export_result']:
1314 b = userdict['sdt_export_result'];
1315 else:
1316 b = None;
1317 print("Error: '"+filename+"' did not deliver any result", file=sys.stderr);
1318 exit(1);
1319 filehash.update(bytes(b, encoding='utf8'));
1320 if options.printtextdump: # For debugging commands
1321 print(str(b));
1322 # Use system variables and commands
1323 elif arg_is_env(filename) or arg_is_shell_command(filename):
1324 orig_command_filename = filename;
1325 # ${ENV} environment variables
1326 match = re.search('^\$[\{]?([^\}\(\)]+)[\}]?$', filename);
1327 if match != None:
1328 current_var = not_allowed_chars.sub(" ", match.group(1));
1329 if print_verbose:
1330 print("# echo $"+ current_var, file=current_outfile);
1331 # Redirect env query to other system
1332 if current_var.startswith("ssh://"):
1333 match = re.search(r"(?i)(ssh://[^/]+/)(.*)$", current_var);
1334 if match != None:
1335 current_ssh = match.group(1);
1336 current_command = r"echo -n ${"+match.group(2)+"}";
1337 filename = '$('+current_ssh+current_command+')';
1338 # Current system
1339 else:
1340 b = os.environ[current_var];
1341 filehash.update(bytes(b, encoding='utf8'));
1342 # Commands $(command)
1343 match = re.search('^\$\((.+)\)$', filename);
1344 if match != None:
1345 if not execute :
1346 error_message = "Executable argument \'"+filename+"\' only allowed with the --execute flag";
1347 print (error_message, file=sys.stderr);
1348 exit(0);
1349 # Clean up command
1350 current_command = not_allowed_chars.sub(" ", match.group(1));
1351 # Expand remote ssh://
1352 current_host = None;
1353 current_executable = None;
1354 current_args = execute_args;
1355 if current_command.startswith("ssh://"):
1356 match = re.search(r"(?i)ssh://([^/]+)/(.*)$", current_command);
1357 if match != None:
1358 current_host = match.group(1);
1359 current_command = match.group(2);
1360 # Protect $, ", and backslashes
1361 current_command = re.sub(r'\\', r'\\', current_command);
1362 current_command = re.sub(r'\$', r'\$', current_command);
1363 current_executable = "/usr/bin/ssh";
1364 current_args = re.sub(r'\$', r'\$', current_args);
1365 current_args = re.sub(r'\"', r'\"', current_args);
1366 # Create command line
1367 current_command_line = user_change+"bash --restricted -c \'"+current_command+"\' "+str(os.getpid())+" "+current_args;
1368 # Add ssh command
1369 if current_executable:
1370 current_command_line = re.sub(r'\"', r'\\"', current_command_line);
1371 current_command_line = current_executable+' '+current_host+' "'+current_command_line+'"';
1372 # Print command
1373 if print_verbose:
1374 print("#", current_command_line, file=current_outfile);
1375 # Spawn command and open a pipe to the output
1376 pipe = subprocess.Popen(current_command_line, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE);
1377 for b in pipe.stdout:
1378 if type(b).__name__ == 'str':
1379 b = bytes(b, encoding='utf8');
1380 filehash.update(b);
1381 if options.printtextdump: # For debugging commands
1382 print(str(b));
1383 # See whether there was an error
1384 pipe.wait();
1385 if pipe.returncode:
1386 error_message = pipe.stderr.read();
1387 print('$('+current_command+')', "\n", str(error_message, encoding='UTF8'), file=sys.stderr);
1388 exit(pipe.returncode);
1389 # Return filename to original command
1390 filename = orig_command_filename;
1391 # lstat() meta information
1392 elif arg_is_stat(filename):
1393 if not os.path.exists(filename.lstrip('?')):
1394 print(filename, "does not exist", file=sys.stderr)
1395 quit();
1396 filestat = os.stat(filename.lstrip('?'));
1397 if my_statusvalues == "": my_statusvalues = 'fmidlugs'
1398 b = "";
1399 if 'f' in my_statusvalues:
1400 b += 'stat('+filename.lstrip('?')+') = '
1401 b += '[';
1402 if 'm' in my_statusvalues:
1403 b += 'st_mode='+str(oct(filestat.st_mode))+', ';
1404 if 'i' in my_statusvalues:
1405 b += 'st_ino='+str(filestat.st_ino)+', ';
1406 if 'd' in my_statusvalues:
1407 b += 'st_dev='+str(filestat.st_dev)+', '
1408 if 'l' in my_statusvalues:
1409 b += 'st_nlink='+str(filestat.st_nlink)+', '
1410 if 'u' in my_statusvalues:
1411 b += 'st_uid='+str(filestat.st_uid)+', '
1412 if 'g' in my_statusvalues:
1413 b += 'st_gid='+str(filestat.st_gid)+', '
1414 if 's' in my_statusvalues:
1415 b += 'st_size='+str(filestat.st_size)+', '
1416 if 'a' in my_statusvalues:
1417 b += 'st_atime='+str(filestat.st_atime)+', '
1418 if 't' in my_statusvalues:
1419 b += 'st_mtime='+str(filestat.st_mtime)+', '
1420 if 'c' in my_statusvalues:
1421 b += 'st_ctime='+str(filestat.st_ctime);
1423 b = b.rstrip(', ') + ']';
1424 filehash.update(bytes(b, encoding='utf8'));
1425 if print_verbose:
1426 print ("# "+ b, file=current_outfile);
1427 # Use file
1428 else:
1429 # open and read the file
1430 if arg_is_plain_file(filename) and not os.path.exists(filename):
1431 print(filename, "does not exist", file=sys.stderr)
1432 quit();
1434 if arg_is_stdin(filename):
1435 total_bytes = 0;
1436 for b in sys.stdin.buffer:
1437 total_bytes += len(b);
1438 if type(b).__name__ == 'str':
1439 b = bytes(b, encoding='utf8');
1440 filehash.update(b);
1441 if options.printtextdump: # For debugging commands
1442 print(str(b));
1443 if total_bytes == 0:
1444 print("ERROR: No bytes read from STDIN. Processing aborted.", file=sys.stderr);
1445 quit(1);
1446 else:
1447 with open_infile(filename, 'rb') as file:
1448 for b in file:
1449 if type(b).__name__ == 'str':
1450 b = bytes(b, encoding='utf8');
1451 filehash.update(b);
1452 if options.printtextdump: # For debugging commands
1453 print(str(b));
1455 current_digest = filehash.hexdigest();
1456 print_name = org_filename;
1457 if my_quiet or arg_is_hidden(org_filename):
1458 file_argnum += 1;
1459 print_name = '['+str(file_argnum)+']';
1460 current_hash_line = current_digest+" *"+print_name
1461 totalhash.update(bytes(current_hash_line, encoding='ascii'));
1463 # Be careful to use this ONLY after totalhash has been updated!
1464 if total_only:
1465 current_hash_line = (len(current_digest)*'-')+" *"+print_name;
1467 # Write output
1468 if not my_check:
1469 if not (my_quiet and total_only) and not (my_allsalts and snum > 1):
1470 print(current_hash_line, file=current_outfile);
1471 elif not (my_quiet or my_allsalts):
1472 if check_hashes[print_name] == (len(current_digest)*'-'):
1473 # Suppress redundant output of empty, ----, lines
1474 if snum <= 1 and pnum <= 1:
1475 print(check_hashes[print_name]+" *"+print_name, file=current_outfile);
1476 elif current_digest != check_hashes[print_name]:
1477 print("FAILED: "+current_hash_line, file=current_outfile);
1478 else:
1479 print("ok"+" *"+print_name, file=current_outfile);
1481 # Handle total hash
1482 current_total_digest = totalhash.hexdigest();
1483 # Write (in)correct salts with the TOTAL HASH
1484 if my_allsalts:
1485 output_salt = my_salt;
1486 j = random.random();
1487 # Randomly create an incorrect salt for failed output
1488 if not my_check:
1489 if j < fail_fraction and snum != selected_salt:
1490 salt = dev_random.read(8);
1491 output_salt = str(binascii.hexlify(salt), 'ascii');
1492 else:
1493 salt_pattern_number += current_salt_power;
1494 current_total_digest_line = "Salt+TOTAL HASH: '"+output_salt+"' '"+current_total_digest+"'";
1495 else: # Standard TOTAL HASH line
1496 current_total_digest_line = current_total_digest+" *"+"TOTAL HASH";
1497 end_time = time.time();
1498 print("# \n# Total hash - Time to completion:", end_time - start_time, "seconds", file=current_outfile);
1499 total_hash_num = 0;
1500 if my_allsalts: total_hash_num = snum-1; # Current TOTAL HASH number of more are used
1501 if not my_check:
1502 print(current_total_digest_line+"\n", file=current_outfile);
1503 elif current_total_digest != total_hash_list[total_hash_num]:
1504 if not my_allsalts: print("FAILED: "+current_total_digest_line+"\n", file=current_outfile);
1505 else:
1506 if my_allsalts: salt_pattern_number += current_salt_power; # Update salt bit pattern
1507 match_number = "";
1508 if len(passphrase_list) > 1 or len(salt_list): match_number = " #"
1509 if len(passphrase_list) > 1: match_number += " passphrase no: "+str(pnum);
1510 if len(salt_list) > 1: match_number += " salt no: "+str(snum);
1511 if not my_allsalts: print("OK"+" *"+"TOTAL HASH"+match_number+"\n", file=current_outfile);
1512 corrsnum = snum;
1513 corrpnum = pnum;
1514 snum += 1;
1515 if my_allsalts: current_salt_power *= 2; # Update current bit position in salt pattern
1516 if my_check and corrpnum == pnum: matched_salt_pattern = salt_pattern_number;
1517 pnum += 1;
1519 if my_check and len(passphrase_list) > 1:
1520 if corrpnum > 0:
1521 print("Passphrase entry:",corrpnum,"matched", file=current_outfile);
1522 else:
1523 print("No passphrase entry matched!", file=current_outfile);
1524 if my_check and (not my_allsalts) and len(salt_list) > 1:
1525 if corrpnum > 0:
1526 if corrsnum > 0:
1527 print("Salt entry:",corrsnum,"matched", file=current_outfile);
1528 else:
1529 print("No salt entry matched!", file=current_outfile);
1530 else:
1531 print("No entry matched", file=current_outfile);
1532 # Print salt bit patterns
1533 elif my_check and my_allsalts:
1534 print("Salt pattern number:", matched_salt_pattern, file=current_outfile);
1535 elif not my_check and my_allsalts:
1536 print("# Salt pattern number:", salt_pattern_number, file=current_private);
1538 # Close output files if necessary
1539 if my_output and my_output != '-':
1540 current_outfile.close();
1541 if my_private and my_private != '-':
1542 current_private.close();