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