17

For config auditing reasons, I want to be able to search my ext3 filesystem for files which have the immutable attribute set (via chattr +i). I can't find any options for find or similar that do this. At this point, I'm afraid I'll have to write my own script to parse lsattr output for each directory. Is there a standard utility that provides a better way?

  • I should have clarified that in my case, I'm auditing just for config management, not intrusion detection, so I don't have to worry too much about newlines, as I know the file names I'm working with won't have them. Nonetheless, the newline issue is worth keeping in mind, so I'll leave my question as-is. – depquid Jun 3 '14 at 16:11
9

It can be partially accomplished by piping the grep command to lsattr command.

lsattr -R | grep +i

However, I believe when you mention the entire ext3 file system the search might involve /proc , /dev and some other directories which if reports some error you just want to ignore. You can probably run the command as,

lsattr -R 2>/dev/null | grep -- "-i-"

You might want to make the grep a bit more strict by using grep's PCRE facility to more explicitly match the "-i-".

lsattr -R 2>/dev/null | grep -P "(?<=-)i(?=-)"

This will then work for situations such as this:

$ lsattr -R 2>/dev/null afile | grep -P "(?<=-)i(?=-)"
----i--------e-- afile

But is imperfect. If there are additional attributes enabled around the immutable flag, then we'll not match them, and this will be fooled by files who's names happen to match the above pattern as well, such as this:

$ lsattr -R 2>/dev/null afile* | grep -P "(?<=-)i(?=-)"
----i--------e-- afile
-------------e-- afile-i-am

We can tighten up the pattern a bit more like this:

$ lsattr -a -R 2>/dev/null afile* | grep -P "(?<=-)i(?=-).* "
----i--------e-- afile

But it's still a bit too fragile and would require additional tweaking depending on the files within your filesystem. Not to mention as @StephaneChazeles has mentioned in comments that this can be gamed fairly easily by the inclusion of newlines with a files name to bypass the above pattern to grep.

References

https://groups.google.com/forum/#!topic/alt.os.linux/LkatROg2SlM

  • 1
    Ha I read the same thread and was about to post. I'll add my extras to yours instead. – slm May 31 '14 at 3:25
  • @slm, you are most welcome to make any changes :) – Ramesh May 31 '14 at 5:59
  • 2
    Probably not good for auditing as one can fake or hide an immutable file by have newline characters in the file name with that approach. Also, it's not uncommon for filenames to have -i- in its name (there are 34 on the system I'm currently logged on). You'll probably want the -a option as well – Stéphane Chazelas May 31 '14 at 7:13
  • 1
    Just out of curiosity, what is the +i supposed to be for in the first example? It doesn't work for me. Also, grepping for -i- assumes that the attributes that appear adjacent to i (such as a) are unset. – depquid Jun 3 '14 at 16:25
  • 1
    Why not simply grep for ^....i? Or at least something like ^[^ ]*i if the i can be in other position than fifth. – Ruslan Jun 24 '17 at 13:39
6

Given that the purpose of the script is auditing, it is especially important to deal correctly with arbitrary file names, e.g. with names containing newlines. This makes it impossible to use lsattr on multiple files simultaneously, since the output of lsattr can be ambiguous in that case.

You can recurse with find and call lsattr on one file at a time. It'll be pretty slow though.

find / -xdev -exec sh -c '
  for i do
     attrs=$(lsattr -d "$i"); attrs=${attrs%% *}
     case $attrs in
       *i*) printf "%s\0" "$i";;
     esac
  done' sh {} +

I recommend using a less cranky language such as Perl, Python or Ruby and doing the work of lsattr by yourself. lsattr operates by issuing a FS_IOC_GETFLAGS ioctl syscall and retrieving the file's inode flags. Here's a Python proof-of-concept.

#!/usr/bin/env python2
import array, fcntl, os, sys
FS_IOC_GETFLAGS = 0x80086601
EXT3_IMMUTABLE_FL = 0x00000010
count = 0
def check(filename):
    fd = os.open(filename, os.O_RDONLY)
    a = array.array('L', [0])
    fcntl.ioctl(fd, FS_IOC_GETFLAGS, a, True)
    if a[0] & EXT3_IMMUTABLE_FL: 
        sys.stdout.write(filename + '\0')
        global count
        count += 1
    os.close(fd)
