OLD | NEW |
1 # -*- coding: utf-8 -*- | 1 # -*- coding: utf-8 -*- |
2 """Parser for Linux UTMP files.""" | 2 """Parser for Linux UTMP files.""" |
3 | 3 |
4 from __future__ import unicode_literals | 4 from __future__ import unicode_literals |
5 | 5 |
6 import os | |
7 import socket | |
8 | |
9 import construct | |
10 | |
11 from dfdatetime import posix_time as dfdatetime_posix_time | 6 from dfdatetime import posix_time as dfdatetime_posix_time |
12 | 7 |
13 from plaso.containers import events | 8 from plaso.containers import events |
14 from plaso.containers import time_events | 9 from plaso.containers import time_events |
15 from plaso.lib import errors | 10 from plaso.lib import errors |
16 from plaso.lib import definitions | 11 from plaso.lib import definitions |
17 from plaso.parsers import interface | 12 from plaso.parsers import dtfabric_parser |
18 from plaso.parsers import logger | |
19 from plaso.parsers import manager | 13 from plaso.parsers import manager |
20 | 14 |
21 | 15 |
22 class UtmpEventData(events.EventData): | 16 class UtmpEventData(events.EventData): |
23 """UTMP event data. | 17 """UTMP event data. |
24 | 18 |
25 Attributes: | 19 Attributes: |
26 computer_name (str): name of the computer. | 20 computer_name (str): name of the computer. |
27 exit (int): exit status. | 21 exit_status (int): exit status. |
28 ip_address (str): IP address from the connection. | 22 ip_address (str): IP address from the connection. |
29 pid (int): process identifier (PID). | 23 pid (int): process identifier (PID). |
30 status (str): login status. | |
31 terminal_id (int): inittab identifier. | |
32 terminal (str): type of terminal. | 24 terminal (str): type of terminal. |
33 user (str): active user name. | 25 terminal_identifier (int): inittab identifier. |
| 26 type (int): type of login. |
| 27 username (str): user name. |
34 """ | 28 """ |
35 | 29 |
36 DATA_TYPE = 'linux:utmp:event' | 30 DATA_TYPE = 'linux:utmp:event' |
37 | 31 |
38 def __init__(self): | 32 def __init__(self): |
39 """Initializes event data.""" | 33 """Initializes event data.""" |
40 super(UtmpEventData, self).__init__(data_type=self.DATA_TYPE) | 34 super(UtmpEventData, self).__init__(data_type=self.DATA_TYPE) |
41 self.computer_name = None | 35 self.computer_name = None |
42 self.exit = None | 36 self.exit_status = None |
43 self.ip_address = None | 37 self.ip_address = None |
44 self.pid = None | 38 self.pid = None |
45 self.status = None | |
46 self.terminal_id = None | |
47 self.terminal = None | 39 self.terminal = None |
48 self.user = None | 40 self.terminal_identifier = None |
| 41 self.type = None |
| 42 self.username = None |
49 | 43 |
50 | 44 |
51 class UtmpParser(interface.FileObjectParser): | 45 class UtmpParser(dtfabric_parser.DtFabricBaseParser): |
52 """Parser for Linux/Unix UTMP files.""" | 46 """Parser for Linux/Unix UTMP files.""" |
53 | 47 |
54 NAME = 'utmp' | 48 NAME = 'utmp' |
55 DESCRIPTION = 'Parser for Linux/Unix UTMP files.' | 49 DESCRIPTION = 'Parser for Linux/Unix UTMP files.' |
56 | 50 |
57 LINUX_UTMP_ENTRY = construct.Struct( | 51 _DEFINITION_FILE = 'utmp.yaml' |
58 'utmp_linux', | |
59 construct.ULInt32('type'), | |
60 construct.ULInt32('pid'), | |
61 construct.String('terminal', 32), | |
62 construct.ULInt32('terminal_id'), | |
63 construct.String('username', 32), | |
64 construct.String('hostname', 256), | |
65 construct.ULInt16('termination'), | |
66 construct.ULInt16('exit'), | |
67 construct.ULInt32('session'), | |
68 construct.ULInt32('timestamp'), | |
69 construct.ULInt32('microseconds'), | |
70 construct.ULInt32('address_a'), | |
71 construct.ULInt32('address_b'), | |
72 construct.ULInt32('address_c'), | |
73 construct.ULInt32('address_d'), | |
74 construct.Padding(20)) | |
75 | 52 |
76 LINUX_UTMP_ENTRY_SIZE = LINUX_UTMP_ENTRY.sizeof() | 53 _EMPTY_IP_ADDRESS = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) |
77 | 54 |
78 STATUS_TYPE = { | 55 _SUPPORTED_TYPES = frozenset(range(0, 10)) |
79 0: 'EMPTY', | |
80 1: 'RUN_LVL', | |
81 2: 'BOOT_TIME', | |
82 3: 'NEW_TIME', | |
83 4: 'OLD_TIME', | |
84 5: 'INIT_PROCESS', | |
85 6: 'LOGIN_PROCESS', | |
86 7: 'USER_PROCESS', | |
87 8: 'DEAD_PROCESS', | |
88 9: 'ACCOUNTING'} | |
89 | 56 |
90 # Set a default test value for few fields, this is supposed to be a text | 57 def _ReadEntry(self, parser_mediator, file_object, file_offset): |
91 # that is highly unlikely to be seen in a terminal field, or a username field. | |
92 # It is important that this value does show up in such fields, but otherwise | |
93 # it can be a free flowing text field. | |
94 _DEFAULT_TEST_VALUE = 'Ekki Fraedilegur Moguleiki, thetta er bull ! = + _<>' | |
95 | |
96 def _GetTextFromNullTerminatedString( | |
97 self, null_terminated_string, default_string='N/A'): | |
98 """Get a UTF-8 text from a raw null terminated string. | |
99 | |
100 Args: | |
101 null_terminated_string: Raw string terminated with null character. | |
102 default_string: The default string returned if the parser fails. | |
103 | |
104 Returns: | |
105 A decoded UTF-8 string or if unable to decode, the supplied default | |
106 string. | |
107 """ | |
108 text, _, _ = null_terminated_string.partition(b'\x00') | |
109 try: | |
110 text = text.decode('utf-8') | |
111 except UnicodeDecodeError: | |
112 logger.warning( | |
113 '[UTMP] Decode UTF8 failed, the message string may be cut short.') | |
114 text = text.decode('utf-8', 'ignore') | |
115 if not text: | |
116 return default_string | |
117 return text | |
118 | |
119 def _ReadEntry(self, parser_mediator, file_object): | |
120 """Reads an UTMP entry. | 58 """Reads an UTMP entry. |
121 | 59 |
122 Args: | 60 Args: |
123 parser_mediator (ParserMediator): mediates interactions between parsers | 61 parser_mediator (ParserMediator): mediates interactions between parsers |
124 and other components, such as storage and dfvfs. | 62 and other components, such as storage and dfvfs. |
125 file_object (dfvfs.FileIO): a file-like object. | 63 file_object (dfvfs.FileIO): a file-like object. |
| 64 file_offset (int): offset of the data relative from the start of |
| 65 the file-like object. |
126 | 66 |
127 Returns: | 67 Returns: |
128 bool: True if the UTMP entry was successfully read. | 68 tuple: contains: |
| 69 |
| 70 int: timestamp, which contains the number of microseconds |
| 71 since January 1, 1970, 00:00:00 UTC. |
| 72 UtmpEventData: event data of the UTMP entry read. |
| 73 |
| 74 Raises: |
| 75 ParseError: if the entry cannot be parsed. |
129 """ | 76 """ |
130 offset = file_object.tell() | 77 entry_map = self._GetDataTypeMap('utmp_entry') |
131 data = file_object.read(self.LINUX_UTMP_ENTRY_SIZE) | |
132 if not data or len(data) != self.LINUX_UTMP_ENTRY_SIZE: | |
133 return False | |
134 | 78 |
135 try: | 79 try: |
136 entry = self.LINUX_UTMP_ENTRY.parse(data) | 80 entry, _ = self._ReadStructureFromFileObject( |
137 except (IOError, construct.FieldError): | 81 file_object, file_offset, entry_map) |
138 logger.warning(( | 82 except (ValueError, errors.ParseError) as exception: |
139 'UTMP entry at 0x{0:x} couldn\'t be parsed.').format(offset)) | 83 raise errors.ParseError(( |
140 return False | 84 'Unable to parse UTMP entry at offset: 0x{0:08x} with error: ' |
| 85 '{1!s}.').format(file_offset, exception)) |
141 | 86 |
142 user = self._GetTextFromNullTerminatedString(entry.username) | 87 if entry.type not in self._SUPPORTED_TYPES: |
143 terminal = self._GetTextFromNullTerminatedString(entry.terminal) | 88 raise errors.UnableToParseFile('Unsupported type: {0:d}'.format( |
| 89 entry.type)) |
| 90 |
| 91 encoding = parser_mediator.codepage or 'utf8' |
| 92 |
| 93 try: |
| 94 username = entry.username.rstrip(b'\x00') |
| 95 username = username.decode(encoding) |
| 96 except UnicodeDecodeError: |
| 97 parser_mediator.ProduceExtractionError('unable to decode username string') |
| 98 username = None |
| 99 |
| 100 try: |
| 101 terminal = entry.terminal.rstrip(b'\x00') |
| 102 terminal = terminal.decode(encoding) |
| 103 except UnicodeDecodeError: |
| 104 parser_mediator.ProduceExtractionError('unable to decode terminal string') |
| 105 terminal = None |
| 106 |
144 if terminal == '~': | 107 if terminal == '~': |
145 terminal = 'system boot' | 108 terminal = 'system boot' |
146 computer_name = self._GetTextFromNullTerminatedString(entry.hostname) | |
147 if computer_name == 'N/A' or computer_name == ':0': | |
148 computer_name = 'localhost' | |
149 status = self.STATUS_TYPE.get(entry.type, 'N/A') | |
150 | 109 |
151 if entry.address_b: | 110 try: |
152 ip_address = '{0:d}.{1:d}.{2:d}.{3:d}'.format( | 111 hostname = entry.hostname.rstrip(b'\x00') |
153 entry.address_a, entry.address_b, entry.address_c, entry.address_d) | 112 hostname = hostname.decode(encoding) |
| 113 except UnicodeDecodeError: |
| 114 parser_mediator.ProduceExtractionError('unable to decode hostname string') |
| 115 hostname = None |
| 116 |
| 117 if not hostname or hostname == ':0': |
| 118 hostname = 'localhost' |
| 119 |
| 120 if entry.ip_address[4:] == self._EMPTY_IP_ADDRESS[4:]: |
| 121 ip_address = self._FormatPackedIPv4Address(entry.ip_address[:4]) |
154 else: | 122 else: |
155 try: | 123 ip_address = self._FormatPackedIPv6Address(entry.ip_address) |
156 ip_address = socket.inet_ntoa( | |
157 construct.ULInt32('int').build(entry.address_a)) | |
158 if ip_address == '0.0.0.0': | |
159 ip_address = 'localhost' | |
160 except (IOError, construct.FieldError, socket.error): | |
161 ip_address = 'N/A' | |
162 | 124 |
| 125 # TODO: add termination status. |
| 126 # TODO: rename event data attributes to match data definition. |
163 event_data = UtmpEventData() | 127 event_data = UtmpEventData() |
164 event_data.computer_name = computer_name | 128 event_data.computer_name = hostname |
165 event_data.exit = entry.exit | 129 event_data.exit_status = entry.exit_status |
166 event_data.ip_address = ip_address | 130 event_data.ip_address = ip_address |
167 event_data.pid = entry.pid | 131 event_data.pid = entry.pid |
168 event_data.status = status | |
169 event_data.terminal_id = entry.terminal_id | |
170 event_data.terminal = terminal | 132 event_data.terminal = terminal |
171 event_data.user = user | 133 event_data.terminal_identifier = entry.terminal_identifier |
| 134 event_data.type = entry.type |
| 135 event_data.username = username |
172 | 136 |
173 timestamp = (entry.timestamp * 1000000) + entry.microseconds | 137 timestamp = entry.microseconds + ( |
174 date_time = dfdatetime_posix_time.PosixTimeInMicroseconds( | 138 entry.timestamp * definitions.MICROSECONDS_PER_SECOND) |
175 timestamp=timestamp) | 139 return timestamp, event_data |
176 event = time_events.DateTimeValuesEvent( | |
177 date_time, definitions.TIME_DESCRIPTION_START) | |
178 parser_mediator.ProduceEventWithEventData(event, event_data) | |
179 | |
180 return True | |
181 | |
182 def _VerifyTextField(self, text): | |
183 """Check if a byte stream is a null terminated string. | |
184 | |
185 Args: | |
186 event_object: text field from the structure. | |
187 | |
188 Returns: | |
189 bool: True if it is a null terminated string, False otherwise. | |
190 """ | |
191 _, _, null_chars = text.partition(b'\x00') | |
192 if not null_chars: | |
193 return False | |
194 return len(null_chars) == null_chars.count(b'\x00') | |
195 | 140 |
196 def ParseFileObject(self, parser_mediator, file_object, **kwargs): | 141 def ParseFileObject(self, parser_mediator, file_object, **kwargs): |
197 """Parses an UTMP file-like object. | 142 """Parses an UTMP file-like object. |
198 | 143 |
199 Args: | 144 Args: |
200 parser_mediator (ParserMediator): mediates interactions between parsers | 145 parser_mediator (ParserMediator): mediates interactions between parsers |
201 and other components, such as storage and dfvfs. | 146 and other components, such as storage and dfvfs. |
202 file_object (dfvfs.FileIO): a file-like object. | 147 file_object (dfvfs.FileIO): a file-like object. |
203 | 148 |
204 Raises: | 149 Raises: |
205 UnableToParseFile: when the file cannot be parsed. | 150 UnableToParseFile: when the file cannot be parsed. |
206 """ | 151 """ |
| 152 file_offset = 0 |
| 153 |
207 try: | 154 try: |
208 structure = self.LINUX_UTMP_ENTRY.parse_stream(file_object) | 155 timestamp, event_data = self._ReadEntry( |
209 except (IOError, construct.FieldError) as exception: | 156 parser_mediator, file_object, file_offset) |
| 157 except errors.ParseError as exception: |
210 raise errors.UnableToParseFile( | 158 raise errors.UnableToParseFile( |
211 'Unable to parse UTMP Header with error: {0!s}'.format(exception)) | 159 'Unable to parse UTMP header with error: {0!s}'.format(exception)) |
212 | 160 |
213 if structure.type not in self.STATUS_TYPE: | 161 if not event_data.username: |
214 raise errors.UnableToParseFile(( | 162 raise errors.UnableToParseFile( |
215 'Not an UTMP file, unknown type ' | 163 'Unable to parse UTMP header with error: missing username') |
216 '[{0:d}].').format(structure.type)) | |
217 | 164 |
218 if not self._VerifyTextField(structure.terminal): | 165 if not timestamp: |
219 raise errors.UnableToParseFile( | 166 raise errors.UnableToParseFile( |
220 'Not an UTMP file, unknown terminal.') | 167 'Unable to parse UTMP header with error: missing timestamp') |
221 | 168 |
222 if not self._VerifyTextField(structure.username): | 169 date_time = dfdatetime_posix_time.PosixTimeInMicroseconds( |
223 raise errors.UnableToParseFile( | 170 timestamp=timestamp) |
224 'Not an UTMP file, unknown username.') | 171 event = time_events.DateTimeValuesEvent( |
| 172 date_time, definitions.TIME_DESCRIPTION_START) |
| 173 parser_mediator.ProduceEventWithEventData(event, event_data) |
225 | 174 |
226 if not self._VerifyTextField(structure.hostname): | 175 file_offset = file_object.tell() |
227 raise errors.UnableToParseFile( | 176 file_size = file_object.get_size() |
228 'Not an UTMP file, unknown hostname.') | |
229 | 177 |
230 # Check few values. | 178 while file_offset < file_size: |
231 terminal = self._GetTextFromNullTerminatedString( | 179 if parser_mediator.abort: |
232 structure.terminal, self._DEFAULT_TEST_VALUE) | 180 break |
233 if terminal == self._DEFAULT_TEST_VALUE: | |
234 raise errors.UnableToParseFile( | |
235 'Not an UTMP file, no terminal set.') | |
236 | 181 |
237 username = self._GetTextFromNullTerminatedString( | 182 try: |
238 structure.username, self._DEFAULT_TEST_VALUE) | 183 timestamp, event_data = self._ReadEntry( |
| 184 parser_mediator, file_object, file_offset) |
| 185 except errors.ParseError as exception: |
| 186 # Note that the utmp file can contain trailing data. |
| 187 break |
239 | 188 |
240 if username == self._DEFAULT_TEST_VALUE: | 189 date_time = dfdatetime_posix_time.PosixTimeInMicroseconds( |
241 raise errors.UnableToParseFile( | 190 timestamp=timestamp) |
242 'Not an UTMP file, no username set.') | 191 event = time_events.DateTimeValuesEvent( |
| 192 date_time, definitions.TIME_DESCRIPTION_START) |
| 193 parser_mediator.ProduceEventWithEventData(event, event_data) |
243 | 194 |
244 if not structure.timestamp: | 195 file_offset = file_object.tell() |
245 raise errors.UnableToParseFile( | |
246 'Not an UTMP file, no timestamp set in the first record.') | |
247 | |
248 file_object.seek(0, os.SEEK_SET) | |
249 while self._ReadEntry(parser_mediator, file_object): | |
250 pass | |
251 | 196 |
252 | 197 |
253 manager.ParsersManager.RegisterParser(UtmpParser) | 198 manager.ParsersManager.RegisterParser(UtmpParser) |
OLD | NEW |