Adapted examples and manual reflecting better handling om binary files in $() and...
[signduterre.git] / signduterre.py
blob653b4ebe8c80cf2fb91298b54ab0d04d0596a1d0
1 #!/usr/bin/python3.0
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 manual = """
17 [[[!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]]]
18 Construct a signature of the installed software state or check the integrity of the installation
19 using a previously made signature.
20 [[[/p]]][[[p]]]
21 Usage: signduterre.py [options] FILE1 FILE2 ...
22 [[[/p]]][[[p]]]
23 Options:[[[/p]]][[[pre]]]
24 -h, --help show this help message and exit
25 -s HEX, --salt=HEX Enter salt in cleartext. If not given, a hexadecimal
26 salt will be suggested. The SUGGESTED[=N] keyword will
27 cause the selection of the suggested string. N is the
28 number of salts generated (default N=1). If N>1, all
29 will be printed and a random one will be used to
30 generate the signature (selection printed to STDERR).
31 -a, --all-salts-pattern
32 Use all salts in sequence, randomly replace salts with
33 incorrect ones in the output to create a pattern of
34 failing hashes indicated by a corresponding integer
35 number. Depends on '--salt SUGGESTED=N'.
36 Implies --total-only.
37 -p TEXT, --passphrase=TEXT
38 Enter passphrase in cleartext, the keyword
39 SUGGESTED[=N] will cause the suggested passphrase to
40 be used. If N>1, N passphrases will be printed to
41 STDERR and a random one will be used (selection
42 printed to STDERR). Entering the name of an existing
43 file (or '-' for STDIN) will cause it to be read and a
44 random passphrase found in the file will be used
45 (creating a signature), or they will all be used in
46 sequence (--check-file).
47 -c FILE, --check-file=FILE
48 Check contents with the output of a previous run from
49 file or STDIN ('-'). Except when the --quiet option is
50 given, the previous output will contain all
51 information needed for the program, but not the
52 passphrase and the --execute option.
53 -i FILE, --input-file=FILE
54 Use names from FILE or STDIN ('-'), use one
55 filename per line
56 -o FILE, --output-file=FILE
57 Print to FILE instead of STDOUT
58 -P FILE, --Private-file=FILE
59 Print private information (passwords etc.) to FILE
60 instead of STDERR
61 -u USER, --user=USER Execute $(cmd) as USER, default 'nobody' (root/sudo
62 only)
63 -S, --Status For each file, add a line with unvarying file status
64 information: st_mode, st_ino, st_dev, st_uid, st_gid,
65 and st_size (like the '?' prefix, default False)
66 --Status-values=MODE Status values to print for --Status, default MODE is
67 'fmidlugs' (file, mode, inode, device, uid, gid, size).
68 Also available (n)l(inks), a(time), (m)t(ime), and
69 c(time).
70 -t, --total-only Only print the total hash, unsets --detailed-view
71 (default True)
72 -d, --detailed-view Print hashes of individual files, is unset by --total-
73 only (default False)
74 -e, --execute Interpret $(cmd) (default False)
75 --execute-args=ARGS Arguments for the $(cmd) commands ($1 ....)
76 -n, --no-execute Explicitely do NOT Interpret $(cmd)
77 --print-execute Print the results of $() command execution to STDERR
78 for debugging purposes
79 -m, --manual Print the manual and exit
80 --manual-html Print the manual in HTML format and exit
81 --manual-make Print the examples in the manual as a makefile and
82 exit
83 -r, --release-notes Print the release notes and exit
84 -l, --license Print license text and exit
85 -v, --verbose Print more information on output
86 -q, --quiet Print minimal information (hide filenames). If the
87 output is used with --check-file, the command line
88 options and arguments must be repeated.
89 [[[/pre]]][[[p]]]
90 FILE1 FILE2 ...
91 Names and paths of one or more files to be checked. All file arguments in SdT accept '-' as the STDIN file
92 (ie, piped data).
93 [[[/p]]][[[p]]]
94 Any name starting with a '$', eg, $PATH, will be interpreted as an environmental variable or a command
95 according to the bash conventions: '$ENV' and '${ENV}' as variables, '$(cmd;cmd...)' as system commands
96 (bash --restricted -c 'cmd;cmd...' PID). Where PID the current Process ID is (available as positional
97 parameter $0). Other parameters can be entered with the --execute-args option ($1 etc). Do not forget to
98 enclose the arguments in single ''-quotes! The commands are scanned for unwanted characters and these
99 are removed (eg, ' and \\, however, escaping $ is allowed, eg, '\\$1'). The use of '$(cmd;cmd...)'
100 requires explicit use of the -e or --execute option.
101 [[[/p]]][[[p]]]
102 If executed as root or sudo, $(cmd;cmd...) will be executed as 'sudo -H -u <user>' which defaults to
103 --user nobody ('--user root' is at your own risk). This will obviously not work when invoked as non-root/sudo.
104 --user root is necessary when you need to check privileged information, eg, you want to check the MBR with
105 '$(dd if=/dev/hda bs=512 count=1 status=noxfer | od -X)'
106 However, as you might use --check-file with files you did not create yourself, it is important to
107 be warned if commands are to be executed.
108 [[[/p]]][[[p]]]
109 Interpretation of $() ONLY works if the -e or --execute options are entered. signduterre.py can easily
110 be adapted to automatically use the setting in the check-file. However, this is deemed insecure and
111 commented out in the distribution version.
112 [[[/p]]][[[p]]]
113 The -n or --no-execute option explicitely supress the interpretation of $(cmd) arguments.
114 [[[/p]]][[[p]]]
115 Meta information from stat() on files is signed when the filename is preceded by a '?'. '?./signduterre.py' will
116 extract (st_mode, st_ino, st_dev, st_nlinks, st_uid, st_gid, st_size) and hash a line of these data (visible
117 with --verbose). The --Status option will automatically add such a line in front of every file. Note that '?'
118 is implied for directories. Both '/' and '?/' produce a hash of, eg,:
119 [[[/p]]][[[pre]]]
120 stat(/) = [st_mode=041775, st_ino=2, st_dev=234881026, st_uid=0, st_gid=80, st_size=1360]
121 [[[/pre]]][[[p]]]
122 The --Status-values=<mode> option selects which status values will be used: f(ile), m(ode), i(node),
123 d(evice), u(id), g(id), s(ize), (n)l(inks), a(time), (m)t(ime), and c(time). Default is
124 --Status-values='fmidugs'. Note that nlinks of a directory include every file in the directory, so this
125 option can check whether files have been added to a directory.
126 [[[/p]]][[[p]]]
127 Arguments enclosed in []-brackets will be hidden in the output. That is, '[/proc/self/exe]' will show up as
128 '[1]' in the output (or '[n]' with n the number of the hidden argument), equivalent to the use of the
129 --quiet option. This means the hidden arguments must be entered again when using the --check-file (-c)
130 option.
131 [[[/p]]][[[p]]]
132 Signature-du-Terroir
133 [[[/p]]][[[p]]]
134 A very simple tool to generate a signature that can be used to test the integrity of files and "states" in
135 a running installation. signduterre.py constructs a signature of the current system state and checks
136 installation state with a previously made signature. The files are hashed with a passphrase to allow detection
137 of compromised systems while running on the same system. The signature checking can be subverted, but the
138 flexibillity of signduterre.py and the fact that the output of any command can be tested should hamper
139 automated root-kit attacks.
140 [[[/p]]][[[p]]]
141 signduterre.py writes a total SHA-256 hash to STDOUT of all the files and commands entered as arguments. It
142 can also write a hash for each individual file (insecure). The output of a signature can be send to a file and
143 later used to check with --check-file. Hashes are calculated with a hashed salt + passphrase sequence
144 pre-pended to create unpredictable hashes. This procedure ensures that an attacker does not know whether or
145 not the correct passphrase has been entered. An attacker can only know when to supply the requested hash
146 values if she knows the passphrase or has copies available of all the tested files and output of commands to
147 calculate the hashes on the fly.
148 [[[/p]]][[[p]]]
149 The Problem
150 [[[/p]]][[[p]]]
151 How to test whether your system has been compromised when you can only use the potentially compromised system.
152 The solution is to store a password encrypted signature (or fingerprint) of your system when you are sure it
153 is in a good state. Then you check whether the system can still distinguish between correct and incorrect passwords
154 when it regenerates the signature. The trick is to use the right data (ie, questions) to generate the signature.
155 [[[/p]]][[[p]]]
156 SECURITY
157 [[[/p]]][[[p]]]
158 When run on a compromised system, signduterre.py can be subverted if the attacker keeps a copy of all the
159 files and command outputs, and reroutes the open() and stat() functions, or simply delegating signduterre.py
160 to a chroot jail with the original system. In principle, signduterre.py only checks whether the computer
161 responds identically to when the signature file was made. There is no theoretic barrier against a compromised
162 computer perfectly simulating the original system when tested, but behaving adversely at other times. Except
163 for running from clean boot media (USB?), I know of no theoretical sound solution to this problem.
164 [[[/p]]][[[p]]]
165 However, this scenario assumes the use of unlimited resources and time. Inside a limited, real computer system,
166 the attacker must make compromises on what can and what cannot be simulated with the available time and
167 hardware. The idea behind signduterre.py is to "ask difficult questions" that increase the cost of simulating
168 the original system high enough to make detection of successful attacks likely.signduterre.py simply intends
169 to raise the bar high enoug. One point is to store the times needed to create the original hashes. This timing
170 can later be used to see whether the new timings are reasonable. If the same hardware takes considerably
171 longer to perform the same calculations, or needs a much longer delay before it starts, the tester might want
172 to see where this time is spent.
173 [[[/p]]][[[p]]]
174 Signature-du-Terroir works on the assumption that any attacker in control of a compromised system cannot
175 predict whether the passphrase entered is correct or not. An attacker can always intercept the in- and output
176 of signduterre. When running with --check-file, this means the program can be made to print out OK
177 irrespective of the tests. A safe use of signduterre.py is to start with a random number of incorrect
178 passphrases and see whether they fail. Alternatively, and easier, is to add a number of unused salts
179 to the check-file and let the attacker guess which one is correct.
180 [[[/p]]][[[p]]]
181 THE CORRECT USE OF signduterre.py IS TO ENTER A RANDOM NUMBER OF INCORRECT PASSPHRASES OR SALTS FOR EACH
182 TEST AND SEE WHETHER IT FAILS AT THE CORRECT INSTANCES!
183 [[[/p]]][[[p]]]
184 On a compromised system, signduterre.py's detailed file testing (--detailed-view) is easily subverted. With a
185 matched file hash, the attacker will know that the correct passphrase has been entered and can print out the
186 stored hashes or 'ok's for the rest of the checks. So if the attacker keeps any entry in the signature file
187 uncompromised, she can intercept the output, test the password on the unchanged entry and substitute the
188 requested hashes for the output if the hash of that entry matches.
189 [[[/p]]][[[p]]]
190 When checking for root-kits and other malware, it is safest to compare the signature files from a different,
191 clean, system. But then you would not need signduterre.py anyway. If you have to work on the system itself,
192 only use the -t or --total-only options to create signatures with a total hash and without individual file
193 hashes. Such a signature can be used to check whether the system is unchanged. Another signature file WITH A
194 DIFFERENT PASSPHRASE can then be used to identify the individual files that have changed. If a detailed
195 signature file has the same passphrase, an attacker could use that other file to read the individual file
196 hashes to check whether the correct passphrase was entered.
197 [[[/p]]][[[p]]]
198 Using the --check-file option in itself is UNsafe. An attacker simply has to print out 'OK' to defeat the
199 check. This attack can be foiled by making it unpredictable when signduterre.py should return 'OK'. This can
200 be done by using a list of salts or passphrases where only one of them (or none!) is correct. Any attacker
201 will have to guess when to return 'OK'.
202 [[[/p]]][[[p]]]
203 As generating and entering wrong passphrases and salts is tedious, users have to be supported in correct use
204 of SdT. To assist users, the '--salt SUGGESTED=<N>' option will generate a number N of salts. When
205 checking, each of these salts is tried in turn. An attacker that is unable to simulate the uncompromised
206 system will have to guess which one of the salts is the correct one, and whether or not the passphrase
207 is correct. This increases the chances of detecting compromised systems. If this is not enough guess
208 work, the '-a', '--all-salts-pattern' option will use all salts in sequence to generate total hashes,
209 but random salts will be changed in the output. This generates a pattern of failed salt tests. This pattern
210 is translated into a bit pattern and printed as an integer ([Fail, Fail, OK, Fail, OK, OK, Fail, OK]
211 = 00101101 (least significant first) = 10110100 (unsigned bin) = 180). On creation of a signature, this
212 number is printed to STDERR, on checking (--check-file) it is printed to STDOUT (note that the number
213 will never become 0 or all Fail). So for '--salt SUGGESTED=<N> --all-salts-pattern' the probability of
214 guessing the correct output goes from 1/N to 1/(2^N - 1). Note that '--all-salts-pattern' will work,
215 but is pointless, without '--salt SUGGESTED=<N>' with N>1.
216 [[[/p]]][[[p]]]
217 The '--passphrase SUGGESTED=N' option will generate and print N passphrases. One of these is chosen at
218 random for the signature. The number of the chosen passphrase is printed on STDERR with the passwords.
219 When checking a file, the stored passphrases can be read in again, either by entering the passphrase
220 file after the --passphrase option ('--passphrase <passphrase file>'), or directly from the --check-file.
221 signduterre.py will print out the result for each of the passphrases.
222 [[[/p]]][[[p]]]
223 Note, that storing passphrases in a file and feeding it to signduterre.py is MUCH less secure than just
224 typing them in. Moreover, it might completely defeat the purpose of signduterre.py. If future experiences
225 cast any more doubt on the security of this option, it will be removed.
226 [[[/p]]][[[p]]]
227 For those who want to know more about what an "ideal attacker" can do, see:[[[br]]]
228 Ken Thompson "Reflections on Trusting Trust"[[[br]]]
229 [[[a href="http://cm.bell-labs.com/who/ken/trust.html"]]]http://cm.bell-labs.com/who/ken/trust.html[[[/a]]][[[br]]]
230 [[[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]]]
231 [[[/p]]][[[p]]]
232 David A Wheeler "Countering Trusting Trust through Diverse Double-Compiling"[[[br]]]
233 [[[a href="http://www.acsa-admin.org/2005/abstracts/47.html"]]]http://www.acsa-admin.org/2005/abstracts/47.html[[[/a]]]
234 [[[/p]]][[[p]]]
235 and the discussion of these at Bruce Schneier's 'Countering "Trusting Trust"'[[[br]]]
236 [[[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]]]
237 [[[/p]]][[[p]]]
238 Manual
239 [[[/p]]][[[p]]]
240 The intent of signduterre.py is to ensure that the signature cannot be subverted even if the system has been
241 compromised by an attacker that has obtained root control over the computer and any existing signature files.
242 [[[/p]]][[[p]]]
243 signduterre.py asks for a passphrase which is PRE-pended to every file before the hash is constructed (unless
244 the passphrase is entered with an option). As long as the passphrase is not compromised, the hashes cannot
245 be reconstructed. A randomly generated, unpadded base-64 encoded 16 Byte password (ie, ~22 characters) is
246 suggested in interactive use. If '--passphrase SUGGESTED' is entered on the command line or no passphrase is
247 enetered when asked, the suggested value will be used. This value is printed to STDERR (the screen or 2) for
248 safe keeping. Please, make sure you store the printed passphrase. For instance:
249 [[[/p]]][[[pre make=example]]]
251 # Simple system sanity test using the 'which' command to establish the paths
252 $ python3.0 signduterre.py --passphrase SUGGESTED --salt SUGGESTED --detailed-view \\
253 `which python3.0 bash ps ls find stat` 2> test-20090630_11-14-03.pwd > test-20090630_11-14-03.sdt
254 $ python3.0 signduterre.py --passphrase test-20090630_11-14-03.pwd --check-file test-20090630_11-14-03.sdt
255 [[[/pre]]][[[p]]]
256 The first command will store the passphrase (and all error messages) in a file 'Signature_20090630_11-14-03.pwd'
257 and the check-file in 'Signature_20090630_11-14-03.sdt'. The second line will test the signature.
258 The signature will be made of the files used for the commands python3.0, bash, ps, ls, find, and stat.
259 These files are found using the 'which' command.
260 [[[/p]]][[[p]]]
261 It is not secure to store files with the passphrase on the system you want to check. However, you could
262 pipe STDERR or --Private-file to some safe site.
263 [[[/p]]][[[p]]]
264 Good passphrases are difficult to remember, so their plaintext form should be protected. To protect the
265 passphrase against rainbow and brute force attacks, the passphrase is concatenated to a salt phrase and
266 hashed before use (SHA-256).
267 [[[/p]]][[[p]]]
268 The salt phrase is requested when constructing a signature. In interactive use, an 8 byte hexadecimal
269 (= 16 character) salt from /dev/urandom is suggested. If '--salt SUGGESTED' is entered on the command line
270 as the salt, the suggested value will be used. The salt is printed in plaintext to the output. The salt will
271 make it more difficult to determine whether the same passphrase has been used to create different signatures.
272 [[[/p]]][[[p]]]
273 At the bottom, a 'TOTAL HASH' line will be printed that hashes all the lines printed for the files. This
274 includes the file names as printed on the hash lines. It is not inconceivable that existing signature files
275 could have been compromised in ways that might be missed when checking the signature. The total hash will
276 point out such changes.
277 [[[/p]]][[[p]]]
278 Examples:[[[/p]]][[[pre make=example]]]
280 # Self test of root directory, python, and signduterre.py using the 'which' command to establish the paths
281 $ python3.0 signduterre.py --detailed-view --salt 436a73e3 --passphrase liauwefa3251EWC -o test-self.sdt \\
282 / `which python3.0 signduterre.py`
283 $ python3.0 signduterre.py --passphrase liauwefa3251EWC -c test-self.sdt
284 [[[/pre]]][[[p]]]
285 Write a signature to the file test-self.sdt and test it with the --check-file option. The signature contains
286 the SHA-256 hashes of the files, /usr/bin/python3.0, signduterre.py, and the status information on the root
287 directory. The salt '436a73e3' and passphrase 'liauwefa3251EWC' are used.
288 [[[/p]]][[[pre make=linux]]]
290 # Self test of root directory, python, and signduterre.py using the the /proc file system
291 $ python3.0 signduterre.py --detailed-view --salt SUGGESTED --passphrase liauwefa3251EWC -o test-self_proc.sdt \\
292 /proc/self/root /proc/self/exe `which signduterre.py`
293 $ python3.0 signduterre.py --passphrase liauwefa3251EWC --check-file test-self_proc.sdt
294 [[[/pre]]][[[p]]]
295 Write a signature to the file test-self_proc.sdt and test it with the --check-file option. The signature
296 contains the SHA-256 hashes of the same files as above, /usr/bin/python3.0, signduterre.py, and the status
297 information on the root directory. However, the python executable and the root directory are now accessed
298 through the /proc file system. The suggested salt is used (written to test-self_proc.sdt) and the passphrase
299 is (again) 'liauwefa3251EWC'.
300 [[[/p]]][[[pre make=example]]]
302 # Test of supporting commands for chkrootkit
303 $ python3.0 signduterre.py --execute --total-only --salt SUGGESTED=8 --passphrase SUGGESTED --Status \\
304 --output-file=test-chkrootkit.sdt --Private-file=test-chkrootkit.pwd \\
305 signduterre.py `which bash awk cut egrep find head id ls netstat ps strings sed uname`
306 $ python3.0 signduterre.py --execute --passphrase test-chkrootkit.pwd --check-file test-chkrootkit.sdt
307 [[[/pre]]][[[p]]]
308 Writes a signature of the requested files to test-chkrootkit.sdt (signature) and private information to
309 test-chkrootkit.pwd (password and selected salt) and checks it in the next line. The files are those of
310 commands required by the chkrootkit program (http://www.chkrootkit.org/), with bash added. The 'which'
311 command will give the paths for the commands. Eight salts are generated, of which only 1 is actually
312 used. When checking, the correct salt should match. This prevents a compromised program from simply
313 printing out OK tot he check. A more comprehensive evation of guessing the correct salt can be obtained
314 by using the '--all-salts-pattern' option.
315 [[[/p]]][[[pre make=linux]]]
317 # Simply lump all "system" files, the PATH environment variable and the first 2 columns of the output of lsmod
318 $ python3.0 signduterre.py --execute --detail --salt SUGGESTED --passphrase liauwefa3251EWC --Status --total-only \\
319 signduterre.py /sbin/* /bin/* /usr/bin/find /usr/bin/stat /usr/bin/python* '${PATH}' \\
320 '$(lsmod | awk "{print \$1, \$2}")' > test-20090625_14-31-54.sdt
322 # Failing check due to missing --execute option
323 $ python3.0 signduterre.py --passphrase liauwefa3251EWC -c test-20090625_14-31-54.sdt
324 $ python3.0 signduterre.py --passphrase liauwefa3251EWC -c test-20090625_14-31-54.sdt --no-execute
326 # Successful check
327 $ python3.0 signduterre.py --execute --passphrase liauwefa3251EWC --check-file test-20090625_14-31-54.sdt
328 [[[/pre]]][[[p]]]
329 Prints a signature to the system test-20090625_14-31-54.sdt and the automatically generated password to
330 test-20090625_14-31-54.pwd. The salt will be automatically determined. The signature contains the SHA-256
331 hashes of the file status and file contents of signduterre.py, /sbin/*, /bin/*, /usr/bin/find,
332 /usr/bin/file, /usr/bin/python* on separate lines, and a hash of the PATH environment variable. Do not
333 display the hash of every single file, which could be insecure, but only the total hash.
334 The first two checks will both fail if test-20090625_14-31-54.sdt contains a $(cmd) entry.
335 The --no-execute option is default and prevents the execute option (if reading the execute option from the
336 signature file has been activated). The last check will succeed (if the files have not been changed).
337 [[[/p]]][[[pre make=example]]]
339 # Use a list of generated passphrases
340 $ python3.0 signduterre.py --salt SUGGESTED --passphrase SUGGESTED=20 signduterre.py \\
341 2> test-20090630_16-44-34.pwd > test-20090630_16-44-34.sdt
342 $ python3.0 signduterre.py -p test-20090630_16-44-34.pwd -c test-20090630_16-44-34.sdt
343 [[[/pre]]][[[p]]]
344 Will generate and print 20 passphrases and print a signature using one randomly chosen passphrase from these
345 20. Everything is written to the files 'test-20090630_16-44-34.pwd' and 'test-20090630_16-44-34.sdt'.
346 Such file names can easily be generated with 'test-`date "+%Y%m%d_%H-%M-%S"`.sdt'.
347 The next command will check all 20 passphrases generated before from the Signature file and print the results.
348 [[[/p]]][[[pre make=example]]]
350 # Use a list of generated salts with a pattern of correct salts
351 $ python3.0 signduterre.py --salt SUGGESTED=16 --passphrase SUGGESTED --all-salts-pattern \\
352 -P test-salt-pattern.pwd -o test-salt-pattern.sdt `which bash stat find ls ps id uname awk gawk perl`
353 $ python3.0 signduterre.py -p test-salt-pattern.pwd -c test-salt-pattern.sdt
354 # Compare to salt pattern number to the one from the check-file
355 $ cat test-salt-pattern.pwd
356 [[[/pre]]][[[p]]]
357 [[[/p]]][[[pre make=sudo]]]
359 # Check MBR and current root directory (sudo and root user)
360 $ sudo python3.0 signduterre.py -u root -s SUGGESTED -p SUGGESTED --Status-values='i' -v -e -t \\
361 --output-file test-boot-sector.sdt --Private-file test-boot-sector.pwd --execute-args=sda \\
362 '?/proc/self/root' `which dd` '$(dd if=/dev/$1 bs=512 count=1 status=noxfer | od -X)'
363 $ sudo python3.0 signduterre.py -u root -e -p test-boot-sector.pwd -c test-boot-sector.sdt
364 [[[/pre]]][[[p]]]
365 Will hash the inode numbers of the effective root directory (eg, chroot) and the executable (python)
366 together with the contents of the MBR (Master Boot Record) on /dev/sda in Hex. It uses suggested salt and
367 passphrase. Accessing /dev/sda is only possible when root, so the command is entered with sudo and
368 --user root. Use the '--print-execute' option if you want to check the output of the dd command.
369 [[[/p]]][[[p]]]
370 The main problem with intrusion detection by comparing file contents is the ability of an attacker
371 to redirect attempts to read a compromised file to a stored copy of the original. So, sha256 or
372 python could be changed to read '/home/attacker/old/ps' when the argument was '/bin/ps'. This would
373 foil any scheme that depends on entering file names in programs. An answer to this threat is to
374 read the bytes in files in as many ways as possible. Therefor, forcing an attacker to change many
375 files which itself would increase the probability of detection of the attack. The following command
376 will read the same (test) file, and generate identical hashes, in many different ways.
377 [[[/p]]][[[pre make=example]]]
379 # Example generating identical signatures of the same text file in different ways
380 $ dd if=signduterre.py 2>/dev/null | \\
381 python3.0 signduterre.py -v -d -s 1234567890abcdef -p poiuytrewq \\
382 --execute --execute-args='signduterre.py' \\
383 signduterre.py - \\
384 '$(cat $1)' \\
385 '$(grep "" $1)' \\
386 '$(awk "{print}" $1)' \\
387 '$(cut -f 1-100 $1)' \\
388 '$(perl -ane "{print \$_}" $1)' \\
389 '$(python3.0 -c "import sys;f=open(sys.argv[1]);sys.stdout.buffer.write(f.buffer.read())" $1;)' \\
390 '$(ruby -e "f=open(ARGV[0]);print f.read();" $1;)'
391 [[[/pre]]][[[p]]]
392 These "commands" do not always return the same bytes (awk), or any bytes at all (grep), from a text
393 file as when used with a binary file. However, if the commands can print the bytes unaltered, the
394 signatures will be identical. That is, the following arguments will work on a binary file:
395 [[[/p]]][[[pre make=example]]]
396 # Example generating identical signatures of the same file in different ways, now for binary files
397 $ dd if=/bin/bash 2>/dev/null | \\
398 python3.0 signduterre.py -v -d -s 1234567890abcdef -p poiuytrewq \\
399 --execute --execute-args='/bin/bash' \\
400 /bin/bash - \\
401 '$(cat $1)' \\
402 '$(perl -ane "{print \$_}" $1)' \\
403 '$(python3.0 -c "import sys;f=open(sys.argv[1]);sys.stdout.buffer.write(f.buffer.read())" $1;)' \\
404 '$(ruby -e "f=open(ARGV[0]);print f.read();" $1;)'
405 [[[/pre]]][[[p]]]
406 Will generate the same identical signatures for /bin/bash, STDIN, '$(cat /bin/bash)' etc.
407 There are obviously many more ways to read out the bytes from the disk or memory. The main point
408 being that it should be difficult to predict for an attacker which commands must be compromised
409 to hide changes in the system.
410 [[[/p]]][[[p]]]
411 The examples can be run as a makefile using make. Use one of the following commands:
412 [[[/p]]][[[pre]]]
413 # General examples
414 python3.0 signduterre.py --manual-make |make -f - example
415 # Linux specific examples
416 python3.0 signduterre.py --manual-make |make -f - linux
417 # Examples requiring sudo
418 python3.0 signduterre.py --manual-make | sudo make -f - sudo
419 [[[/pre]]][[[/body]]][[[/html]]]
422 license = """
423 Signature-du-Terroir
424 Construct a signature of the installed software state or check a previously made signature.
426 copyright 2009, R.J.J.H. van Son
428 This program is free software: you can redistribute it and/or modify
429 it under the terms of the GNU General Public License as published by
430 the Free Software Foundation, either version 3 of the License, or
431 (at your option) any later version.
433 This program is distributed in the hope that it will be useful,
434 but WITHOUT ANY WARRANTY; without even the implied warranty of
435 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
436 GNU General Public License for more details.
438 You should have received a copy of the GNU General Public License
439 along with this program. If not, see <http://www.gnu.org/licenses/>.
440 """;
442 # Note that only release notes are put here
443 # See git repository for detailed change comments:
444 # git clone git://repo.or.cz/signduterre.git
445 releasenotes = """
446 20090730 - Release v0.4
447 20090724 - Added '--all-salts-pattern' and HTML formatting in manual
448 20090723 - Added URL support for all files. Does not yet work due to bug in Python 3.0
449 20090723 - Added '-' for STDIN
450 20090717 - Added --execute-args
451 20090716 - Release v0.3
452 20090713 - Added --quiet option
453 20090712 - moved from /dev/random to /dev/urandom
454 20090702 - Replaced -g with -p SUGGESTED[=N]
455 20090702 - Generating and testing lists of random salts
456 20090701 - Release v0.2
457 20090630 - Generating and testing random passphrases
458 20090630 - --execute works on $(cmd) only, nlinks in ?path and ? implied for directories
459 20090630 - Ported to Python 3.0
461 20090628 - Release v0.1b
462 20090628 - Added release-notes
464 20090626 - Release v0.1a
465 20090626 - Initial commit to Git
466 """;
468 #############################################################################
470 # IMPORT & INITIALIZATION #
472 #############################################################################
474 import sys;
475 import os;
476 import subprocess;
477 import stat;
478 import subprocess;
479 # if sys.stdout.isatty(): import readline;
480 import binascii;
481 import hashlib;
482 import re;
483 import time;
484 from optparse import OptionParser;
485 import base64;
486 import random;
487 import struct;
488 import urllib.request;
489 import urllib.error;
491 # Limit the characters that can be used in $(cmd) commands
492 # Only allow the escape of '$'
493 not_allowed_chars = re.compile('([^\w\ \.\/\"\|\;\,\-\$\[\]\{\}\(\)\@\`\!\*\=\\\\]|([\\\\]+([^\$\"\\\\]|$)))');
495 programname = "Signature-du-Terroir";
496 version = "0.4";
498 def open_infile(filename, mode):
499 if filename == '-':
500 return sys.stdin;
501 elif filename.find('://') > -1:
502 print("URL:", filename, file=current_private);
503 return urllib.request.urlopen(filename);
504 else :
505 if not os.path.isfile(filename):
506 print(filename, "does not exist", file=sys.stderr)
507 quit();
508 return open(filename, mode);
510 def open_outfile(filename, mode):
511 if filename == '-':
512 return sys.stdout;
513 elif filename.find('://') > -1:
514 print("URL:", filename, file=current_private);
515 return urllib.request.urlopen(filename);
516 else :
517 return open(filename, mode);
519 current_outfile = sys.stdout;
520 current_private = sys.stderr;
522 #############################################################################
524 # OPTION HANDLING #
526 #############################################################################
528 parser = OptionParser()
529 parser.add_option("-s", "--salt", metavar="HEX",
530 dest="salt", default=False,
531 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).")
532 parser.add_option("-a", "--all-salts-pattern",
533 dest="allsalts", default=False, action="store_true",
534 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.")
535 parser.add_option("-p", "--passphrase", metavar="TEXT",
536 dest="passphrase", default=False,
537 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).")
538 parser.add_option("-c", "--check-file",
539 dest="check", default=False, metavar="FILE",
540 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.")
541 parser.add_option("-i", "--input-file",
542 dest="input", default=False, metavar="FILE",
543 help="Use names from FILE or STDIN ('-'), use one filename per line")
544 parser.add_option("-o", "--output-file",
545 dest="output", default=False, metavar="FILE",
546 help="Print to FILE instead of STDOUT")
547 parser.add_option("-P", "--Private-file",
548 dest="private", default=False, metavar="FILE",
549 help="Print private information (passwords etc.) to FILE instead of STDERR")
550 parser.add_option("-u", "--user",
551 dest="user", default="nobody", metavar="USER",
552 help="Execute $(cmd) as USER, default 'nobody' (root/sudo only)")
553 parser.add_option("-S", "--Status",
554 dest="status", default=False, action="store_true",
555 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)")
556 parser.add_option("--Status-values",
557 dest="statusvalues", default="fmidugs", metavar="MODE",
558 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).")
559 parser.add_option("-t", "--total-only",
560 dest="total", default=False, action="store_true",
561 help="Only print the total hash, unsets --detailed-view (default True)")
562 parser.add_option("-d", "--detailed-view",
563 dest="detail", default=False, action="store_true",
564 help="Print hashes of individual files, is unset by --total-only (default False)")
565 parser.add_option("-e", "--execute",
566 dest="execute", default=False, action="store_true",
567 help="Interpret $(cmd) (default False)")
568 parser.add_option("--execute-args",
569 dest="executeargs", default='', metavar="ARGS",
570 help="Arguments for the $(cmd) commands ($1 ....)")
571 parser.add_option("-n", "--no-execute",
572 dest="noexecute", default=False, action="store_true",
573 help="Explicitely do NOT Interpret $(cmd)")
574 parser.add_option("--print-hexdump",
575 dest="printhexdump", default=False, action="store_true",
576 help="Print hexadecimal dump of input bytes to STDERR for debugging purposes")
577 parser.add_option("-m", "--manual",
578 dest="manual", default=False, action="store_true",
579 help="Print the manual and exit")
580 parser.add_option("--manual-html",
581 dest="manualhtml", default=False, action="store_true",
582 help="Print the manual in HTML format and exit")
583 parser.add_option("--manual-make",
584 dest="manualmake", default=False, action="store_true",
585 help="Print the examples in the manual as a makefile and exit")
586 parser.add_option("-r", "--release-notes",
587 dest="releasenotes", default=False, action="store_true",
588 help="Print the release notes and exit")
589 parser.add_option("-l", "--license",
590 dest="license", default=False, action="store_true",
591 help="Print license text and exit")
592 parser.add_option("-v", "--verbose",
593 dest="verbose", default=False, action="store_true",
594 help="Print more information on output")
595 parser.add_option("-q", "--quiet",
596 dest="quiet", default=False, action="store_true",
597 help="Print minimal information (hide filenames). If the output is used with --check-file, the command line options and arguments must be repeated.")
599 (options, check_filenames) = parser.parse_args();
602 # Start with opening any non-default output files
603 my_output = False;
604 if options.output:
605 current_outfile = open_outfile(options.output, 'w');
606 my_output = options.output;
608 my_private = False;
609 if options.private:
610 current_private = open_outfile(options.private, 'w');
611 my_private = options.private;
613 print("# Program: "+programname + " version " + version, file=current_outfile);
614 print("#", time.strftime("%Y/%m/%d %H:%M:%S", time.localtime()), "("+time.tzname[0]+")\n", file=current_outfile);
616 # Print license
617 if options.license:
618 print (license, file=sys.stderr);
619 exit(0);
620 # Print manual
621 if options.manual:
622 htmltags = re.compile('\[\[\[[^\]]*\]\]\]');
623 cleartext_manual = htmltags.sub('', manual);
624 print (cleartext_manual, file=sys.stdout);
625 exit(0);
626 # Print HTML manual
627 if options.manualhtml:
628 protleftanglesbracks = re.compile('\<');
629 protrightanglesbracks = re.compile('\>');
630 leftanglesbracks = re.compile('\[\[\[');
631 rightanglesbracks = re.compile('\]\]\]');
632 html_manual = protleftanglesbracks.sub('&lt;', manual);
633 html_manual = protrightanglesbracks.sub('&gt;', html_manual);
634 html_manual = leftanglesbracks.sub('<', html_manual);
635 html_manual = rightanglesbracks.sub('>', html_manual);
636 print (html_manual, file=sys.stdout);
637 exit(0);
638 # Print manual examples
639 if options.manualmake:
640 make_manual = re.sub("\$ ", "\t", manual);
641 make_manual = re.sub("\#", "\t#", make_manual);
642 make_manual = re.sub(r"\\\s*\n", '', make_manual);
643 make_manual = re.sub(r"\$", r'$$', make_manual);
644 # Protect "single" [ brackets
645 make_manual = re.sub(r"(^|[^\[])\[([^\[]|$)", r"\1&#91;\2", make_manual);
646 extrexamples = re.compile(r"\[\[\[pre\s+make\=?(\w*)\s*\]\]\]\n([^\[]*)\n\[\[\[/pre\s*\]\]\]", re.IGNORECASE|re.MULTILINE|re.DOTALL);
647 exampleiter = extrexamples.finditer(make_manual);
648 makefile_list = [];
649 for match in exampleiter:
650 # We had to convert any '[' in the command. Now convert them back.
651 command_text = re.sub(r"\&\#91\;", '[', match.group(2));
652 makefile_list.append(match.group(1)+":\n"+command_text);
654 previous_cat = 'NOT A VALUE';
655 makefile_list.sort()
656 for line in makefile_list:
657 (category, commands) = line.split(':\n');
658 if category != previous_cat:
659 previous_cat = category;
660 print("\n"+previous_cat+":", file=sys.stdout);
661 print(commands, file=sys.stdout);
662 # Clean option
663 print("\nclean:\n\trm test-*.sdt test-*.pwd", file=sys.stdout);
664 exit(0);
665 # Print release notes
666 if options.releasenotes:
667 print ("Version: "+version, file=sys.stderr);
668 print (releasenotes, file=sys.stderr);
669 exit(0);
671 my_salt = options.salt;
672 my_allsalts = options.allsalts;
673 my_passphrase = options.passphrase;
674 my_check = options.check;
675 my_status = options.status;
676 my_statusvalues = options.statusvalues;
677 my_verbose = options.verbose and not options.quiet;
678 my_quiet = options.quiet;
679 execute = options.execute;
680 execute_args = options.executeargs;
681 if options.noexecute: execute = False;
682 input_file = options.input;
684 # Set total-only with the correct default
685 total_only = True;
686 total_only = not options.detail;
687 if options.total: total_only = options.total;
688 if my_allsalts: total_only = my_allsalts; # All alts pattern only sensible with total-only
689 if my_check: total_only = False;
691 my_user = options.user;
692 # Things might be executed as another user
693 user_change = '';
694 if os.getuid() == 0:
695 user_change = 'sudo -H -u '+my_user+' ';
696 if not my_quiet: print("User: "+my_user, file=current_outfile);
698 # Execute option
699 if execute:
700 text_execute = "True";
701 else:
702 text_execute = "False";
704 if execute and not my_quiet:
705 print("Execute system commands: "+text_execute+"\n", file=current_outfile);
706 if execute_args != '': print("Execute arguments: '"+execute_args+"'\n", file=current_outfile);
708 # --quiet option
709 if my_quiet: print("Quiet: True\n", file=current_outfile);
711 # --quiet option
712 if my_statusvalues != 'fmidugs': print("Status-values: '"+my_statusvalues+"'\n", file=current_outfile);
714 #############################################################################
716 # ARGUMENT PROCESSING #
718 #############################################################################
720 # Measure time intervals
721 start_time = time.time();
723 dev_random = open("/dev/urandom", 'rb');
725 # Read the check file
726 passphrase_list = [];
727 salt_list = [];
728 check_hashes = {};
729 total_hash_list = [];
730 if my_check:
731 highest_arg_used = 0;
732 print("# Checking: "+my_check+"\n", file=current_outfile);
733 arg_list = check_filenames;
734 check_filenames = [];
735 with open_infile(my_check, 'r') as c:
736 for line in c:
737 match = re.search("Execute system commands:\s+(True|False)", line);
738 if match != None:
739 # Uncomment the next line if you want automatic --execute from the check-file (DANGEROUS)
740 # execute = match.group(1).upper() == 'TRUE';
741 continue;
743 match = re.search("Execute arguments:\s+\'([\w\$\s\-\+\/]*)\'", line);
744 if match != None:
745 execute_args = match.group(1);
746 continue;
748 match = re.search("Quiet:\s+(True|False)", line);
749 if match != None:
750 my_quiet = match.group(1).upper() == 'TRUE';
751 if my_quiet: my_verbose = False;
752 continue;
754 match = re.search("Salt\s*\+\s*TOTAL HASH\s*\:\s+\'([\w]*)\'\s+\'([\w]*)\'", line);
755 if match != None:
756 salt_list.append(match.group(1));
757 total_hash_list.append(match.group(2));
758 my_allsalts = True; # Salt+TOTAL HASH imples all-salts-pattern
759 continue;
761 match = re.search("Salt\:\s+\'([\w]*)\'", line);
762 if match != None:
763 salt_list.append(match.group(1));
764 continue;
766 match = re.search("Salt\s*\+\s*TOTAL HASH\s*\:\s+\'([\w]+)\'\s+\'([a-f0-9]+)\'", line);
767 if match != None:
768 salt_list.append(match.group(1));
769 total_hash_list.append(match.group(2));
770 continue;
772 match = re.search("User\:\s+\'([\w]*)\'", line);
773 if match != None:
774 # Uncomment the next line if you want automatic --user from the check-file (DANGEROUS)
775 # my_user = match.group(1);
776 continue;
778 match = re.search("Passphrase\:\s+\'([^\']*)\'", line);
779 if match != None:
780 passphrase_list.append(match.group(1));
781 continue;
783 match = re.search("Status-values\:\s+\'([\w]*)\'", line);
784 if match != None:
785 my_statusvalues = match.group(1);
786 continue;
788 match = re.search("^\s*([a-f0-9]+)\s+\*(TOTAL HASH)\s*$", line)
789 if match != None:
790 total_hash_list.append(match.group(1));
791 continue;
793 match = re.search("^\s*([a-f0-9\-]+)\s+\*\[([0-9]+)\]\s*$", line)
794 if match != None:
795 filenumber = int(match.group(2));
796 if filenumber > highest_arg_used: highest_arg_used = filenumber;
797 # Watch out, arguments count from 0
798 check_filenames.append(arg_list[filenumber - 1]);
799 check_hashes['['+match.group(2)+']'] = match.group(1);
800 continue;
802 match = re.search("^\s*([a-f0-9\-]+)\s+\*(.*)\s*$", line)
803 if match != None:
804 check_filenames.append(match.group(2));
805 # Catch --execute error as early as possible
806 if match.group(2).startswith('$(') and not execute:
807 error_message = "Executable argument \'"+match.group(2)+"\' only allowed with the --execute flag";
808 print (error_message, file=sys.stderr);
809 if not sys.stdout.isatty(): print(error_message, file=current_outfile);
810 exit(0);
811 check_hashes[match.group(2)] = match.group(1);
812 continue;
813 for i in range(highest_arg_used, len(arg_list)):
814 check_filenames.append(arg_list[i]);
815 check_hashes['['+str(i+1)+']'] = (64*'-');
817 # Read input-file
818 if input_file:
819 with open_infile(input_file, 'r') as i:
820 for line in i:
821 # Clean up filename
822 current_filename = re.sub('[^\w\-\.\/\$\{\(\)\}\?\[\]]', '', line);
823 check_filenames.append(current_filename);
824 if my_check: check_hashes['['+str(i+1)+']'] = (64*'-');
826 stat_list = [];
827 for x in check_filenames:
828 if os.path.isdir(x):
829 x = '?'+x;
830 if my_status and not x.startswith(('?', '$')):
831 stat_list.append('?'+x);
832 stat_list.append(x);
833 check_filenames = stat_list;
835 # Seed Pseudo Random Number Generator
836 seed = dev_random.read(16);
837 random.seed(seed);
839 # Read suggested salts from /dev/(u)random if needed
840 if my_salt:
841 if my_salt.startswith('SUGGESTED'):
842 N=1;
843 match = re.search("([0-9][0-9]*)$", my_salt);
844 if match != None:
845 N = int(match.group(1));
846 for i in range(0,N):
847 salt = dev_random.read(8);
848 salt_list.append(str(binascii.hexlify(salt), 'ascii'));
849 else:
850 salt_list.append(my_salt);
851 elif len(salt_list) == 0:
852 salt = dev_random.read(8);
853 sys.stderr.write("Enter salt (suggest \'"+str(binascii.hexlify(salt), 'ascii')+"\'): ");
854 new_salt = input();
855 if not new_salt: new_salt = str(binascii.hexlify(salt), 'ascii');
856 salt_list.append(new_salt);
858 # If not combining salts with TOTAL HASH, print salts now
859 if not my_allsalts:
860 for my_salt in salt_list:
861 print("Salt: \'"+my_salt+"\'", file=current_outfile);
863 # Get passphrase
864 if my_passphrase and(my_passphrase == '-' or os.path.isfile(my_passphrase)):
865 with open_infile(my_passphrase, 'r') as file:
866 for line in file:
867 match = re.search("Passphrase\:\s+\'([^\']*)\'", line);
868 if match != None:
869 passphrase_list.append(match.group(1));
870 elif not my_passphrase and len(passphrase_list) == 0:
871 suggest_passphrase = dev_random.read(16);
872 sys.stderr.write("Enter passphrase (suggest \'"+str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=')+"\'): ");
873 # How kan we make this unreadable on input?
874 current_passphrase = input();
875 if not current_passphrase:
876 current_passphrase = str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=');
877 print("Passphrase: \'"+current_passphrase+"\'", file=current_private);
878 passphrase_list.append(current_passphrase);
879 elif my_passphrase.startswith('SUGGESTED'):
880 N = 1;
881 match = re.search("([0-9][0-9]*)$", my_passphrase);
882 if match != None:
883 N = int(match.group(1));
884 j = int(random.random()*N);
885 for i in range(0, N):
886 suggest_passphrase = dev_random.read(16);
887 current_passphrase = str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=');
888 print("Passphrase: \'"+current_passphrase+"\'", file=current_private);
889 passphrase_list.append(current_passphrase);
890 else:
891 passphrase_list.append(my_passphrase);
893 selected_salt = 1;
894 fail_fraction = 0.5;
895 if not my_check:
896 if len(passphrase_list) > 1:
897 j = int(random.random()*len(passphrase_list));
898 passphrase_list = [passphrase_list[j]];
899 print("# Selected passphrase:", j+1, file=current_private);
900 if len(salt_list) > 1:
901 j = int(random.random()*len(salt_list));
902 # Make sure at least 1 salt will match and print the selection if only one is used
903 selected_salt = j+1;
904 if not my_allsalts:
905 salt_list = [salt_list[selected_salt-1]];
906 print("# Selected salt:", selected_salt, file=current_private);
907 else:
908 salt_N = len(salt_list);
909 fail_fraction = (salt_N/2.0)/(salt_N - 1);
910 else:
911 fail_fraction = 0;
913 # Close /dev/(u)random
914 dev_random.close;
916 #############################################################################
918 # SIGNATURE CREATION AND CHECKING #
920 #############################################################################
922 end_time = time.time();
923 print("# Preparation time:", end_time - start_time, "seconds\n", file=current_outfile);
925 pnum = 1;
926 snum = 1;
927 corrpnum = 0;
928 corrsnum = 0;
929 matched_salt_pattern = -1;
930 salt_pattern_number = -1;
932 for my_passphrase in passphrase_list:
933 snum = 1;
934 # Initialize salt pattern
935 if my_allsalts:
936 salt_pattern_number = 0;
937 current_salt_power = 1;
939 for my_salt in salt_list:
940 print("# Start signature: ", end='', file=current_outfile);
941 if len(passphrase_list) > 1: print("passphrase -", pnum, end='', file=current_outfile);
942 if len(salt_list) > 1: print(" salt -", snum, end='', file=current_outfile);
943 print("", file=current_outfile);
945 # Should everything be printed?
946 print_verbose = my_verbose and not (my_allsalts and snum > 1);
948 file_argnum = 0;
949 start_time = time.time();
950 # Construct the passphrase hash
951 passphrase = hashlib.sha256();
953 passphrase.update(bytes(my_salt, encoding='ascii'));
954 passphrase.update(bytes(my_passphrase, encoding='ascii'));
956 # Create prefix which is a hash of the salt+passphrase
957 prefix = passphrase.hexdigest();
959 # Create signature and write output
960 totalhash = hashlib.sha256();
961 totalhash.update(bytes(prefix, encoding='ascii'));
962 for org_filename in check_filenames:
963 # Create file hash object
964 filehash = hashlib.sha256();
965 filehash.update(bytes(prefix, encoding='ascii'));
966 # Remove []
967 filename = org_filename.strip('[').rstrip(']');
968 # Use system variables and commands
969 if filename.startswith('$'):
970 # Commands $(command)
971 match = re.search('^\$\((.+)\)$', filename);
972 if match != None:
973 if not execute :
974 error_message = "Executable argument \'"+filename+"\' only allowed with the --execute flag";
975 print (error_message, file=sys.stderr);
976 if not sys.stdout.isatty(): print(error_message, file=current_outfile);
977 exit(0);
978 current_command = not_allowed_chars.sub(" ", match.group(1));
979 current_command_line = user_change+"bash --restricted -c \'"+current_command+"\' "+str(os.getpid())+" "+execute_args;
980 # Print command
981 if print_verbose :
982 print("#", current_command_line, file=current_outfile);
983 # Spawn command and open a pipe to the output
984 pipe = subprocess.Popen(current_command_line, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE);
985 for b in pipe.stdout:
986 if type(b).__name__ == 'str':
987 b = bytes(b, encoding='utf8');
988 filehash.update(b);
989 if options.printhexdump: # For debugging commands
990 print(str(binascii.hexlify(b), 'ascii'), file=sys.stderr);
991 # See whether there was an error
992 pipe.wait();
993 if pipe.returncode:
994 error_message = pipe.stderr.read();
995 print('$('+current_command+')', "\n", str(error_message, encoding='UTF8'), file=sys.stderr);
996 exit(pipe.returncode);
997 # ${ENV} environment variables
998 match = re.search('^\$[\{]?([^\}\(\)]+)[\}]?$', filename);
999 if match != None:
1000 current_var = not_allowed_chars.sub(" ", match.group(1));
1001 if print_verbose:
1002 print("# echo $"+ current_var, file=current_outfile);
1003 b = os.environ[current_var];
1004 filehash.update(bytes(b, encoding='utf8'));
1005 # lstat() meta information
1006 elif filename.startswith('?'):
1007 if not os.path.exists(filename.lstrip('?')):
1008 print(filename, "does not exist", file=sys.stderr)
1009 quit();
1010 filestat = os.stat(filename.lstrip('?'));
1011 if my_statusvalues == "": my_statusvalues = 'fmidlugs'
1012 b = "";
1013 if 'f' in my_statusvalues:
1014 b += 'stat('+filename.lstrip('?')+') = '
1015 b += '[';
1016 if 'm' in my_statusvalues:
1017 b += 'st_mode='+str(oct(filestat.st_mode))+', ';
1018 if 'i' in my_statusvalues:
1019 b += 'st_ino='+str(filestat.st_ino)+', ';
1020 if 'd' in my_statusvalues:
1021 b += 'st_dev='+str(filestat.st_dev)+', '
1022 if 'l' in my_statusvalues:
1023 b += 'st_nlink='+str(filestat.st_nlink)+', '
1024 if 'u' in my_statusvalues:
1025 b += 'st_uid='+str(filestat.st_uid)+', '
1026 if 'g' in my_statusvalues:
1027 b += 'st_gid='+str(filestat.st_gid)+', '
1028 if 's' in my_statusvalues:
1029 b += 'st_size='+str(filestat.st_size)+', '
1030 if 'a' in my_statusvalues:
1031 b += 'st_atime='+str(filestat.st_atime)+', '
1032 if 't' in my_statusvalues:
1033 b += 'st_mtime='+str(filestat.st_mtime)+', '
1034 if 'c' in my_statusvalues:
1035 b += 'st_ctime='+str(filestat.st_ctime);
1037 b = b.rstrip(', ') + ']';
1038 filehash.update(bytes(b, encoding='utf8'));
1039 if print_verbose:
1040 print ("# "+ b, file=current_outfile);
1041 # Use file
1042 else:
1043 # open and read the file
1044 if filename != '-' and filename.find('://') == -1 and not os.path.exists(filename):
1045 print(filename, "does not exist", file=sys.stderr)
1046 quit();
1048 if filename == '-':
1049 for b in sys.stdin.buffer:
1050 if type(b).__name__ == 'str':
1051 b = bytes(b, encoding='utf8');
1052 filehash.update(b);
1053 if options.printhexdump: # For debugging commands
1054 print(str(binascii.hexlify(b), 'ascii'), file=sys.stderr);
1056 else:
1057 with open_infile(filename, 'rb') as file:
1058 for b in file:
1059 if type(b).__name__ == 'str':
1060 b = bytes(b, encoding='utf8');
1061 filehash.update(b);
1062 if options.printhexdump: # For debugging commands
1063 print(str(binascii.hexlify(b), 'ascii'), file=sys.stderr);
1065 current_digest = filehash.hexdigest();
1066 print_name = filename;
1067 if my_quiet or org_filename.startswith('['):
1068 file_argnum += 1;
1069 print_name = '['+str(file_argnum)+']';
1070 current_hash_line = current_digest+" *"+print_name
1071 totalhash.update(bytes(current_hash_line, encoding='ascii'));
1073 # Be careful to use this ONLY after totalhash has been updated!
1074 if total_only:
1075 current_hash_line = (len(current_digest)*'-')+" *"+print_name;
1077 # Write output
1078 if not my_check:
1079 if not (my_quiet and total_only) and not (my_allsalts and snum > 1):
1080 print(current_hash_line, file=current_outfile);
1081 elif not (my_quiet or my_allsalts):
1082 if check_hashes[print_name] == (len(current_digest)*'-'):
1083 # Suppress redundant output of empty, ----, lines
1084 if snum <= 1 and pnum <= 1:
1085 print(check_hashes[print_name]+" *"+print_name, file=current_outfile);
1086 elif current_digest != check_hashes[print_name]:
1087 print("DIFFERENT: "+current_hash_line, file=current_outfile);
1088 else:
1089 print("ok"+" *"+print_name, file=current_outfile);
1091 # Handle total hash
1092 current_total_digest = totalhash.hexdigest();
1093 # Write (in)correct salts with the TOTAL HASH
1094 if my_allsalts:
1095 output_salt = my_salt;
1096 j = random.random();
1097 # Randomly create an incorrect salt for failed output
1098 if not my_check:
1099 if j < fail_fraction and snum != selected_salt:
1100 salt = dev_random.read(8);
1101 output_salt = str(binascii.hexlify(salt), 'ascii');
1102 else:
1103 salt_pattern_number += current_salt_power;
1104 current_total_digest_line = "Salt+TOTAL HASH: '"+output_salt+"' '"+current_total_digest+"'";
1105 else: # Standard TOTAL HASH line
1106 current_total_digest_line = current_total_digest+" *"+"TOTAL HASH";
1107 end_time = time.time();
1108 print("# \n# Total hash - Time to completion:", end_time - start_time, "seconds", file=current_outfile);
1109 total_hash_num = 0;
1110 if my_allsalts: total_hash_num = snum-1; # Current TOTAL HASH number of more are used
1111 if not my_check:
1112 print(current_total_digest_line+"\n", file=current_outfile);
1113 elif current_total_digest != total_hash_list[total_hash_num]:
1114 if not my_allsalts: print("DIFFERENT: "+current_total_digest_line+"\n", file=current_outfile);
1115 else:
1116 if my_allsalts: salt_pattern_number += current_salt_power; # Update salt bit pattern
1117 match_number = "";
1118 if len(passphrase_list) > 1 or len(salt_list): match_number = " #"
1119 if len(passphrase_list) > 1: match_number += " passphrase no: "+str(pnum);
1120 if len(salt_list) > 1: match_number += " salt no: "+str(snum);
1121 if not my_allsalts: print("OK"+" *"+"TOTAL HASH"+match_number+"\n", file=current_outfile);
1122 corrsnum = snum;
1123 corrpnum = pnum;
1124 snum += 1;
1125 if my_allsalts: current_salt_power *= 2; # Update current bit position in salt pattern
1126 if my_check and corrpnum == pnum: matched_salt_pattern = salt_pattern_number;
1127 pnum += 1;
1129 if my_check and len(passphrase_list) > 1:
1130 if corrpnum > 0:
1131 print("Passphrase entry:",corrpnum,"matched", file=current_outfile);
1132 else:
1133 print("No passphrase entry matched!", file=current_outfile);
1134 if my_check and (not my_allsalts) and len(salt_list) > 1:
1135 if corrpnum > 0:
1136 if corrsnum > 0:
1137 print("Salt entry:",corrsnum,"matched", file=current_outfile);
1138 else:
1139 print("No salt entry matched!", file=current_outfile);
1140 else:
1141 print("No entry matched", file=current_outfile);
1142 # Print salt bit patterns
1143 elif my_check and my_allsalts:
1144 print("Salt pattern number:", matched_salt_pattern, file=current_outfile);
1145 elif not my_check and my_allsalts:
1146 print("# Salt pattern number:", salt_pattern_number, file=current_private);
1148 # Close output files if necessary
1149 if my_output and my_output != '-':
1150 current_outfile.close();
1151 if my_private and my_private != '-':
1152 current_private.close();