Small changes to makefile output
[signduterre.git] / signduterre.py
blob408aa30e2b04bf3dbbc6044ff22227379c5c8b14
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 or --Private-file to some safe site.
329 [[[/p]]][[[p]]]
330 Good passphrases are difficult to remember, so their plaintext form should be protected. To protect the
331 passphrase against rainbow and brute force attacks, the passphrase is concatenated to a salt phrase and
332 hashed before use (SHA-256).
333 [[[/p]]][[[p]]]
334 The salt phrase is requested when constructing a signature. In interactive use, an 8 byte hexadecimal
335 (= 16 character) salt from [[[i]]]/dev/urandom[[[/i]]] is suggested. If '--salt SUGGESTED' is entered on the command line
336 as the salt, the suggested value will be used. The salt is printed in plaintext to the output. The salt will
337 make it more difficult to determine whether the same passphrase has been used to create different signatures.
338 [[[/p]]][[[p]]]
339 At the bottom, a 'TOTAL HASH' line will be printed that hashes all the lines printed for the files. This
340 includes the file names as printed on the hash lines. It is not inconceivable that existing signature files
341 could have been compromised in ways that might be missed when checking the signature. The total hash will
342 point out such changes.
343 [[[/p]]][[[p]]]
344 Examples:[[[/p]]][[[pre make=example2]]]
345 # make: example2
346 # Self test of root directory, python, and signduterre.py using the 'which' command to establish the paths
347 $ python3.0 signduterre.py --detailed-view --salt 436a73e3 --passphrase liauwefa3251EWC -o test-self.sdt \\
348 / `which python3.0 signduterre.py`
349 $ python3.0 signduterre.py --passphrase liauwefa3251EWC -c test-self.sdt
350 [[[/pre]]][[[LONG]]][[[p]]]
351 Write a signature to the file test-self.sdt and test it with the --check-file option. The signature contains
352 the SHA-256 hashes of the files, [[[i]]]/usr/bin/python3.0[[[/i]]], [[[i]]]signduterre.py[[[/i]]], and the status information on the root
353 directory. The salt '436a73e3' and passphrase 'liauwefa3251EWC' are used.
354 [[[/p]]][[[/LONG]]][[[pre make=procfs1]]]
355 # make: procfs1
356 # Self test of root directory, python, and signduterre.py using the the /proc file system
357 $ python3.0 signduterre.py --detailed-view --salt SUGGESTED --passphrase liauwefa3251EWC -o test-self_proc.sdt \\
358 /proc/self/root /proc/self/exe `which signduterre.py`
359 $ python3.0 signduterre.py --passphrase liauwefa3251EWC --check-file test-self_proc.sdt
360 [[[/pre]]][[[LONG]]][[[p]]]
361 Write a signature to the file test-self_proc.sdt and test it with the --check-file option. The signature
362 contains the SHA-256 hashes of the same files as above, [[[i]]]/usr/bin/python3.0[[[/i]]], [[[i]]]signduterre.py[[[/i]]], and the status
363 information on the root directory. However, the python executable and the root directory are now accessed
364 through the [[[i]]]/proc[[[/i]]] file system. The suggested salt is used (written to test-self_proc.sdt) and the passphrase
365 is (again) 'liauwefa3251EWC'.
366 [[[/p]]][[[/LONG]]][[[pre make=example3]]]
367 # make: example3
368 # Test of supporting commands for chkrootkit
369 $ python3.0 signduterre.py --execute --total-only --salt SUGGESTED=8 --passphrase SUGGESTED --Status \\
370 --output-file=test-chkrootkit.sdt --Private-file=test-chkrootkit.pwd \\
371 signduterre.py `which bash awk cut egrep find head id ls netstat ps strings sed uname`
372 $ python3.0 signduterre.py --execute --passphrase test-chkrootkit.pwd --check-file test-chkrootkit.sdt
373 [[[/pre]]][[[LONG]]][[[p]]]
374 Writes a signature of the requested files to test-chkrootkit.sdt (signature) and private information to
375 test-chkrootkit.pwd (password and selected salt) and checks it in the next line. The files are those of
376 commands required by the [[[i]]]chkrootkit[[[/i]]] program (http://www.chkrootkit.org/), with bash added. The 'which'
377 command will give the paths for the commands. Eight salts are generated, of which only 1 is actually
378 used. When checking, the correct salt should match. This prevents a compromised program from simply
379 printing out OK tot he check. A more comprehensive evation of guessing the correct salt can be obtained
380 by using the '--all-salts-pattern' option.
381 [[[/p]]][[[/LONG]]][[[pre make=procfs2]]]
382 # make: procfs2
383 # Simply lump all "system" files, the PATH environment variable and the first 2 columns of the output of lsmod
384 $ python3.0 signduterre.py --execute --detail --salt SUGGESTED --passphrase liauwefa3251EWC --Status --total-only \\
385 signduterre.py /sbin/* /bin/* /usr/bin/find /usr/bin/stat /usr/bin/python* '${PATH}' \\
386 '$(lsmod | awk "{print \$1, \$2}")' > test-20090625_14-31-54.sdt
388 # Failing check due to missing --execute option
389 $ python3.0 signduterre.py --passphrase liauwefa3251EWC -c test-20090625_14-31-54.sdt
390 $ python3.0 signduterre.py --passphrase liauwefa3251EWC -c test-20090625_14-31-54.sdt --no-execute
392 # Successful check
393 $ python3.0 signduterre.py --execute --passphrase liauwefa3251EWC --check-file test-20090625_14-31-54.sdt
394 [[[/pre]]][[[LONG]]][[[p]]]
395 Prints a signature to the system test-20090625_14-31-54.sdt and the automatically generated password to
396 test-20090625_14-31-54.pwd. The salt will be automatically determined. The signature contains the SHA-256
397 hashes of the file status and file contents of [[[i]]]signduterre.py, /sbin/*, /bin/*, /usr/bin/find,
398 /usr/bin/file, /usr/bin/python*[[[/i]]] on separate lines, and a hash of the PATH environment variable. Do not
399 display the hash of every single file, which could be insecure, but only the total hash.
400 The first two checks will both fail if test-20090625_14-31-54.sdt contains a $(cmd) entry.
401 The --no-execute option is default and prevents the execute option (if reading the execute option from the
402 signature file has been activated). The last check will succeed (if the files have not been changed).
403 [[[/p]]][[[/LONG]]][[[pre make=example4]]]
404 # make: example4
405 # Use a list of generated passphrases
406 $ python3.0 signduterre.py --salt SUGGESTED --passphrase SUGGESTED=20 signduterre.py \\
407 2> test-20090630_16-44-34.pwd > test-20090630_16-44-34.sdt
408 $ python3.0 signduterre.py -p test-20090630_16-44-34.pwd -c test-20090630_16-44-34.sdt
409 [[[/pre]]][[[LONG]]][[[p]]]
410 Will generate and print 20 passphrases and print a signature using one randomly chosen passphrase from these
411 20. Everything is written to the files 'test-20090630_16-44-34.pwd' and 'test-20090630_16-44-34.sdt'.
412 Such file names can easily be generated with 'test-`date "+%Y%m%d_%H-%M-%S"`.sdt'.
413 The next command will check all 20 passphrases generated before from the Signature file and print the results.
414 [[[/p]]][[[/LONG]]][[[pre make=example5]]]
415 # make: example5
416 # Use a list of generated salts with a pattern of correct salts
417 $ python3.0 signduterre.py --salt SUGGESTED=16 --passphrase SUGGESTED --all-salts-pattern \\
418 -P test-salt-pattern.pwd -o test-salt-pattern.sdt `which bash stat find ls ps id uname awk gawk perl`
419 $ python3.0 signduterre.py -p test-salt-pattern.pwd -c test-salt-pattern.sdt
420 # Compare to salt pattern number to the one from the check-file
421 $ cat test-salt-pattern.pwd
422 [[[/pre]]][[[LONG]]][[[p]]]
423 As the previous, but with a pattern of random correct and incorrect salts. The salt pattern number
424 indicates which salts were and were not correct.
425 [[[/p]]][[[/LONG]]][[[pre make=sudo1]]]
426 # make: sudo1
427 # Check MBR and current root directory (sudo and root user)
428 $ sudo python3.0 signduterre.py -u root -s SUGGESTED -p SUGGESTED --Status-values='i' -v -e -t \\
429 --output-file test-boot-sector.sdt --Private-file test-boot-sector.pwd --execute-args=sda \\
430 '?/proc/self/root' `which dd` '$(dd if=/dev/$1 bs=512 count=1 status=noxfer | od -X)'
431 $ sudo python3.0 signduterre.py -u root -e -p test-boot-sector.pwd -c test-boot-sector.sdt
432 [[[/pre]]][[[LONG]]][[[p]]]
433 Will hash the inode numbers of the effective root directory (eg, chroot) and the executable (python)
434 together with the contents of the MBR (Master Boot Record) on [[[i]]]/dev/sda[[[/i]]] in Hex. It uses suggested salt and
435 passphrase. Accessing [[[i]]]/dev/sda[[[/i]]] is only possible when [[[i]]]root[[[/i]]], so the command is entered with [[[i]]]sudo[[[/i]]] and
436 '--user root'. Use the '--print-execute' option if you want to check the output of the [[[i]]]dd[[[/i]]] command.
437 [[[/p]]][[[p]]]
438 The main problem with intrusion detection by comparing file contents is the ability of an attacker
439 to redirect attempts to read a compromised file to a stored copy of the original. So, [[[i]]]sha256sum[[[/i]]] or
440 python could be changed to read [[[i]]]'/home/attacker/old/ps'[[[/i]]] when the argument was [[[i]]]'/bin/ps'[[[/i]]]. This would
441 foil any scheme that depends on entering file names in programs. An answer to this threat is to
442 read the bytes in files in as many ways as possible. Therefor, forcing an attacker to change many
443 files which itself would increase the probability of detection of the attack. The following command
444 will read the same (test) file, and generate identical hashes, in many different ways.
445 [[[/p]]][[[/LONG]]][[[pre make=example6]]]
446 # make: example6
447 # Example generating identical signatures of the same text file in different ways
448 $ dd if=signduterre.py 2>/dev/null | \\
449 python3.0 signduterre.py -v -d -s 1234567890abcdef -p poiuytrewq \\
450 --execute --execute-args='signduterre.py' \\
451 signduterre.py - \\
452 '$(cat $1)' \\
453 '$(grep "" $1)' \\
454 '$(awk "{print}" $1)' \\
455 '$(cut -f 1-100 $1)' \\
456 '$(perl -ane "{print \$_}" $1)' \\
457 '$(python3.0 -c "import sys;f=open(sys.argv[1]);sys.stdout.buffer.write(f.buffer.read())" $1;)' \\
458 '$(ruby -e "f=open(ARGV[0]);print f.read();" $1;)'
459 [[[/pre]]][[[LONG]]][[[p]]]
460 These "commands" do not always return the same bytes (awk), or any bytes at all (grep), from a text
461 file as when used with a binary file. However, if the commands can print the bytes unaltered, the
462 signatures will be identical. That is, the following arguments will work on a binary file:
463 [[[/p]]][[[/LONG]]][[[pre make=example6]]]
464 # make: example6
465 # Example generating identical signatures of the same file in different ways, now for binary files
466 $ dd if=/bin/bash 2>/dev/null | \\
467 python3.0 signduterre.py -v -d -s 1234567890abcdef -p poiuytrewq \\
468 --execute --execute-args='/bin/bash' \\
469 /bin/bash - \\
470 '$(cat $1)' \\
471 '$(perl -ane "{print \$_}" $1)' \\
472 '$(python3.0 -c "import sys;f=open(sys.argv[1]);sys.stdout.buffer.write(f.buffer.read())" $1;)' \\
473 '$(ruby -e "f=open(ARGV[0]);print f.read();" $1;)'
474 [[[/pre]]][[[LONG]]][[[p]]]
475 Will generate the same identical signatures for [[[i]]]/bin/bash[[[/i]]], [[[i]]]STDIN[[[/i]]], [[[i]]]'$(cat /bin/bash)'[[[/i]]] etc.
476 There are obviously many more ways to read out the bytes from the disk or memory. The main point
477 being that it should be difficult to predict for an attacker which commands must be compromised
478 to hide changes in the system.
479 [[[/p]]][[[ /long]]][[[p]]]
480 The examples can be run as a makefile using make. Use one of the following commands:
481 [[[/p]]][[[pre]]]
482 # General examples, use them all
483 python3.0 signduterre.py --manual-make |make -f - example
484 # Linux specific examples using the second procfs example
485 python3.0 signduterre.py --manual-make |make -f - procfs2
486 # Examples requiring sudo, using first
487 python3.0 signduterre.py --manual-make | sudo make -f - sudo1
488 [[[/pre]]][[[/body]]][[[/html]]]
491 license = """
492 Signature-du-Terroir
493 Construct a signature of the installed software state or check a previously made signature.
495 copyright 2009, R.J.J.H. van Son
497 This program is free software: you can redistribute it and/or modify
498 it under the terms of the GNU General Public License as published by
499 the Free Software Foundation, either version 3 of the License, or
500 (at your option) any later version.
502 This program is distributed in the hope that it will be useful,
503 but WITHOUT ANY WARRANTY; without even the implied warranty of
504 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
505 GNU General Public License for more details.
507 You should have received a copy of the GNU General Public License
508 along with this program. If not, see <http://www.gnu.org/licenses/>.
509 """;
511 # Note that only release notes are put here
512 # See git repository for detailed change comments:
513 # git clone git://repo.or.cz/signduterre.git
514 releasenotes = """
515 20090807 - DIFFERENT became FAIL in --check-file
516 20090730 - Release v0.4
517 20090724 - Added '--all-salts-pattern' and HTML formatting in manual
518 20090723 - Added URL support for all files. Does not yet work due to bug in Python 3.0
519 20090723 - Added '-' for STDIN
520 20090717 - Added --execute-args
521 20090716 - Release v0.3
522 20090713 - Added --quiet option
523 20090712 - moved from /dev/random to /dev/urandom
524 20090702 - Replaced -g with -p SUGGESTED[=N]
525 20090702 - Generating and testing lists of random salts
526 20090701 - Release v0.2
527 20090630 - Generating and testing random passphrases
528 20090630 - --execute works on $(cmd) only, nlinks in ?path and ? implied for directories
529 20090630 - Ported to Python 3.0
531 20090628 - Release v0.1b
532 20090628 - Added release-notes
534 20090626 - Release v0.1a
535 20090626 - Initial commit to Git
536 """;
538 #############################################################################
540 # IMPORT & INITIALIZATION #
542 #############################################################################
544 import sys;
545 import os;
546 import subprocess;
547 import stat;
548 import subprocess;
549 # if sys.stdout.isatty(): import readline;
550 import binascii;
551 import hashlib;
552 import re;
553 import time;
554 from optparse import OptionParser;
555 import base64;
556 import random;
557 import struct;
558 import urllib.request;
559 import urllib.error;
561 # Limit the characters that can be used in $(cmd) commands
562 # Only allow the escape of '$'
563 not_allowed_chars = re.compile('([^\w\ \.\/\"\|\;\,\-\$\[\]\{\}\(\)\@\`\!\*\=\\\\]|([\\\\]+([^\$\"\\\\]|$)))');
565 programname = "Signature-du-Terroir";
566 version = "0.4";
568 def open_infile(filename, mode):
569 if filename == '-':
570 return sys.stdin;
571 elif filename.find('://') > -1:
572 print("URL:", filename, file=current_private);
573 return urllib.request.urlopen(filename);
574 else :
575 if not os.path.isfile(filename):
576 print(filename, "does not exist", file=sys.stderr)
577 quit();
578 return open(filename, mode);
580 def open_outfile(filename, mode):
581 if filename == '-':
582 return sys.stdout;
583 elif filename.find('://') > -1:
584 print("URL:", filename, file=current_private);
585 return urllib.request.urlopen(filename);
586 else :
587 return open(filename, mode);
589 current_outfile = sys.stdout;
590 current_private = sys.stderr;
592 #############################################################################
594 # OPTION HANDLING #
596 #############################################################################
598 parser = OptionParser()
599 parser.add_option("-s", "--salt", metavar="HEX",
600 dest="salt", default=False,
601 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).")
602 parser.add_option("-a", "--all-salts-pattern",
603 dest="allsalts", default=False, action="store_true",
604 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.")
605 parser.add_option("-p", "--passphrase", metavar="TEXT",
606 dest="passphrase", default=False,
607 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).")
608 parser.add_option("-c", "--check-file",
609 dest="check", default=False, metavar="FILE",
610 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.")
611 parser.add_option("-i", "--input-file",
612 dest="input", default=False, metavar="FILE",
613 help="Use names from FILE or STDIN ('-'), use one filename per line")
614 parser.add_option("-o", "--output-file",
615 dest="output", default=False, metavar="FILE",
616 help="Print to FILE instead of STDOUT")
617 parser.add_option("-P", "--Private-file",
618 dest="private", default=False, metavar="FILE",
619 help="Print private information (passwords etc.) to FILE instead of STDERR")
620 parser.add_option("-u", "--user",
621 dest="user", default="nobody", metavar="USER",
622 help="Execute $(cmd) as USER, default 'nobody' (root/sudo only)")
623 parser.add_option("-S", "--Status",
624 dest="status", default=False, action="store_true",
625 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)")
626 parser.add_option("--Status-values",
627 dest="statusvalues", default="fmidugs", metavar="MODE",
628 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).")
629 parser.add_option("-t", "--total-only",
630 dest="total", default=False, action="store_true",
631 help="Only print the total hash, unsets --detailed-view (default True)")
632 parser.add_option("-d", "--detailed-view",
633 dest="detail", default=False, action="store_true",
634 help="Print hashes of individual files, is unset by --total-only (default False)")
635 parser.add_option("-e", "--execute",
636 dest="execute", default=False, action="store_true",
637 help="Interpret $(cmd) (default False)")
638 parser.add_option("--execute-args",
639 dest="executeargs", default='', metavar="ARGS",
640 help="Arguments for the $(cmd) commands ($1 ....)")
641 parser.add_option("-n", "--no-execute",
642 dest="noexecute", default=False, action="store_true",
643 help="Explicitely do NOT Interpret $(cmd)")
644 parser.add_option("--print-hexdump",
645 dest="printhexdump", default=False, action="store_true",
646 help="Print hexadecimal dump of input bytes to STDERR for debugging purposes")
647 parser.add_option("-m", "--manual",
648 dest="manual", default=False, action="store_true",
649 help="Print a short version of the manual and exit")
650 parser.add_option("--manual-long",
651 dest="manuallong", default=False, action="store_true",
652 help="Print the long version of the manual and exit")
653 parser.add_option("--manual-html",
654 dest="manualhtml", default=False, action="store_true",
655 help="Print the manual in HTML format and exit")
656 parser.add_option("--manual-make",
657 dest="manualmake", default=False, action="store_true",
658 help="Print the examples in the manual as a makefile and exit")
659 parser.add_option("-r", "--release-notes",
660 dest="releasenotes", default=False, action="store_true",
661 help="Print the release notes and exit")
662 parser.add_option("-l", "--license",
663 dest="license", default=False, action="store_true",
664 help="Print license text and exit")
665 parser.add_option("-v", "--verbose",
666 dest="verbose", default=False, action="store_true",
667 help="Print more information on output")
668 parser.add_option("-q", "--quiet",
669 dest="quiet", default=False, action="store_true",
670 help="Print minimal information (hide filenames). If the output is used with --check-file, the command line options and arguments must be repeated.")
672 (options, check_filenames) = parser.parse_args();
675 # Start with opening any non-default output files
676 my_output = False;
677 if options.output:
678 current_outfile = open_outfile(options.output, 'w');
679 my_output = options.output;
681 my_private = False;
682 if options.private:
683 current_private = open_outfile(options.private, 'w');
684 my_private = options.private;
686 print("# Program: "+programname + " version " + version, file=current_outfile);
687 print("#", time.strftime("%Y/%m/%d %H:%M:%S", time.localtime()), "("+time.tzname[0]+")\n", file=current_outfile);
689 # Print license
690 if options.license:
691 print (license, file=sys.stderr);
692 exit(0);
693 # Print manual
694 if options.manual or options.manuallong:
695 cleartext_manual = re.sub(r"(?i)\[\[\[\s*(/?)\s*LONG\s*\]\]\]", r'[[[\1LONG]]]', manual);
696 if not options.manuallong:
697 currentstart = cleartext_manual.find('[[[LONG]]]');
698 while currentstart > -1:
699 currentend = cleartext_manual.find('[[[/LONG]]]', currentstart)+len('[[[/LONG]]]');
700 (firstpart, secondpart) = cleartext_manual.split(cleartext_manual[currentstart:currentend]);
701 cleartext_manual = firstpart+secondpart;
702 currentstart = cleartext_manual.find('[[[LONG]]]');
703 htmltags = re.compile('\[\[\[[^\]]*\]\]\]');
704 cleartext_manual = htmltags.sub('', cleartext_manual);
705 print (cleartext_manual, file=sys.stdout);
706 exit(0);
707 # Print HTML manual
708 if options.manualhtml:
709 protleftanglesbracks = re.compile('\<');
710 protrightanglesbracks = re.compile('\>');
711 leftanglesbracks = re.compile('\[\[\[');
712 rightanglesbracks = re.compile('\]\]\]');
713 html_manual = re.sub(r"(?i)\[\[\[\s*(/?)\s*LONG\s*\]\]\]", '', manual);
714 html_manual = protleftanglesbracks.sub('&lt;', html_manual);
715 html_manual = protrightanglesbracks.sub('&gt;', html_manual);
716 html_manual = leftanglesbracks.sub('<', html_manual);
717 html_manual = rightanglesbracks.sub('>', html_manual);
718 print (html_manual, file=sys.stdout);
719 exit(0);
720 # Print manual examples as makefile
721 if options.manualmake:
722 make_manual = re.sub("\$ ", "\t", manual);
723 make_manual = re.sub("\#", "\t#", make_manual);
724 make_manual = re.sub(r"\\\s*\n", '', make_manual);
725 make_manual = re.sub(r"\$", r'$$', make_manual);
726 # Protect "single" [ brackets
727 make_manual = re.sub(r"(^|[^\[])\[([^\[]|$)", r"\1&#91;\2", make_manual);
728 extrexamples = re.compile(r"\[\[\[pre\s+make\=?([a-zA-Z]*)([0-9]*)\s*\]\]\]\n([^\[]*)\n\[\[\[/pre\s*\]\]\]", re.IGNORECASE|re.MULTILINE|re.DOTALL);
729 exampleiter = extrexamples.finditer(make_manual);
730 makefile_list = [];
731 group_list={};
732 group_list['all'] = "all: ";
733 for match in exampleiter:
734 # We had to convert any '[' in the command. Now convert them back.
735 command_text = re.sub(r"\&\#91\;", '[', match.group(3));
736 makefile_list.append(match.group(1)+match.group(2)+":\n"+command_text);
737 if len(match.group(2)) > 0:
738 if not match.group(1) in group_list.keys():
739 group_list[match.group(1)] = match.group(1)+": ";
740 group_list['all'] += match.group(1)+" ";
741 group_list[match.group(1)] += match.group(1)+match.group(2)+" ";
742 else:
743 group_list['all'] += match.group(1)+" ";
744 print("help: \n\t@echo 'Use \"make -f - "+re.sub(':', '', group_list['all'])+"\"'", file=sys.stdout);
745 for group in group_list:
746 print(group_list[group]+"\n", file=sys.stdout);
747 makefile_list.sort()
748 previous_cat = 'NOT A VALUE';
749 for line in makefile_list:
750 (category, commands) = line.split(':\n');
751 if category != previous_cat:
752 previous_cat = category;
753 print("\n"+previous_cat+":", file=sys.stdout);
754 print(commands, file=sys.stdout);
755 # Clean option
756 print("\nclean:\n\trm test-*.sdt test-*.pwd", file=sys.stdout);
757 exit(0);
758 # Print release notes
759 if options.releasenotes:
760 print ("Version: "+version, file=sys.stderr);
761 print (releasenotes, file=sys.stderr);
762 exit(0);
764 my_salt = options.salt;
765 my_allsalts = options.allsalts;
766 my_passphrase = options.passphrase;
767 my_check = options.check;
768 my_status = options.status;
769 my_statusvalues = options.statusvalues;
770 my_verbose = options.verbose and not options.quiet;
771 my_quiet = options.quiet;
772 execute = options.execute;
773 execute_args = options.executeargs;
774 if options.noexecute: execute = False;
775 input_file = options.input;
777 # Set total-only with the correct default
778 total_only = True;
779 total_only = not options.detail;
780 if options.total: total_only = options.total;
781 if my_allsalts: total_only = my_allsalts; # All alts pattern only sensible with total-only
782 if my_check: total_only = False;
784 my_user = options.user;
785 # Things might be executed as another user
786 user_change = '';
787 if os.getuid() == 0:
788 user_change = 'sudo -H -u '+my_user+' ';
789 if not my_quiet: print("User: "+my_user, file=current_outfile);
791 # Execute option
792 if execute:
793 text_execute = "True";
794 else:
795 text_execute = "False";
797 if execute and not my_quiet:
798 print("Execute system commands: "+text_execute+"\n", file=current_outfile);
799 if execute_args != '': print("Execute arguments: '"+execute_args+"'\n", file=current_outfile);
801 # --quiet option
802 if my_quiet: print("Quiet: True\n", file=current_outfile);
804 # --quiet option
805 if my_statusvalues != 'fmidugs': print("Status-values: '"+my_statusvalues+"'\n", file=current_outfile);
807 #############################################################################
809 # ARGUMENT PROCESSING #
811 #############################################################################
813 # Measure time intervals
814 start_time = time.time();
816 dev_random = open("/dev/urandom", 'rb');
818 # Read the check file
819 passphrase_list = [];
820 salt_list = [];
821 check_hashes = {};
822 total_hash_list = [];
823 if my_check:
824 highest_arg_used = 0;
825 print("# Checking: "+my_check+"\n", file=current_outfile);
826 arg_list = check_filenames;
827 check_filenames = [];
828 with open_infile(my_check, 'r') as c:
829 for line in c:
830 match = re.search("Execute system commands:\s+(True|False)", line);
831 if match != None:
832 # Uncomment the next line if you want automatic --execute from the check-file (DANGEROUS)
833 # execute = match.group(1).upper() == 'TRUE';
834 continue;
836 match = re.search("Execute arguments:\s+\'([\w\$\s\-\+\/]*)\'", line);
837 if match != None:
838 execute_args = match.group(1);
839 continue;
841 match = re.search("Quiet:\s+(True|False)", line);
842 if match != None:
843 my_quiet = match.group(1).upper() == 'TRUE';
844 if my_quiet: my_verbose = False;
845 continue;
847 match = re.search("Salt\s*\+\s*TOTAL HASH\s*\:\s+\'([\w]*)\'\s+\'([\w]*)\'", line);
848 if match != None:
849 salt_list.append(match.group(1));
850 total_hash_list.append(match.group(2));
851 my_allsalts = True; # Salt+TOTAL HASH imples all-salts-pattern
852 continue;
854 match = re.search("Salt\:\s+\'([\w]*)\'", line);
855 if match != None:
856 salt_list.append(match.group(1));
857 continue;
859 match = re.search("Salt\s*\+\s*TOTAL HASH\s*\:\s+\'([\w]+)\'\s+\'([a-f0-9]+)\'", line);
860 if match != None:
861 salt_list.append(match.group(1));
862 total_hash_list.append(match.group(2));
863 continue;
865 match = re.search("User\:\s+\'([\w]*)\'", line);
866 if match != None:
867 # Uncomment the next line if you want automatic --user from the check-file (DANGEROUS)
868 # my_user = match.group(1);
869 continue;
871 match = re.search("Passphrase\:\s+\'([^\']*)\'", line);
872 if match != None:
873 passphrase_list.append(match.group(1));
874 continue;
876 match = re.search("Status-values\:\s+\'([\w]*)\'", line);
877 if match != None:
878 my_statusvalues = match.group(1);
879 continue;
881 match = re.search("^\s*([a-f0-9]+)\s+\*(TOTAL HASH)\s*$", line)
882 if match != None:
883 total_hash_list.append(match.group(1));
884 continue;
886 match = re.search("^\s*([a-f0-9\-]+)\s+\*\[([0-9]+)\]\s*$", line)
887 if match != None:
888 filenumber = int(match.group(2));
889 if filenumber > highest_arg_used: highest_arg_used = filenumber;
890 # Watch out, arguments count from 0
891 check_filenames.append(arg_list[filenumber - 1]);
892 check_hashes['['+match.group(2)+']'] = match.group(1);
893 continue;
895 match = re.search("^\s*([a-f0-9\-]+)\s+\*(.*)\s*$", line)
896 if match != None:
897 check_filenames.append(match.group(2));
898 # Catch --execute error as early as possible
899 if match.group(2).startswith('$(') and not execute:
900 error_message = "Executable argument \'"+match.group(2)+"\' only allowed with the --execute flag";
901 print (error_message, file=sys.stderr);
902 if not sys.stdout.isatty(): print(error_message, file=current_outfile);
903 exit(0);
904 check_hashes[match.group(2)] = match.group(1);
905 continue;
906 for i in range(highest_arg_used, len(arg_list)):
907 check_filenames.append(arg_list[i]);
908 check_hashes['['+str(i+1)+']'] = (64*'-');
910 # Read input-file
911 if input_file:
912 with open_infile(input_file, 'r') as i:
913 for line in i:
914 # Clean up filename
915 current_filename = re.sub('[^\w\-\.\/\$\{\(\)\}\?\[\]]', '', line);
916 check_filenames.append(current_filename);
917 if my_check: check_hashes['['+str(i+1)+']'] = (64*'-');
919 stat_list = [];
920 for x in check_filenames:
921 if os.path.isdir(x):
922 x = '?'+x;
923 if my_status and not x.startswith(('?', '$')):
924 stat_list.append('?'+x);
925 stat_list.append(x);
926 check_filenames = stat_list;
928 # Seed Pseudo Random Number Generator
929 seed = dev_random.read(16);
930 random.seed(seed);
932 # Read suggested salts from /dev/(u)random if needed
933 if my_salt:
934 if my_salt.startswith('SUGGESTED'):
935 N=1;
936 match = re.search("([0-9][0-9]*)$", my_salt);
937 if match != None:
938 N = int(match.group(1));
939 for i in range(0,N):
940 salt = dev_random.read(8);
941 salt_list.append(str(binascii.hexlify(salt), 'ascii'));
942 else:
943 salt_list.append(my_salt);
944 elif len(salt_list) == 0:
945 salt = dev_random.read(8);
946 sys.stderr.write("Enter salt (suggest \'"+str(binascii.hexlify(salt), 'ascii')+"\'): ");
947 new_salt = input();
948 if not new_salt: new_salt = str(binascii.hexlify(salt), 'ascii');
949 salt_list.append(new_salt);
951 # If not combining salts with TOTAL HASH, print salts now
952 if not my_allsalts:
953 for my_salt in salt_list:
954 print("Salt: \'"+my_salt+"\'", file=current_outfile);
956 # Get passphrase
957 if my_passphrase and(my_passphrase == '-' or os.path.isfile(my_passphrase)):
958 with open_infile(my_passphrase, 'r') as file:
959 for line in file:
960 match = re.search("Passphrase\:\s+\'([^\']*)\'", line);
961 if match != None:
962 passphrase_list.append(match.group(1));
963 elif not my_passphrase and len(passphrase_list) == 0:
964 suggest_passphrase = dev_random.read(16);
965 sys.stderr.write("Enter passphrase (suggest \'"+str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=')+"\'): ");
966 # How kan we make this unreadable on input?
967 current_passphrase = input();
968 if not current_passphrase:
969 current_passphrase = str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=');
970 print("Passphrase: \'"+current_passphrase+"\'", file=current_private);
971 passphrase_list.append(current_passphrase);
972 elif my_passphrase.startswith('SUGGESTED'):
973 N = 1;
974 match = re.search("([0-9][0-9]*)$", my_passphrase);
975 if match != None:
976 N = int(match.group(1));
977 j = int(random.random()*N);
978 for i in range(0, N):
979 suggest_passphrase = dev_random.read(16);
980 current_passphrase = str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=');
981 print("Passphrase: \'"+current_passphrase+"\'", file=current_private);
982 passphrase_list.append(current_passphrase);
983 else:
984 passphrase_list.append(my_passphrase);
986 selected_salt = 1;
987 fail_fraction = 0.5;
988 if not my_check:
989 if len(passphrase_list) > 1:
990 j = int(random.random()*len(passphrase_list));
991 passphrase_list = [passphrase_list[j]];
992 print("# Selected passphrase:", j+1, file=current_private);
993 if len(salt_list) > 1:
994 j = int(random.random()*len(salt_list));
995 # Make sure at least 1 salt will match and print the selection if only one is used
996 selected_salt = j+1;
997 if not my_allsalts:
998 salt_list = [salt_list[selected_salt-1]];
999 print("# Selected salt:", selected_salt, file=current_private);
1000 else:
1001 salt_N = len(salt_list);
1002 fail_fraction = (salt_N/2.0)/(salt_N - 1);
1003 else:
1004 fail_fraction = 0;
1006 # Close /dev/(u)random
1007 dev_random.close;
1009 #############################################################################
1011 # SIGNATURE CREATION AND CHECKING #
1013 #############################################################################
1015 end_time = time.time();
1016 print("# Preparation time:", end_time - start_time, "seconds\n", file=current_outfile);
1018 pnum = 1;
1019 snum = 1;
1020 corrpnum = 0;
1021 corrsnum = 0;
1022 matched_salt_pattern = -1;
1023 salt_pattern_number = -1;
1025 for my_passphrase in passphrase_list:
1026 snum = 1;
1027 # Initialize salt pattern
1028 if my_allsalts:
1029 salt_pattern_number = 0;
1030 current_salt_power = 1;
1032 for my_salt in salt_list:
1033 print("# Start signature: ", end='', file=current_outfile);
1034 if len(passphrase_list) > 1: print("passphrase -", pnum, end='', file=current_outfile);
1035 if len(salt_list) > 1: print(" salt -", snum, end='', file=current_outfile);
1036 print("", file=current_outfile);
1038 # Should everything be printed?
1039 print_verbose = my_verbose and not (my_allsalts and snum > 1);
1041 file_argnum = 0;
1042 start_time = time.time();
1043 # Construct the passphrase hash
1044 passphrase = hashlib.sha256();
1046 passphrase.update(bytes(my_salt, encoding='ascii'));
1047 passphrase.update(bytes(my_passphrase, encoding='ascii'));
1049 # Create prefix which is a hash of the salt+passphrase
1050 prefix = passphrase.hexdigest();
1052 # Create signature and write output
1053 totalhash = hashlib.sha256();
1054 totalhash.update(bytes(prefix, encoding='ascii'));
1055 for org_filename in check_filenames:
1056 # Create file hash object
1057 filehash = hashlib.sha256();
1058 filehash.update(bytes(prefix, encoding='ascii'));
1059 # Remove []
1060 filename = org_filename.strip('[').rstrip(']');
1061 # Use system variables and commands
1062 if filename.startswith('$'):
1063 # Commands $(command)
1064 match = re.search('^\$\((.+)\)$', filename);
1065 if match != None:
1066 if not execute :
1067 error_message = "Executable argument \'"+filename+"\' only allowed with the --execute flag";
1068 print (error_message, file=sys.stderr);
1069 if not sys.stdout.isatty(): print(error_message, file=current_outfile);
1070 exit(0);
1071 current_command = not_allowed_chars.sub(" ", match.group(1));
1072 current_command_line = user_change+"bash --restricted -c \'"+current_command+"\' "+str(os.getpid())+" "+execute_args;
1073 # Print command
1074 if print_verbose :
1075 print("#", current_command_line, file=current_outfile);
1076 # Spawn command and open a pipe to the output
1077 pipe = subprocess.Popen(current_command_line, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE);
1078 for b in pipe.stdout:
1079 if type(b).__name__ == 'str':
1080 b = bytes(b, encoding='utf8');
1081 filehash.update(b);
1082 if options.printhexdump: # For debugging commands
1083 print(str(binascii.hexlify(b), 'ascii'), file=sys.stderr);
1084 # See whether there was an error
1085 pipe.wait();
1086 if pipe.returncode:
1087 error_message = pipe.stderr.read();
1088 print('$('+current_command+')', "\n", str(error_message, encoding='UTF8'), file=sys.stderr);
1089 exit(pipe.returncode);
1090 # ${ENV} environment variables
1091 match = re.search('^\$[\{]?([^\}\(\)]+)[\}]?$', filename);
1092 if match != None:
1093 current_var = not_allowed_chars.sub(" ", match.group(1));
1094 if print_verbose:
1095 print("# echo $"+ current_var, file=current_outfile);
1096 b = os.environ[current_var];
1097 filehash.update(bytes(b, encoding='utf8'));
1098 # lstat() meta information
1099 elif filename.startswith('?'):
1100 if not os.path.exists(filename.lstrip('?')):
1101 print(filename, "does not exist", file=sys.stderr)
1102 quit();
1103 filestat = os.stat(filename.lstrip('?'));
1104 if my_statusvalues == "": my_statusvalues = 'fmidlugs'
1105 b = "";
1106 if 'f' in my_statusvalues:
1107 b += 'stat('+filename.lstrip('?')+') = '
1108 b += '[';
1109 if 'm' in my_statusvalues:
1110 b += 'st_mode='+str(oct(filestat.st_mode))+', ';
1111 if 'i' in my_statusvalues:
1112 b += 'st_ino='+str(filestat.st_ino)+', ';
1113 if 'd' in my_statusvalues:
1114 b += 'st_dev='+str(filestat.st_dev)+', '
1115 if 'l' in my_statusvalues:
1116 b += 'st_nlink='+str(filestat.st_nlink)+', '
1117 if 'u' in my_statusvalues:
1118 b += 'st_uid='+str(filestat.st_uid)+', '
1119 if 'g' in my_statusvalues:
1120 b += 'st_gid='+str(filestat.st_gid)+', '
1121 if 's' in my_statusvalues:
1122 b += 'st_size='+str(filestat.st_size)+', '
1123 if 'a' in my_statusvalues:
1124 b += 'st_atime='+str(filestat.st_atime)+', '
1125 if 't' in my_statusvalues:
1126 b += 'st_mtime='+str(filestat.st_mtime)+', '
1127 if 'c' in my_statusvalues:
1128 b += 'st_ctime='+str(filestat.st_ctime);
1130 b = b.rstrip(', ') + ']';
1131 filehash.update(bytes(b, encoding='utf8'));
1132 if print_verbose:
1133 print ("# "+ b, file=current_outfile);
1134 # Use file
1135 else:
1136 # open and read the file
1137 if filename != '-' and filename.find('://') == -1 and not os.path.exists(filename):
1138 print(filename, "does not exist", file=sys.stderr)
1139 quit();
1141 if filename == '-':
1142 for b in sys.stdin.buffer:
1143 if type(b).__name__ == 'str':
1144 b = bytes(b, encoding='utf8');
1145 filehash.update(b);
1146 if options.printhexdump: # For debugging commands
1147 print(str(binascii.hexlify(b), 'ascii'), file=sys.stderr);
1149 else:
1150 with open_infile(filename, 'rb') as file:
1151 for b in file:
1152 if type(b).__name__ == 'str':
1153 b = bytes(b, encoding='utf8');
1154 filehash.update(b);
1155 if options.printhexdump: # For debugging commands
1156 print(str(binascii.hexlify(b), 'ascii'), file=sys.stderr);
1158 current_digest = filehash.hexdigest();
1159 print_name = filename;
1160 if my_quiet or org_filename.startswith('['):
1161 file_argnum += 1;
1162 print_name = '['+str(file_argnum)+']';
1163 current_hash_line = current_digest+" *"+print_name
1164 totalhash.update(bytes(current_hash_line, encoding='ascii'));
1166 # Be careful to use this ONLY after totalhash has been updated!
1167 if total_only:
1168 current_hash_line = (len(current_digest)*'-')+" *"+print_name;
1170 # Write output
1171 if not my_check:
1172 if not (my_quiet and total_only) and not (my_allsalts and snum > 1):
1173 print(current_hash_line, file=current_outfile);
1174 elif not (my_quiet or my_allsalts):
1175 if check_hashes[print_name] == (len(current_digest)*'-'):
1176 # Suppress redundant output of empty, ----, lines
1177 if snum <= 1 and pnum <= 1:
1178 print(check_hashes[print_name]+" *"+print_name, file=current_outfile);
1179 elif current_digest != check_hashes[print_name]:
1180 print("FAILED: "+current_hash_line, file=current_outfile);
1181 else:
1182 print("ok"+" *"+print_name, file=current_outfile);
1184 # Handle total hash
1185 current_total_digest = totalhash.hexdigest();
1186 # Write (in)correct salts with the TOTAL HASH
1187 if my_allsalts:
1188 output_salt = my_salt;
1189 j = random.random();
1190 # Randomly create an incorrect salt for failed output
1191 if not my_check:
1192 if j < fail_fraction and snum != selected_salt:
1193 salt = dev_random.read(8);
1194 output_salt = str(binascii.hexlify(salt), 'ascii');
1195 else:
1196 salt_pattern_number += current_salt_power;
1197 current_total_digest_line = "Salt+TOTAL HASH: '"+output_salt+"' '"+current_total_digest+"'";
1198 else: # Standard TOTAL HASH line
1199 current_total_digest_line = current_total_digest+" *"+"TOTAL HASH";
1200 end_time = time.time();
1201 print("# \n# Total hash - Time to completion:", end_time - start_time, "seconds", file=current_outfile);
1202 total_hash_num = 0;
1203 if my_allsalts: total_hash_num = snum-1; # Current TOTAL HASH number of more are used
1204 if not my_check:
1205 print(current_total_digest_line+"\n", file=current_outfile);
1206 elif current_total_digest != total_hash_list[total_hash_num]:
1207 if not my_allsalts: print("FAILED: "+current_total_digest_line+"\n", file=current_outfile);
1208 else:
1209 if my_allsalts: salt_pattern_number += current_salt_power; # Update salt bit pattern
1210 match_number = "";
1211 if len(passphrase_list) > 1 or len(salt_list): match_number = " #"
1212 if len(passphrase_list) > 1: match_number += " passphrase no: "+str(pnum);
1213 if len(salt_list) > 1: match_number += " salt no: "+str(snum);
1214 if not my_allsalts: print("OK"+" *"+"TOTAL HASH"+match_number+"\n", file=current_outfile);
1215 corrsnum = snum;
1216 corrpnum = pnum;
1217 snum += 1;
1218 if my_allsalts: current_salt_power *= 2; # Update current bit position in salt pattern
1219 if my_check and corrpnum == pnum: matched_salt_pattern = salt_pattern_number;
1220 pnum += 1;
1222 if my_check and len(passphrase_list) > 1:
1223 if corrpnum > 0:
1224 print("Passphrase entry:",corrpnum,"matched", file=current_outfile);
1225 else:
1226 print("No passphrase entry matched!", file=current_outfile);
1227 if my_check and (not my_allsalts) and len(salt_list) > 1:
1228 if corrpnum > 0:
1229 if corrsnum > 0:
1230 print("Salt entry:",corrsnum,"matched", file=current_outfile);
1231 else:
1232 print("No salt entry matched!", file=current_outfile);
1233 else:
1234 print("No entry matched", file=current_outfile);
1235 # Print salt bit patterns
1236 elif my_check and my_allsalts:
1237 print("Salt pattern number:", matched_salt_pattern, file=current_outfile);
1238 elif not my_check and my_allsalts:
1239 print("# Salt pattern number:", salt_pattern_number, file=current_private);
1241 # Close output files if necessary
1242 if my_output and my_output != '-':
1243 current_outfile.close();
1244 if my_private and my_private != '-':
1245 current_private.close();