for x in sys.argv[1:]:
    for (dirpath, dirnames, filenames) in os.walk(x):
        for name in dirnames + filenames:
            check(os.path.join(dirpath, name))
if count != 0: exit(1)
  • 1
    FYI on my system FS_IOC_GETFLAGS is 0x80046601. – antonone Sep 4 '15 at 10:25
  • 1
    The value of FS_IOC_GETFLAGS depends on sizeof(long). See e.g. the following bash command to find out what the macro expands to in C: gcc -E - <<< $'#include <linux/fs.h>\nFS_IOC_GETFLAGS' | tail -n1. I got from it the following expression: (((2U) << (((0 +8)+8)+14)) | ((('f')) << (0 +8)) | (((1)) << 0) | ((((sizeof(long)))) << ((0 +8)+8))), which simplifies to (2U << 30) | ('f' << 8) | 1 | (sizeof(long) << 16). – Ruslan Jun 24 '17 at 13:51
3

To deal with arbitrary file names (including those containing newline characters), the usual trick is to find files inside .//. instead of .. Because // cannot normally occur while traversing the directory tree, you're sure that a // signals the start of a new filename in the find (or here lsattr -R) output.

lsattr -R .//. | awk '
  function process() {
    i = index(record, " ")
    if (i && index(substr(record,1,i), "i"))
      print substr(record, i+4)
  }
  {
    if (/\/\//) {
      process()
      record=$0
    } else {
      record = record "\n" $0
    }
  }
  END{process()}'

Note that the output will still be newline separated. If you need to post-process it, you'll have to adapt it. For instance, you could add a -v ORS='\0' to be able to feed it to GNU's xargs -r0.

Also note that lsattr -R (at least 1.42.13) cannot report the flags of files whose path is larger than PATH_MAX (usually 4096), so someone could hide such an immutable file by moving its parent directory (or any of the path components that lead to it, except itself as it's immutable) into a very deep directory.

A work around would be to use find with -execdir:

find . -execdir sh -c '
  a=$(lsattr -d "$1") &&
    case ${a%% *} in
      (*i*) ;;
      (*) false
    esac' sh {} \; -print0

Now, with -print0, that's post-processable, but if you intend to do anything with those paths, note that any system call on file paths greater than PATH_MAX would still fail and directory components could have be renamed in the interval.

If we're to get a reliable report on a directory tree that's potentially writable by others, there are a few more issues inherent to the lsattr command itself that we'd need to mention:

  • the way lsattr -R . traverses the directory tree, it is subject to race conditions. One can make it descend to directories outside of the directory tree routed at . by replacing some directories with symlinks at the right moment.
  • even lsattr -d file has a race condition. Those attributes are only applicable to regular files or directories. So lsattr does a lstat() first to check that the file is of the right types and then does open() followed by ioctl() to retrieve the attributes. But it calls open() without O_NOFOLLOW (nor O_NOCTTY). Someone could replace file with a symlink to /dev/watchdog for instance between the lstat() and open() and cause the system to reboot. It should do open(O_PATH|O_NOFOLLOW) followed by fstat(), openat() and ioctl() here to avoid the race conditions.
2

Thanks to Ramesh, slm and Stéphane for pointing me in the right direction (I was missing the -R switch for lsattr). Unfortunately, none of the answers so far worked correctly for me.

I came up with the following:

lsattr -aR .//. | sed -rn '/i.+\.\/\/\./s/\.\/\///p'

This protects against newlines being used to make a file appear as being immutable when it is not. It does not protect against files that are set as immutable and have newlines in their filenames. But since such a file would have to be made that way by root, I can be confident that such files don't exist on my filesystem for my use case. (This method is not suitable for intrusion detection in cases where the root user may be compromised, but then neither is using the same system's lsattr utility which is also owned by the same root user.)

  • Only root can add the immutable bit to a file, but potentially other users can later rename path components that lead to those files, so the file path may contain newline. Also a user can create a file path (not immutable) that would fool your script into thinking another file is immutable. – Stéphane Chazelas Nov 1 '17 at 19:20
