Debugging of the use of proc_PID
[signduterre.git] / signduterre.py
blob3611b2e35137bf48366757faed8ad03d9bb08364
1 #!/usr/bin/python3
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 --file-source=PATH Read all files from PATH. The PATH-string is prepended
67 to every plain file-path that is read for a signature.
68 Remote files can be checked with
69 'ssh://<user>@<host>[/path]'. A shell command that
70 prints out the file can be entered as '$(<cmd>)'. The
71 filepath will be substituted for any '{}' string in
72 the command, or appended tot the command (without
73 white-space). The option overrules any File source
74 specification in the --check-file.
75 -P FILE, --Private-file=FILE
76 Print private information (passwords etc.) to FILE
77 instead of STDERR.
78 -u USER, --user=USER Execute $(cmd) as USER, default 'nobody' (root/sudo
79 only)
80 -S, --Status For each file, add a line with unvarying file status
81 information: st_mode, st_ino, st_dev, st_uid, st_gid,
82 and st_size (like the '?' prefix, default False)
83 --Status-values=MODE Status values to print for --Status, default MODE is
84 'fmidugs' (file, mode, inode, device, uid, gid, size).
85 Also available (n)l(inks) a(time), (m)t(ime), and
86 c(time).
87 -t, --total-only Only print the total hash, unsets --detailed-view
88 (default True)
89 -d, --detailed-view Print hashes of individual files, is unset by --total-
90 only (default False)
91 -e, --execute Interpret $(cmd) (default False)
92 --execute-args=ARGS Arguments for the $(cmd) commands ($1 ....)
93 -n, --no-execute Explicitely do NOT Interpret $(cmd)
94 --import=FILE Import python modules (comma separated list without extension)
95 --print-textdump Print printable character+hexadecimal dump of input
96 bytes to STDERR for debugging purposes
97 --message=TEXT Add a comment message about the test
98 -m, --manual Print a short version of the manual and exit
99 --manual-long Print the long version of the manual and exit
100 --manual-html Print the manual in HTML format and exit
101 --manual-make Print the examples in the manual as a makefile and
102 exit
103 -r, --release-notes Print the release notes and exit
104 -l, --license Print license text and exit
105 -v, --verbose Print more information on output
106 -q, --quiet Print minimal information (hide filenames). If the
107 output is used with --check-file, the command line
108 options and arguments must be repeated.
109 [[[/pre]]][[[p]]]
110 FILE1 FILE2 ...
111 Names and paths of one or more files to be checked. All file arguments in SdT accept '-' as the STDIN file
112 (ie, piped data). Can use ssh://<user>@<host>/path pseudo-URLs for checking files at remote sites. Arguments
113 of any type can take an appended range parameter '[<start>:<end>:<offset>]' or '[<start>:+<length>:<offset>]'.
114 <offset>+<start> bytes are skipped and only <length>=<end>-<start> bytes are written. Leaving out the second
115 argument, ie, '[<start>:]', means all bytes after <start> to the end of the file or stream are used. The
116 ':<offset>' argument is optional. All <start>, <end>, <length>, and <offset> arguments can be entered in
117 decimal (default), hexadecimal (0x0000..), octal (0o0000..), and binary (0B0000..) representations.
118 [[[/p]]][[[p]]]
119 Any name starting with a '$', eg, $PATH, will be interpreted as an environmental variable or a command
120 according to the bash conventions: '$ENV' and '${ENV}' as variables, '$(cmd;cmd...)' as system commands
121 (bash --restricted -c 'cmd;cmd...' PID). Where PID the current Process ID is (available as positional
122 parameter $0). Other parameters can be entered with the --execute-args option ($1 etc). Do not forget to
123 enclose the arguments in single ''-quotes! The commands are scanned for unwanted characters and these
124 are removed (eg, ' and \\, however, escaping $ is allowed, eg, '\\$1'). The use of '$(cmd;cmd...)'
125 requires explicit use of the -e or --execute option.
126 [[[/p]]][[[p]]]
127 Note that byte range slices '$(cmd)[<start>:<end>]' do work, but only [[[em]]]after[[[/em]]] the command
128 has completed. So, the file version, '/dev/kmem[0xc04838a0:+88]', will simply use 88 bytes as in
129 '$(dd if=/dev/kmem bs=1 skip=3225958560 count=88)'. However, '$(dd if=/dev/kmem bs=1)[0xc04838a0:+88]' will
130 [[[em]]]first[[[/em]]] read all of /dev/kmem, and only then extract the 88 bytes. In general, this is not
131 the desired procedure (/dev/kmem contains all of the physical RAM). Note that the remote
132 '--file-soource=ssh://...' option preserves the file slice behavior as the file reads are changed into the
133 equivalent 'dd skip=<start> count=<length>' commands.
134 [[[/p]]][[[p]]]
135 Any string '@(python code)' will be evaluated as python 3 code. The '--execute' option is obligatory.
136 Note that the outer ()-brackets are removed. You can extend the program by importing modules with the
137 '--import <module>,<module>,....' option. The python code will be interpreted as a function body,
138 complete with obligatory return statement(s), and wrapped in a function definition. This function will
139 be executed in a separate namespace and the 'return'ed value will be exported and hashed. The current PID
140 is available as 'argv[0]' and the --execute-args argument values are available as list elements 'argv[1]',
141 'argv[2]', etc. @() statements are executed inside the running signduterre program and cannot be used to
142 querry a remote system with ssh:// pseudo-URL constructs.
143 [[[/p]]][[[p]]]
144 If executed as root or sudo, $(cmd;cmd...) will be executed as 'sudo -H -u <user>' which defaults to
145 --user nobody ('--user root' is at your own risk). This will obviously not work when invoked as non-root/sudo.
146 --user root is necessary when you need to check privileged information, eg, you want to check the MBR with
147 '$(dd if=/dev/hda bs=512 count=1 | od -X)'
148 However, as you might use --check-file with files you did not create yourself, it is important to
149 be warned if commands are to be executed.
150 [[[/p]]][[[p]]]
151 Interpretation of $() ONLY works if the -e or --execute options are entered. signduterre.py can easily
152 be adapted to automatically use the setting in the check-file. However, this is deemed insecure and
153 commented out in the distribution version.
154 [[[/p]]][[[p]]]
155 The -n or --no-execute option explicitely supress the interpretation of $(cmd) arguments.
156 [[[/p]]][[[p]]]
157 Meta information from stat() on files is signed when the filename is preceded by a '?'. '?./signduterre.py' will
158 extract (st_mode, st_ino, st_dev, st_nlinks, st_uid, st_gid, st_size) and hash a line of these data (visible
159 with --verbose). The --Status option will automatically add such a line in front of every file. Note that '?'
160 is implied for directories. Both '/' and '?/' produce a hash of, eg,:
161 [[[/p]]][[[pre]]]
162 stat(/) = [st_mode=041775, st_ino=2, st_dev=234881026, st_uid=0, st_gid=80, st_size=1360]
163 [[[/pre]]][[[p]]]
164 The --Status-values=<mode> option selects which status values will be used: f(ile), m(ode), i(node),
165 d(evice), u(id), g(id), s(ize), (n)l(inks), a(time), (m)t(ime), and c(time). Default is
166 --Status-values='fmidugs'. Note that nlinks of a directory include every file in the directory, so this
167 option can check whether files have been added to a directory.
168 [[[/p]]][[[p]]]
169 Arguments enclosed in []-brackets will be hidden in the output. That is, '[/proc/self/exe]' will show up as
170 '[1]' in the output (or '[n]' with n the number of the hidden argument), equivalent to the use of the
171 --quiet option. This means the hidden arguments must be entered again when using the --check-file (-c)
172 option.
173 [[[/p]]][[[p]]]
174 Signature-du-Terroir
175 [[[/p]]][[[p]]]
176 A very simple tool to generate a signature that can be used to test the integrity of files and "states" in
177 a running installation. signduterre.py constructs a signature of the current system state and checks
178 installation state with a previously made signature. The files are hashed with a passphrase to allow detection
179 of compromised systems while running on the same system. The signature checking can be subverted, but the
180 flexibillity of signduterre.py and the fact that the output of any command can be tested should hamper
181 automated root-kit attacks.
182 [[[/p]]][[[p]]]
183 signduterre.py writes a total SHA-256 hash to STDOUT of all the files and commands entered as arguments. It
184 can also write a hash for each individual file (insecure). The output of a signature can be send to a file and
185 later used to check with --check-file. Hashes are calculated with a hashed salt + passphrase sequence
186 pre-pended to create unpredictable hashes. This procedure ensures that an attacker does not know whether or
187 not the correct passphrase has been entered. An attacker can only know when to supply the requested hash
188 values if she knows the passphrase or has copies available of all the tested files and output of commands to
189 calculate the hashes on the fly.
190 [[[/p]]][[[LONG]]][[[p]]]
191 The Problem
192 [[[/p]]][[[p]]]
193 The problem SdT tries to solve is how to test whether your system has been compromised when you can only use
194 the potentially compromised system? The solution is to store a password encrypted signature (or fingerprint)
195 of your system when you are sure it is in a good state. Then you check whether the system can still
196 distinguish between correct and incorrect passwords when it regenerates the signature. The trick is to use
197 the right data (ie, questions) to generate the signature.
198 [[[/p]]][[[p]]]
199 The underlying idea is that some bits have to be changed to compromise a system. That is, program
200 files have been altered, settings and accounts changed, new processes are running or existing processes
201 altered. The most common situation is that some system programs have been changed to hide the traces of
202 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
203 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
204 malicious processes that are running. Such wholescale adaptations of running systems can be executed
205 using standard, off-the-shelf application suits, so called rootkits. There are applications that can
206 detect common (known) rootkits and other malicious programs, eg, [[[i]]]chkrootkit[[[/i]]] ([[[a
207 href="http://www.chkrootkit.org/"]]]www.chkrootkit.org[[[/a]]]) and
208 [[[i]]]rootkit hunter[[[/i]]] ([[[a
209 href="http://www.rootkit.nl"]]]www.rootkit.nl[[[/a]]]). However, these rootkit detectors also use existing commands on the
210 potentially compromised system, so a rootkit can hide from them too.
211 [[[/p]]][[[p]]]
212 There are two obvious directions to guard against rootkits. One is to continuously run a process that
213 looks for attempts to install a rootkit and other malicious activities. The other is to take a snapshot
214 of the system in a known good state, and then flag changes in relevant areas, eg, like [[[i]]]Tripwire[[[/i]]]
215 ([[[a href="http://sourceforge.net/projects/tripwire/"
216 ]]]http://sourceforge.net/projects/tripwire/[[[/a]]]) and [[[i]]]Radmind[[[/i]]] ([[[a
217 href="http://rsug.itd.umich.edu/software/radmind/"
218 ]]]http://rsug.itd.umich.edu/software/radmind/[[[/a]]]).
219 Signature-du-Terroir takes the second route, it creates a signature of a set of relevant files and
220 command output, and checks later whether these have not been changed. However, when running such a test
221 on a compromised system, the attacker can theoretically "fool" any (automated) test. In practise, time
222 and other precious resources will limit what an attacker can accomplish. The idea is to raise the bar
223 for rootkits high enough to make them not worthwhile. SdT tries to make using signatures easy (cheap)
224 and subverting it difficult (expensive).
225 [[[/p]]][[[p]]]
226 As an illustration of the problem SdT treis to solve, take the [[[i]]]sha256sum[[[/i]]] command which generates file
227 hashes (signatures) using the SHA256 algorithm. Hashes can be generated and checked with this command:
228 [[[/p]]][[[pre]]]
229 # Use of sha256sum to check integrity of ps and ls commands
230 $ sha256sum /bin/ps /bin/ls > ps-ls.sh256
231 $ sha256sum -c ps-ls.sh256
232 [[[/pre]]][[[p]]]
233 A compromised file will show up as FAILED. This is ok for unintentional changes to the files. However, a
234 malicious attacker could easily replace [[[i]]]/usr/bin/sha256sum[[[/i]]] with a program that would replace the hash of
235 malicious replacements of these files with the hash sums of the original files. There are three easy ways
236 of doing that. Either simply say 'ok' when checking the file, print out the stored old hash value whenever
237 an altered file is requested by name, or look for the hash of the new, malicious replacement and print out
238 the old hash sum instead. The former two are easy to circumvent, the last one is somewhat less easy.
239 [[[/p]]][[[p]]]
240 The first solution to these avoidance strategies is to generate the signatures with a passphrase and random
241 string (salt). As long as the attacker does not know the passphrase, the only way to subvert SdT is to store
242 the original bits in the files and calculate the signature the moment SdT is called. As the attacker does
243 not know when the correct password or salt is entered, it is not possible to simply answer OK or repeat the
244 stored earlier results instead of calculating them de-novo.
245 [[[/p]]][[[p]]]
246 To be able to serve up the original bits, instead of the bits used on the compromised system, when asked
247 for the hashes, the attacker must divert attempts to read the files by SdT, but not at other moments.
248 There are many ways to do this, eg, running python in a chroot-jail, changing python itself, changing other
249 programs. To accommodate these diversion strategies, SdT allows to read data from each and every command
250 that can supply it. So, a binary file can be entered by name, with eg, cat, dd, perl, python, ruby, or read
251 from the [[[i]]]/proc[[[/i]]] system (if it is a running process), or from STDIN or shell subprocesses. For instance,
252 to protect against running in a chroot-jail, the inode number and device of the root directory can be read
253 from [[[i]]]/proc/self/root[[[/i]]], or [[[i]]]/proc/<PID>/root[[[/i]]], or simply from [[[i]]]/[[[/i]]].
254 [[[/p]]][[[/LONG]]][[[p]]]
255 Signature creation: Passphrases, salts, and hashes
256 [[[/p]]][[[p]]]
257 Good passphrases are difficult to remember, so their plaintext form should be protected. To protect the
258 passphrase against rainbow and brute force attacks, the passphrase is concatenated to a salt phrase and
259 hashed before use (SHA-256).
260 [[[/p]]][[[p]]]
261 The salt phrase is requested when constructing a signature. In interactive use, an 8 byte hexadecimal
262 (= 16 character) salt from [[[i]]]/dev/urandom[[[/i]]] is suggested. If '--salt SUGGESTED' is entered on the command line
263 as the salt, the suggested value will be used. The salt is printed in plaintext to the output. The salt will
264 make it more difficult to determine whether the same passphrase has been used to create different signatures.
265 [[[/p]]][[[p]]]
266 At the bottom, a 'TOTAL HASH' line will be printed that hashes all the lines printed for the files. This
267 includes the file names as printed on the hash lines. It is not inconceivable that existing signature files
268 could have been compromised in ways that might be missed when checking the signature. The total hash will
269 point out such changes.
270 [[[/p]]][[[p]]]
271 SECURITY
272 [[[/p]]][[[LONG]]][[[p]]]
273 When run on a compromised system, signduterre.py can be subverted if the attacker keeps a copy of all the
274 files and command outputs, and reroutes the open() and stat() functions, or simply delegating signduterre.py
275 to a chroot jail with the original system. In principle, signduterre.py only checks whether the computer
276 responds identically to when the signature file was made. There is no theoretic barrier against a compromised
277 computer perfectly simulating the original system when tested, but behaving adversely at other times. Except
278 for running from clean boot media (USB?), I know of no theoretical sound solution to this problem.
279 [[[/p]]][[[p]]]
280 However, this scenario assumes the use of unlimited resources and time. Inside a limited, real computer system,
281 the attacker must make compromises on what can and what cannot be simulated with the available time and
282 hardware. The idea behind signduterre.py is to "ask difficult questions" that increase the cost of simulating
283 the original system high enough to make detection of successful attacks likely.signduterre.py simply intends
284 to raise the bar high enoug. One point is to store the times needed to create the original hashes. This timing
285 can later be used to see whether the new timings are reasonable. If the same hardware takes considerably
286 longer to perform the same calculations, or needs a much longer delay before it starts, the tester might want
287 to see where this time is spent.
288 [[[/p]]][[[/LONG]]][[[p]]]
289 Signature-du-Terroir works on the assumption that any attacker in control of a compromised system cannot
290 predict whether the passphrase entered is correct or not. An attacker can always intercept the in- and output
291 of signduterre. When running with --check-file, this means the program can be made to print out OK
292 irrespective of the tests. A safe use of signduterre.py is to start with a random number of incorrect
293 passphrases and see whether they fail. Alternatively, and easier, is to add a number of unused salts
294 to the check-file and let the attacker guess which one is correct.
295 [[[/p]]][[[p]]]
296 THE CORRECT USE OF signduterre.py IS TO ENTER A RANDOM NUMBER OF INCORRECT PASSPHRASES OR SALTS FOR EACH
297 TEST AND SEE WHETHER IT FAILS AT THE CORRECT INSTANCES!
298 [[[/p]]][[[p]]]
299 On a compromised system, signduterre.py's detailed file testing (--detailed-view) is easily subverted. With a
300 matched file hash, the attacker will know that the correct passphrase has been entered and can print out the
301 stored hashes or 'ok's for the rest of the checks. So if the attacker keeps any entry in the signature file
302 uncompromised, she can intercept the output, test the password on the unchanged entry and substitute the
303 requested hashes for the output if the hash of that entry matches.
304 [[[/p]]][[[LONG]]][[[p]]]
305 When checking for root-kits and other malware, it is safest to compare the signature files from a different,
306 clean, system. But then you would not need signduterre.py anyway. If you have to work on the system itself,
307 only use the -t or --total-only options to create signatures with a total hash and without individual file
308 hashes. Such a signature can be used to check whether the system is unchanged. Another signature file WITH A
309 DIFFERENT PASSPHRASE can then be used to identify the individual files that have changed. If a detailed
310 signature file has the same passphrase, an attacker could use that other file to read the individual file
311 hashes to check whether the correct passphrase was entered.
312 [[[/p]]][[[/LONG]]][[[p]]]
313 Using the --check-file option in itself is UNsafe. An attacker simply has to print out 'OK' to defeat the
314 check. This attack can be foiled by making it unpredictable when signduterre.py should return 'OK'. This can
315 be done by using a list of salts or passphrases where only one of them (or none!) is correct. Any attacker
316 will have to guess when to return 'OK'.
317 [[[/p]]][[[LONG]]][[[p]]]
318 As generating and entering wrong passphrases and salts is tedious, users have to be supported in correct use
319 of SdT. To assist users, the '--salt SUGGESTED=<N>' option will generate a number N of salts. When
320 checking, each of these salts is tried in turn. An attacker that is unable to simulate the uncompromised
321 system will have to guess which one of the salts is the correct one, and whether or not the passphrase
322 is correct. This increases the chances of detecting compromised systems. If this is not enough guess
323 work, the '-a', '--all-salts-pattern' option will use all salts in sequence to generate total hashes,
324 but random salts will be changed in the output. This generates a pattern of failed salt tests. This pattern
325 is translated into a bit pattern and printed as an integer ([Fail, Fail, OK, Fail, OK, OK, Fail, OK]
326 = 00101101 (least significant first) = 10110100 (unsigned bin) = 180). On creation of a signature, this
327 number is printed to STDERR, on checking (--check-file) it is printed to STDOUT (note that the number
328 will never become 0 or all Fail). So for '--salt SUGGESTED=<N> --all-salts-pattern' the probability of
329 guessing the correct output goes from 1/N to 1/(2^N - 1). Note that '--all-salts-pattern' will work,
330 but is pointless, without '--salt SUGGESTED=<N>' with N>1.
331 [[[/p]]][[[p]]]
332 The '--passphrase SUGGESTED=N' option will generate and print N passphrases. One of these is chosen at
333 random for the signature. The number of the chosen passphrase is printed on STDERR with the passwords.
334 When checking a file, the stored passphrases can be read in again, either by entering the passphrase
335 file after the --passphrase option ('--passphrase <passphrase file>'), or directly from the --check-file.
336 signduterre.py will print out the result for each of the passphrases.
337 [[[/p]]][[[p]]]
338 Note, that storing passphrases in a file and feeding it to signduterre.py is MUCH less secure than just
339 typing them in. Moreover, it might completely defeat the purpose of signduterre.py. If future experiences
340 cast any more doubt on the security of this option, it will be removed.
341 [[[/p]]][[[p]]]
342 For those who want to know more about what an "ideal attacker" can do, see:[[[br]]]
343 Ken Thompson "Reflections on Trusting Trust"[[[br]]]
344 [[[a href="http://cm.bell-labs.com/who/ken/trust.html"]]]http://cm.bell-labs.com/who/ken/trust.html[[[/a]]][[[br]]]
345 [[[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]]]
346 [[[/p]]][[[p]]]
347 David A Wheeler "Countering Trusting Trust through Diverse Double-Compiling"[[[br]]]
348 [[[a href="http://www.acsa-admin.org/2005/abstracts/47.html"]]]http://www.acsa-admin.org/2005/abstracts/47.html[[[/a]]]
349 [[[/p]]][[[p]]]
350 and the discussion of these at Bruce Schneier's 'Countering "Trusting Trust"'[[[br]]]
351 [[[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]]]
352 [[[/p]]][[[/LONG]]][[[p]]]
353 Manual
354 [[[/p]]][[[p]]]
355 The intent of signduterre.py is to ensure that the signature cannot be subverted even if the system has been
356 compromised by an attacker that has obtained root control over the computer and any existing signature files.
357 [[[/p]]][[[p]]]
358 signduterre.py asks for a passphrase which is PRE-pended to every file before the hash is constructed (unless
359 the passphrase is entered with an option). As long as the passphrase is not compromised, the hashes cannot
360 be reconstructed. A randomly generated, unpadded base-64 encoded 16 Byte password (ie, ~22 characters) is
361 suggested in interactive use. If '--passphrase SUGGESTED' is entered on the command line or no passphrase is
362 enetered when asked, the suggested value will be used. This value is printed to STDERR (the screen or 2) for
363 safe keeping. Please, make sure you store the printed passphrase. For instance:
364 [[[/p]]][[[pre make=example1]]]
365 # make: example1
366 # Simple system sanity test using the 'which' command to establish the paths
367 $ python3 signduterre.py --passphrase SUGGESTED --salt SUGGESTED --detailed-view \\
368 `which python3 bash ps ls find stat` 2> test-20090630_11-14-03.pwd > test-20090630_11-14-03.sdt
369 $ python3 signduterre.py --passphrase test-20090630_11-14-03.pwd --check-file test-20090630_11-14-03.sdt
370 [[[/pre]]][[[p]]]
371 The first command will store the passphrase (and all error messages) in a file 'Signature_20090630_11-14-03.pwd'
372 and the check-file in 'Signature_20090630_11-14-03.sdt'. The second line will test the signature.
373 The signature will be made of the files used for the commands python3, bash, ps, ls, find, and stat.
374 These files are found using the 'which' command.
375 [[[/p]]][[[p]]]
376 Working with remote systems
377 [[[/p]]][[[p]]]
378 It is not secure to store files with the passphrase on the system you want to check. However, you could
379 pipe STDERR to some safe site.
380 [[[/p]]][[[pre]]]
381 # Send passphrase over ssh tunnel to safe site
382 $ python3 signduterre.py --passphrase SUGGESTED --salt SUGGESTED `which bash python3` \\
383 -o test-safe-store.sdt 2>&1 | ssh user@safe.host.site 'dd of=/home/user/safe/test-safe-store.pwd'
384 [[[/pre]]][[[p]]]
385 As the security of the passphrases is important and off-site storrage of files is often prudent or convenient,
386 this tunneling construct has been automated in all in- and output as a pseudo-URL: 'ssh://<user>@<host></path>',
387 eg, 'ssh://user@safe.host.site/home/user/safe/test-safe-store.pwd'. It is not possible to enter a
388 password in such a pseudo-URL, so the automatical login into the host system must be configured in SSH.[[[br /]]]
389 [[[em]]]Note: There are severe security risks involved when using SSH to login into another system if the
390 originating system is compromised[[[/em]]].
391 [[[/p]]][[[p]]]
392 The pseudo-url can be used with the [[[i]]]--output-file, --Private-file, --input-file, --check-file, --passphrase[[[/i]]]
393 options as well as for the actual file, ${ENV}, and $(cmd) arguments used to determine the signatures. The latter
394 allows to check files on remote systems, or to repeat a check from a remote system using the [[[i]]]--file-source[[[/i]]]
395 option (only works with plain files, ${ENV}, and $(cmd), not for @(python code), directories, or --Status arguments).
396 For instance:
397 [[[/p]]][[[pre]]]
398 # Use ssh:// pseudo-url to send passphrase to safe.host.site
399 $ python3 signduterre.py --passphrase SUGGESTED --salt SUGGESTED `which bash python3` \\
400 -o ssh://user@safe.host.site/home/user/safe/test-safe-store.sdt \\
401 -P ssh://user@safe.host.site/home/user/safe/test-safe-store.pwd
402 # Check files on remote compromised.host.site while running test program on safe.host.site
403 $ python3 signduterre.py --passphrase test-safe-store.pwd --check-file test-safe-store.sdt \\
404 --file-source ssh://user@compromised.host.site
405 [[[/pre]]][[[p]]]
406 To execute a remote $(cmd) argument, write $(ssh://<user>@<host>/cmd). Be aware that nested "-quotes might cause
407 problems. ${ENV} can be written as ${ssh://<user>@<host>/ENV}. When using a --file-source argument that starts
408 with 'ssh://', the $(cmd) and ${ENV} commands are internally rewritten into the above form. In both forms,
409 as well as the arguments entered with --execute-args, any '$' and '"' symbols are protected by '\$' and '\"' to
410 be evaluated at the host system, as they would be evaluated locally by the ssh command line. This might not
411 always work out as planned, so take care when using these pseudo-URLS. Note that no <path> argument will be used.
412 [[[/p]]][[[p]]]
413 The next example uses the ssh:// pseudo-URL to read the data in an alternative way on [[[i]]]localhost[[[/i]]]. Obviously, storing the
414 plain text passphrase on the same system makes it a rather pointless excersize. The example only works if your have
415 (open)SSH server and clients installed and appended the '~/.ssh/id_dsa.pub' or '~/.ssh/id_rsa.pub' file to '~/.ssh/authorized_keys',
416 and you used ssh-add or another application to open the key.
417 [[[/p]]][[[pre make=ssh1]]]
418 # make: ssh1
419 # Use ssh:// pseudo-url to read data in an alternative way
420 $ python3 signduterre.py --passphrase SUGGESTED --salt SUGGESTED -v -d -e `which dd` '$(cat `which dd`)' \\
421 -o test-safe-store.sdt \\
422 -P ssh://`whoami`@localhost${PWD}/test-safe-store.pwd
423 # check files the standard way
424 $ python3 signduterre.py -e --passphrase ssh://`whoami`@localhost${PWD}/test-safe-store.pwd --check-file test-safe-store.sdt
425 # Check files using ssh on localhost
426 $ python3 signduterre.py -e --passphrase ssh://`whoami`@localhost${PWD}/test-safe-store.pwd --check-file test-safe-store.sdt \\
427 --file-source ssh://`whoami`@localhost
428 [[[/pre]]][[[p]]]
429 Examples:[[[/p]]][[[pre make=example2]]]
430 # make: example2
431 # Self test of root directory, python, and signduterre.py using the 'which' command to establish the paths
432 $ python3 signduterre.py --detailed-view --salt 436a73e3 --passphrase liauwefa3251EWC -o test-self.sdt \\
433 / `which python3 signduterre.py`
434 $ python3 signduterre.py --passphrase liauwefa3251EWC -c test-self.sdt
435 [[[/pre]]][[[LONG]]][[[p]]]
436 Write a signature to the file test-self.sdt and test it with the --check-file option. The signature contains
437 the SHA-256 hashes of the files, [[[i]]]/usr/bin/python3[[[/i]]], [[[i]]]signduterre.py[[[/i]]], and the status information on the root
438 directory. The salt '436a73e3' and passphrase 'liauwefa3251EWC' are used.
439 [[[/p]]][[[/LONG]]][[[pre make=procfs1]]]
440 # make: procfs1
441 # Self test of root directory, python, and signduterre.py using the the /proc file system
442 $ python3 signduterre.py --detailed-view --salt SUGGESTED --passphrase liauwefa3251EWC -o test-self_proc.sdt \\
443 /proc/self/root /proc/self/exe `which signduterre.py`
444 $ python3 signduterre.py --passphrase liauwefa3251EWC --check-file test-self_proc.sdt
445 [[[/pre]]][[[LONG]]][[[p]]]
446 Write a signature to the file test-self_proc.sdt and test it with the --check-file option. The signature
447 contains the SHA-256 hashes of the same files as above, [[[i]]]/usr/bin/python3[[[/i]]], [[[i]]]signduterre.py[[[/i]]], and the status
448 information on the root directory. However, the python executable and the root directory are now accessed
449 through the [[[i]]]/proc[[[/i]]] file system. The suggested salt is used (written to test-self_proc.sdt) and the passphrase
450 is (again) 'liauwefa3251EWC'.
451 [[[/p]]][[[/LONG]]][[[pre make=example3]]]
452 # make: example3
453 # Test of supporting commands for chkrootkit
454 $ python3 signduterre.py --execute --total-only --salt SUGGESTED=8 --passphrase SUGGESTED --Status \\
455 --output-file=test-chkrootkit.sdt --Private-file=test-chkrootkit.pwd \\
456 signduterre.py `which bash awk cut egrep find head id ls netstat ps strings sed uname`
457 $ python3 signduterre.py --execute --passphrase test-chkrootkit.pwd --check-file test-chkrootkit.sdt
458 [[[/pre]]][[[LONG]]][[[p]]]
459 Writes a signature of the requested files to test-chkrootkit.sdt (signature) and private information to
460 test-chkrootkit.pwd (password and selected salt) and checks it in the next line. The files are those of
461 commands required by the [[[i]]]chkrootkit[[[/i]]] program (http://www.chkrootkit.org/), with bash added. The 'which'
462 command will give the paths for the commands. Eight salts are generated, of which only 1 is actually
463 used. When checking, the correct salt should match. This prevents a compromised program from simply
464 printing out OK tot he check. A more comprehensive evation of guessing the correct salt can be obtained
465 by using the '--all-salts-pattern' option.
466 [[[/p]]][[[/LONG]]][[[pre make=procfs2]]]
467 # make: procfs2
468 # Simply lump all "system" files, the PATH environment variable and the first 2 columns of the output of lsmod
469 $ python3 signduterre.py --execute --detail --salt SUGGESTED --passphrase liauwefa3251EWC --Status --total-only \\
470 signduterre.py /sbin/* /bin/* /usr/bin/find /usr/bin/stat /usr/bin/python3 '${PATH}' \\
471 '$(lsmod | awk "{print \$1, \$2}")' > test-20090625_14-31-54.sdt
473 # Failing check due to missing --execute option
474 $ python3 signduterre.py --passphrase liauwefa3251EWC -c test-20090625_14-31-54.sdt
475 $ python3 signduterre.py --passphrase liauwefa3251EWC -c test-20090625_14-31-54.sdt --no-execute
477 # Successful check
478 $ python3 signduterre.py --execute --passphrase liauwefa3251EWC --check-file test-20090625_14-31-54.sdt
479 [[[/pre]]][[[LONG]]][[[p]]]
480 Prints a signature to the system test-20090625_14-31-54.sdt and the automatically generated password to
481 test-20090625_14-31-54.pwd. The salt will be automatically determined. The signature contains the SHA-256
482 hashes of the file status and file contents of [[[i]]]signduterre.py, /sbin/*, /bin/*, /usr/bin/find,
483 /usr/bin/file, /usr/bin/python*[[[/i]]] on separate lines, and a hash of the PATH environment variable. Do not
484 display the hash of every single file, which could be insecure, but only the total hash.
485 The first two checks will both fail if test-20090625_14-31-54.sdt contains a $(cmd) entry.
486 The --no-execute option is default and prevents the execute option (if reading the execute option from the
487 signature file has been activated). The last check will succeed (if the files have not been changed).
488 [[[/p]]][[[/LONG]]][[[pre make=example4]]]
489 # make: example4
490 # Use a list of generated passphrases
491 $ python3 signduterre.py --salt SUGGESTED --passphrase SUGGESTED=20 signduterre.py \\
492 2> test-20090630_16-44-34.pwd > test-20090630_16-44-34.sdt
493 $ python3 signduterre.py -p test-20090630_16-44-34.pwd -c test-20090630_16-44-34.sdt
494 [[[/pre]]][[[LONG]]][[[p]]]
495 Will generate and print 20 passphrases and print a signature using one randomly chosen passphrase from these
496 20. Everything is written to the files 'test-20090630_16-44-34.pwd' and 'test-20090630_16-44-34.sdt'.
497 Such file names can easily be generated with 'test-`date "+%Y%m%d_%H-%M-%S"`.sdt'.
498 The next command will check all 20 passphrases generated before from the Signature file and print the results.
499 [[[/p]]][[[/LONG]]][[[pre make=example5]]]
500 # make: example5
501 # Use a list of generated salts with a pattern of correct salts
502 $ python3 signduterre.py --salt SUGGESTED=16 --passphrase SUGGESTED --all-salts-pattern \\
503 -P test-salt-pattern.pwd -o test-salt-pattern.sdt `which bash stat find ls ps id uname awk gawk perl`
504 $ python3 signduterre.py -p test-salt-pattern.pwd -c test-salt-pattern.sdt
505 # Compare to salt pattern number to the one from the check-file
506 $ cat test-salt-pattern.pwd
507 [[[/pre]]][[[LONG]]][[[p]]]
508 As the previous, but with a pattern of random correct and incorrect salts. The salt pattern number
509 indicates which salts were and were not correct.
510 [[[/p]]][[[/LONG]]][[[pre make=sudo1]]]
511 # make: sudo1
512 # Check MBR and current root directory (sudo and root user)
513 $ sudo python3 signduterre.py -u root -s SUGGESTED -p SUGGESTED --Status-values='i' -v -e -t \\
514 --output-file test-boot-sector.sdt --Private-file test-boot-sector.pwd --execute-args=sda \\
515 '?/proc/self/root' `which dd` '$(dd if=/dev/$1 bs=512 count=1 | od -X)'
516 $ sudo python3 signduterre.py -u root -e -p test-boot-sector.pwd -c test-boot-sector.sdt
517 [[[/pre]]][[[LONG]]][[[p]]]
518 Will hash the inode numbers of the effective root directory (eg, chroot) and the executable (python)
519 together with the contents of the MBR (Master Boot Record) on [[[i]]]/dev/sda[[[/i]]] in Hex. It uses suggested salt and
520 passphrase. Accessing [[[i]]]/dev/sda[[[/i]]] is only possible when [[[i]]]root[[[/i]]], so the command is entered with [[[i]]]sudo[[[/i]]] and
521 '--user root'. Use the '--print-execute' option if you want to check the output of the [[[i]]]dd[[[/i]]] command.
522 [[[/p]]][[[p]]]
523 The main problem with intrusion detection by comparing file contents is the ability of an attacker
524 to redirect attempts to read a compromised file to a stored copy of the original. So, [[[i]]]sha256sum[[[/i]]] or
525 python could be changed to read [[[i]]]'/home/attacker/old/ps'[[[/i]]] when the argument was [[[i]]]'/bin/ps'[[[/i]]]. This would
526 foil any scheme that depends on entering file names in programs. An answer to this threat is to
527 read the bytes in files in as many ways as possible. Therefor, forcing an attacker to change many
528 files which itself would increase the probability of detection of the attack. The following command
529 will read the same (test) file, and generate identical hashes, in many different ways.
530 [[[/p]]][[[/LONG]]][[[pre make=example6]]]
531 # make: example6
532 # Example generating identical signatures of the same text file in different ways
533 $ dd if=signduterre.py 2>/dev/null | \\
534 python3 signduterre.py -v -d -s 1234567890abcdef -p poiuytrewq \\
535 --execute --execute-args='signduterre.py' \\
536 signduterre.py - \\
537 '$(cat $1)' \\
538 '$(grep "" $1)' \\
539 '$(awk "{print}" $1)' \\
540 '$(cut -f 1-100 $1)' \\
541 '$(perl -ane "{print \$_}" $1)' \\
542 '$(python3 -c "import sys;f=open(sys.argv[1]);sys.stdout.buffer.write(f.buffer.read())" $1;)' \\
543 '$(ruby -e "f=open(ARGV[0]);print f.read();" $1;)'
544 [[[/pre]]][[[LONG]]][[[p]]]
545 These "commands" do not always return the same bytes (awk), or any bytes at all (grep), from a text
546 file as when used with a binary file. However, if the commands can print the bytes unaltered, the
547 signatures will be identical. That is, the following arguments will work on a binary file:
548 [[[/p]]][[[/LONG]]][[[pre make=example6]]]
549 # make: example6
550 # Example generating identical signatures of the same file in different ways, now for binary files
551 $ dd if=/bin/bash 2>/dev/null | \\
552 python3 signduterre.py -v -d -s 1234567890abcdef -p poiuytrewq \\
553 --execute --execute-args='/bin/bash' \\
554 /bin/bash - \\
555 '$(cat $1)' \\
556 '$(perl -ane "{print \$_}" $1)' \\
557 '$(python3 -c "import sys;f=open(sys.argv[1]);sys.stdout.buffer.write(f.buffer.read())" $1;)' \\
558 '$(ruby -e "f=open(ARGV[0]);print f.read();" $1;)'
559 [[[/pre]]][[[LONG]]][[[p]]]
560 Will generate the same identical signatures for [[[i]]]/bin/bash[[[/i]]], [[[i]]]STDIN[[[/i]]], [[[i]]]'$(cat /bin/bash)'[[[/i]]] etc.
561 There are obviously many more ways to read out the bytes from the disk or memory. The main point
562 being that it should be difficult to predict for an attacker which commands must be compromised
563 to hide changes in the system.
564 [[[/p]]][[[/long]]][[[p]]]
565 In case of a real compromised system, it is conceivable that the signatures will need to be checked using known
566 good statically linked programs, eg, cat or dd from a cyptographically secured container like ecryptfs or an
567 encrypted loopback device. An existing signature can be tested against such statically linked programs using
568 the "--file-source '$(<cmd>)'" option. In this option, the plain file path will be substituted for every
569 occurence of the string '{}' in the command. If no '{}' is present in the command, the file will simply be
570 appended to the command. So, '$(/bin/dd if=)' is equivalent to '$(/bin/dd if={})' and '$(/bin/cat )' is
571 equivalent to '$(/bin/cat {})'. Note the trailing space in '$(/bin/cat )'.
572 [[[/p]]][[[pre make=example7]]]
573 # make: example7
574 # Create standard signature
575 $ python3 signduterre.py --passphrase SUGGESTED --salt SUGGESTED --detailed-view --verbose \\
576 `which python3 bash ps ls find stat lsof` 2> test-20090825_14_48-23.pwd > test-20090825_14_48-23.sdt
577 # Standard check
578 $ python3 signduterre.py --passphrase test-20090825_14_48-23.pwd --check-file test-20090825_14_48-23.sdt -v
579 # Example generating identical signatures checking with --file-source $(dd if=)
580 $ python3 signduterre.py --passphrase test-20090825_14_48-23.pwd --check-file test-20090825_14_48-23.sdt -v \\
581 --execute --file-source '$(dd if=)'
582 # Example generating identical signatures checking with --file-source $(cat ) (note the space between 'cat' and ')')
583 $ python3 signduterre.py --passphrase test-20090825_14_48-23.pwd --check-file test-20090825_14_48-23.sdt -v \\
584 --execute --file-source '$(cat )'
585 # Example generating identical signatures checking with --file-source $(perl -e "{open(F, \"<{}\");print <F>;};)
586 $ python3 signduterre.py --passphrase test-20090825_14_48-23.pwd --check-file test-20090825_14_48-23.sdt -v \\
587 --execute --file-source '$(perl -e "{open(F, \\"<{}\\");print <F>;};")'
588 [[[/pre]]][[[p]]]
589 The integrity of a running 'cat' command can be checked with module proc_PID that will create a signature of all
590 files loaded with the 'cat' command from the information in the /proc/<pid>/maps file using the inode numbers
591 (sudo only). Debugfs will read the blocks directly from the medium using the inode tables without using the filenames.
592 [[[/p]]][[[pre make=sudo2]]]
593 # make: sudo2
594 # Use module proc_PID to check the integrety of 'cat' and all libraries loaded with it
595 # Check out the workings of proc_PID.py with '$ python3 proc_PID.py'
596 # The actual output of the module used in the signature can be inspected with --print-textdump
597 $ sudo python3 signduterre.py -p poiuytrewq --salt SUGGESTED --detailed-view \\
598 --verbose --execute -u root -o test-proc_PID.sdt --import proc_PID \\
599 '@(return proc_PID.paths("cat","inode"))' '@(return proc_PID.fileSHA("cat", mainprefix))' \\
600 '@(return proc_PID.inodeSHA("cat", "", mainprefix))' '@(return proc_PID.mapsSHA("cat", mainprefix))'
601 # Check the results
602 $ sudo python3 signduterre.py -p poiuytrewq --detailed-view --verbose --execute -u root \\
603 --check-file test-proc_PID.sdt --import proc_PID
604 [[[/pre]]][[[p]]]
605 The examples can be run as a makefile using make. Use one of the following commands:
606 [[[/p]]][[[pre]]]
607 # General examples, use them all
608 python3 signduterre.py --manual-make |make -f - example
609 # Linux specific examples using the second procfs example
610 python3 signduterre.py --manual-make |make -f - procfs2
611 # Examples requiring sudo, using first
612 python3 signduterre.py --manual-make | sudo make -f - sudo1
613 [[[/pre]]][[[p]]]
614 Known Bugs:
615 [[[/p]]][[[p]]]
616 - Reading files from STDIN (-) does not work if ssh:// has been used before as input for,
617 eg, file arguments, --check-file or --passphrase
618 [[[/p]]][[[pre make=sshbug1]]]
619 # make: sshbug1
620 # '-' stdin before ssh:// is fine
621 $ dd if=/bin/ps 2>/dev/null | python3.0 signduterre.py -edv -p SUGGESTED -s SUGGESTED \
622 /bin/ps - ssh://`whoami`@localhost/bin/ps
623 # '-' stdin after ssh:// FAILs
624 $ dd if=/bin/ps 2>/dev/null | python3.0 signduterre.py -edv -p SUGGESTED -s SUGGESTED \
625 /bin/ps ssh://`whoami`@localhost/bin/ps -
626 [[[/pre]]][[[p]]]
627 - Reading URLs as file arguments should work when Python treats URLs identical
628 to file descriptors. For the technically inclined:
629 when:[[[br /]]]
630 [[[tt]]]with urllib.request.urlopen(url) as f:[[[/tt]]][[[br /]]]
631 works, URLs can be entered where ever file paths can be entered..
632 [[[/p]]][[[/body]]][[[/html]]]
635 license = """
636 Signature-du-Terroir
637 Construct a signature of the installed software state or check a previously made signature.
639 copyright 2009, R.J.J.H. van Son
641 This program is free software: you can redistribute it and/or modify
642 it under the terms of the GNU General Public License as published by
643 the Free Software Foundation, either version 3 of the License, or
644 (at your option) any later version.
646 This program is distributed in the hope that it will be useful,
647 but WITHOUT ANY WARRANTY; without even the implied warranty of
648 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
649 GNU General Public License for more details.
651 You should have received a copy of the GNU General Public License
652 along with this program. If not, see <http://www.gnu.org/licenses/>.
653 """;
655 # Note that only release notes are put here
656 # See git repository for detailed change comments:
657 # git clone git://repo.or.cz/signduterre.git
658 # http://repo.or.cz/w/signduterre.git
659 releasenotes = """
660 20090826 - Added [<start>:<end>] byte slices for every argument
661 20090825 - Added --source-file $(cmd) as substitute file readers
662 20090820 - Release v0.6RC
663 20090820 - Added extensibility, or plugins, with functional @(python code) execution
664 20090817 - Replaced --print-hexdump by --print-textdump
665 20090817 - Implemented ssh:// with ${ENV}
666 20090814 - Release v0.5RC
667 20090811 - Implemented ssh tunnel for commands
668 20090810 - Added --file-source=PATH option
669 20090810 - Added ssh tunnel for all file i/o (ssh://...)
670 20090807 - DIFFERENT became FAIL in --check-file
671 20090730 - Release v0.4
672 20090724 - Added '--all-salts-pattern' and HTML formatting in manual
673 20090723 - Added URL support for all files. Does not yet work due to bug in Python 3.0
674 20090723 - Added '-' for STDIN
675 20090717 - Added --execute-args
676 20090716 - Release v0.3
677 20090713 - Added --quiet option
678 20090712 - moved from /dev/random to /dev/urandom
679 20090702 - Replaced -g with -p SUGGESTED[=N]
680 20090702 - Generating and testing lists of random salts
681 20090701 - Release v0.2
682 20090630 - Generating and testing random passphrases
683 20090630 - --execute works on $(cmd) only, nlinks in ?path and ? implied for directories
684 20090630 - Ported to Python 3.0
686 20090628 - Release v0.1b
687 20090628 - Added release-notes
689 20090626 - Release v0.1a
690 20090626 - Initial commit to Git
691 """;
693 #############################################################################
695 # IMPORT & INITIALIZATION #
697 #############################################################################
699 import sys;
700 import os;
701 import subprocess;
702 import stat;
703 import subprocess;
704 # if sys.stdout.isatty(): import readline;
705 import binascii;
706 import hashlib;
707 import re;
708 import time;
709 from optparse import OptionParser;
710 import base64;
711 import random;
712 import struct;
713 import urllib.request;
714 import urllib.error;
716 # Limit the characters that can be used in $(cmd) commands
717 # Only allow the escape of '$'
718 not_allowed_chars = re.compile('([^\w\ \.\/\"\|\;\:\,\-\$\[\]\{\}\(\)\@\`\!\*\=\\\\\<\>]|([\\\\]+([^\$\"\\\\]|$)))');
720 programname = "Signature-du-Terroir";
721 version = "0.6RC";
724 # Open files or pipes for in/output, use mode = 'b' if binary is needed
725 def open_infile(filename, mode):
726 if filename == '-':
727 return sys.stdin;
728 elif filename.lower().find('ssh://') > -1:
729 match = re.search('(?i)ssh://([^/]+)/(.*)$', filename);
730 tunnel_command = 'ssh '+match.group(1)+' "dd if=/'+match.group(2)+' "';
731 if 'b' in mode:
732 pipe = subprocess.Popen(tunnel_command, shell=True, stdout=subprocess.PIPE);
733 else:
734 pipe = subprocess.Popen(tunnel_command, shell=True, stdout=subprocess.PIPE, universal_newlines=True);
735 return pipe.stdout;
736 elif filename.find('://') > -1:
737 print("URL:", filename, file=current_private);
738 return urllib.request.urlopen(filename);
739 else:
740 if not os.path.isfile(filename):
741 print(filename, "does not exist", file=sys.stderr)
742 quit();
743 return open(filename, mode);
745 def open_outfile(filename, mode):
746 if filename == '-':
747 return sys.stdout;
748 elif filename.lower().find('ssh://') > -1:
749 match = re.search('(?i)ssh://([^/]+)/(.*)$', filename);
750 tunnel_command = 'ssh '+match.group(1)+' "dd of=/'+match.group(2)+' "';
751 if 'b' in mode:
752 pipe = subprocess.Popen(tunnel_command, shell=True, stdin=subprocess.PIPE);
753 else:
754 pipe = subprocess.Popen(tunnel_command, shell=True, stdin=subprocess.PIPE, universal_newlines=True);
755 return pipe.stdin;
756 elif filename.find('://') > -1:
757 print("URL:", filename, file=current_private);
758 return urllib.request.urlopen(filename);
759 else :
760 return open(filename, mode);
762 current_outfile = sys.stdout;
763 current_private = sys.stderr;
765 # Determine which kind of argument you have
766 def arg_is_shell_command (argument):
767 return_value = False;
768 if argument.startswith('$(') and argument.endswith(')'):
769 return_value = argument[2:-1];
770 return return_value;
772 def arg_is_env (argument): # -> Env/False
773 return_value = False;
774 if arg_is_shell_command(argument): return return_value;
775 match = re.match(r'\$\{?([^\}]+)\}?$', argument);
776 if match != None:
777 return_value = match.group(1);
778 return return_value;
780 def arg_is_python_script (argument): # -> Script/False
781 return_value = False;
782 if argument.startswith('@(') and argument.endswith(')'):
783 return_value = argument[2:-1];
784 return return_value;
786 def arg_is_hidden (argument): # -> Exposed arg/False
787 return_value = False;
788 if argument.startswith('[') and argument.endswith(']'):
789 return_value = argument[1:-1];
790 return return_value;
792 def arg_is_tunnel (argument): # -> True/False
793 return argument.find('ssh://') > -1;
795 def arg_is_stat (argument): # -> Path/False
796 return_value = False;
797 if argument.startswith('?'):
798 return_value = argument[1:];
799 return return_value;
801 def arg_is_stdin (argument): # -> True/False
802 return argument == '-';
804 def arg_is_URL (argument): # -> True/False
805 return re.search(r'\://', argument) and not arg_is_tunnel(argument);
807 def arg_is_dir (argument): # -> True/False
808 return os.path.isdir(argument);
810 # Plain files are defined as NOT something else
811 def arg_is_plain_file (argument): # -> True/False
812 not_file = False;
813 not_file = not_file or arg_is_env(argument)
814 not_file = not_file or arg_is_shell_command (argument);
815 not_file = not_file or arg_is_python_script (argument);
816 not_file = not_file or arg_is_tunnel (argument);
817 not_file = not_file or arg_is_stdin (argument);
818 # not_file = not_file or arg_is_URL (argument);
819 not_file = not_file or arg_is_dir (argument);
820 not_file = not_file or arg_is_stat (argument);
822 return not not_file;
824 # Supportive functions
825 # Convert Hexadecimal, 0XFFFF, Octal, 0o7777, Binary, 0B111, and decimal, 9999, strings to int
826 def convertString2Int (number):
827 result = 0;
828 # If this is a number
829 if number > ' ' and re.search(r'(?i)[^oxb0-9A-F]', number) == None:
830 # Is it hexadecimal?
831 if number.upper().startswith('0X') or re.search(r'(?i)[A-F]', number) != None:
832 result=int(number, 16);
833 # Is it octal
834 elif number.upper().startswith('0O'):
835 result=int(number, 8);
836 # Is it binary?
837 elif number.upper().startswith('0B'):
838 result=int(number, 2);
839 # Then it must be decimal
840 else:
841 result = int(number);
842 return result;
844 #############################################################################
846 # OPTION HANDLING #
848 #############################################################################
850 parser = OptionParser()
851 parser.add_option("-s", "--salt", metavar="HEX",
852 dest="salt", default=False,
853 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).")
854 parser.add_option("-a", "--all-salts-pattern",
855 dest="allsalts", default=False, action="store_true",
856 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.")
857 parser.add_option("-p", "--passphrase", metavar="TEXT",
858 dest="passphrase", default=False,
859 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).")
860 parser.add_option("-c", "--check-file",
861 dest="check", default=False, metavar="FILE",
862 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.")
863 parser.add_option("-i", "--input-file",
864 dest="input", default=False, metavar="FILE",
865 help="Use names from FILE or STDIN ('-'), use one filename per line.")
866 parser.add_option("-o", "--output-file",
867 dest="output", default=False, metavar="FILE",
868 help="Print to FILE instead of STDOUT.")
869 parser.add_option("--file-source",
870 dest="filesource", default=False, metavar="PATH",
871 help="Read all files from PATH. The PATH-string is prepended to every plain file-path that is read for a signature. Remote files can be checked with 'ssh://<user>@<host>[/path]'. "
872 + "A shell command that prints out the file can be entered as '$(<cmd>)'. The filepath will be substituted for any '{}' string in the command, or appended tot the command (without white-space). "
873 + "The option overrules any File source specification in the --check-file.")
874 parser.add_option("-P", "--Private-file",
875 dest="private", default=False, metavar="FILE",
876 help="Print private information (passwords etc.) to FILE instead of STDERR.")
877 parser.add_option("-u", "--user",
878 dest="user", default="nobody", metavar="USER",
879 help="Execute $(cmd) as USER, default 'nobody' (root/sudo only)")
880 parser.add_option("-S", "--Status",
881 dest="status", default=False, action="store_true",
882 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)")
883 parser.add_option("--Status-values",
884 dest="statusvalues", default="fmidugs", metavar="MODE",
885 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).")
886 parser.add_option("-t", "--total-only",
887 dest="total", default=False, action="store_true",
888 help="Only print the total hash, unsets --detailed-view (default True)")
889 parser.add_option("-d", "--detailed-view",
890 dest="detail", default=False, action="store_true",
891 help="Print hashes of individual files, is unset by --total-only (default False)")
892 parser.add_option("-e", "--execute",
893 dest="execute", default=False, action="store_true",
894 help="Interpret $(cmd) (default False)")
895 parser.add_option("--execute-args",
896 dest="executeargs", default='', metavar="ARGS",
897 help="Arguments for the $(cmd) commands ($1 ....)")
898 parser.add_option("-n", "--no-execute",
899 dest="noexecute", default=False, action="store_true",
900 help="Explicitely do NOT Interpret $(cmd)")
901 parser.add_option("--import",
902 dest="importfile", default='', metavar="FILE",
903 help="Import python modules (comma separated list)")
904 parser.add_option("--print-textdump",
905 dest="printtextdump", default=False, action="store_true",
906 help="Print printable character+hexadecimal dump of input bytes to STDERR for debugging purposes")
907 parser.add_option("--message",
908 dest="message", default='', metavar="TEXT",
909 help="Add a comment message about the test")
910 parser.add_option("-m", "--manual",
911 dest="manual", default=False, action="store_true",
912 help="Print a short version of the manual and exit")
913 parser.add_option("--manual-long",
914 dest="manuallong", default=False, action="store_true",
915 help="Print the long version of the manual and exit")
916 parser.add_option("--manual-html",
917 dest="manualhtml", default=False, action="store_true",
918 help="Print the manual in HTML format and exit")
919 parser.add_option("--manual-make",
920 dest="manualmake", default=False, action="store_true",
921 help="Print the examples in the manual as a makefile and exit")
922 parser.add_option("-r", "--release-notes",
923 dest="releasenotes", default=False, action="store_true",
924 help="Print the release notes and exit")
925 parser.add_option("-l", "--license",
926 dest="license", default=False, action="store_true",
927 help="Print license text and exit")
928 parser.add_option("-v", "--verbose",
929 dest="verbose", default=False, action="store_true",
930 help="Print more information on output")
931 parser.add_option("-q", "--quiet",
932 dest="quiet", default=False, action="store_true",
933 help="Print minimal information (hide filenames). If the output is used with --check-file, the command line options and arguments must be repeated.")
935 (options, check_filenames) = parser.parse_args();
938 # Start with opening any non-default output files
939 my_output = False;
940 if options.output:
941 current_outfile = open_outfile(options.output, 'w');
942 my_output = options.output;
944 my_private = False;
945 if options.private:
946 current_private = open_outfile(options.private, 'w');
947 my_private = options.private;
949 print("# Program: "+programname + " version " + version, file=current_outfile);
950 print("#", time.strftime("%Y/%m/%d %H:%M:%S", time.localtime()), "("+time.tzname[0]+")\n", file=current_outfile);
952 # Print license
953 if options.license:
954 print (license, file=sys.stderr);
955 exit(0);
956 # Print manual
957 if options.manual or options.manuallong:
958 cleartext_manual = re.sub(r"(?i)\[\[\[\s*(/?)\s*LONG\s*\]\]\]", r'[[[\1LONG]]]', manual);
959 if not options.manuallong:
960 currentstart = cleartext_manual.find('[[[LONG]]]');
961 while currentstart > -1:
962 currentend = cleartext_manual.find('[[[/LONG]]]', currentstart)+len('[[[/LONG]]]');
963 (firstpart, secondpart) = cleartext_manual.split(cleartext_manual[currentstart:currentend]);
964 cleartext_manual = firstpart+secondpart;
965 currentstart = cleartext_manual.find('[[[LONG]]]');
966 htmltags = re.compile('\[\[\[[^\]]*\]\]\]');
967 cleartext_manual = htmltags.sub('', cleartext_manual);
968 print (cleartext_manual, file=sys.stdout);
969 exit(0);
970 # Print HTML manual
971 if options.manualhtml:
972 protleftanglesbracks = re.compile('\<');
973 protrightanglesbracks = re.compile('\>');
974 leftanglesbracks = re.compile('\[\[\[');
975 rightanglesbracks = re.compile('\]\]\]');
976 html_manual = re.sub(r"(?i)\[\[\[\s*(/?)\s*LONG\s*\]\]\]", '', manual);
977 html_manual = protleftanglesbracks.sub('&lt;', html_manual);
978 html_manual = protrightanglesbracks.sub('&gt;', html_manual);
979 html_manual = leftanglesbracks.sub('<', html_manual);
980 html_manual = rightanglesbracks.sub('>', html_manual);
981 print (html_manual, file=sys.stdout);
982 exit(0);
983 # Print manual examples as makefile
984 if options.manualmake:
985 make_manual = re.sub("\$ ", "\t", manual);
986 make_manual = re.sub("\#", "\t#", make_manual);
987 make_manual = re.sub(r"\\\s*\n", '', make_manual);
988 make_manual = re.sub(r"\$", r'$$', make_manual);
989 # Protect "single" [ brackets
990 make_manual = re.sub(r"(^|[^\[])\[([^\[]|$)", r"\1&#91;\2", make_manual);
991 extrexamples = re.compile(r"\[\[\[pre\s+make\=?([a-zA-Z]*)([0-9]*)\s*\]\]\]\n([^\[]*)\n\[\[\[/pre\s*\]\]\]", re.IGNORECASE|re.MULTILINE|re.DOTALL);
992 exampleiter = extrexamples.finditer(make_manual);
993 makefile_list = [];
994 group_list={};
995 group_list['all'] = "all: ";
996 for match in exampleiter:
997 # We had to convert any '[' in the command. Now convert them back.
998 command_text = re.sub(r"\&\#91\;", '[', match.group(3));
999 makefile_list.append(match.group(1)+match.group(2)+":\n"+command_text);
1000 if len(match.group(2)) > 0:
1001 if not match.group(1) in group_list.keys():
1002 group_list[match.group(1)] = match.group(1)+": ";
1003 group_list['all'] += match.group(1)+" ";
1004 group_list[match.group(1)] += match.group(1)+match.group(2)+" ";
1005 else:
1006 group_list['all'] += match.group(1)+" ";
1007 print("help: \n\t@echo 'Use \"make -f - "+re.sub(':', '', group_list['all'])+"\"'", file=sys.stdout);
1008 for group in group_list:
1009 print(group_list[group]+"\n", file=sys.stdout);
1010 makefile_list.sort()
1011 previous_cat = 'NOT A VALUE';
1012 for line in makefile_list:
1013 (category, commands) = line.split(':\n');
1014 if category != previous_cat:
1015 previous_cat = category;
1016 print("\n"+previous_cat+":", file=sys.stdout);
1017 print(commands, file=sys.stdout);
1018 # Clean option
1019 print("\nclean:\n\trm test-*.sdt test-*.pwd", file=sys.stdout);
1020 exit(0);
1021 # Print release notes
1022 if options.releasenotes:
1023 print ("Version: "+version, file=sys.stderr);
1024 print (releasenotes, file=sys.stderr);
1025 exit(0);
1027 my_salt = options.salt;
1028 my_allsalts = options.allsalts;
1029 my_passphrase = options.passphrase;
1030 my_check = options.check;
1031 my_status = options.status;
1032 my_statusvalues = options.statusvalues;
1033 my_verbose = options.verbose and not options.quiet;
1034 my_quiet = options.quiet;
1035 execute = options.execute;
1036 execute_args = options.executeargs;
1037 if options.noexecute: execute = False;
1038 input_file = options.input;
1039 my_filesource = options.filesource;
1040 if my_filesource: print("File source: '"+my_filesource+"'\n", file=current_outfile);
1041 my_message = options.message;
1042 my_importfiles = options.importfile;
1043 if my_importfiles:
1044 print("Import: '"+my_importfiles+"'\n", file=current_outfile);
1046 # Set total-only with the correct default
1047 total_only = True;
1048 total_only = not options.detail;
1049 if options.total: total_only = options.total;
1050 if my_allsalts: total_only = my_allsalts; # All alts pattern only sensible with total-only
1051 if my_check: total_only = False;
1053 my_user = options.user;
1054 # Things might be executed as another user
1055 user_change = '';
1056 if os.getuid() == 0:
1057 user_change = 'sudo -H -u '+my_user+' ';
1058 if not my_quiet: print("User: "+my_user, file=current_outfile);
1060 # Execute option
1061 if execute:
1062 text_execute = "True";
1063 else:
1064 text_execute = "False";
1066 if execute and not my_quiet:
1067 print("Execute system commands: "+text_execute+"\n", file=current_outfile);
1068 if execute_args != '': print("Execute arguments: '"+execute_args+"'\n", file=current_outfile);
1070 # --quiet option
1071 if my_quiet: print("Quiet: True\n", file=current_outfile);
1073 # --Status-values option
1074 if my_statusvalues != 'fmidugs': print("Status-values: '"+my_statusvalues+"'\n", file=current_outfile);
1076 # --message option
1077 if len(my_message) > 0: print("Message: '''"+my_message+"'''\n", file=current_outfile);
1079 #############################################################################
1081 # ARGUMENT PROCESSING #
1083 #############################################################################
1085 # Measure time intervals
1086 start_time = time.time();
1088 dev_random = open("/dev/urandom", 'rb');
1090 # Read the check file
1091 passphrase_list = [];
1092 salt_list = [];
1093 check_hashes = {};
1094 total_hash_list = [];
1095 if my_check:
1096 highest_arg_used = 0;
1097 print("# Checking: "+my_check+"\n", file=current_outfile);
1098 arg_list = check_filenames;
1099 check_filenames = [];
1100 with open_infile(my_check, 'r') as c:
1101 for line in c:
1102 match = re.search(r"Execute system commands:\s+(True|False)", line);
1103 if match != None:
1104 # Uncomment the next line if you want automatic --execute from the check-file (DANGEROUS)
1105 # execute = match.group(1).upper() == 'TRUE';
1106 continue;
1108 match = re.search(r"Execute arguments:\s+\'([\w\$\s\-\+\/]*)\'", line);
1109 if match != None:
1110 execute_args = match.group(1);
1111 continue;
1113 match = re.search(r"Quiet:\s+(True|False)", line);
1114 if match != None:
1115 my_quiet = match.group(1).upper() == 'TRUE';
1116 if my_quiet: my_verbose = False;
1117 continue;
1119 match = re.search(r"File source:\s+\'([\w\ \-\+\`\"\[\]\{\}\@\$\=\:\/\(\)\<\>\.\,\;\?\*\&\^\%\\]+)\'", line);
1120 if not my_filesource and match != None:
1121 my_filesource = match.group(1);
1122 continue;
1124 match = re.search(r"Salt\s*\+\s*TOTAL HASH\s*\:\s+\'([\w]*)\'\s+\'([\w]*)\'", line);
1125 if match != None:
1126 salt_list.append(match.group(1));
1127 total_hash_list.append(match.group(2));
1128 my_allsalts = True; # Salt+TOTAL HASH imples all-salts-pattern
1129 continue;
1131 match = re.search(r"Salt\:\s+\'([\w]*)\'", line);
1132 if match != None:
1133 salt_list.append(match.group(1));
1134 continue;
1136 match = re.search("Salt\s*\+\s*TOTAL HASH\s*\:\s+\'([\w]+)\'\s+\'([a-f0-9]+)\'", line);
1137 if match != None:
1138 salt_list.append(match.group(1));
1139 total_hash_list.append(match.group(2));
1140 continue;
1142 match = re.search("User\:\s+\'([\w]*)\'", line);
1143 if match != None:
1144 # Uncomment the next line if you want automatic --user from the check-file (DANGEROUS)
1145 # my_user = match.group(1);
1146 continue;
1148 match = re.search("Passphrase\:\s+\'([^\']*)\'", line);
1149 if match != None:
1150 passphrase_list.append(match.group(1));
1151 continue;
1153 match = re.search("Status-values\:\s+\'([\w]*)\'", line);
1154 if match != None:
1155 my_statusvalues = match.group(1);
1156 continue;
1158 match = re.search(r"Import\:\s+\'([^\']*)\'", line);
1159 if match != None:
1160 my_importfiles = match.group(1);
1161 continue;
1163 match = re.search("Message\:\s+\'\'\'(.*)$", line);
1164 if match != None:
1165 my_message = match.group(1);
1166 my_message = my_message[0:my_message.find("'''")];
1167 print("Message: '''"+my_message+"'''\n", file=current_outfile);
1168 continue;
1170 match = re.search("^\s*([a-f0-9]+)\s+\*(TOTAL HASH)\s*$", line)
1171 if match != None:
1172 total_hash_list.append(match.group(1));
1173 continue;
1175 match = re.search("^\s*([a-f0-9\-]+)\s+\*\[([0-9]+)\]\s*$", line)
1176 if match != None:
1177 filenumber = int(match.group(2));
1178 if filenumber > highest_arg_used: highest_arg_used = filenumber;
1179 # Watch out, arguments count from 0
1180 check_filenames.append(arg_list[filenumber - 1]);
1181 check_hashes['['+match.group(2)+']'] = match.group(1);
1182 continue;
1184 match = re.search("^\s*([a-f0-9\-]+)\s+\*(.*)\s*$", line)
1185 if match != None:
1186 check_filenames.append(match.group(2));
1187 # Catch --execute error as early as possible
1188 if match.group(2).startswith('$(') and not execute:
1189 error_message = "Executable argument \'"+match.group(2)+"\' only allowed with the --execute flag";
1190 print (error_message, file=sys.stderr);
1191 if not sys.stdout.isatty(): print(error_message, file=current_outfile);
1192 exit(0);
1193 check_hashes[match.group(2)] = match.group(1);
1194 continue;
1195 for i in range(highest_arg_used, len(arg_list)):
1196 check_filenames.append(arg_list[i]);
1197 check_hashes['['+str(i+1)+']'] = (64*'-');
1199 # Read input-file
1200 if input_file:
1201 with open_infile(input_file, 'r') as i:
1202 for line in i:
1203 # Clean up filename
1204 current_filename = re.sub('[^\w\-\.\/\$\{\(\)\}\?\[\]]', '', line);
1205 check_filenames.append(current_filename);
1206 if my_check: check_hashes['['+str(i+1)+']'] = (64*'-');
1208 stat_list = [];
1209 for x in check_filenames:
1210 if os.path.isdir(x):
1211 x = '?'+x;
1212 if my_status and not x.startswith(('?', '$')):
1213 stat_list.append('?'+x);
1214 stat_list.append(x);
1215 check_filenames = stat_list;
1217 # Seed Pseudo Random Number Generator
1218 seed = dev_random.read(16);
1219 random.seed(seed);
1221 # Read suggested salts from /dev/(u)random if needed
1222 if my_salt:
1223 if my_salt.startswith('SUGGESTED'):
1224 N=1;
1225 match = re.search("([0-9][0-9]*)$", my_salt);
1226 if match != None:
1227 N = int(match.group(1));
1228 for i in range(0,N):
1229 salt = dev_random.read(8);
1230 salt_list.append(str(binascii.hexlify(salt), 'ascii'));
1231 else:
1232 salt_list.append(my_salt);
1233 elif len(salt_list) == 0:
1234 salt = dev_random.read(8);
1235 sys.stderr.write("Enter salt (suggest \'"+str(binascii.hexlify(salt), 'ascii')+"\'): ");
1236 new_salt = input();
1237 if not new_salt: new_salt = str(binascii.hexlify(salt), 'ascii');
1238 salt_list.append(new_salt);
1240 # If not combining salts with TOTAL HASH, print salts now
1241 if not my_allsalts:
1242 for my_salt in salt_list:
1243 print("Salt: \'"+my_salt+"\'", file=current_outfile);
1245 # Get passphrase
1246 if my_passphrase and(my_passphrase == '-' or my_passphrase.find("://") > -1 or os.path.isfile(my_passphrase)):
1247 with open_infile(my_passphrase, 'r') as file:
1248 for line in file:
1249 match = re.search("Passphrase\:\s+\'([^\']*)\'", line);
1250 if match != None:
1251 passphrase_list.append(match.group(1));
1252 elif not my_passphrase and len(passphrase_list) == 0:
1253 suggest_passphrase = dev_random.read(16);
1254 suggest_string = "";
1255 if not my_check:
1256 suggest_string = "(suggest \'"+str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=')+"\')";
1257 sys.stderr.write("Enter passphrase"+suggest_string+": ");
1258 # How kan we make this unreadable on input?
1259 current_passphrase = input();
1260 if not current_passphrase:
1261 current_passphrase = str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=');
1262 print("Passphrase: \'"+current_passphrase+"\'", file=current_private);
1263 passphrase_list.append(current_passphrase);
1264 elif my_passphrase.startswith('SUGGESTED'):
1265 N = 1;
1266 match = re.search("([0-9][0-9]*)$", my_passphrase);
1267 if match != None:
1268 N = int(match.group(1));
1269 j = int(random.random()*N);
1270 for i in range(0, N):
1271 suggest_passphrase = dev_random.read(16);
1272 current_passphrase = str(base64.b64encode(suggest_passphrase), 'ascii').rstrip('=');
1273 print("Passphrase: \'"+current_passphrase+"\'", file=current_private);
1274 passphrase_list.append(current_passphrase);
1275 else:
1276 passphrase_list.append(my_passphrase);
1278 selected_salt = 1;
1279 fail_fraction = 0.5;
1280 if not my_check:
1281 if len(passphrase_list) > 1:
1282 j = int(random.random()*len(passphrase_list));
1283 passphrase_list = [passphrase_list[j]];
1284 print("# Selected passphrase:", j+1, file=current_private);
1285 if len(salt_list) > 1:
1286 j = int(random.random()*len(salt_list));
1287 # Make sure at least 1 salt will match and print the selection if only one is used
1288 selected_salt = j+1;
1289 if not my_allsalts:
1290 salt_list = [salt_list[selected_salt-1]];
1291 print("# Selected salt:", selected_salt, file=current_private);
1292 else:
1293 salt_N = len(salt_list);
1294 fail_fraction = (salt_N/2.0)/(salt_N - 1);
1295 else:
1296 fail_fraction = 0;
1298 # Close /dev/(u)random
1299 dev_random.close;
1301 #############################################################################
1303 # SIGNATURE CREATION AND CHECKING #
1305 #############################################################################
1307 end_time = time.time();
1308 print("# Preparation time:", end_time - start_time, "seconds\n", file=current_outfile);
1310 pnum = 1;
1311 snum = 1;
1312 corrpnum = 0;
1313 corrsnum = 0;
1314 matched_salt_pattern = -1;
1315 salt_pattern_number = -1;
1317 for my_passphrase in passphrase_list:
1318 snum = 1;
1319 # Initialize salt pattern
1320 if my_allsalts:
1321 salt_pattern_number = 0;
1322 current_salt_power = 1;
1324 for my_salt in salt_list:
1325 print("# Start signature: ", end='', file=current_outfile);
1326 if len(passphrase_list) > 1: print("passphrase -", pnum, end='', file=current_outfile);
1327 if len(salt_list) > 1: print(" salt -", snum, end='', file=current_outfile);
1328 print("", file=current_outfile);
1330 # Should everything be printed?
1331 print_verbose = my_verbose and not (my_allsalts and snum > 1);
1333 file_argnum = 0;
1334 start_time = time.time();
1335 # Construct the passphrase hash
1336 passphrase = hashlib.sha256();
1338 passphrase.update(bytes(my_salt, encoding='ascii'));
1339 passphrase.update(bytes(my_passphrase, encoding='ascii'));
1341 # Create prefix which is a hash of the salt+passphrase
1342 prefix = passphrase.hexdigest();
1344 ##########################################
1346 # Create signature and write output #
1348 ##########################################
1350 totalhash = hashlib.sha256();
1351 totalhash.update(bytes(prefix, encoding='ascii'));
1352 for org_filename in check_filenames:
1353 # Create file hash object
1354 filehash = hashlib.sha256();
1355 filehash.update(bytes(prefix, encoding='ascii'));
1356 # Remove []
1357 filename = org_filename;
1358 if org_filename.startswith('[') and org_filename.endswith(']'):
1359 filename = filename[1:len(filename)-1];
1360 # Select input bytes to use for signature
1361 input_start = 0;
1362 input_length = 0;
1363 if filename.endswith(']'):
1364 input_offset = 0;
1365 interval = filename[filename.rfind('[')+1:len(filename)-1];
1366 filename = filename[0:filename.rfind('[')];
1367 (new_start, new_end, new_offset) = ('', '', '');
1368 # Add missing offset
1369 if re.search(r'\:[^\:]*\:', interval) == None: interval += ':0';
1370 if interval > ' ': (new_start, new_end, new_offset) = interval.split(':');
1371 input_start = convertString2Int(new_start);
1372 input_end = convertString2Int(new_end.lstrip('+'));
1373 input_offset = convertString2Int(new_offset);
1374 if new_end.startswith('+'):
1375 input_length = input_end;
1376 else:
1377 input_length = input_end - input_start;
1378 if input_start >=0 and input_offset > 0 : input_start += input_offset;
1380 # Preprocessing filename to include "external" file sources
1381 if my_filesource:
1382 # Insert different file reader as a shell command
1383 if arg_is_shell_command(my_filesource) and arg_is_plain_file(filename):
1384 current_source_command = arg_is_shell_command(my_filesource);
1385 if current_source_command.find('{}') > -1:
1386 current_source_command = re.sub(r'\{\}', filename, current_source_command);
1387 else:
1388 current_source_command += filename;
1389 filename = "$("+current_source_command+")";
1390 # Insert remote ssh:// if necessary and not allready a tunnel
1391 elif arg_is_tunnel(my_filesource) and not arg_is_tunnel(filename):
1392 if arg_is_env(filename) :
1393 match = re.search(r"(?i)ssh://([^/]+)", my_filesource);
1394 if match != None: filename = "${ssh://"+match.group(1)+"/"+arg_is_env(filename)+"}";
1395 elif arg_is_shell_command(filename):
1396 match = re.search(r"(?i)ssh://([^/]+)", my_filesource);
1397 if match != None: filename = "$(ssh://"+match.group(1)+"/"+arg_is_shell_command(filename)+")";
1398 elif arg_is_plain_file(filename):
1399 filename = my_filesource+filename;
1400 # Handle file tunnels: convert 'ssh://<user>@<host>/<path>' to
1401 # $(ssh://<user>@<host>/dd if=/<path>)
1402 if arg_is_tunnel(filename) and filename.startswith("ssh://"):
1403 match = re.search(r"(?i)ssh://([^/]+)/(.*)$", filename);
1404 if match != None:
1405 host = match.group(1);
1406 path = match.group(2);
1407 if arg_is_stat(path):
1408 print("Error: status not possible in tunnels - "+filename);
1409 else:
1410 if input_start > 0 or input_length > 0:
1411 filename = '$(ssh://'+host+'/dd if=/'+path+' bs=1 skip='+str(input_start)+' count='+str(input_length)+')';
1412 # The start and end have been dealt with, remove them
1413 input_start = 0;
1414 input_length = 0;
1415 else:
1416 filename = '$(ssh://'+host+'/dd if=/'+path+')';
1417 print(filename);
1418 # Use python @() constructs
1419 orig_command_filename = filename;
1420 if arg_is_python_script(filename):
1421 if not execute:
1422 error_message = "Executable argument \'"+filename+"\' only allowed with the --execute flag";
1423 print (error_message, file=sys.stderr);
1424 exit(1);
1425 # Check for dangerous constructs
1426 if re.search(r'\bimport\b', filename):
1427 print("Error: Import statement not allowed, please use --import option", file=sys.stderr);
1428 exit(1);
1429 # Construct a code wrapper around the user supplied code
1430 script_input = arg_is_python_script(filename);
1431 statement_string = "";
1432 if my_importfiles: statement_string += "import "+re.sub(',', ', ', my_importfiles)+';\nglobal mainprefix;\n';
1433 statement_string += "def sdt_exec_code():\n\t"+re.sub(r'\\n', "\n\t", re.sub(r'\\t', '\t', script_input));
1434 # Compile and execute code in a limited namespace
1435 user_code = compile(statement_string+"\nsdt_export_result = sdt_exec_code()\n", '<user code>', 'exec');
1436 userdict = {'sdt_export_result' : None};
1437 userdict['mainprefix'] = bytes(prefix, encoding='ascii');
1438 argvlist = [];
1439 argvlist.append(os.getpid());
1440 for value in execute_args.split():
1441 argvlist.append(value);
1442 userdict['argv'] = argvlist;
1443 # Execute code
1444 exec(user_code, userdict);
1445 # Use result
1446 b = None;
1447 if userdict['sdt_export_result']:
1448 b = userdict['sdt_export_result'];
1449 else:
1450 b = None;
1451 print("Error: '"+filename+"' did not deliver any result", file=sys.stderr);
1452 exit(1);
1453 if input_start > 0:
1454 b = b[input_start:];
1455 if input_length > 0:
1456 b = b[0:input_length];
1457 filehash.update(bytes(b, encoding='utf8'));
1458 if options.printtextdump: # For debugging commands
1459 print(str(b));
1460 # Use system variables and commands
1461 # ${ENV} environment variables
1462 if arg_is_env(filename):
1463 current_var = not_allowed_chars.sub(" ", arg_is_env(filename));
1464 if print_verbose:
1465 print("# echo $"+ current_var, file=current_outfile);
1466 # Redirect env query to other system
1467 if current_var.startswith("ssh://"):
1468 match = re.search(r"(?i)(ssh://[^/]+/)(.*)$", current_var);
1469 if match != None:
1470 current_ssh = match.group(1);
1471 current_command = r"echo -n ${"+match.group(2)+"}";
1472 filename = '$('+current_ssh+current_command+')';
1473 # Current system
1474 else:
1475 b = os.environ[current_var];
1476 if input_start > 0:
1477 b = b[input_start:];
1478 if input_length > 0:
1479 b = b[0:input_length];
1480 filehash.update(bytes(b, encoding='utf8'));
1481 if options.printtextdump: # For debugging commands
1482 print(str(b));
1483 # Commands $(command)
1484 if arg_is_shell_command(filename):
1485 if not execute :
1486 error_message = "Executable argument \'"+filename+"\' only allowed with the --execute flag";
1487 print (error_message, file=sys.stderr);
1488 exit(0);
1489 # Clean up command
1490 current_command = not_allowed_chars.sub(" ", arg_is_shell_command(filename));
1491 # Expand remote ssh://
1492 current_host = None;
1493 current_executable = None;
1494 current_args = execute_args;
1495 if current_command.startswith("ssh://"):
1496 match = re.search(r"(?i)ssh://([^/]+)/(.*)$", current_command);
1497 if match != None:
1498 current_host = match.group(1);
1499 current_command = match.group(2);
1500 # Protect $, ", and backslashes
1501 current_command = re.sub(r'\\', r'\\', current_command);
1502 current_command = re.sub(r'\$', r'\$', current_command);
1503 current_executable = "/usr/bin/ssh";
1504 current_args = re.sub(r'\$', r'\$', current_args);
1505 current_args = re.sub(r'\"', r'\"', current_args);
1506 # Create command line
1507 current_command_line = user_change+"bash --restricted -c \'"+current_command+"\' "+str(os.getpid())+" "+current_args;
1508 # Add ssh command
1509 if current_executable:
1510 current_command_line = re.sub(r'\"', r'\\"', current_command_line);
1511 current_command_line = current_executable+' '+current_host+' "'+current_command_line+'"';
1512 # Print command
1513 if print_verbose:
1514 print("#", current_command_line, file=current_outfile);
1515 # Spawn command and open a pipe to the output
1516 pipe = subprocess.Popen(current_command_line, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE);
1517 if input_start > 0:
1518 pipe.stdout.read(input_start);
1519 bytes_processed = 0;
1520 for b in pipe.stdout:
1521 if input_length > 0:
1522 if bytes_processed >= input_length:
1523 break;
1524 elif bytes_processed + len(b) <= input_length:
1525 bytes_processed += len(b);
1526 else:
1527 b = b[0:(input_length - bytes_processed)];
1528 bytes_processed = input_length;
1529 if type(b).__name__ == 'str':
1530 b = bytes(b, encoding='utf8');
1531 filehash.update(b);
1532 if options.printtextdump: # For debugging commands
1533 print(str(b));
1534 # See whether there was an error
1535 pipe.wait();
1536 if pipe.returncode:
1537 error_message = pipe.stderr.read();
1538 print('$('+current_command+')', "\n", str(error_message, encoding='UTF8'), file=sys.stderr);
1539 exit(pipe.returncode);
1540 # stat() meta information
1541 if arg_is_stat(filename):
1542 if not os.path.exists(arg_is_stat(filename)):
1543 print(filename, "does not exist", file=sys.stderr)
1544 quit();
1545 filestat = os.stat(arg_is_stat(filename));
1546 if my_statusvalues == "": my_statusvalues = 'fmidlugs'
1547 b = "";
1548 if 'f' in my_statusvalues:
1549 b += 'stat('+filename.lstrip('?')+') = '
1550 b += '[';
1551 if 'm' in my_statusvalues:
1552 b += 'st_mode='+str(oct(filestat.st_mode))+', ';
1553 if 'i' in my_statusvalues:
1554 b += 'st_ino='+str(filestat.st_ino)+', ';
1555 if 'd' in my_statusvalues:
1556 b += 'st_dev='+str(filestat.st_dev)+', '
1557 if 'l' in my_statusvalues:
1558 b += 'st_nlink='+str(filestat.st_nlink)+', '
1559 if 'u' in my_statusvalues:
1560 b += 'st_uid='+str(filestat.st_uid)+', '
1561 if 'g' in my_statusvalues:
1562 b += 'st_gid='+str(filestat.st_gid)+', '
1563 if 's' in my_statusvalues:
1564 b += 'st_size='+str(filestat.st_size)+', '
1565 if 'a' in my_statusvalues:
1566 b += 'st_atime='+str(filestat.st_atime)+', '
1567 if 't' in my_statusvalues:
1568 b += 'st_mtime='+str(filestat.st_mtime)+', '
1569 if 'c' in my_statusvalues:
1570 b += 'st_ctime='+str(filestat.st_ctime);
1572 b = b.rstrip(', ') + ']';
1573 # Not sure whether this makes sense at all
1574 if input_start > 0:
1575 b = b[input_start:];
1576 if input_length > 0:
1577 b = b[0:input_length];
1578 filehash.update(bytes(b, encoding='utf8'));
1579 if print_verbose:
1580 print ("# "+ b, file=current_outfile);
1581 if options.printtextdump: # For debugging commands
1582 print(str(b));
1584 # STDIN
1585 if arg_is_stdin(filename):
1586 total_bytes = 0;
1587 if input_start > 0:
1588 sys.stdin.buffer.read(input_start);
1589 bytes_processed = 0;
1590 for b in sys.stdin.buffer:
1591 if input_length > 0:
1592 if bytes_processed >= input_length:
1593 break;
1594 elif bytes_processed + len(b) <= input_length:
1595 bytes_processed += len(b);
1596 else:
1597 b = b[0:(input_length - bytes_processed)];
1598 bytes_processed = input_length;
1599 total_bytes += len(b);
1600 if type(b).__name__ == 'str':
1601 b = bytes(b, encoding='utf8');
1602 filehash.update(b);
1603 if options.printtextdump: # For debugging commands
1604 print(str(b));
1605 if total_bytes == 0:
1606 print("ERROR: No bytes read from STDIN. Processing aborted.", file=sys.stderr);
1607 quit(1);
1609 # Use plain file
1610 if arg_is_plain_file(filename):
1611 # Open and read the file
1612 with open_infile(filename, 'rb') as file:
1613 if input_start > 0:
1614 file.seek(input_start);
1615 bytes_processed = 0;
1616 for b in file:
1617 if input_length > 0:
1618 if bytes_processed >= input_length:
1619 break;
1620 elif bytes_processed + len(b) <= input_length:
1621 bytes_processed += len(b);
1622 else:
1623 b = b[0:(input_length - bytes_processed)];
1624 bytes_processed = input_length;
1625 if type(b).__name__ == 'str':
1626 b = bytes(b, encoding='utf8');
1627 filehash.update(b);
1628 if options.printtextdump: # For debugging commands
1629 print(str(b));
1631 # Print the signature of the current argument
1632 current_digest = filehash.hexdigest();
1633 print_name = org_filename;
1634 if my_quiet or arg_is_hidden(org_filename):
1635 file_argnum += 1;
1636 print_name = '['+str(file_argnum)+']';
1637 current_hash_line = current_digest+" *"+print_name
1638 # Add current signature to total signature (including the argument!)
1639 totalhash.update(bytes(current_hash_line, encoding='ascii'));
1641 # Be careful to use this ONLY after totalhash has been updated!
1642 if total_only:
1643 current_hash_line = (len(current_digest)*'-')+" *"+print_name;
1645 # Write output
1646 if not my_check:
1647 if not (my_quiet and total_only) and not (my_allsalts and snum > 1):
1648 print(current_hash_line, file=current_outfile);
1649 elif not (my_quiet or my_allsalts):
1650 if check_hashes[print_name] == (len(current_digest)*'-'):
1651 # Suppress redundant output of empty, ----, lines
1652 if snum <= 1 and pnum <= 1:
1653 print(check_hashes[print_name]+" *"+print_name, file=current_outfile);
1654 elif current_digest != check_hashes[print_name]:
1655 print("FAILED: "+current_hash_line, file=current_outfile);
1656 else:
1657 print("ok"+" *"+print_name, file=current_outfile);
1659 # Handle total hash
1660 current_total_digest = totalhash.hexdigest();
1661 # Write (in)correct salts with the TOTAL HASH
1662 if my_allsalts:
1663 output_salt = my_salt;
1664 j = random.random();
1665 # Randomly create an incorrect salt for failed output
1666 if not my_check:
1667 if j < fail_fraction and snum != selected_salt:
1668 salt = dev_random.read(8);
1669 output_salt = str(binascii.hexlify(salt), 'ascii');
1670 else:
1671 salt_pattern_number += current_salt_power;
1672 current_total_digest_line = "Salt+TOTAL HASH: '"+output_salt+"' '"+current_total_digest+"'";
1673 else: # Standard TOTAL HASH line
1674 current_total_digest_line = current_total_digest+" *"+"TOTAL HASH";
1675 end_time = time.time();
1676 print("# \n# Total hash - Time to completion:", end_time - start_time, "seconds", file=current_outfile);
1677 total_hash_num = 0;
1678 if my_allsalts: total_hash_num = snum-1; # Current TOTAL HASH number of more are used
1679 if not my_check:
1680 print(current_total_digest_line+"\n", file=current_outfile);
1681 elif current_total_digest != total_hash_list[total_hash_num]:
1682 if not my_allsalts: print("FAILED: "+current_total_digest_line+"\n", file=current_outfile);
1683 else:
1684 if my_allsalts: salt_pattern_number += current_salt_power; # Update salt bit pattern
1685 match_number = "";
1686 if len(passphrase_list) > 1 or len(salt_list): match_number = " #"
1687 if len(passphrase_list) > 1: match_number += " passphrase no: "+str(pnum);
1688 if len(salt_list) > 1: match_number += " salt no: "+str(snum);
1689 if not my_allsalts: print("OK"+" *"+"TOTAL HASH"+match_number+"\n", file=current_outfile);
1690 corrsnum = snum;
1691 corrpnum = pnum;
1692 snum += 1;
1693 if my_allsalts: current_salt_power *= 2; # Update current bit position in salt pattern
1694 if my_check and corrpnum == pnum: matched_salt_pattern = salt_pattern_number;
1695 pnum += 1;
1697 if my_check and len(passphrase_list) > 1:
1698 if corrpnum > 0:
1699 print("Passphrase entry:",corrpnum,"matched", file=current_outfile);
1700 else:
1701 print("No passphrase entry matched!", file=current_outfile);
1702 if my_check and (not my_allsalts) and len(salt_list) > 1:
1703 if corrpnum > 0:
1704 if corrsnum > 0:
1705 print("Salt entry:",corrsnum,"matched", file=current_outfile);
1706 else:
1707 print("No salt entry matched!", file=current_outfile);
1708 else:
1709 print("No entry matched", file=current_outfile);
1710 # Print salt bit patterns
1711 elif my_check and my_allsalts:
1712 print("Salt pattern number:", matched_salt_pattern, file=current_outfile);
1713 elif not my_check and my_allsalts:
1714 print("# Salt pattern number:", salt_pattern_number, file=current_private);
1716 # Close output files if necessary
1717 if my_output and my_output != '-':
1718 current_outfile.close();
1719 if my_private and my_private != '-':
1720 current_private.close();