From 7371119f28f85d1fd7db79aa1b59bafd2772cda1 Mon Sep 17 00:00:00 2001 From: Christian Boltz Date: Sun, 24 Oct 2021 12:54:20 +0200 Subject: [PATCH 1/6] Move get_last_login_timestamp() into apparmor.notify This is a preparation to make adding tests easier. --- utils/aa-notify | 44 +-------------------------- utils/apparmor/notify.py | 64 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 43 deletions(-) create mode 100644 utils/apparmor/notify.py diff --git a/utils/aa-notify b/utils/aa-notify index 6cb7c393f..4bb65abad 100755 --- a/utils/aa-notify +++ b/utils/aa-notify @@ -34,7 +34,6 @@ import os import re import sys import time -import struct import notify2 import psutil import pwd @@ -45,6 +44,7 @@ import apparmor.ui as aaui import apparmor.config as aaconfig from apparmor.common import DebugLogger, open_file_read from apparmor.fail import enable_aa_exception_handler +from apparmor.notify import get_last_login_timestamp from apparmor.translations import init_translation import LibAppArmor # C-library to parse one log line @@ -61,48 +61,6 @@ def get_user_login(): return username -def get_last_login_timestamp(username): - '''Directly read wtmp and get last login for user as epoch timestamp''' - timestamp = 0 - filename = '/var/log/wtmp' - last_login = 0 - - debug_logger.debug('Username: {}'.format(username)) - - with open(filename, "rb") as wtmp_file: - offset = 0 - wtmp_filesize = os.path.getsize(filename) - debug_logger.debug('WTMP filesize: {}'.format(wtmp_filesize)) - while offset < wtmp_filesize: - wtmp_file.seek(offset) - offset += 384 # Increment for next entry - - type = struct.unpack(" +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of version 2 of the GNU General Public +# License as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# ---------------------------------------------------------------------- + +import os +import struct + +from apparmor.common import AppArmorBug, DebugLogger + +debug_logger = DebugLogger('apparmor.notify') + + +def get_last_login_timestamp(username): + '''Directly read wtmp and get last login for user as epoch timestamp''' + timestamp = 0 + filename = '/var/log/wtmp' + last_login = 0 + + debug_logger.debug('Username: {}'.format(username)) + + with open(filename, "rb") as wtmp_file: + offset = 0 + wtmp_filesize = os.path.getsize(filename) + debug_logger.debug('WTMP filesize: {}'.format(wtmp_filesize)) + while offset < wtmp_filesize: + wtmp_file.seek(offset) + offset += 384 # Increment for next entry + + type = struct.unpack(" Date: Sun, 24 Oct 2021 13:10:25 +0200 Subject: [PATCH 2/6] Add unittest for get_last_login_timestamp ... starting with a x86_64 wtmp example file --- utils/apparmor/notify.py | 3 +- utils/test/test-notify.py | 31 ++++++++++++++++++ utils/test/wtmp-examples/wtmp-x86_64 | Bin 0 -> 768 bytes utils/test/wtmp-examples/wtmp-x86_64-expected | 3 ++ 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 utils/test/test-notify.py create mode 100644 utils/test/wtmp-examples/wtmp-x86_64 create mode 100644 utils/test/wtmp-examples/wtmp-x86_64-expected diff --git a/utils/apparmor/notify.py b/utils/apparmor/notify.py index 214e6c189..d88c12245 100644 --- a/utils/apparmor/notify.py +++ b/utils/apparmor/notify.py @@ -21,10 +21,9 @@ from apparmor.common import AppArmorBug, DebugLogger debug_logger = DebugLogger('apparmor.notify') -def get_last_login_timestamp(username): +def get_last_login_timestamp(username, filename='/var/log/wtmp'): '''Directly read wtmp and get last login for user as epoch timestamp''' timestamp = 0 - filename = '/var/log/wtmp' last_login = 0 debug_logger.debug('Username: {}'.format(username)) diff --git a/utils/test/test-notify.py b/utils/test/test-notify.py new file mode 100644 index 000000000..6bbafa107 --- /dev/null +++ b/utils/test/test-notify.py @@ -0,0 +1,31 @@ +#! /usr/bin/python3 +# ------------------------------------------------------------------ +# +# Copyright (C) 2021 Christian Boltz +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of version 2 of the GNU General Public +# License published by the Free Software Foundation. +# +# ------------------------------------------------------------------ + +import unittest +from common_test import AATest, setup_all_loops + +from apparmor.notify import get_last_login_timestamp + +class TestGet_last_login_timestamp(AATest): + tests = [ + (['wtmp-x86_64', 'root' ], 1635070346), # Sun Oct 24 12:12:26 CEST 2021 + (['wtmp-x86_64', 'whoever' ], 0), + ] + + def _run_test(self, params, expected): + filename, user = params + filename = 'wtmp-examples/%s' % filename + self.assertEqual(get_last_login_timestamp(user, filename), expected) + + +setup_all_loops(__name__) +if __name__ == '__main__': + unittest.main(verbosity=1) diff --git a/utils/test/wtmp-examples/wtmp-x86_64 b/utils/test/wtmp-examples/wtmp-x86_64 new file mode 100644 index 0000000000000000000000000000000000000000..7620d49d5c2510727a2fb2af2f8825c4f1e6c823 GIT binary patch literal 768 zcmd;JU|^7BWnd^MDb_atvPS{sA<$`9n%LmXM3h17KvkJUgg+>9it_VI@EMw$pO;yZ sU!<3rmsXUhmtT;YS6o`0s+V7sj!)O{lj{P8KU*sU!+{l5jD&m#0Q_DS+W-In literal 0 HcmV?d00001 diff --git a/utils/test/wtmp-examples/wtmp-x86_64-expected b/utils/test/wtmp-examples/wtmp-x86_64-expected new file mode 100644 index 000000000..643b40817 --- /dev/null +++ b/utils/test/wtmp-examples/wtmp-x86_64-expected @@ -0,0 +1,3 @@ +root pts/0 monitor.infra.op Sun Oct 24 12:12 gone - no logout + +wtmp-x86_64 begins Sun Oct 24 12:12:25 2021 From af8d5021a5cf3b3a1718148d5f69006128873b76 Mon Sep 17 00:00:00 2001 From: Christian Boltz Date: Sun, 24 Oct 2021 13:15:10 +0200 Subject: [PATCH 3/6] fix reading 'type' from wtmp 'type' is a short (see "ut_type" in wtmp(5)), therefore only read two bytes and unpack them as short. Afterwards read two padding bytes to /dev/null. This accidently worked on x86_64 because it's little endian, but will fail on big endian architectures. --- utils/apparmor/notify.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/utils/apparmor/notify.py b/utils/apparmor/notify.py index d88c12245..81d702972 100644 --- a/utils/apparmor/notify.py +++ b/utils/apparmor/notify.py @@ -1,6 +1,7 @@ #! /usr/bin/python3 # ---------------------------------------------------------------------- # Copyright (C) 2018–2019 Otto Kekäläinen +# Copyright (C) 2021 Christian Boltz # # This program is free software; you can redistribute it and/or # modify it under the terms of version 2 of the GNU General Public @@ -36,8 +37,9 @@ def get_last_login_timestamp(username, filename='/var/log/wtmp'): wtmp_file.seek(offset) offset += 384 # Increment for next entry - type = struct.unpack(" Date: Sun, 24 Oct 2021 14:19:10 +0200 Subject: [PATCH 4/6] Add support for reading s390x and aarch64 wtmp file Both aarch64 and s390x have a bigger wtmp record size (16 bytes more than x86_64, 400 bytes total). The byte position of the timestamp is also different on each architecture. To make things even more interesting, s390x is big endian. Fixes: https://bugzilla.opensuse.org/show_bug.cgi?id=1181155 --- utils/apparmor/notify.py | 46 ++++++++++++++++-- utils/test/test-notify.py | 5 ++ utils/test/wtmp-examples/wtmp-aarch64 | Bin 0 -> 800 bytes .../wtmp-aarch64-expected-output | 5 ++ utils/test/wtmp-examples/wtmp-s390x | Bin 0 -> 15200 bytes .../wtmp-examples/wtmp-s390x-expected-output | 13 +++++ 6 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 utils/test/wtmp-examples/wtmp-aarch64 create mode 100644 utils/test/wtmp-examples/wtmp-aarch64-expected-output create mode 100644 utils/test/wtmp-examples/wtmp-s390x create mode 100644 utils/test/wtmp-examples/wtmp-s390x-expected-output diff --git a/utils/apparmor/notify.py b/utils/apparmor/notify.py index 81d702972..1101a2934 100644 --- a/utils/apparmor/notify.py +++ b/utils/apparmor/notify.py @@ -22,6 +22,16 @@ from apparmor.common import AppArmorBug, DebugLogger debug_logger = DebugLogger('apparmor.notify') +def sane_timestamp(timestamp): + ''' Check if the given timestamp is in a date range that makes sense for a wtmp file ''' + + if timestamp < 946681200: # 2000-01-01 + return False + elif timestamp > 2524604400: # 2050-01-01 + return False + + return True + def get_last_login_timestamp(username, filename='/var/log/wtmp'): '''Directly read wtmp and get last login for user as epoch timestamp''' timestamp = 0 @@ -33,11 +43,37 @@ def get_last_login_timestamp(username, filename='/var/log/wtmp'): offset = 0 wtmp_filesize = os.path.getsize(filename) debug_logger.debug('WTMP filesize: {}'.format(wtmp_filesize)) + + if wtmp_filesize < 356: + return 0 # (nearly) empty wtmp file, no entries + + # detect architecture based on utmp format differences + wtmp_file.seek(340) # first possible timestamp position + timestamp_x86_64 = struct.unpack("L", wtmp_file.read(4))[0] + debug_logger.debug('WTMP timestamps: x86_64 %s, aarch64 %s, s390x %s' % (timestamp_x86_64, timestamp_aarch64, timestamp_s390x)) + + if sane_timestamp(timestamp_x86_64): + endianness = '<' # little endian + extra_offset_before = 0 + extra_offset_after = 0 + elif sane_timestamp(timestamp_aarch64): + endianness = '<' # little endian + extra_offset_before = 4 + extra_offset_after = 12 + elif sane_timestamp(timestamp_s390x): + endianness = '>' # big endian + extra_offset_before = 8 + extra_offset_after = 8 + else: + raise AppArmorBug('Your /var/log/wtmp is broken or has an unknown format. Please open a bugreport with /var/log/wtmp and the output of "last" attached!') + while offset < wtmp_filesize: wtmp_file.seek(offset) - offset += 384 # Increment for next entry + offset += 384 + extra_offset_before + extra_offset_after # Increment for next entry - type = struct.unpack(" zL?McFi!M>!gN{L`DCm-hjqMUa2)!S>j^i@#`yWRSI=j!@VbD$BZA9ost=J!8?zA}1vWGK=%K&*kn`Bs6@_~i;qjX(uCp2o95bwr)mj5j zz5MyS%;N*TpmHA2=ku`24Cn3eDD7XBctozghGTFZ(DN=FGpvpdkJ9IlGLLJ|U`aR+ zXukxG8CFM!M`^#k%%gK0mW21CG^8IjoXO{U2k!QJ#a8_&w=%2idGz`#w?%NYHT~L@ z>etf!k_Y_VUjO2kfPmYyHT_z}gQ!e+M48xo5~tmw3y#=+qOqv@Q38#8n~X&|#o^J8 z? z_bbi=st?5j<`J|$p!GxXfO!O6KbFY)p#B~Xm0Rj(EKwKd`RdHFbV}KQ->)DZsy@o1 z$zB8SVEw3lA5}j}rm=rJsgEOUR^4KYzkZaQhySZlGYF3+rb6{f9jpeckEY4ZsE?Cz z(`qB%R?4*Ecn1c))a~YI7n^^uYi_7Hdvh|+_I#x|u>E#94{03bP@#ZbJa&o?elG3W zR=89Su=+UqelzMLl`>(`e@#d)W?wUztuEN)rVVX!s8zs**%{1S0BxWjEDP7>IhN5F5cnY jrK(0*`_&1@>%)}sK>l7-)rTW1{f+R5@R27`v_Ad=&1z#R literal 0 HcmV?d00001 diff --git a/utils/test/wtmp-examples/wtmp-s390x-expected-output b/utils/test/wtmp-examples/wtmp-s390x-expected-output new file mode 100644 index 000000000..de4d5fff6 --- /dev/null +++ b/utils/test/wtmp-examples/wtmp-s390x-expected-output @@ -0,0 +1,13 @@ +linux1@opensuse03:~> last +linux1 pts/0 77.21.253.246 Thu Jul 15 13:06 still logged in +root pts/0 77.21.253.246 Thu Jul 15 13:06 - 13:06 (00:00) +linux1 pts/0 77.21.253.246 Thu Jul 15 13:01 - 13:05 (00:04) +linux1 pts/0 94.134.117.140 Thu Jul 15 08:15 - 08:16 (00:01) +linux1 pts/0 10.6.22.160 Tue Jul 13 07:42 - 07:42 (00:00) +reboot system boot 5.3.18-24.67-def Tue Jul 13 07:41 still running +linux1 pts/0 10.6.22.160 Tue Jul 13 07:41 - 07:41 (00:00) +linux1 pts/0 10.6.22.160 Tue Jul 13 07:37 - 07:41 (00:03) +reboot system boot 5.3.18-24.64-def Tue Jul 13 07:30 - 07:41 (00:11) + +wtmp beginnt Tue Jul 13 07:30:36 2021 +linux1@opensuse03:~> From c2c2cf005c3c4c02c03769a08972ad1362a13348 Mon Sep 17 00:00:00 2001 From: Christian Boltz Date: Sun, 24 Oct 2021 15:26:26 +0200 Subject: [PATCH 5/6] Add truncated and ancient wtmp example to tests A too-small file can't contain something useful. Also, a wtmp file with a timestamp from pre-2000 is beyond what you'd expect on a 2021 system. --- utils/test/test-notify.py | 8 ++++++++ utils/test/wtmp-examples/wtmp-truncated | Bin 0 -> 300 bytes utils/test/wtmp-examples/wtmp-x86_64-past | Bin 0 -> 768 bytes .../test/wtmp-examples/wtmp-x86_64-past-expected | 3 +++ 4 files changed, 11 insertions(+) create mode 100644 utils/test/wtmp-examples/wtmp-truncated create mode 100644 utils/test/wtmp-examples/wtmp-x86_64-past create mode 100644 utils/test/wtmp-examples/wtmp-x86_64-past-expected diff --git a/utils/test/test-notify.py b/utils/test/test-notify.py index 2a910eada..73716c6dd 100644 --- a/utils/test/test-notify.py +++ b/utils/test/test-notify.py @@ -12,6 +12,7 @@ import unittest from common_test import AATest, setup_all_loops +from apparmor.common import AppArmorBug from apparmor.notify import get_last_login_timestamp class TestGet_last_login_timestamp(AATest): @@ -23,6 +24,8 @@ class TestGet_last_login_timestamp(AATest): (['wtmp-s390x', 'whoever' ], 0), (['wtmp-aarch64', 'guillaume' ], 1611562789), # Mon Jan 25 09:19:49 CET 2021 (['wtmp-aarch64', 'whoever' ], 0), + (['wtmp-truncated', 'root' ], 0), + (['wtmp-truncated', 'whoever' ], 0), ] def _run_test(self, params, expected): @@ -30,6 +33,11 @@ class TestGet_last_login_timestamp(AATest): filename = 'wtmp-examples/%s' % filename self.assertEqual(get_last_login_timestamp(user, filename), expected) + def test_date_1999(self): + with self.assertRaises(AppArmorBug): + # wtmp-x86_64-past is hand-edited to Thu Dec 30 00:00:00 CET 1999, which is outside the expected data range + get_last_login_timestamp('root', 'wtmp-examples/wtmp-x86_64-past') + setup_all_loops(__name__) if __name__ == '__main__': diff --git a/utils/test/wtmp-examples/wtmp-truncated b/utils/test/wtmp-examples/wtmp-truncated new file mode 100644 index 0000000000000000000000000000000000000000..3b34f439c28bc4239a1cc869470b01173f34fbc1 GIT binary patch literal 300 acmd;JU|^7BWnd^MDb_atvPS{#5C8xkMgiRb literal 0 HcmV?d00001 diff --git a/utils/test/wtmp-examples/wtmp-x86_64-past b/utils/test/wtmp-examples/wtmp-x86_64-past new file mode 100644 index 0000000000000000000000000000000000000000..4cdbf693926caddcb6675466659cfe622830bb73 GIT binary patch literal 768 zcmd;JU|^7BWnd^MDb_atvPS{sAy6%S0|1<*7LWh{ literal 0 HcmV?d00001 diff --git a/utils/test/wtmp-examples/wtmp-x86_64-past-expected b/utils/test/wtmp-examples/wtmp-x86_64-past-expected new file mode 100644 index 000000000..1352ccbf7 --- /dev/null +++ b/utils/test/wtmp-examples/wtmp-x86_64-past-expected @@ -0,0 +1,3 @@ +root pts/0 blast.from.the.p Thu Dec 30 00:00 gone - no logout + +wtmp-x86_64-past begins Thu Dec 30 00:00:00 1999 From b4cc405b803ab0cb3bf2797cfed3e512b491e917 Mon Sep 17 00:00:00 2001 From: Christian Boltz Date: Sun, 24 Oct 2021 15:29:25 +0200 Subject: [PATCH 6/6] Add tests for sane_timestamp Ensure that pre-2000 and post-2050 dates get rejected, and something in between gets accepted. This also extends coverage to 100% - before, the post-2050 branch was not covered. --- utils/test/test-notify.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/utils/test/test-notify.py b/utils/test/test-notify.py index 73716c6dd..f82bf68e0 100644 --- a/utils/test/test-notify.py +++ b/utils/test/test-notify.py @@ -13,7 +13,17 @@ import unittest from common_test import AATest, setup_all_loops from apparmor.common import AppArmorBug -from apparmor.notify import get_last_login_timestamp +from apparmor.notify import get_last_login_timestamp, sane_timestamp + +class TestSane_timestamp(AATest): + tests = [ + (2524704400, False), # Sun Jan 2 03:46:40 CET 2050 + ( 944780400, False), # Fri Dec 10 00:00:00 CET 1999 + (1635026400, True ), # Sun Oct 24 00:00:00 CEST 2021 + ] + + def _run_test(self, params, expected): + self.assertEqual(sane_timestamp(params), expected) class TestGet_last_login_timestamp(AATest): tests = [