5 # 2. IMPORT & INITIALIZATION
7 # 4. ARGUMENT PROCESSING
8 # 5. SIGNATURE CREATION AND CHECKING
10 #############################################################################
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
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.
29 Usage: signduterre.py [options] FILE1 FILE2 ...
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
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
64 -o FILE, --output-file=FILE
65 Print to FILE instead of STDOUT.
66 --file-source=PATH Read all files from PATH. The PATH-string is prepended
67 to every plain file-path that is read for a signature.
68 Remote files can be checked with
69 'ssh://<user>@<host>[/path]'. The option overrules
70 any File source specification in the --check-file.
71 -P FILE, --Private-file=FILE
72 Print private information (passwords etc.) to FILE
74 -u USER, --user=USER Execute $(cmd) as USER, default 'nobody' (root/sudo
76 -S, --Status For each file, add a line with unvarying file status
77 information: st_mode, st_ino, st_dev, st_uid, st_gid,
78 and st_size (like the '?' prefix, default False)
79 --Status-values=MODE Status values to print for --Status, default MODE is
80 'fmidugs' (file, mode, inode, device, uid, gid, size).
81 Also available (n)l(inks) a(time), (m)t(ime), and
83 -t, --total-only Only print the total hash, unsets --detailed-view
85 -d, --detailed-view Print hashes of individual files, is unset by --total-
87 -e, --execute Interpret $(cmd) (default False)
88 --execute-args=ARGS Arguments for the $(cmd) commands ($1 ....)
89 -n, --no-execute Explicitely do NOT Interpret $(cmd)
90 --print-hexdump Print hexadecimal dump of input bytes to STDERR for
92 -m, --manual Print a short version of the manual and exit
93 --manual-long Print the long version of the manual and exit
94 --manual-html Print the manual in HTML format and exit
95 --manual-make Print the examples in the manual as a makefile and
97 -r, --release-notes Print the release notes and exit
98 -l, --license Print license text and exit
99 -v, --verbose Print more information on output
100 -q, --quiet Print minimal information (hide filenames). If the
101 output is used with --check-file, the command line
102 options and arguments must be repeated.
105 Names and paths of one or more files to be checked. All file arguments in SdT accept '-' as the STDIN file
106 (ie, piped data). Can use ssh://<user>@<host>/path pseudo-URLs for checking files at remote sites.
108 Any name starting with a '$', eg, $PATH, will be interpreted as an environmental variable or a command
109 according to the bash conventions: '$ENV' and '${ENV}' as variables, '$(cmd;cmd...)' as system commands
110 (bash --restricted -c 'cmd;cmd...' PID). Where PID the current Process ID is (available as positional
111 parameter $0). Other parameters can be entered with the --execute-args option ($1 etc). Do not forget to
112 enclose the arguments in single ''-quotes! The commands are scanned for unwanted characters and these
113 are removed (eg, ' and \\, however, escaping $ is allowed, eg, '\\$1'). The use of '$(cmd;cmd...)'
114 requires explicit use of the -e or --execute option.
116 If executed as root or sudo, $(cmd;cmd...) will be executed as 'sudo -H -u <user>' which defaults to
117 --user nobody ('--user root' is at your own risk). This will obviously not work when invoked as non-root/sudo.
118 --user root is necessary when you need to check privileged information, eg, you want to check the MBR with
119 '$(dd if=/dev/hda bs=512 count=1 status=noxfer | od -X)'
120 However, as you might use --check-file with files you did not create yourself, it is important to
121 be warned if commands are to be executed.
123 Interpretation of $() ONLY works if the -e or --execute options are entered. signduterre.py can easily
124 be adapted to automatically use the setting in the check-file. However, this is deemed insecure and
125 commented out in the distribution version.
127 The -n or --no-execute option explicitely supress the interpretation of $(cmd) arguments.
129 Meta information from stat() on files is signed when the filename is preceded by a '?'. '?./signduterre.py' will
130 extract (st_mode, st_ino, st_dev, st_nlinks, st_uid, st_gid, st_size) and hash a line of these data (visible
131 with --verbose). The --Status option will automatically add such a line in front of every file. Note that '?'
132 is implied for directories. Both '/' and '?/' produce a hash of, eg,:
134 stat(/) = [st_mode=041775, st_ino=2, st_dev=234881026, st_uid=0, st_gid=80, st_size=1360]
136 The --Status-values=<mode> option selects which status values will be used: f(ile), m(ode), i(node),
137 d(evice), u(id), g(id), s(ize), (n)l(inks), a(time), (m)t(ime), and c(time). Default is
138 --Status-values='fmidugs'. Note that nlinks of a directory include every file in the directory, so this
139 option can check whether files have been added to a directory.
141 Arguments enclosed in []-brackets will be hidden in the output. That is, '[/proc/self/exe]' will show up as
142 '[1]' in the output (or '[n]' with n the number of the hidden argument), equivalent to the use of the
143 --quiet option. This means the hidden arguments must be entered again when using the --check-file (-c)
148 A very simple tool to generate a signature that can be used to test the integrity of files and "states" in
149 a running installation. signduterre.py constructs a signature of the current system state and checks
150 installation state with a previously made signature. The files are hashed with a passphrase to allow detection
151 of compromised systems while running on the same system. The signature checking can be subverted, but the
152 flexibillity of signduterre.py and the fact that the output of any command can be tested should hamper
153 automated root-kit attacks.
155 signduterre.py writes a total SHA-256 hash to STDOUT of all the files and commands entered as arguments. It
156 can also write a hash for each individual file (insecure). The output of a signature can be send to a file and
157 later used to check with --check-file. Hashes are calculated with a hashed salt + passphrase sequence
158 pre-pended to create unpredictable hashes. This procedure ensures that an attacker does not know whether or
159 not the correct passphrase has been entered. An attacker can only know when to supply the requested hash
160 values if she knows the passphrase or has copies available of all the tested files and output of commands to
161 calculate the hashes on the fly.
162 [[[/p]]][[[LONG]]][[[p]]]
165 The problem SdT tries to solve is how to test whether your system has been compromised when you can only use
166 the potentially compromised system? The solution is to store a password encrypted signature (or fingerprint)
167 of your system when you are sure it is in a good state. Then you check whether the system can still
168 distinguish between correct and incorrect passwords when it regenerates the signature. The trick is to use
169 the right data (ie, questions) to generate the signature.
171 The underlying idea is that some bits have to be changed to compromise a system. That is, program
172 files have been altered, settings and accounts changed, new processes are running or existing processes
173 altered. The most common situation is that some system programs have been changed to hide the traces of
174 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
175 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
176 malicious processes that are running. Such wholescale adaptations of running systems can be executed
177 using standard, off-the-shelf application suits, so called rootkits. There are applications that can
178 detect common (known) rootkits and other malicious programs, eg, [[[i]]]chkrootkit[[[/i]]] ([[[a
179 href="http://www.chkrootkit.org/"]]]www.chkrootkit.org[[[/a]]]) and
180 [[[i]]]rootkit hunter[[[/i]]] ([[[a
181 href="http://www.rootkit.nl"]]]www.rootkit.nl[[[/a]]]). However, these rootkit detectors also use existing commands on the
182 potentially compromised system, so a rootkit can hide from them too.
184 There are two obvious directions to guard against rootkits. One is to continuously run a process that
185 looks for attempts to install a rootkit and other malicious activities. The other is to take a snapshot
186 of the system in a known good state, and then flag changes in relevant areas, eg, like [[[i]]]Tripwire[[[/i]]]
187 ([[[a href="http://sourceforge.net/projects/tripwire/"
188 ]]]http://sourceforge.net/projects/tripwire/[[[/a]]]) and [[[i]]]Radmind[[[/i]]] ([[[a
189 href="http://rsug.itd.umich.edu/software/radmind/"
190 ]]]http://rsug.itd.umich.edu/software/radmind/[[[/a]]]).
191 Signature-du-Terroir takes the second route, it creates a signature of a set of relevant files and
192 command output, and checks later whether these have not been changed. However, when running such a test
193 on a compromised system, the attacker can theoretically "fool" any (automated) test. In practise, time
194 and other precious resources will limit what an attacker can accomplish. The idea is to raise the bar
195 for rootkits high enough to make them not worthwhile. SdT tries to make using signatures easy (cheap)
196 and subverting it difficult (expensive).
198 As an illustration of the problem SdT treis to solve, take the [[[i]]]sha256sum[[[/i]]] command which generates file
199 hashes (signatures) using the SHA256 algorithm. Hashes can be generated and checked with this command:
201 # Use of sha256sum to check integrity of ps and ls commands
202 $ sha256sum /bin/ps /bin/ls > ps-ls.sh256
203 $ sha256sum -c ps-ls.sh256
205 A compromised file will show up as FAILED. This is ok for unintentional changes to the files. However, a
206 malicious attacker could easily replace [[[i]]]/usr/bin/sha256sum[[[/i]]] with a program that would replace the hash of
207 malicious replacements of these files with the hash sums of the original files. There are three easy ways
208 of doing that. Either simply say 'ok' when checking the file, print out the stored old hash value whenever
209 an altered file is requested by name, or look for the hash of the new, malicious replacement and print out
210 the old hash sum instead. The former two are easy to circumvent, the last one is somewhat less easy.
212 The first solution to these avoidance strategies is to generate the signatures with a passphrase and random
213 string (salt). As long as the attacker does not know the passphrase, the only way to subvert SdT is to store
214 the original bits in the files and calculate the signature the moment SdT is called. As the attacker does
215 not know when the correct password or salt is entered, it is not possible to simply answer OK or repeat the
216 stored earlier results instead of calculating them de-novo.
218 To be able to serve up the original bits, instead of the bits used on the compromised system, when asked
219 for the hashes, the attacker must divert attempts to read the files by SdT, but not at other moments.
220 There are many ways to do this, eg, running python in a chroot-jail, changing python itself, changing other
221 programs. To accommodate these diversion strategies, SdT allows to read data from each and every command
222 that can supply it. So, a binary file can be entered by name, with eg, cat, dd, perl, python, ruby, or read
223 from the [[[i]]]/proc[[[/i]]] system (if it is a running process), or from STDIN or shell subprocesses. For instance,
224 to protect against running in a chroot-jail, the inode number and device of the root directory can be read
225 from [[[i]]]/proc/self/root[[[/i]]], or [[[i]]]/proc/<PID>/root[[[/i]]], or simply from [[[i]]]/[[[/i]]].
226 [[[/p]]][[[/LONG]]][[[p]]]
227 Signature creation: Passphrases, salts, and hashes
229 Good passphrases are difficult to remember, so their plaintext form should be protected. To protect the
230 passphrase against rainbow and brute force attacks, the passphrase is concatenated to a salt phrase and
231 hashed before use (SHA-256).
233 The salt phrase is requested when constructing a signature. In interactive use, an 8 byte hexadecimal
234 (= 16 character) salt from [[[i]]]/dev/urandom[[[/i]]] is suggested. If '--salt SUGGESTED' is entered on the command line
235 as the salt, the suggested value will be used. The salt is printed in plaintext to the output. The salt will
236 make it more difficult to determine whether the same passphrase has been used to create different signatures.
238 At the bottom, a 'TOTAL HASH' line will be printed that hashes all the lines printed for the files. This
239 includes the file names as printed on the hash lines. It is not inconceivable that existing signature files
240 could have been compromised in ways that might be missed when checking the signature. The total hash will
241 point out such changes.
244 [[[/p]]][[[LONG]]][[[p]]]
245 When run on a compromised system, signduterre.py can be subverted if the attacker keeps a copy of all the
246 files and command outputs, and reroutes the open() and stat() functions, or simply delegating signduterre.py
247 to a chroot jail with the original system. In principle, signduterre.py only checks whether the computer
248 responds identically to when the signature file was made. There is no theoretic barrier against a compromised
249 computer perfectly simulating the original system when tested, but behaving adversely at other times. Except
250 for running from clean boot media (USB?), I know of no theoretical sound solution to this problem.
252 However, this scenario assumes the use of unlimited resources and time. Inside a limited, real computer system,
253 the attacker must make compromises on what can and what cannot be simulated with the available time and
254 hardware. The idea behind signduterre.py is to "ask difficult questions" that increase the cost of simulating
255 the original system high enough to make detection of successful attacks likely.signduterre.py simply intends
256 to raise the bar high enoug. One point is to store the times needed to create the original hashes. This timing
257 can later be used to see whether the new timings are reasonable. If the same hardware takes considerably
258 longer to perform the same calculations, or needs a much longer delay before it starts, the tester might want
259 to see where this time is spent.
260 [[[/p]]][[[/LONG]]][[[p]]]
261 Signature-du-Terroir works on the assumption that any attacker in control of a compromised system cannot
262 predict whether the passphrase entered is correct or not. An attacker can always intercept the in- and output
263 of signduterre. When running with --check-file, this means the program can be made to print out OK
264 irrespective of the tests. A safe use of signduterre.py is to start with a random number of incorrect
265 passphrases and see whether they fail. Alternatively, and easier, is to add a number of unused salts
266 to the check-file and let the attacker guess which one is correct.
268 THE CORRECT USE OF signduterre.py IS TO ENTER A RANDOM NUMBER OF INCORRECT PASSPHRASES OR SALTS FOR EACH
269 TEST AND SEE WHETHER IT FAILS AT THE CORRECT INSTANCES!
271 On a compromised system, signduterre.py's detailed file testing (--detailed-view) is easily subverted. With a
272 matched file hash, the attacker will know that the correct passphrase has been entered and can print out the
273 stored hashes or 'ok's for the rest of the checks. So if the attacker keeps any entry in the signature file
274 uncompromised, she can intercept the output, test the password on the unchanged entry and substitute the
275 requested hashes for the output if the hash of that entry matches.
276 [[[/p]]][[[LONG]]][[[p]]]
277 When checking for root-kits and other malware, it is safest to compare the signature files from a different,
278 clean, system. But then you would not need signduterre.py anyway. If you have to work on the system itself,
279 only use the -t or --total-only options to create signatures with a total hash and without individual file
280 hashes. Such a signature can be used to check whether the system is unchanged. Another signature file WITH A
281 DIFFERENT PASSPHRASE can then be used to identify the individual files that have changed. If a detailed
282 signature file has the same passphrase, an attacker could use that other file to read the individual file
283 hashes to check whether the correct passphrase was entered.
284 [[[/p]]][[[/LONG]]][[[p]]]
285 Using the --check-file option in itself is UNsafe. An attacker simply has to print out 'OK' to defeat the
286 check. This attack can be foiled by making it unpredictable when signduterre.py should return 'OK'. This can
287 be done by using a list of salts or passphrases where only one of them (or none!) is correct. Any attacker
288 will have to guess when to return 'OK'.
289 [[[/p]]][[[LONG]]][[[p]]]
290 As generating and entering wrong passphrases and salts is tedious, users have to be supported in correct use
291 of SdT. To assist users, the '--salt SUGGESTED=<N>' option will generate a number N of salts. When
292 checking, each of these salts is tried in turn. An attacker that is unable to simulate the uncompromised
293 system will have to guess which one of the salts is the correct one, and whether or not the passphrase
294 is correct. This increases the chances of detecting compromised systems. If this is not enough guess
295 work, the '-a', '--all-salts-pattern' option will use all salts in sequence to generate total hashes,
296 but random salts will be changed in the output. This generates a pattern of failed salt tests. This pattern
297 is translated into a bit pattern and printed as an integer ([Fail, Fail, OK, Fail, OK, OK, Fail, OK]
298 = 00101101 (least significant first) = 10110100 (unsigned bin) = 180). On creation of a signature, this
299 number is printed to STDERR, on checking (--check-file) it is printed to STDOUT (note that the number
300 will never become 0 or all Fail). So for '--salt SUGGESTED=<N> --all-salts-pattern' the probability of
301 guessing the correct output goes from 1/N to 1/(2^N - 1). Note that '--all-salts-pattern' will work,
302 but is pointless, without '--salt SUGGESTED=<N>' with N>1.
304 The '--passphrase SUGGESTED=N' option will generate and print N passphrases. One of these is chosen at
305 random for the signature. The number of the chosen passphrase is printed on STDERR with the passwords.
306 When checking a file, the stored passphrases can be read in again, either by entering the passphrase
307 file after the --passphrase option ('--passphrase <passphrase file>'), or directly from the --check-file.
308 signduterre.py will print out the result for each of the passphrases.
310 Note, that storing passphrases in a file and feeding it to signduterre.py is MUCH less secure than just
311 typing them in. Moreover, it might completely defeat the purpose of signduterre.py. If future experiences
312 cast any more doubt on the security of this option, it will be removed.
314 For those who want to know more about what an "ideal attacker" can do, see:[[[br]]]
315 Ken Thompson "Reflections on Trusting Trust"[[[br]]]
316 [[[a href="http://cm.bell-labs.com/who/ken/trust.html"]]]http://cm.bell-labs.com/who/ken/trust.html[[[/a]]][[[br]]]
317 [[[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]]]
319 David A Wheeler "Countering Trusting Trust through Diverse Double-Compiling"[[[br]]]
320 [[[a href="http://www.acsa-admin.org/2005/abstracts/47.html"]]]http://www.acsa-admin.org/2005/abstracts/47.html[[[/a]]]
322 and the discussion of these at Bruce Schneier's 'Countering "Trusting Trust"'[[[br]]]
323 [[[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]]]
324 [[[/p]]][[[/LONG]]][[[p]]]
327 The intent of signduterre.py is to ensure that the signature cannot be subverted even if the system has been
328 compromised by an attacker that has obtained root control over the computer and any existing signature files.
330 signduterre.py asks for a passphrase which is PRE-pended to every file before the hash is constructed (unless
331 the passphrase is entered with an option). As long as the passphrase is not compromised, the hashes cannot
332 be reconstructed. A randomly generated, unpadded base-64 encoded 16 Byte password (ie, ~22 characters) is
333 suggested in interactive use. If '--passphrase SUGGESTED' is entered on the command line or no passphrase is
334 enetered when asked, the suggested value will be used. This value is printed to STDERR (the screen or 2) for
335 safe keeping. Please, make sure you store the printed passphrase. For instance:
336 [[[/p]]][[[pre make=example1]]]
338 # Simple system sanity test using the 'which' command to establish the paths
339 $ python3.0 signduterre.py --passphrase SUGGESTED --salt SUGGESTED --detailed-view \\
340 `which python3.0 bash ps ls find stat` 2> test-20090630_11-14-03.pwd > test-20090630_11-14-03.sdt
341 $ python3.0 signduterre.py --passphrase test-20090630_11-14-03.pwd --check-file test-20090630_11-14-03.sdt
343 The first command will store the passphrase (and all error messages) in a file 'Signature_20090630_11-14-03.pwd'
344 and the check-file in 'Signature_20090630_11-14-03.sdt'. The second line will test the signature.
345 The signature will be made of the files used for the commands python3.0, bash, ps, ls, find, and stat.
346 These files are found using the 'which' command.
348 Working with remote systems
350 It is not secure to store files with the passphrase on the system you want to check. However, you could
351 pipe STDERR to some safe site.
353 # Send passphrase over ssh tunnel to safe site
354 $ python3.0 signduterre.py --passphrase SUGGESTED --salt SUGGESTED `which bash python3.0` \\
355 -o test-safe-store.sdt 2>&1 | ssh user@safe.host.site 'dd of=/home/user/safe/test-safe-store.pwd'
357 As the security of the passphrases is important and off-site storrage of files is often prudent or convenient,
358 this tunneling construct has been automated in all in- and output as a pseudo-URL: 'ssh://<user>@<host></path>',
359 eg, 'ssh://user@safe.host.site/home/user/safe/test-safe-store.pwd'. It is not possible to enter a
360 password in such a pseudo-URL, so the automatical login into the host system must be configured in SSH.[[[br /]]]
361 [[[em]]]Note: There are severe security risks involved when using SSH to login into another system if the
362 originating system is compromised[[[/em]]].
364 The pseudo-url can be used with the [[[i]]]--output-file, --Private-file, --input-file, --check-file, --passphrase[[[/i]]]
365 options as well as for the actual file and $(cmd) arguments used to determine the signatures. The latter
366 allows to check files on remote systems, or to repeat a check from a remote system using the [[[i]]]--file-source[[[/i]]]
367 option (only works with plain files and $(cmd), not for directories, --Status, or ${} arguments). For instance:
369 # Use ssh:// pseudo-url to send passphrase to safe.host.site
370 $ python3.0 signduterre.py --passphrase SUGGESTED --salt SUGGESTED `which bash python3.0` \\
371 -o ssh://user@safe.host.site/home/user/safe/test-safe-store.sdt \\
372 -P ssh://user@safe.host.site/home/user/safe/test-safe-store.pwd
373 # Check files on remote compromised.host.site while running test program on safe.host.site
374 $ python3.0 signduterre.py --passphrase test-safe-store.pwd --check-file test-safe-store.sdt \\
375 --file-source ssh://user@compromised.host.site
377 To execute a remote $(cmd) argument, write $(ssh://<user>@<host>/cmd). Be aware that too many nested "-quotes might cause
378 problems. When using a --file-source argument that starts with 'ssh://', the $(cmd) commands are internally rewritten
379 into the above form. Note that no <path> argument will be used.
381 The next example uses the ssh:// pseudo-URL to read the data in an alternative way on [[[i]]]localhost[[[/i]]]. Obviously, storing the
382 plain text passphrase on the same system makes it a rather pointless excersize. The example only works if your
383 '~/.ssh/id_dsa.pub' or '~/.ssh/id_rsa.pub' file has been appended to '~/.ssh/authorized_keys' and you used ssh-add or
384 another application to open the key.
385 [[[/p]]][[[pre make=ssh1]]]
387 # Use ssh:// pseudo-url to read data in an alternative way
388 $ python3.0 signduterre.py --passphrase SUGGESTED --salt SUGGESTED -v -d -e `which dd` '$(cat `which dd`)' \\
389 -o test-safe-store.sdt \\
390 -P ssh://`whoami`@localhost${PWD}/test-safe-store.pwd
391 # check files the standard way
392 $ python3.0 signduterre.py -e --passphrase ssh://`whoami`@localhost${PWD}/test-safe-store.pwd --check-file test-safe-store.sdt
393 # Check files using ssh on localhost
394 $ python3.0 signduterre.py -e --passphrase ssh://`whoami`@localhost${PWD}/test-safe-store.pwd --check-file test-safe-store.sdt \\
395 --file-source ssh://`whoami`@localhost
397 Examples:[[[/p]]][[[pre make=example2]]]
399 # Self test of root directory, python, and signduterre.py using the 'which' command to establish the paths
400 $ python3.0 signduterre.py --detailed-view --salt 436a73e3 --passphrase liauwefa3251EWC -o test-self.sdt \\
401 / `which python3.0 signduterre.py`
402 $ python3.0 signduterre.py --passphrase liauwefa3251EWC -c test-self.sdt
403 [[[/pre]]][[[LONG]]][[[p]]]
404 Write a signature to the file test-self.sdt and test it with the --check-file option. The signature contains
405 the SHA-256 hashes of the files, [[[i]]]/usr/bin/python3.0[[[/i]]], [[[i]]]signduterre.py[[[/i]]], and the status information on the root
406 directory. The salt '436a73e3' and passphrase 'liauwefa3251EWC' are used.
407 [[[/p]]][[[/LONG]]][[[pre make=procfs1]]]
409 # Self test of root directory, python, and signduterre.py using the the /proc file system
410 $ python3.0 signduterre.py --detailed-view --salt SUGGESTED --passphrase liauwefa3251EWC -o test-self_proc.sdt \\
411 /proc/self/root /proc/self/exe `which signduterre.py`
412 $ python3.0 signduterre.py --passphrase liauwefa3251EWC --check-file test-self_proc.sdt
413 [[[/pre]]][[[LONG]]][[[p]]]
414 Write a signature to the file test-self_proc.sdt and test it with the --check-file option. The signature
415 contains the SHA-256 hashes of the same files as above, [[[i]]]/usr/bin/python3.0[[[/i]]], [[[i]]]signduterre.py[[[/i]]], and the status
416 information on the root directory. However, the python executable and the root directory are now accessed
417 through the [[[i]]]/proc[[[/i]]] file system. The suggested salt is used (written to test-self_proc.sdt) and the passphrase
418 is (again) 'liauwefa3251EWC'.
419 [[[/p]]][[[/LONG]]][[[pre make=example3]]]
421 # Test of supporting commands for chkrootkit
422 $ python3.0 signduterre.py --execute --total-only --salt SUGGESTED=8 --passphrase SUGGESTED --Status \\
423 --output-file=test-chkrootkit.sdt --Private-file=test-chkrootkit.pwd \\
424 signduterre.py `which bash awk cut egrep find head id ls netstat ps strings sed uname`
425 $ python3.0 signduterre.py --execute --passphrase test-chkrootkit.pwd --check-file test-chkrootkit.sdt
426 [[[/pre]]][[[LONG]]][[[p]]]
427 Writes a signature of the requested files to test-chkrootkit.sdt (signature) and private information to
428 test-chkrootkit.pwd (password and selected salt) and checks it in the next line. The files are those of
429 commands required by the [[[i]]]chkrootkit[[[/i]]] program (http://www.chkrootkit.org/), with bash added. The 'which'
430 command will give the paths for the commands. Eight salts are generated, of which only 1 is actually
431 used. When checking, the correct salt should match. This prevents a compromised program from simply
432 printing out OK tot he check. A more comprehensive evation of guessing the correct salt can be obtained
433 by using the '--all-salts-pattern' option.
434 [[[/p]]][[[/LONG]]][[[pre make=procfs2]]]
436 # Simply lump all "system" files, the PATH environment variable and the first 2 columns of the output of lsmod
437 $ python3.0 signduterre.py --execute --detail --salt SUGGESTED --passphrase liauwefa3251EWC --Status --total-only \\
438 signduterre.py /sbin/* /bin/* /usr/bin/find /usr/bin/stat /usr/bin/python* '${PATH}' \\
439 '$(lsmod | awk "{print \$1, \$2}")' > test-20090625_14-31-54.sdt
441 # Failing check due to missing --execute option
442 $ python3.0 signduterre.py --passphrase liauwefa3251EWC -c test-20090625_14-31-54.sdt
443 $ python3.0 signduterre.py --passphrase liauwefa3251EWC -c test-20090625_14-31-54.sdt --no-execute
446 $ python3.0 signduterre.py --execute --passphrase liauwefa3251EWC --check-file test-20090625_14-31-54.sdt
447 [[[/pre]]][[[LONG]]][[[p]]]
448 Prints a signature to the system test-20090625_14-31-54.sdt and the automatically generated password to
449 test-20090625_14-31-54.pwd. The salt will be automatically determined. The signature contains the SHA-256
450 hashes of the file status and file contents of [[[i]]]signduterre.py, /sbin/*, /bin/*, /usr/bin/find,
451 /usr/bin/file, /usr/bin/python*[[[/i]]] on separate lines, and a hash of the PATH environment variable. Do not
452 display the hash of every single file, which could be insecure, but only the total hash.
453 The first two checks will both fail if test-20090625_14-31-54.sdt contains a $(cmd) entry.
454 The --no-execute option is default and prevents the execute option (if reading the execute option from the
455 signature file has been activated). The last check will succeed (if the files have not been changed).
456 [[[/p]]][[[/LONG]]][[[pre make=example4]]]
458 # Use a list of generated passphrases
459 $ python3.0 signduterre.py --salt SUGGESTED --passphrase SUGGESTED=20 signduterre.py \\
460 2> test-20090630_16-44-34.pwd > test-20090630_16-44-34.sdt
461 $ python3.0 signduterre.py -p test-20090630_16-44-34.pwd -c test-20090630_16-44-34.sdt
462 [[[/pre]]][[[LONG]]][[[p]]]
463 Will generate and print 20 passphrases and print a signature using one randomly chosen passphrase from these
464 20. Everything is written to the files 'test-20090630_16-44-34.pwd' and 'test-20090630_16-44-34.sdt'.
465 Such file names can easily be generated with 'test-`date "+%Y%m%d_%H-%M-%S"`.sdt'.
466 The next command will check all 20 passphrases generated before from the Signature file and print the results.
467 [[[/p]]][[[/LONG]]][[[pre make=example5]]]
469 # Use a list of generated salts with a pattern of correct salts
470 $ python3.0 signduterre.py --salt SUGGESTED=16 --passphrase SUGGESTED --all-salts-pattern \\
471 -P test-salt-pattern.pwd -o test-salt-pattern.sdt `which bash stat find ls ps id uname awk gawk perl`
472 $ python3.0 signduterre.py -p test-salt-pattern.pwd -c test-salt-pattern.sdt
473 # Compare to salt pattern number to the one from the check-file
474 $ cat test-salt-pattern.pwd
475 [[[/pre]]][[[LONG]]][[[p]]]
476 As the previous, but with a pattern of random correct and incorrect salts. The salt pattern number
477 indicates which salts were and were not correct.
478 [[[/p]]][[[/LONG]]][[[pre make=sudo1]]]
480 # Check MBR and current root directory (sudo and root user)
481 $ sudo python3.0 signduterre.py -u root -s SUGGESTED -p SUGGESTED --Status-values='i' -v -e -t \\
482 --output-file test-boot-sector.sdt --Private-file test-boot-sector.pwd --execute-args=sda \\
483 '?/proc/self/root' `which dd` '$(dd if=/dev/$1 bs=512 count=1 status=noxfer | od -X)'
484 $ sudo python3.0 signduterre.py -u root -e -p test-boot-sector.pwd -c test-boot-sector.sdt
485 [[[/pre]]][[[LONG]]][[[p]]]
486 Will hash the inode numbers of the effective root directory (eg, chroot) and the executable (python)
487 together with the contents of the MBR (Master Boot Record) on [[[i]]]/dev/sda[[[/i]]] in Hex. It uses suggested salt and
488 passphrase. Accessing [[[i]]]/dev/sda[[[/i]]] is only possible when [[[i]]]root[[[/i]]], so the command is entered with [[[i]]]sudo[[[/i]]] and
489 '--user root'. Use the '--print-execute' option if you want to check the output of the [[[i]]]dd[[[/i]]] command.
491 The main problem with intrusion detection by comparing file contents is the ability of an attacker
492 to redirect attempts to read a compromised file to a stored copy of the original. So, [[[i]]]sha256sum[[[/i]]] or
493 python could be changed to read [[[i]]]'/home/attacker/old/ps'[[[/i]]] when the argument was [[[i]]]'/bin/ps'[[[/i]]]. This would
494 foil any scheme that depends on entering file names in programs. An answer to this threat is to
495 read the bytes in files in as many ways as possible. Therefor, forcing an attacker to change many
496 files which itself would increase the probability of detection of the attack. The following command
497 will read the same (test) file, and generate identical hashes, in many different ways.
498 [[[/p]]][[[/LONG]]][[[pre make=example6]]]
500 # Example generating identical signatures of the same text file in different ways
501 $ dd if=signduterre.py 2>/dev/null | \\
502 python3.0 signduterre.py -v -d -s 1234567890abcdef -p poiuytrewq \\
503 --execute --execute-args='signduterre.py' \\
507 '$(awk "{print}" $1)' \\
508 '$(cut -f 1-100 $1)' \\
509 '$(perl -ane "{print \$_}" $1)' \\
510 '$(python3.0 -c "import sys;f=open(sys.argv[1]);sys.stdout.buffer.write(f.buffer.read())" $1;)' \\
511 '$(ruby -e "f=open(ARGV[0]);print f.read();" $1;)'
512 [[[/pre]]][[[LONG]]][[[p]]]
513 These "commands" do not always return the same bytes (awk), or any bytes at all (grep), from a text
514 file as when used with a binary file. However, if the commands can print the bytes unaltered, the
515 signatures will be identical. That is, the following arguments will work on a binary file:
516 [[[/p]]][[[/LONG]]][[[pre make=example6]]]
518 # Example generating identical signatures of the same file in different ways, now for binary files
519 $ dd if=/bin/bash 2>/dev/null | \\
520 python3.0 signduterre.py -v -d -s 1234567890abcdef -p poiuytrewq \\
521 --execute --execute-args='/bin/bash' \\
524 '$(perl -ane "{print \$_}" $1)' \\
525 '$(python3.0 -c "import sys;f=open(sys.argv[1]);sys.stdout.buffer.write(f.buffer.read())" $1;)' \\
526 '$(ruby -e "f=open(ARGV[0]);print f.read();" $1;)'
527 [[[/pre]]][[[LONG]]][[[p]]]
528 Will generate the same identical signatures for [[[i]]]/bin/bash[[[/i]]], [[[i]]]STDIN[[[/i]]], [[[i]]]'$(cat /bin/bash)'[[[/i]]] etc.
529 There are obviously many more ways to read out the bytes from the disk or memory. The main point
530 being that it should be difficult to predict for an attacker which commands must be compromised
531 to hide changes in the system.
532 [[[/p]]][[[ /long]]][[[p]]]
533 The examples can be run as a makefile using make. Use one of the following commands:
535 # General examples, use them all
536 python3.0 signduterre.py --manual-make |make -f - example
537 # Linux specific examples using the second procfs example
538 python3.0 signduterre.py --manual-make |make -f - procfs2
539 # Examples requiring sudo, using first
540 python3.0 signduterre.py --manual-make | sudo make -f - sudo1
541 [[[/pre]]][[[/body]]][[[/html]]]
546 Construct a signature of the installed software state or check a previously made signature.
548 copyright 2009, R.J.J.H. van Son
550 This program is free software: you can redistribute it and/or modify
551 it under the terms of the GNU General Public License as published by
552 the Free Software Foundation, either version 3 of the License, or
553 (at your option) any later version.
555 This program is distributed in the hope that it will be useful,
556 but WITHOUT ANY WARRANTY; without even the implied warranty of
557 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
558 GNU General Public License for more details.
560 You should have received a copy of the GNU General Public License
561 along with this program. If not, see <http://www.gnu.org/licenses/>.
564 # Note that only release notes are put here
565 # See git repository for detailed change comments:
566 # git clone git://repo.or.cz/signduterre.git
567 # http://repo.or.cz/w/signduterre.git
569 20090811 - Release candidate v0.5RC
570 20090811 - Implemented ssh tunnel for commands
571 20090810 - Added --file-source=PATH option
572 20090810 - Added ssh tunnel for all file i/o (ssh://...)
573 20090807 - DIFFERENT became FAIL in --check-file
574 20090730 - Release v0.4
575 20090724 - Added '--all-salts-pattern' and HTML formatting in manual
576 20090723 - Added URL support for all files. Does not yet work due to bug in Python 3.0
577 20090723 - Added '-' for STDIN
578 20090717 - Added --execute-args
579 20090716 - Release v0.3
580 20090713 - Added --quiet option
581 20090712 - moved from /dev/random to /dev/urandom
582 20090702 - Replaced -g with -p SUGGESTED[=N]
583 20090702 - Generating and testing lists of random salts
584 20090701 - Release v0.2
585 20090630 - Generating and testing random passphrases
586 20090630 - --execute works on $(cmd) only, nlinks in ?path and ? implied for directories
587 20090630 - Ported to Python 3.0
589 20090628 - Release v0.1b
590 20090628 - Added release-notes
592 20090626 - Release v0.1a
593 20090626 - Initial commit to Git
596 #############################################################################
598 # IMPORT & INITIALIZATION #
600 #############################################################################
607 # if sys.stdout.isatty(): import readline;
612 from optparse
import OptionParser
;
616 import urllib
.request
;
619 # Limit the characters that can be used in $(cmd) commands
620 # Only allow the escape of '$'
621 not_allowed_chars
= re
.compile('([^\w\ \.\/\"\|\;\:\,\-\$\[\]\{\}\(\)\@\`\!\*\=\\\\]|([\\\\]+([^\$\"\\\\]|$)))');
623 programname
= "Signature-du-Terroir";
626 # Open files or pipes for in/output, use mode = 'b' if binary is needed
627 def open_infile(filename
, mode
):
630 elif filename
.lower().find('ssh://') > -1:
631 match
= re
.search('(?i)ssh://([^/]+)/(.*)$', filename
);
632 tunnel_command
= 'ssh '+match
.group(1)+' "dd if='+match
.group(2)+' "';
634 pipe
= subprocess
.Popen(tunnel_command
, shell
=True, stdout
=subprocess
.PIPE
);
636 pipe
= subprocess
.Popen(tunnel_command
, shell
=True, stdout
=subprocess
.PIPE
, universal_newlines
=True);
638 elif filename
.find('://') > -1:
639 print("URL:", filename
, file=current_private
);
640 return urllib
.request
.urlopen(filename
);
642 if not os
.path
.isfile(filename
):
643 print(filename
, "does not exist", file=sys
.stderr
)
645 return open(filename
, mode
);
647 def open_outfile(filename
, mode
):
650 elif filename
.lower().find('ssh://') > -1:
651 match
= re
.search('(?i)ssh://([^/]+)/(.*)$', filename
);
652 tunnel_command
= 'ssh '+match
.group(1)+' "dd of=/'+match
.group(2)+' "';
654 pipe
= subprocess
.Popen(tunnel_command
, shell
=True, stdin
=subprocess
.PIPE
);
656 pipe
= subprocess
.Popen(tunnel_command
, shell
=True, stdin
=subprocess
.PIPE
, universal_newlines
=True);
658 elif filename
.find('://') > -1:
659 print("URL:", filename
, file=current_private
);
660 return urllib
.request
.urlopen(filename
);
662 return open(filename
, mode
);
664 current_outfile
= sys
.stdout
;
665 current_private
= sys
.stderr
;
667 #############################################################################
671 #############################################################################
673 parser
= OptionParser()
674 parser
.add_option("-s", "--salt", metavar
="HEX",
675 dest
="salt", default
=False,
676 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).")
677 parser
.add_option("-a", "--all-salts-pattern",
678 dest
="allsalts", default
=False, action
="store_true",
679 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.")
680 parser
.add_option("-p", "--passphrase", metavar
="TEXT",
681 dest
="passphrase", default
=False,
682 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).")
683 parser
.add_option("-c", "--check-file",
684 dest
="check", default
=False, metavar
="FILE",
685 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.")
686 parser
.add_option("-i", "--input-file",
687 dest
="input", default
=False, metavar
="FILE",
688 help="Use names from FILE or STDIN ('-'), use one filename per line.")
689 parser
.add_option("-o", "--output-file",
690 dest
="output", default
=False, metavar
="FILE",
691 help="Print to FILE instead of STDOUT.")
692 parser
.add_option("--file-source",
693 dest
="filesource", default
=False, metavar
="PATH",
694 help="Read all files from PATH. The PATH-string is prepended to every plain file-path that is read for a signature. Remote files can be checked with 'ssh://<user>@<host>[/path]'. The option overrules any File source specification in the --check-file.")
695 parser
.add_option("-P", "--Private-file",
696 dest
="private", default
=False, metavar
="FILE",
697 help="Print private information (passwords etc.) to FILE instead of STDERR.")
698 parser
.add_option("-u", "--user",
699 dest
="user", default
="nobody", metavar
="USER",
700 help="Execute $(cmd) as USER, default 'nobody' (root/sudo only)")
701 parser
.add_option("-S", "--Status",
702 dest
="status", default
=False, action
="store_true",
703 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)")
704 parser
.add_option("--Status-values",
705 dest
="statusvalues", default
="fmidugs", metavar
="MODE",
706 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).")
707 parser
.add_option("-t", "--total-only",
708 dest
="total", default
=False, action
="store_true",
709 help="Only print the total hash, unsets --detailed-view (default True)")
710 parser
.add_option("-d", "--detailed-view",
711 dest
="detail", default
=False, action
="store_true",
712 help="Print hashes of individual files, is unset by --total-only (default False)")
713 parser
.add_option("-e", "--execute",
714 dest
="execute", default
=False, action
="store_true",
715 help="Interpret $(cmd) (default False)")
716 parser
.add_option("--execute-args",
717 dest
="executeargs", default
='', metavar
="ARGS",
718 help="Arguments for the $(cmd) commands ($1 ....)")
719 parser
.add_option("-n", "--no-execute",
720 dest
="noexecute", default
=False, action
="store_true",
721 help="Explicitely do NOT Interpret $(cmd)")
722 parser
.add_option("--print-hexdump",
723 dest
="printhexdump", default
=False, action
="store_true",
724 help="Print hexadecimal dump of input bytes to STDERR for debugging purposes")
725 parser
.add_option("-m", "--manual",
726 dest
="manual", default
=False, action
="store_true",
727 help="Print a short version of the manual and exit")
728 parser
.add_option("--manual-long",
729 dest
="manuallong", default
=False, action
="store_true",
730 help="Print the long version of the manual and exit")
731 parser
.add_option("--manual-html",
732 dest
="manualhtml", default
=False, action
="store_true",
733 help="Print the manual in HTML format and exit")
734 parser
.add_option("--manual-make",
735 dest
="manualmake", default
=False, action
="store_true",
736 help="Print the examples in the manual as a makefile and exit")
737 parser
.add_option("-r", "--release-notes",
738 dest
="releasenotes", default
=False, action
="store_true",
739 help="Print the release notes and exit")
740 parser
.add_option("-l", "--license",
741 dest
="license", default
=False, action
="store_true",
742 help="Print license text and exit")
743 parser
.add_option("-v", "--verbose",
744 dest
="verbose", default
=False, action
="store_true",
745 help="Print more information on output")
746 parser
.add_option("-q", "--quiet",
747 dest
="quiet", default
=False, action
="store_true",
748 help="Print minimal information (hide filenames). If the output is used with --check-file, the command line options and arguments must be repeated.")
750 (options
, check_filenames
) = parser
.parse_args();
753 # Start with opening any non-default output files
756 current_outfile
= open_outfile(options
.output
, 'w');
757 my_output
= options
.output
;
761 current_private
= open_outfile(options
.private
, 'w');
762 my_private
= options
.private
;
764 print("# Program: "+programname
+ " version " + version
, file=current_outfile
);
765 print("#", time
.strftime("%Y/%m/%d %H:%M:%S", time
.localtime()), "("+time
.tzname
[0]+")\n", file=current_outfile
);
769 print (license
, file=sys
.stderr
);
772 if options
.manual
or options
.manuallong
:
773 cleartext_manual
= re
.sub(r
"(?i)\[\[\[\s*(/?)\s*LONG\s*\]\]\]", r
'[[[\1LONG]]]', manual
);
774 if not options
.manuallong
:
775 currentstart
= cleartext_manual
.find('[[[LONG]]]');
776 while currentstart
> -1:
777 currentend
= cleartext_manual
.find('[[[/LONG]]]', currentstart
)+len('[[[/LONG]]]');
778 (firstpart
, secondpart
) = cleartext_manual
.split(cleartext_manual
[currentstart
:currentend
]);
779 cleartext_manual
= firstpart
+secondpart
;
780 currentstart
= cleartext_manual
.find('[[[LONG]]]');
781 htmltags
= re
.compile('\[\[\[[^\]]*\]\]\]');
782 cleartext_manual
= htmltags
.sub('', cleartext_manual
);
783 print (cleartext_manual
, file=sys
.stdout
);
786 if options
.manualhtml
:
787 protleftanglesbracks
= re
.compile('\<');
788 protrightanglesbracks
= re
.compile('\>');
789 leftanglesbracks
= re
.compile('\[\[\[');
790 rightanglesbracks
= re
.compile('\]\]\]');
791 html_manual
= re
.sub(r
"(?i)\[\[\[\s*(/?)\s*LONG\s*\]\]\]", '', manual
);
792 html_manual
= protleftanglesbracks
.sub('<', html_manual
);
793 html_manual
= protrightanglesbracks
.sub('>', html_manual
);
794 html_manual
= leftanglesbracks
.sub('<', html_manual
);
795 html_manual
= rightanglesbracks
.sub('>', html_manual
);
796 print (html_manual
, file=sys
.stdout
);
798 # Print manual examples as makefile
799 if options
.manualmake
:
800 make_manual
= re
.sub("\$ ", "\t", manual
);
801 make_manual
= re
.sub("\#", "\t#", make_manual
);
802 make_manual
= re
.sub(r
"\\\s*\n", '', make_manual
);
803 make_manual
= re
.sub(r
"\$", r
'$$', make_manual
);
804 # Protect "single" [ brackets
805 make_manual
= re
.sub(r
"(^|[^\[])\[([^\[]|$)", r
"\1[\2", make_manual
);
806 extrexamples
= re
.compile(r
"\[\[\[pre\s+make\=?([a-zA-Z]*)([0-9]*)\s*\]\]\]\n([^\[]*)\n\[\[\[/pre\s*\]\]\]", re
.IGNORECASE|re
.MULTILINE|re
.DOTALL
);
807 exampleiter
= extrexamples
.finditer(make_manual
);
810 group_list
['all'] = "all: ";
811 for match
in exampleiter
:
812 # We had to convert any '[' in the command. Now convert them back.
813 command_text
= re
.sub(r
"\&\#91\;", '[', match
.group(3));
814 makefile_list
.append(match
.group(1)+match
.group(2)+":\n"+command_text
);
815 if len(match
.group(2)) > 0:
816 if not match
.group(1) in group_list
.keys():
817 group_list
[match
.group(1)] = match
.group(1)+": ";
818 group_list
['all'] += match
.group(1)+" ";
819 group_list
[match
.group(1)] += match
.group(1)+match
.group(2)+" ";
821 group_list
['all'] += match
.group(1)+" ";
822 print("help: \n\t@echo 'Use \"make -f - "+re
.sub(':', '', group_list
['all'])+"\"'", file=sys
.stdout
);
823 for group
in group_list
:
824 print(group_list
[group
]+"\n", file=sys
.stdout
);
826 previous_cat
= 'NOT A VALUE';
827 for line
in makefile_list
:
828 (category
, commands
) = line
.split(':\n');
829 if category
!= previous_cat
:
830 previous_cat
= category
;
831 print("\n"+previous_cat
+":", file=sys
.stdout
);
832 print(commands
, file=sys
.stdout
);
834 print("\nclean:\n\trm test-*.sdt test-*.pwd", file=sys
.stdout
);
836 # Print release notes
837 if options
.releasenotes
:
838 print ("Version: "+version
, file=sys
.stderr
);
839 print (releasenotes
, file=sys
.stderr
);
842 my_salt
= options
.salt
;
843 my_allsalts
= options
.allsalts
;
844 my_passphrase
= options
.passphrase
;
845 my_check
= options
.check
;
846 my_status
= options
.status
;
847 my_statusvalues
= options
.statusvalues
;
848 my_verbose
= options
.verbose
and not options
.quiet
;
849 my_quiet
= options
.quiet
;
850 execute
= options
.execute
;
851 execute_args
= options
.executeargs
;
852 if options
.noexecute
: execute
= False;
853 input_file
= options
.input;
854 my_filesource
= options
.filesource
;
855 if my_filesource
: print("File source: '"+my_filesource
+"'\n", file=current_outfile
);
857 # Set total-only with the correct default
859 total_only
= not options
.detail
;
860 if options
.total
: total_only
= options
.total
;
861 if my_allsalts
: total_only
= my_allsalts
; # All alts pattern only sensible with total-only
862 if my_check
: total_only
= False;
864 my_user
= options
.user
;
865 # Things might be executed as another user
868 user_change
= 'sudo -H -u '+my_user
+' ';
869 if not my_quiet
: print("User: "+my_user
, file=current_outfile
);
873 text_execute
= "True";
875 text_execute
= "False";
877 if execute
and not my_quiet
:
878 print("Execute system commands: "+text_execute
+"\n", file=current_outfile
);
879 if execute_args
!= '': print("Execute arguments: '"+execute_args
+"'\n", file=current_outfile
);
882 if my_quiet
: print("Quiet: True\n", file=current_outfile
);
885 if my_statusvalues
!= 'fmidugs': print("Status-values: '"+my_statusvalues
+"'\n", file=current_outfile
);
887 #############################################################################
889 # ARGUMENT PROCESSING #
891 #############################################################################
893 # Measure time intervals
894 start_time
= time
.time();
896 dev_random
= open("/dev/urandom", 'rb');
898 # Read the check file
899 passphrase_list
= [];
902 total_hash_list
= [];
904 highest_arg_used
= 0;
905 print("# Checking: "+my_check
+"\n", file=current_outfile
);
906 arg_list
= check_filenames
;
907 check_filenames
= [];
908 with
open_infile(my_check
, 'r') as c
:
910 match
= re
.search("Execute system commands:\s+(True|False)", line
);
912 # Uncomment the next line if you want automatic --execute from the check-file (DANGEROUS)
913 # execute = match.group(1).upper() == 'TRUE';
916 match
= re
.search("Execute arguments:\s+\'([\w\$\s\-\+\/]*)\'", line
);
918 execute_args
= match
.group(1);
921 match
= re
.search("Quiet:\s+(True|False)", line
);
923 my_quiet
= match
.group(1).upper() == 'TRUE';
924 if my_quiet
: my_verbose
= False;
927 match
= re
.search("File source:\s+\'([\w\-\./\@\:]+)\'", line
);
928 if not my_filesource
and match
!= None:
929 my_filesource
= match
.group(1);
932 match
= re
.search("Salt\s*\+\s*TOTAL HASH\s*\:\s+\'([\w]*)\'\s+\'([\w]*)\'", line
);
934 salt_list
.append(match
.group(1));
935 total_hash_list
.append(match
.group(2));
936 my_allsalts
= True; # Salt+TOTAL HASH imples all-salts-pattern
939 match
= re
.search("Salt\:\s+\'([\w]*)\'", line
);
941 salt_list
.append(match
.group(1));
944 match
= re
.search("Salt\s*\+\s*TOTAL HASH\s*\:\s+\'([\w]+)\'\s+\'([a-f0-9]+)\'", line
);
946 salt_list
.append(match
.group(1));
947 total_hash_list
.append(match
.group(2));
950 match
= re
.search("User\:\s+\'([\w]*)\'", line
);
952 # Uncomment the next line if you want automatic --user from the check-file (DANGEROUS)
953 # my_user = match.group(1);
956 match
= re
.search("Passphrase\:\s+\'([^\']*)\'", line
);
958 passphrase_list
.append(match
.group(1));
961 match
= re
.search("Status-values\:\s+\'([\w]*)\'", line
);
963 my_statusvalues
= match
.group(1);
966 match
= re
.search("^\s*([a-f0-9]+)\s+\*(TOTAL HASH)\s*$", line
)
968 total_hash_list
.append(match
.group(1));
971 match
= re
.search("^\s*([a-f0-9\-]+)\s+\*\[([0-9]+)\]\s*$", line
)
973 filenumber
= int(match
.group(2));
974 if filenumber
> highest_arg_used
: highest_arg_used
= filenumber
;
975 # Watch out, arguments count from 0
976 check_filenames
.append(arg_list
[filenumber
- 1]);
977 check_hashes
['['+match
.group(2)+']'] = match
.group(1);
980 match
= re
.search("^\s*([a-f0-9\-]+)\s+\*(.*)\s*$", line
)
982 check_filenames
.append(match
.group(2));
983 # Catch --execute error as early as possible
984 if match
.group(2).startswith('$(') and not execute
:
985 error_message
= "Executable argument \'"+match
.group(2)+"\' only allowed with the --execute flag";
986 print (error_message
, file=sys
.stderr
);
987 if not sys
.stdout
.isatty(): print(error_message
, file=current_outfile
);
989 check_hashes
[match
.group(2)] = match
.group(1);
991 for i
in range(highest_arg_used
, len(arg_list
)):
992 check_filenames
.append(arg_list
[i
]);
993 check_hashes
['['+str(i
+1)+']'] = (64*'-');
997 with
open_infile(input_file
, 'r') as i
:
1000 current_filename
= re
.sub('[^\w\-\.\/\$\{\(\)\}\?\[\]]', '', line
);
1001 check_filenames
.append(current_filename
);
1002 if my_check
: check_hashes
['['+str(i
+1)+']'] = (64*'-');
1005 for x
in check_filenames
:
1006 if os
.path
.isdir(x
):
1008 if my_status
and not x
.startswith(('?', '$')):
1009 stat_list
.append('?'+x
);
1010 stat_list
.append(x
);
1011 check_filenames
= stat_list
;
1013 # Seed Pseudo Random Number Generator
1014 seed
= dev_random
.read(16);
1017 # Read suggested salts from /dev/(u)random if needed
1019 if my_salt
.startswith('SUGGESTED'):
1021 match
= re
.search("([0-9][0-9]*)$", my_salt
);
1023 N
= int(match
.group(1));
1024 for i
in range(0,N
):
1025 salt
= dev_random
.read(8);
1026 salt_list
.append(str(binascii
.hexlify(salt
), 'ascii'));
1028 salt_list
.append(my_salt
);
1029 elif len(salt_list
) == 0:
1030 salt
= dev_random
.read(8);
1031 sys
.stderr
.write("Enter salt (suggest \'"+str(binascii
.hexlify(salt
), 'ascii')+"\'): ");
1033 if not new_salt
: new_salt
= str(binascii
.hexlify(salt
), 'ascii');
1034 salt_list
.append(new_salt
);
1036 # If not combining salts with TOTAL HASH, print salts now
1038 for my_salt
in salt_list
:
1039 print("Salt: \'"+my_salt
+"\'", file=current_outfile
);
1042 if my_passphrase
and(my_passphrase
== '-' or my_passphrase
.find("://") > -1 or os
.path
.isfile(my_passphrase
)):
1043 with
open_infile(my_passphrase
, 'r') as file:
1045 match
= re
.search("Passphrase\:\s+\'([^\']*)\'", line
);
1047 passphrase_list
.append(match
.group(1));
1048 elif not my_passphrase
and len(passphrase_list
) == 0:
1049 suggest_passphrase
= dev_random
.read(16);
1050 sys
.stderr
.write("Enter passphrase (suggest \'"+str(base64
.b64encode(suggest_passphrase
), 'ascii').rstrip('=')+"\'): ");
1051 # How kan we make this unreadable on input?
1052 current_passphrase
= input();
1053 if not current_passphrase
:
1054 current_passphrase
= str(base64
.b64encode(suggest_passphrase
), 'ascii').rstrip('=');
1055 print("Passphrase: \'"+current_passphrase
+"\'", file=current_private
);
1056 passphrase_list
.append(current_passphrase
);
1057 elif my_passphrase
.startswith('SUGGESTED'):
1059 match
= re
.search("([0-9][0-9]*)$", my_passphrase
);
1061 N
= int(match
.group(1));
1062 j
= int(random
.random()*N
);
1063 for i
in range(0, N
):
1064 suggest_passphrase
= dev_random
.read(16);
1065 current_passphrase
= str(base64
.b64encode(suggest_passphrase
), 'ascii').rstrip('=');
1066 print("Passphrase: \'"+current_passphrase
+"\'", file=current_private
);
1067 passphrase_list
.append(current_passphrase
);
1069 passphrase_list
.append(my_passphrase
);
1072 fail_fraction
= 0.5;
1074 if len(passphrase_list
) > 1:
1075 j
= int(random
.random()*len(passphrase_list
));
1076 passphrase_list
= [passphrase_list
[j
]];
1077 print("# Selected passphrase:", j
+1, file=current_private
);
1078 if len(salt_list
) > 1:
1079 j
= int(random
.random()*len(salt_list
));
1080 # Make sure at least 1 salt will match and print the selection if only one is used
1081 selected_salt
= j
+1;
1083 salt_list
= [salt_list
[selected_salt
-1]];
1084 print("# Selected salt:", selected_salt
, file=current_private
);
1086 salt_N
= len(salt_list
);
1087 fail_fraction
= (salt_N
/2.0)/(salt_N
- 1);
1091 # Close /dev/(u)random
1094 #############################################################################
1096 # SIGNATURE CREATION AND CHECKING #
1098 #############################################################################
1100 end_time
= time
.time();
1101 print("# Preparation time:", end_time
- start_time
, "seconds\n", file=current_outfile
);
1107 matched_salt_pattern
= -1;
1108 salt_pattern_number
= -1;
1110 for my_passphrase
in passphrase_list
:
1112 # Initialize salt pattern
1114 salt_pattern_number
= 0;
1115 current_salt_power
= 1;
1117 for my_salt
in salt_list
:
1118 print("# Start signature: ", end
='', file=current_outfile
);
1119 if len(passphrase_list
) > 1: print("passphrase -", pnum
, end
='', file=current_outfile
);
1120 if len(salt_list
) > 1: print(" salt -", snum
, end
='', file=current_outfile
);
1121 print("", file=current_outfile
);
1123 # Should everything be printed?
1124 print_verbose
= my_verbose
and not (my_allsalts
and snum
> 1);
1127 start_time
= time
.time();
1128 # Construct the passphrase hash
1129 passphrase
= hashlib
.sha256();
1131 passphrase
.update(bytes(my_salt
, encoding
='ascii'));
1132 passphrase
.update(bytes(my_passphrase
, encoding
='ascii'));
1134 # Create prefix which is a hash of the salt+passphrase
1135 prefix
= passphrase
.hexdigest();
1137 # Create signature and write output
1138 totalhash
= hashlib
.sha256();
1139 totalhash
.update(bytes(prefix
, encoding
='ascii'));
1140 for org_filename
in check_filenames
:
1141 # Create file hash object
1142 filehash
= hashlib
.sha256();
1143 filehash
.update(bytes(prefix
, encoding
='ascii'));
1145 filename
= org_filename
.strip('[').rstrip(']');
1146 # Use system variables and commands
1147 if filename
.startswith('$'):
1148 # Commands $(command)
1149 match
= re
.search('^\$\((.+)\)$', filename
);
1152 error_message
= "Executable argument \'"+filename
+"\' only allowed with the --execute flag";
1153 print (error_message
, file=sys
.stderr
);
1154 if not sys
.stdout
.isatty(): print(error_message
, file=current_outfile
);
1157 current_command
= not_allowed_chars
.sub(" ", match
.group(1));
1158 # Insert remote ssh:// if necessary
1159 if my_filesource
and current_command
.find('://') == -1 :
1160 match
= re
.search(r
"(?i)ssh://([^/]+)", my_filesource
);
1161 current_command
= "ssh://"+match
.group(1)+"/"+current_command
;
1162 # Expand remote ssh://
1163 current_host
= None;
1164 current_executable
= None;
1165 if current_command
.startswith("ssh://"):
1166 match
= re
.search(r
"(?i)ssh://([^/]+)/(.*)$", current_command
);
1168 current_host
= match
.group(1);
1169 current_command
= match
.group(2);
1170 current_executable
= "/usr/bin/ssh";
1171 # Create command line
1172 current_command_line
= user_change
+"bash --restricted -c \'"+current_command
+"\' "+str(os
.getpid())+" "+execute_args
;
1174 if current_executable
:
1175 current_command_line
= re
.sub(r
'\"', r
'\\"', current_command_line
);
1176 current_command_line
= current_executable
+' '+current_host
+' "'+current_command_line
+'"';
1179 print("#", current_command_line
, file=current_outfile
);
1180 # Spawn command and open a pipe to the output
1181 pipe
= subprocess
.Popen(current_command_line
, shell
=True, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
);
1182 for b
in pipe
.stdout
:
1183 if type(b
).__name
__ == 'str':
1184 b
= bytes(b
, encoding
='utf8');
1186 if options
.printhexdump
: # For debugging commands
1187 print(str(binascii
.hexlify(b
), 'ascii'), file=sys
.stderr
);
1188 # See whether there was an error
1191 error_message
= pipe
.stderr
.read();
1192 print('$('+current_command
+')', "\n", str(error_message
, encoding
='UTF8'), file=sys
.stderr
);
1193 exit(pipe
.returncode
);
1194 # ${ENV} environment variables
1195 match
= re
.search('^\$[\{]?([^\}\(\)]+)[\}]?$', filename
);
1197 current_var
= not_allowed_chars
.sub(" ", match
.group(1));
1199 print("# echo $"+ current_var
, file=current_outfile
);
1200 b
= os
.environ
[current_var
];
1201 filehash
.update(bytes(b
, encoding
='utf8'));
1202 # lstat() meta information
1203 elif filename
.startswith('?'):
1204 if not os
.path
.exists(filename
.lstrip('?')):
1205 print(filename
, "does not exist", file=sys
.stderr
)
1207 filestat
= os
.stat(filename
.lstrip('?'));
1208 if my_statusvalues
== "": my_statusvalues
= 'fmidlugs'
1210 if 'f' in my_statusvalues
:
1211 b
+= 'stat('+filename
.lstrip('?')+') = '
1213 if 'm' in my_statusvalues
:
1214 b
+= 'st_mode='+str(oct(filestat
.st_mode
))+', ';
1215 if 'i' in my_statusvalues
:
1216 b
+= 'st_ino='+str(filestat
.st_ino
)+', ';
1217 if 'd' in my_statusvalues
:
1218 b
+= 'st_dev='+str(filestat
.st_dev
)+', '
1219 if 'l' in my_statusvalues
:
1220 b
+= 'st_nlink='+str(filestat
.st_nlink
)+', '
1221 if 'u' in my_statusvalues
:
1222 b
+= 'st_uid='+str(filestat
.st_uid
)+', '
1223 if 'g' in my_statusvalues
:
1224 b
+= 'st_gid='+str(filestat
.st_gid
)+', '
1225 if 's' in my_statusvalues
:
1226 b
+= 'st_size='+str(filestat
.st_size
)+', '
1227 if 'a' in my_statusvalues
:
1228 b
+= 'st_atime='+str(filestat
.st_atime
)+', '
1229 if 't' in my_statusvalues
:
1230 b
+= 'st_mtime='+str(filestat
.st_mtime
)+', '
1231 if 'c' in my_statusvalues
:
1232 b
+= 'st_ctime='+str(filestat
.st_ctime
);
1234 b
= b
.rstrip(', ') + ']';
1235 filehash
.update(bytes(b
, encoding
='utf8'));
1237 print ("# "+ b
, file=current_outfile
);
1240 # open and read the file
1241 if filename
!= '-' and filename
.find('://') == -1 and not os
.path
.exists(filename
):
1242 print(filename
, "does not exist", file=sys
.stderr
)
1246 for b
in sys
.stdin
.buffer:
1247 if type(b
).__name
__ == 'str':
1248 b
= bytes(b
, encoding
='utf8');
1250 if options
.printhexdump
: # For debugging commands
1251 print(str(binascii
.hexlify(b
), 'ascii'), file=sys
.stderr
);
1254 current_filename
= filename
;
1255 if my_filesource
and current_filename
.find('://') == -1 :
1256 current_filename
= my_filesource
+current_filename
;
1257 with
open_infile(current_filename
, 'rb') as file:
1259 if type(b
).__name
__ == 'str':
1260 b
= bytes(b
, encoding
='utf8');
1262 if options
.printhexdump
: # For debugging commands
1263 print(str(binascii
.hexlify(b
), 'ascii'), file=sys
.stderr
);
1265 current_digest
= filehash
.hexdigest();
1266 print_name
= filename
;
1267 if my_quiet
or org_filename
.startswith('['):
1269 print_name
= '['+str(file_argnum
)+']';
1270 current_hash_line
= current_digest
+" *"+print_name
1271 totalhash
.update(bytes(current_hash_line
, encoding
='ascii'));
1273 # Be careful to use this ONLY after totalhash has been updated!
1275 current_hash_line
= (len(current_digest
)*'-')+" *"+print_name
;
1279 if not (my_quiet
and total_only
) and not (my_allsalts
and snum
> 1):
1280 print(current_hash_line
, file=current_outfile
);
1281 elif not (my_quiet
or my_allsalts
):
1282 if check_hashes
[print_name
] == (len(current_digest
)*'-'):
1283 # Suppress redundant output of empty, ----, lines
1284 if snum
<= 1 and pnum
<= 1:
1285 print(check_hashes
[print_name
]+" *"+print_name
, file=current_outfile
);
1286 elif current_digest
!= check_hashes
[print_name
]:
1287 print("FAILED: "+current_hash_line
, file=current_outfile
);
1289 print("ok"+" *"+print_name
, file=current_outfile
);
1292 current_total_digest
= totalhash
.hexdigest();
1293 # Write (in)correct salts with the TOTAL HASH
1295 output_salt
= my_salt
;
1296 j
= random
.random();
1297 # Randomly create an incorrect salt for failed output
1299 if j
< fail_fraction
and snum
!= selected_salt
:
1300 salt
= dev_random
.read(8);
1301 output_salt
= str(binascii
.hexlify(salt
), 'ascii');
1303 salt_pattern_number
+= current_salt_power
;
1304 current_total_digest_line
= "Salt+TOTAL HASH: '"+output_salt
+"' '"+current_total_digest
+"'";
1305 else: # Standard TOTAL HASH line
1306 current_total_digest_line
= current_total_digest
+" *"+"TOTAL HASH";
1307 end_time
= time
.time();
1308 print("# \n# Total hash - Time to completion:", end_time
- start_time
, "seconds", file=current_outfile
);
1310 if my_allsalts
: total_hash_num
= snum
-1; # Current TOTAL HASH number of more are used
1312 print(current_total_digest_line
+"\n", file=current_outfile
);
1313 elif current_total_digest
!= total_hash_list
[total_hash_num
]:
1314 if not my_allsalts
: print("FAILED: "+current_total_digest_line
+"\n", file=current_outfile
);
1316 if my_allsalts
: salt_pattern_number
+= current_salt_power
; # Update salt bit pattern
1318 if len(passphrase_list
) > 1 or len(salt_list
): match_number
= " #"
1319 if len(passphrase_list
) > 1: match_number
+= " passphrase no: "+str(pnum
);
1320 if len(salt_list
) > 1: match_number
+= " salt no: "+str(snum
);
1321 if not my_allsalts
: print("OK"+" *"+"TOTAL HASH"+match_number
+"\n", file=current_outfile
);
1325 if my_allsalts
: current_salt_power
*= 2; # Update current bit position in salt pattern
1326 if my_check
and corrpnum
== pnum
: matched_salt_pattern
= salt_pattern_number
;
1329 if my_check
and len(passphrase_list
) > 1:
1331 print("Passphrase entry:",corrpnum
,"matched", file=current_outfile
);
1333 print("No passphrase entry matched!", file=current_outfile
);
1334 if my_check
and (not my_allsalts
) and len(salt_list
) > 1:
1337 print("Salt entry:",corrsnum
,"matched", file=current_outfile
);
1339 print("No salt entry matched!", file=current_outfile
);
1341 print("No entry matched", file=current_outfile
);
1342 # Print salt bit patterns
1343 elif my_check
and my_allsalts
:
1344 print("Salt pattern number:", matched_salt_pattern
, file=current_outfile
);
1345 elif not my_check
and my_allsalts
:
1346 print("# Salt pattern number:", salt_pattern_number
, file=current_private
);
1348 # Close output files if necessary
1349 if my_output
and my_output
!= '-':
1350 current_outfile
.close();
1351 if my_private
and my_private
!= '-':
1352 current_private
.close();