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