os_asl_log_files_*_configure are completely broken #120

Open
opened 2026-01-19 18:29:18 +00:00 by michael · 0 comments
Owner

Originally created by @nihil-admirari on GitHub.

Originally assigned to: @brodjieski on GitHub.

os_asl_log_files_owner_group_configure and os_asl_log_files_permissions_configure both use the following code to collect the names of log files:

/usr/bin/grep -e '^>' /etc/asl.conf /etc/asl/* | /usr/bin/awk '{ print $2 }')

According to man asl.conf:

  1. Lines starting with > configure output options for files or directories to be referenced later by query rules. os_asl_log_files_permissions_configure sets 640 regardless, making directories non-listable.
  2. Query rules can have file and directory (store_dir is used de facto even though it is not mentioned in the man) configured inline. Above code completely ignores files configured inline.
  3. Filenames are not required to be absolute:

    If the pathname specified is not an absolute path, syslogd will treat the given path as relative to /var/log (for /etc/asl.conf), or for other output modules relative to /var/log/module/NAME where NAME is the module name.

  4. If log rotation is in use, actual filename will have a timestamp or a counter appended to it.
  5. Ownership is root:admin by default, but can be configured directly within configuration files by setting uid=0 gid=0, and similar for permissions mode=0640 (or mode 0750 for directories). Provided fixes do not modify configuration files themselves, which means that newly created log files won't have correct group and mode set.
  6. According to man asl.conf there is an additional parameter
    access     Sets read access controls for messages that match the
               associated query pattern.  syslogd will restrict read
               access to matching messages to a specific user and
               group.  The user ID number and group ID number must fol-
               low the ``access'' keyword as parameters.
    
    Files which have it, configure access to root:admin (0:80). I've no idea whether it should be hardened to root:wheel.

Examples

On my machine (Sonoma 14.2.1):

/usr/bin/grep -e '^>' /etc/asl.conf /etc/asl/* | /usr/bin/awk '{ print $2 }')

produces

/var/log/asl/Logs/aslmanager
system.log
cdscheduler.log
/var/log/com.apple.contacts.ContactsAutocomplete
/var/log/CoreDuet/coreduetd.log
/var/log/mail.log
/private/var/log/keybagd.log
/private/var/log/keybagd.log
/Library/Logs/CrashReporter
  1. /var/log/com.apple.contacts.ContactsAutocomplete
    /Library/Logs/CrashReporter
    

    are directories and should have 750 permissions, not 640.

  2. /etc/asl.conf has ? [= Facility com.apple.alf.logging] file appfirewall.log file_max=5M all_max=50M, but appfirewall.log is not mentioned in the output at all.

    /etc/asl/com.apple.MessageTracer has * store_dir /var/log/DiagnosticMessaged ttl=30, which is also missed.

    There are other missing log files beyond these two.

  3. system.log was mentioned in /etc/asl.conf, which means it's actually /var/log/system.log on disc.

    cdscheduler.log was mentioned in /etc/asl/com.apple.cdscheduler, which means it's actually /var/log//module/com.apple.cdscheduler/cdscheduler.log on disc.

  4. /var/log/asl/Logs/aslmanager has style=lcl-b and is actually stored as /var/log/asl/Logs/aslmanager.%Y%m%dT%H%M%S%z on disc. stat doesn't glob potential suffixes:

    /usr/bin/stat -f '%Su:%Sg:%N' $(/usr/bin/grep -e '^>' /etc/asl.conf /etc/asl/* | /usr/bin/awk '{ print $2 }')
    

Suggestions for filesystem checks

Log filenames collection:

logfiles=$(
    for f in /etc/asl{.conf,/*}; do
        if [[ $f == /etc/asl.conf ]]; then
            log_dir=/var/log/
        else
            log_dir="/var/log/module/$(basename "$f")/"
        fi

        awk -v log_dir="$log_dir" '
            function print_log(j) {
                print $j ~ "^/" ? $j : log_dir$j
            }

            $1 == ">" { print_log(2) }

            $1 ~ /^[?*]$/ {
                for (i = 2; i <= NF; ++i) {
                    if ($i ~ "^(file|directory|store_dir)$") {
                        print_log(++i)
                    }
                }
            }
        ' "$f"
    done | sort -u
)

Checking for ownership.

Notes:

  1. Uses zsh globbing, won't run in bash.
  2. awk 'END { print NR }' doesn't require xargs to remove spaces compared to wc -l.
/usr/bin/find "${(f)^logfiles}"*(N) ! -user root -or ! -group wheel \
    | /usr/bin/awk 'END { print NR }'

Fixing ownership:

/usr/bin/find "${(f)^logfiles}"*(N) ! -user root -or ! -group wheel \
    -execdir /usr/sbin/chown root:wheel {} +

Checking for permissions:

/usr/bin/find "${(f)^logfiles}"*(N) \
    \( -type d ! -perm 750 \) -or \( ! -type d ! -perm 640 \) \
| /usr/bin/awk 'END { print NR }'

Fixing permissions:

/usr/bin/find "${(f)^logfiles}"*(N) -type d ! -perm 750 \
    -execdir /bin/chmod 750 {} +
/usr/bin/find "${(f)^logfiles}"*(N) ! -type d ! -perm 640 \
    -execdir /bin/chmod 640 {} +

Suggestions for configuration file checks

All code below assumes that:

  1. Only one of file, directory, or store_dir is actually possible.
  2. Appending uid=0 gid=0 mode=0640 to the very end is correct and actually affects the file in question (rather than breaking the configuration file completely).
  3. If a file was configured with > then a reference to it from ? or * doesn't need to be configured again (unless inline overrides for uid, gid or mode are present).

Checking uid and gid in configuration files:

uid_gid_err=$(
    awk -v l=0 -v err=0 '
        function count_err(has_uid, has_gid, i) {
            has_uid = has_gid = 0
            for (i = 2; i <= NF; ++i) {
                if ($i ~ /^uid=/) {
                    ++has_uid
                    if ($i != "uid=0") { ++err }
                } else if ($i ~ /^gid=/) {
                    ++has_gid
                    if ($i != "gid=0") { ++err }
                }
            }
            if (has_uid != 0 || has_uid != 1) { ++err }
            if (has_gid != 1) { ++err }
        }

        function file_seen(j, i) {
            for (i in logfiles) {
                if (logfiles[i] == $j) {
                    return 1
                }
            }
            return 0
        }

        $1 == ">" {
            logfiles[l++] = $2
            count_err()
        }

        $1 == "?" || $1 == "*" {
            for (i = 2; i <= NF; ++i) {
                if ($i ~ /^(file|directory|store_dir)$/) {
                    if (!file_seen(++i) || $0 ~ /[[:space:]][ug]id=/) {
                        count_err()
                    }
                    break
                }
            }
        }

        END { print err }
    ' /etc/asl{.conf,/*}
)
echo "$uid_gid_err"

Fixing uid and gid in configuration files:

for conf in /etc/asl{.conf,/*}; do
    conf_content=$(< "$conf")
    awk -v l=0 '
        function fix_uid_gid() {
            gsub(/([[:space:]]+[ug]id=[[:digit:]]+)+([[:space:]]+|$)/, " ")
            sub(/[[:space:]]*$/, " uid=0 gid=0")
        }

        function file_seen(j, i) {
            for (i in logfiles) {
                if (logfiles[i] == $j) {
                    return 1
                }
            }
            return 0
        }

        $1 == ">" {
            logfiles[l++] = $2
            fix_uid_gid()
        }

        $1 == "?" || $1 == "*" {
            for (i = 2; i <= NF; ++i) {
                if ($i ~ /^(file|directory|store_dir)$/) {
                    if (!file_seen(++i) || $0 ~ /[[:space:]][ug]id=/) {
                        fix_uid_gid()
                    }
                    break
                }
            }
        }

        { print }
    ' > "$conf" <<< "$conf_content"
done

Checking mode in configuration files (cannot really know whether the file is a directory, assumes that directory already have execute permissions set):

mode_err=$(
    awk -v l=0 -v err=0 '
        function count_err(has_mode, i) {
            has_mode = 0
            for (i = 2; i <= NF; ++i) {
                if ($i ~ /^mode=/) {
                    ++has_mode
                    if ($i != "mode=0750" || $i != "mode=0640") { ++err }
                }
            }
            if (has_mode != 1) { ++err }
        }

        function file_seen(j, i) {
            for (i in logfiles) {
                if (logfiles[i] == $j) {
                    return 1
                }
            }
            return 0
        }

        $1 == ">" {
            logfiles[l++] = $2
            count_err()
        }

        $1 == "?" || $1 == "*" {
            for (i = 2; i <= NF; ++i) {
                if ($i ~ /^(file|directory|store_dir)$/) {
                    if (!file_seen(++i) || $0 ~ /[[:space:]]mode=/) {
                        count_err()
                    }
                    break
                }
            }
        }

        END { print err }
    ' /etc/asl{.conf,/*}
)

Fixing mode in configuration files:

for conf in /etc/asl{.conf,/*}; do
    conf_content=$(< "$conf")
    awk -v l=0 '
        function fix_mode(mode) {
            mode = $0 ~ /[[:space:]]mode=07/ ? " mode=0750" : " mode=0640"
            gsub(/([[:space:]]+mode=[[:digit:]]+)+([[:space:]]+|$)/, " ")
            sub(/[[:space:]]*$/, mode)
        }

        function file_seen(j, i) {
            for (i in logfiles) {
                if (logfiles[i] == $j) {
                    return 1
                }
            }
            return 0
        }

        $1 == ">" {
            logfiles[l++] = $2
            fix_mode()
        }

        $1 == "?" || $1 == "*" {
            for (i = 2; i <= NF; ++i) {
                if ($i ~ /^(file|directory|store_dir)$/) {
                    if (!file_seen(++i) || $0 ~ /[[:space:]]mode=/) {
                        fix_mode()
                    }
                    break
                }
            }
        }

        { print }
    ' > "$conf" <<< "$conf_content"
done

Changes these configuration file fixes make can be inspected by running:

for conf in /etc/asl{.conf,/*}; do
    conf_content=$(< "$conf")
    diff --color=always --unified "$conf" <(awk -v l=0 '
        COPY FIX FOR EITHER UID/GID OR MODE HERE
    ' <<< "$conf_content")
done
Originally created by @nihil-admirari on GitHub. Originally assigned to: @brodjieski on GitHub. [`os_asl_log_files_owner_group_configure`](https://github.com/usnistgov/macos_security/blob/sonoma/rules/os/os_asl_log_files_owner_group_configure.yaml) and [`os_asl_log_files_permissions_configure`](https://github.com/usnistgov/macos_security/blob/sonoma/rules/os/os_asl_log_files_permissions_configure.yaml) both use the following code to collect the names of log files: ```sh /usr/bin/grep -e '^>' /etc/asl.conf /etc/asl/* | /usr/bin/awk '{ print $2 }') ``` According to [man asl.conf](https://www.manpagez.com/man/5/asl.conf/): 1. Lines starting with `>` configure output options for files or **directories** to be referenced later by query rules. `os_asl_log_files_permissions_configure` sets 640 regardless, making directories non-listable. 2. Query rules can have `file` and `directory` (`store_dir` is used de facto even though it is not mentioned in the `man`) configured inline. Above code completely ignores files configured inline. 3. Filenames are not required to be absolute: > If the pathname specified is not an absolute path, `syslogd` will treat the given path as relative to `/var/log` (for `/etc/asl.conf`), or for other output modules relative to `/var/log/module/NAME` where `NAME` is the module name. 4. If log rotation is in use, actual filename will have a timestamp or a counter appended to it. 5. Ownership is `root:admin` by default, but can be configured directly within configuration files by setting `uid=0 gid=0`, and similar for permissions `mode=0640` (or `mode 0750` for directories). Provided fixes do not modify configuration files themselves, which means that newly created log files won't have correct group and mode set. 6. According to `man asl.conf` there is an additional parameter ``` access Sets read access controls for messages that match the associated query pattern. syslogd will restrict read access to matching messages to a specific user and group. The user ID number and group ID number must fol- low the ``access'' keyword as parameters. ``` Files which have it, configure access to `root:admin` (0:80). I've no idea whether it should be hardened to `root:wheel`. ### Examples On my machine (Sonoma 14.2.1): ```sh /usr/bin/grep -e '^>' /etc/asl.conf /etc/asl/* | /usr/bin/awk '{ print $2 }') ``` produces ``` /var/log/asl/Logs/aslmanager system.log cdscheduler.log /var/log/com.apple.contacts.ContactsAutocomplete /var/log/CoreDuet/coreduetd.log /var/log/mail.log /private/var/log/keybagd.log /private/var/log/keybagd.log /Library/Logs/CrashReporter ``` 1. ``` /var/log/com.apple.contacts.ContactsAutocomplete /Library/Logs/CrashReporter ``` are directories and should have 750 permissions, not 640. 2. `/etc/asl.conf` has `? [= Facility com.apple.alf.logging] file appfirewall.log file_max=5M all_max=50M`, but `appfirewall.log` is not mentioned in the output at all. `/etc/asl/com.apple.MessageTracer` has `* store_dir /var/log/DiagnosticMessaged ttl=30`, which is also missed. There are other missing log files beyond these two. 3. `system.log` was mentioned in `/etc/asl.conf`, which means it's actually `/var/log/system.log` on disc. `cdscheduler.log` was mentioned in `/etc/asl/com.apple.cdscheduler`, which means it's actually `/var/log//module/com.apple.cdscheduler/cdscheduler.log` on disc. 4. `/var/log/asl/Logs/aslmanager` has `style=lcl-b` and is actually stored as `/var/log/asl/Logs/aslmanager.%Y%m%dT%H%M%S%z` on disc. `stat` doesn't glob potential suffixes: ```sh /usr/bin/stat -f '%Su:%Sg:%N' $(/usr/bin/grep -e '^>' /etc/asl.conf /etc/asl/* | /usr/bin/awk '{ print $2 }') ``` ### Suggestions for filesystem checks Log filenames collection: ```sh logfiles=$( for f in /etc/asl{.conf,/*}; do if [[ $f == /etc/asl.conf ]]; then log_dir=/var/log/ else log_dir="/var/log/module/$(basename "$f")/" fi awk -v log_dir="$log_dir" ' function print_log(j) { print $j ~ "^/" ? $j : log_dir$j } $1 == ">" { print_log(2) } $1 ~ /^[?*]$/ { for (i = 2; i <= NF; ++i) { if ($i ~ "^(file|directory|store_dir)$") { print_log(++i) } } } ' "$f" done | sort -u ) ``` Checking for ownership. Notes: 1. Uses zsh globbing, won't run in bash. 2. `awk 'END { print NR }'` doesn't require `xargs` to remove spaces compared to `wc -l`. ```sh /usr/bin/find "${(f)^logfiles}"*(N) ! -user root -or ! -group wheel \ | /usr/bin/awk 'END { print NR }' ``` Fixing ownership: ```sh /usr/bin/find "${(f)^logfiles}"*(N) ! -user root -or ! -group wheel \ -execdir /usr/sbin/chown root:wheel {} + ``` Checking for permissions: ```sh /usr/bin/find "${(f)^logfiles}"*(N) \ \( -type d ! -perm 750 \) -or \( ! -type d ! -perm 640 \) \ | /usr/bin/awk 'END { print NR }' ``` Fixing permissions: ```sh /usr/bin/find "${(f)^logfiles}"*(N) -type d ! -perm 750 \ -execdir /bin/chmod 750 {} + /usr/bin/find "${(f)^logfiles}"*(N) ! -type d ! -perm 640 \ -execdir /bin/chmod 640 {} + ``` ### Suggestions for configuration file checks All code below assumes that: 1. Only one of `file`, `directory`, or `store_dir` is actually possible. 2. Appending ` uid=0 gid=0 mode=0640` to the very end is correct and actually affects the `file` in question (rather than breaking the configuration file completely). 3. If a file was configured with `>` then a reference to it from `?` or `*` doesn't need to be configured again (unless inline overrides for `uid`, `gid` or `mode` are present). Checking `uid` and `gid` in configuration files: ```sh uid_gid_err=$( awk -v l=0 -v err=0 ' function count_err(has_uid, has_gid, i) { has_uid = has_gid = 0 for (i = 2; i <= NF; ++i) { if ($i ~ /^uid=/) { ++has_uid if ($i != "uid=0") { ++err } } else if ($i ~ /^gid=/) { ++has_gid if ($i != "gid=0") { ++err } } } if (has_uid != 0 || has_uid != 1) { ++err } if (has_gid != 1) { ++err } } function file_seen(j, i) { for (i in logfiles) { if (logfiles[i] == $j) { return 1 } } return 0 } $1 == ">" { logfiles[l++] = $2 count_err() } $1 == "?" || $1 == "*" { for (i = 2; i <= NF; ++i) { if ($i ~ /^(file|directory|store_dir)$/) { if (!file_seen(++i) || $0 ~ /[[:space:]][ug]id=/) { count_err() } break } } } END { print err } ' /etc/asl{.conf,/*} ) echo "$uid_gid_err" ``` Fixing `uid` and `gid` in configuration files: ```sh for conf in /etc/asl{.conf,/*}; do conf_content=$(< "$conf") awk -v l=0 ' function fix_uid_gid() { gsub(/([[:space:]]+[ug]id=[[:digit:]]+)+([[:space:]]+|$)/, " ") sub(/[[:space:]]*$/, " uid=0 gid=0") } function file_seen(j, i) { for (i in logfiles) { if (logfiles[i] == $j) { return 1 } } return 0 } $1 == ">" { logfiles[l++] = $2 fix_uid_gid() } $1 == "?" || $1 == "*" { for (i = 2; i <= NF; ++i) { if ($i ~ /^(file|directory|store_dir)$/) { if (!file_seen(++i) || $0 ~ /[[:space:]][ug]id=/) { fix_uid_gid() } break } } } { print } ' > "$conf" <<< "$conf_content" done ``` Checking `mode` in configuration files (cannot really know whether the file is a directory, assumes that directory already have execute permissions set): ```sh mode_err=$( awk -v l=0 -v err=0 ' function count_err(has_mode, i) { has_mode = 0 for (i = 2; i <= NF; ++i) { if ($i ~ /^mode=/) { ++has_mode if ($i != "mode=0750" || $i != "mode=0640") { ++err } } } if (has_mode != 1) { ++err } } function file_seen(j, i) { for (i in logfiles) { if (logfiles[i] == $j) { return 1 } } return 0 } $1 == ">" { logfiles[l++] = $2 count_err() } $1 == "?" || $1 == "*" { for (i = 2; i <= NF; ++i) { if ($i ~ /^(file|directory|store_dir)$/) { if (!file_seen(++i) || $0 ~ /[[:space:]]mode=/) { count_err() } break } } } END { print err } ' /etc/asl{.conf,/*} ) ``` Fixing `mode` in configuration files: ```sh for conf in /etc/asl{.conf,/*}; do conf_content=$(< "$conf") awk -v l=0 ' function fix_mode(mode) { mode = $0 ~ /[[:space:]]mode=07/ ? " mode=0750" : " mode=0640" gsub(/([[:space:]]+mode=[[:digit:]]+)+([[:space:]]+|$)/, " ") sub(/[[:space:]]*$/, mode) } function file_seen(j, i) { for (i in logfiles) { if (logfiles[i] == $j) { return 1 } } return 0 } $1 == ">" { logfiles[l++] = $2 fix_mode() } $1 == "?" || $1 == "*" { for (i = 2; i <= NF; ++i) { if ($i ~ /^(file|directory|store_dir)$/) { if (!file_seen(++i) || $0 ~ /[[:space:]]mode=/) { fix_mode() } break } } } { print } ' > "$conf" <<< "$conf_content" done ``` Changes these configuration file fixes make can be inspected by running: ```sh for conf in /etc/asl{.conf,/*}; do conf_content=$(< "$conf") diff --color=always --unified "$conf" <(awk -v l=0 ' COPY FIX FOR EITHER UID/GID OR MODE HERE ' <<< "$conf_content") done ```
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: usnistgov/macos_security#120