2

Using find -exec is too slow, parsing output of lsattr is unreliable similarly to that of ls, using Python as in the answer by Gilles requires to choose the constant for ioctl depending on whether the Python interpreter is 32- or 64-bit...

The problem at hand is more or less low-level, so let's go lower level: C++ is not that bad as a scripting language :) As a bonus, it has access to system C headers with full power of the C preprocessor.

The following program searches for immutable files, staying within one filesystem, i.e. never crosses mount points. To search the apparent tree, crossing mount points as needed, remove FTW_MOUNT flag in the nftw call. Also it doesn't follow symlinks. To do follow them, remove FTW_PHYS flag.

#define _FILE_OFFSET_BITS 64
#include <iostream>
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/fs.h>
#include <sys/stat.h>
#include <ftw.h>

bool isImmutable(const char* path)
{
    static const int EXT3_IMMUTABLE_FLAG=0x10;

    const int fd=open(path,O_RDONLY|O_NONBLOCK|O_LARGEFILE);
    if(fd<=0)
    {
        perror(("Failed to open file \""+std::string(path)+"\"").c_str());
        return false;
    }
    unsigned long attrs;
    if(ioctl(fd,FS_IOC_GETFLAGS,&attrs)==-1)
    {
        perror(("Failed to get flags for file \""+std::string(path)+"\"").c_str());
        close(fd);
        return false;
    }
    close(fd);
    return attrs & EXT3_IMMUTABLE_FLAG;
}

int processPath(const char* path, const struct stat* info, int type, FTW* ftwbuf)
{
    switch(type)
    {
    case FTW_DNR:
        std::cerr << "Failed to read directory: " << path << "\n";
        return 0;
    case FTW_F:
        if(isImmutable(path))
            std::cout << path << '\n';
        return 0;
    }
    return 0;
}

int main(int argc, char** argv)
{
    if(argc!=2)
    {
        std::cerr << "Usage: " << argv[0] << " dir\n";
        return 1;
    }
    static const int maxOpenFDs=15;
    if(nftw(argv[1],processPath,maxOpenFDs,FTW_PHYS|FTW_MOUNT))
    {
        perror("nftw failed");
        return 1;
    }
}
-1

Instead of piping the output to grep, why not just use awk to only match the 'i' in the first field of the output?

lsattr -Ra 2>/dev/null /|awk '$1 ~ /i/ && $1 !~ /^\// {print}'

In fact, I run this daily via cron to scan the /etc directory on hundreds of servers and send the output to syslog. I can then generate a daily report via Splunk:

lsattr -Ra 2>/dev/null /etc|awk '$1 ~ /i/ && $1 !~ /^\// {print "Immutable_file="$2}'|logger -p local0.notice -t find_immutable
  • Your first code snippet has a typo and your second one doesn't find immutable files on my system. – depquid Jul 2 '14 at 21:19
  • Fixed typo in first command. Perhaps the second one isn't finding any immutable files because there aren't any? – Rootdev Jul 3 '14 at 0:19
  • I didn't notice the second command was only looking in /etc. But both commands incorrectly find a non-immutable file created with touch `"echo -e "bogus\n---------i---e-- changeable"`" – depquid Jul 3 '14 at 15:27
  • It says in my original post that I am running that via cron to scan the /etc directory. I can't help it if you didn't read either the post or the command before running it. And yes, you can probably construct an edge case to fool just about any search if you feel like it, but since you were so quick to point out the typo in my original command (missing the last '), I'll point out that your command doesn't work as written so won't create anything! :-) – Rootdev Jul 3 '14 at 23:25
  • My bad. Try this: touch "`echo -e 'bogus\n---------i---e-- changeable'`" – depquid Jul 4 '14 at 3:33
-1

Very simple, go to the suspect folder and run the command:

lsattr -laR | grep *immutable

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.