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