Hacking finger

Hey folks, some day in the past I was talking with guys on #ctrl-c and I dont even remember why but someone said something about the program finger, before this I never had heard about finger before, its seems cool thing to share information in a shared environment like ~tildes, this is a direct quote from wikipedia about finger: The program would supply information such as whether a user is currently logged-on, e-mail address, full name etc. As well as standard user information, finger displays the contents of the .project and .plan files in the user's home directory. Often this file (maintained by the user) contains either useful information about the user's current activities, similar to micro-blogging, or alternatively all manner of humor. In our case we will focus on the all manner of humor, at least a single manner of humor is enough :| the idea behind this text it to make the finger output dynamic. As we know by the previous description the finger will display contents in the .plan file in the user home, as a quick test I just created a file with "Hi Ho!" content and used finger on my self(lol):
giggles@ctrl-c:~$ echo "Hi Ho!" > .plan giggles@ctrl-c:~$ finger giggles Login: giggles Name: Directory: /home/giggles Shell: /bin/bash On since Sat Jul 1 14:05 (CDT) on pts/5 from tmux(1308688).%8 1 minute 12 seconds idle On since Wed Jun 28 18:14 (CDT) on pts/9 from tmux(1308688).%5 1 hour 49 minutes idle On since Sun Jun 18 16:32 (CDT) on pts/15 from tmux(1308688).%0 4 days 5 hours idle On since Sat Jul 1 13:59 (CDT) on pts/19 from tmux(1308688).%7 3 hours 52 minutes idle On since Sat Jul 1 13:56 (CDT) on pts/86 from tmux(1308688).%6 3 hours 43 minutes idle On since Sat Jul 1 13:09 (CDT) on pts/98 from xxx.xxx.xxx.xxx 1 second idle On since Tue Jun 27 12:57 (CDT) on pts/99 from tmux(1308688).%2 4 hours 16 minutes idle No mail. Plan: Hi Ho!
Ok, it worked as expected, the .plan file is on our output, but I wonder if we can use a cool thing instead of just a static file, before trying anything I looked in documentation and found about the ~/.fingerrc script, and wow it does everything I wanted, if this exists then the output of the script will be used instead of default finger output, so I just tried this but it dont work ~.~ Ok, lets look on finger man page and see if there is something which help us:
giggles@ctrl-c:~$ man finger ... HISTORY The finger command appeared in 3.0BSD. Linux NetKit (0.17) August 15, 1999 Linux NetKit (0.17)
I just kept the useful part here, because the main reason the script dont worked is that finger on ctrl-c server is the BSD version which come packaged in Linux NetKit and I was looking on GNU version documentation duh!, But I have an idea to overcome this and reach goal. We just to use a fifo instead of a regular file and monitor it for readings. In theory the finger will open the file, read and then output. So we just need to write to our fifo to give finger the dynamic content! To test this I just created my .plan file as a fifo and tried to write "ohaio" on the fifo before using finger.
giggles@ctrl-c:~$ mkfifo .plan giggles@ctrl-c:~$ echo "ohaio" > .plan
Then from another terminal(tmux panel for real) just finger the user as following
giggles@ctrl-c:~$ finger giggles Login: giggles Name: Directory: /home/giggles Shell: /bin/bash On since Sat Jul 1 14:05 (CDT) on pts/5 from tmux(1308688).%8 ... No mail. No Plan.
Hmm, our echo still blocked trying to write on fifo and the finger said there is No Plan :(, ok time to get more info, so this time we run finger again using strace(could be ltrace too)
giggles@ctrl-c:~$ strace finger giggles .... lstat("/home/giggles/.plan", {st_mode=S_IFIFO|0664, st_size=0, ...}) = 0 write(1, "No Plan.\n", 9No Plan. ) = 9 exit_group(0) = ? +++ exited with 0 +++
Hmm, its using lstat in the file and just going straight saying there is no plan, I guess its checking if its a regular file. To make sure this is the case we can just download this version of finger, there is a lot of mirrors where you can download any program from NetKit, I just downloaded bsd-finger-0.17.tar.gz and after extracting the source a grepped it for lstat()
giggles@ctrl-c:~/lab$ grep -sr lstat bsd-finger-0.17 bsd-finger-0.17/finger/lprint.c: if (lstat(tbuf, &sbuf1) || !S_ISREG(sbuf1.st_mode)) return 0;
OH nice!, We spoted where and why our fifo is being ignored, we cant just use a fifo as our .plan file because its being checked! Never imagened someone will really check if the file is regular... I will also reproduce the entire function here
static int show_text(const char *directory, const char *file_name, const char *header) { int ch, lastc = 0, fd; FILE *fp; struct stat sbuf1, sbuf2; snprintf(tbuf, TBUFLEN, "%s/%s", directory, file_name); if (lstat(tbuf, &sbuf1) || !S_ISREG(sbuf1.st_mode)) return 0; fd = open(tbuf, O_RDONLY); if (fd<0) return 0; if (fstat(fd, &sbuf2)) { close(fd); return 0; } /* if we didn't get the same file both times, bail */ if (sbuf1.st_dev!=sbuf2.st_dev || sbuf1.st_ino!=sbuf2.st_ino) { close(fd); return 0; } fp = fdopen(fd, "r"); if (fp == NULL) { close(fd); return 0; } xprintf("%s", header); while ((ch = getc(fp)) != EOF) { xputc(ch); lastc = ch; } if (lastc != '\n') xputc('\n'); fclose(fp); return 1; }
The first idea that I have to give me time to write dynamic content was trying to fool finger making use of race condition between the the lstat() and open() but unfortunately the developer of finger is clever and he check the st_dev and st_ino fields of the opened file to make sure lstated file and opened file are the same, no luck on this idea it seems that its really impossible to make finger to reach the while loop if our file(.plan) is not created as a regular file. Thinking more about this I still want finger to open my lovely fifo, and maybe there is way, if linux reuse inode number when you remove and create a file in sequence, then we have a chance to fool all conditions tests the show_test() function, to validate this I just use a one-liner to create .plan as regular file, check the inode, remove .plan and create it as a fifo and check the inode again, I also created a useless file between the tests to make sure it really works as expected, here is the results:
giggles@ctrl-c:~$ touch .plan; ls -il .plan; rm .plan; mkfifo .plan; ls -il .plan; rm .plan 612397 -rw-rw-r-- 1 giggles giggles 0 Jul 2 06:13 .plan 612397 prw-rw-r-- 1 giggles giggles 0 Jul 2 06:13 .plan giggles@ctrl-c:~$ touch k giggles@ctrl-c:~$ touch .plan; ls -il .plan; rm .plan; mkfifo .plan; ls -il .plan; rm .plan 612399 -rw-rw-r-- 1 giggles giggles 0 Jul 2 06:13 .plan 612399 prw-rw-r-- 1 giggles giggles 0 Jul 2 06:13 .plan giggles@ctrl-c:~$ touch .plan; ls -il .plan; rm .plan; mkfifo .plan; ls -il .plan; rm .plan 612400 -rw-rw-r-- 1 giggles giggles 0 Jul 2 06:13 .plan 612400 prw-rw-r-- 1 giggles giggles 0 Jul 2 06:13 .plan
Thats good for us, if we can unlink the file and recreate it fast as sonic we can fool finger, this is just forfun since its not possible to rely in race condition because it will be very hard to make the things happen in the correct order, but as a simple proof of concept I create the following program race.c which create .plan and replace it with a fifo and then write a message if some other process had open the fifo to read (this process should be finger in our case):
#include #include #include #include #include int main(int argc, char **argv) { again: int fd = creat(".plan", S_IRWXU|S_IRWXO|S_IRWXG); close(fd); unlink(".plan"); mkfifo(".plan", S_IRWXU|S_IRWXO|S_IRWXG); int fifo = open(".plan", O_WRONLY|O_NONBLOCK); if (fifo>0) { write(fifo, "We love ^C\n", 11); close(fifo); goto end; } unlink(".plan"); goto again; end: printf("We won!\n"); return 0; }
This program loop until it successfully manage to win the race condition, I ran it on tmux using strace until the victory, and for this I needed to run finger in a infinity loop too, here is the results, in the top is the race program and down is the one-liner running finger
giggles@ctrl-c:~$ strace ./a.out ... creat(".plan", 0777) = 3 close(3) = 0 unlink(".plan") = 0 mknodat(AT_FDCWD, ".plan", S_IFIFO|0777) = 0 openat(AT_FDCWD, ".plan", O_WRONLY|O_NONBLOCK) = -1 ENXIO (No such device or address) unlink(".plan") = 0 creat(".plan", 0777) = 3 close(3) = 0 unlink(".plan") = 0 mknodat(AT_FDCWD, ".plan", S_IFIFO|0777) = 0 openat(AT_FDCWD, ".plan", O_WRONLY|O_NONBLOCK) = 3 write(3, "We love ^C\n", 11) = 11 close(3) = 0 newfstatat(1, "", {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x70), ...}, AT_EMPTY_PATH) = 0 getrandom("\x28\xce\xa5\x06\x6a\x9c\x9e\x3f", 8, GRND_NONBLOCK) = 8 brk(NULL) = 0x55b371c46000 brk(0x55b371c67000) = 0x55b371c67000 write(1, "We won!\n", 8We won!
giggles@ctrl-c:~$ while true; do finger giggles | grep "\^"; done We love ^C
What happened here was that finger used lstat and "confirmed" that .plan is a regular file, but before it calls open() our program removed .plan and created a fifo with the same name, fast enough to reuse the same inode number, after this all the checks go fine and finger did not know it opened a fifo, since it opens it readonly mode, our race program will finally manage to open the writing end of fifo and send the content to finger through the fifo! As you can see it need to happen in a very specific order. I know that this dont make possible to make finger content dynamic in a deterministic manner, but exploiting this race condition to bypass the checks and showing that finger can read content from a non regular file is cool enough to be worth to add it all here, even that we dont reached our goal yet, we found a little bug in finger :) This is the end of this article, I tried to research a bit on how can I lock a file on linux, but all ways to do this is not reliable and need to cooperation of other process, by default its not possible to make a regular file to block on read so we have time to add content. Well maybe someone know a way to make open() or read() to block on a regular file, I got really curious about this, if you have an idea let me know ^.^