How to search for files with immutable attribute set?

It can be partially accomplished by piping the lsattr command through the grep 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 might report some errors that 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 whose 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


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
S_IFMT =  0o170000
S_IFDIR = 0o040000
S_IFREG = 0o100000
FS_IOC_GETFLAGS = 0x80086601
EXT3_IMMUTABLE_FL = 0x00000010
count = 0
def check(filename):
    mode = os.lstat(filename).st_mode
    if mode & S_IFMT not in [S_IFREG, S_IFDIR]:
        return
    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)

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.

Tags:

Find

Ext3