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