Парсим проприетарный формат .lnk под Linux’ом

в 15:15, , рубрики: c++, linux, парсинг, системное администрирование, метки: , ,

С чего началась эта история: однажды перестала работать синхронизация между двумя серверами. На одном из серверов (под управлением Windows) в расшаренной папке лежали документы, а на втором (под управлением Debian) был поднят апач с webdav. В папке на первом сервере было несколько подпапок. В одной лежали документы, а в остальных были сделаны ярлыки на документы, таким образом документы были рассортированы по подпапкам. И содержимое папки на первом сервере синхронизировалось с папкой на втором сервере следующим образом: копировалось содержимое папки, а затем ярлыки заменялись на файлы, на которые они указывали. То есть, если ярлык, к примеру, указывал на документ corporate-template-65178.doc, то ярлык удалялся, а на его место помещался этот самый corporate-template-65178.doc

Эта система работала три года, а потом внезапно работать перестала, безо всяких предупреждений.
И её надо было во что бы то ни стало починить.

Если еще интересно, вэлкам под кат.

Итак. Был проведен разбор полетов, опрошено большое количество людей, перекопано море документации, но нигде не было найдено никакой информации о том, кто это делал и когда. Когда началось перекапывание серверов, был найден сервер, который являлся почтовым шлюзом. И именно на нем были обнаружены скрипты на vbs и cmd, которые и занимались синхронизацией. Сначала из расшаренной папки копировалось rsync'ом содержимое в локальную, потом ярлыки заменялись на документы посредством vbs-скрипта, а затем через cmd с использованием scp для Windows (без пароля, с ключом) файлы копировались в webdav, и в процессе копирования терялось время последнего изменения.
А перестала работать эта система потому, что изменились ключи sshd, соответственно, файлы перестали копироваться в webdav.

Администраторы почтового шлюза были очень удивлены тем, что на их сервере, где не должно быть больше никакого функционала, кроме отправки почты, были обнаружены столь странные скрипты и задание в планировщике, осуществляющее указанные действия. Вместе с этим они негативно и без цензуры высказались на тему использования этого сервера в качестве перевалочной базы.

Схема действительно была организована странно.
С первого сервера на второй данные копировал третий, который вообще никакого отношения к первым двум не имел.
Практически сразу же в голову пришла мысль избавиться от посредника, тем самым упростив схему работы системы.

Поскольку серверов два, скрипты синхронизации надо было делать на каком-то из них.
Сервер на Windows использовать оказалось невозможно в связи с тем, что административных прав на него не было, и единственной задачей, которую он выполнял, было функционирование как файловый сервер.

Таким образом, остался только один вариант — реализовать подобный функционал на linux'е.
Задача сразу же осложнилась тем, что в свободном доступе не было средств для парсинга проприетарного формата виндовых ярлыков (формата .lnk). С другой стороны, это придало задаче интереса.

Задачу решать надо было срочно, и к помощи был призван гугл, в котором нашлась аж целая одна программка, и та уже в бинарном виде. Поскольку неизвестно, что там внутри, решено было не использовать эту программу, а написать свою.

В том же гугле был найден документ, полученный реверс-инжинирингом, который описывает структуру файлов этого формата.
Он и был использован как источник информации.

Сама система включила в себя следующие части:
1) Программа на языке С, которая парсит ярлыки
2) скрипты синхронизации и обработки
3) задание в кроне

Итак,
1) Программа

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

   struct TLinkHeaderInfo{
        //4 bytes         Always 4C 00 00 00          This is how windows knows it is a shortcut file
       unsigned long Signature;
        //16 bytes       GUID for shortcut files       The current GUID for shortcuts.
        //It may change in the future. 01 14 02 00 00 00 00 00 C0 00 00 00 00 00 46
       unsigned char GUID[16];
        //4 bytes         Shortcut flags                    Shortcut flags are explained below
       unsigned long ShortcutFlags;
        //4 bytes         Target file flags                   Flags are explained below
       unsigned long TargetFileFlags;
        //8 bytes         Creation time
       unsigned char CreationTime[8];
        //8 bytes         Last Access time
       unsigned char LastAccessTime[8];
        //8 bytes         Modification time
       unsigned char ModificationTime[8];
        //4 bytes         File length
        //The length of the target file. 0 if the target is not a file.
        //This value is used to find the target when the link is broken.
       unsigned long FileLength;
        //4 bytes         Icon number
        //If the file has a custom icon (set by the flags bit 6),
        //then this long integer indicates the index of the icon to use. Otherwise it is zero.
       unsigned long IconNumber;
        //4 bytes         Show Window
        //the ShowWnd value to pass to the target application when starting it.
        //1:Normal Window 2:Minimized 3:Maximized
       unsigned long ShowWindow;
        //      4 bytes         Hot Key                 The hot key assigned for this shortcut
       unsigned long HotKey;
        //      4 bytes         Reserved                Always 0
       unsigned long Reserved1;
        //      4 bytes         Reserved                Always 0
       unsigned long Reserved2;
   };
   
void main(int argc, char** argv)
{
    
    const int HEADER_SIZE=76;
    const int FILE_ALLOCATION_INFO_HEADER_SIZE=28;
    char TempString[255];
    int i, LinkFileSize;
    char LinkFileName[255];
    char ExtractedFileName[255];
    FILE* LinkFile;
    struct stat FileStatInfo;
    struct TLinkHeaderInfo HeaderStructure;
    size_t result;
    long TempInt=0; 
    
    char LinkFileContent[10240];
    
    
    if(strcmp(argv[1],"")==0)
    {
      printf("No parameter used.n");
      return;
    };
    
    printf("FileName: %sn",argv[1]);
    LinkFile=fopen(argv[1],"r");
    if(LinkFile==NULL)
    {
      fclose(LinkFile);
      printf("No file found.n");
      return;
    };
    
    // Open LNK File and parse it for file name
    
    // Get it size
    stat(argv[1],&FileStatInfo);
    printf("Link Filesize: %dn", FileStatInfo.st_size);
    LinkFileSize=FileStatInfo.st_size;
    
    // read file to array
    result = fread (LinkFileContent,1,LinkFileSize,LinkFile);
    if (result != LinkFileSize)
    {
        printf ("Reading errorn");
    };
    fclose(LinkFile);
    
    HeaderStructure.Signature=LinkFileContent[0]*65536*256;
    HeaderStructure.Signature+=LinkFileContent[1]*65536;
    HeaderStructure.Signature+=LinkFileContent[2]*256+LinkFileContent[3];
    printf("Signature: %Xn",HeaderStructure.Signature);
    
    printf("GUID: ");
    for(i=0;i<16;i++)
        {
            HeaderStructure.GUID[i]=LinkFileContent[4+i];
            printf(" %X",HeaderStructure.GUID[i]);
        };
        printf("n");
    
    HeaderStructure.ShortcutFlags=LinkFileContent[23]*65536*256;
    HeaderStructure.ShortcutFlags+=LinkFileContent[22]*65536;
    HeaderStructure.ShortcutFlags+=LinkFileContent[21]*256+LinkFileContent[20];
    printf("Shortcut Flags: %Xn",HeaderStructure.ShortcutFlags);

    HeaderStructure.TargetFileFlags=LinkFileContent[27]*65536*256;
    HeaderStructure.TargetFileFlags+=LinkFileContent[26]*65536;
    HeaderStructure.TargetFileFlags+=LinkFileContent[25]*256+LinkFileContent[24];
    printf("Target File Flags: %Xn",HeaderStructure.TargetFileFlags);
    
    for(i=0;i<8;i++){HeaderStructure.CreationTime[i]=LinkFileContent[28+i];};
    for(i=0;i<8;i++){HeaderStructure.LastAccessTime[i]=LinkFileContent[36+i];};
    for(i=0;i<8;i++){HeaderStructure.LastAccessTime[i]=LinkFileContent[44+i];};
    
    HeaderStructure.FileLength=LinkFileContent[55]*65536*256;
    HeaderStructure.FileLength+=LinkFileContent[54]*65536;
    HeaderStructure.FileLength+=LinkFileContent[53]*256+LinkFileContent[52];
    printf("Target File Size (bytes): %dn",HeaderStructure.FileLength);
    
    HeaderStructure.IconNumber=LinkFileContent[59]*65536*256;
    HeaderStructure.IconNumber+=LinkFileContent[58]*65536;
    HeaderStructure.IconNumber+=LinkFileContent[57]*256+LinkFileContent[56];
    printf("IconNumber: %Xn",HeaderStructure.IconNumber);
    
    HeaderStructure.ShowWindow=LinkFileContent[63]*65536*256;
    HeaderStructure.ShowWindow+=LinkFileContent[62]*65536;
    HeaderStructure.ShowWindow+=LinkFileContent[61]*256+LinkFileContent[60];
    printf("ShowWindow Attributes: %Xn",HeaderStructure.IconNumber);
    
    HeaderStructure.HotKey=LinkFileContent[67]*65536*256;
    HeaderStructure.HotKey+=LinkFileContent[66]*65536;
    HeaderStructure.HotKey+=LinkFileContent[65]*256+LinkFileContent[64];
    printf("HotKey Attributes: %Xn",HeaderStructure.HotKey);
    
    // Now extract file name and path
    
    unsigned int OFFSET=76+(unsigned char)LinkFileContent[76];
    OFFSET+=(unsigned char)LinkFileContent[77]*256+(unsigned char)LinkFileContent[78]+2;
    
    OFFSET+=LinkFileContent[OFFSET];
    printf("Share name: ");
    printf("%sn",&LinkFileContent[OFFSET]);

    OFFSET+=strlen(&LinkFileContent[OFFSET]);
    OFFSET++;

    printf("File name: ");
    printf("%sn",&LinkFileContent[OFFSET]);
}

Ничего изощренного в программе нет.
Компилируем ее

gcc ./lnkinfo.c -o lnkinfo

Выкидываем отладочную информацию

strip ./lnkinfo

В исходник не включен разбор флагов типа местонахождения файла (сетевая папка, съемный диск и т.д.), при желании можно дополнить исходник, внимательно прочитав документ, ссылка на который дана выше.

Самый главный функционал готов, теперь надо его применить.

2) Скрипты.
Первый скрипт осуществляет основные операции
Находится он, скажем, в папке /usr/doc-sync

#!/bin/bash
DOMAIN=MYDOMAIN
USER=MYUSER
PASSWORD=MYPASSWORD

mount.cifs "//server/doc" /mnt/doc -o ro,dom=$DOMAIN,user=$USER,pass=$PASSWORD

cd /usr/doc-sync 

WHERE_TO_PUT=/usr/local/webdav/files/docs
rm -rf $WHERE_TO_PUT/*
rsync -avz /mnt/doc/DocFolder1 $WHERE_TO_PUT
rsync -avz /mnt/doc/DocFolder2 $WHERE_TO_PUT
rsync -avz /mnt/doc/DocFolder3 $WHERE_TO_PUT
umount /mnt/doc


# Находим все файлы с расширением .lnk, прогоняем их программой lnkinfo и выдергиваем из вывода
# название самого файла ярлыка и его таргета (файла, на который он указывает)
find $WHERE_TO_PUT -name "*.lnk" | sed -e 's/ /@/g' | awk '{printf "./lnkinfo "$1" | grep ile | grep ame:n"}' 
| sed -e 's/@/ /g' 
| sed -e 's/(/\(/g' 
| sed -e 's/)/\)/g' 
| sed -e 's/ /\ /g' 
| sed -e 's/lnkinfo\ /lnkinfo /g' 
| sed -e 's/\ grep/ grep/g'
| sed -e 's/grep\/grep/g'
| sed -e 's/\ |/ |/g'
> ./haba-haba.sh && chmod a+x ./haba-haba.sh

# Динамически генерируем скрипт и запускаем его:

PARAMETERS=`./haba-haba.sh | sed -e 's/ /@/g'`
rm ./haba-haba.sh

HABA2_EXIST=`ls | grep haba-haba2.sh`
if [ "$HABA2_EXIST" != "" ]
then
    rm ./haba-haba2.sh
fi

# Найдем таким образом все файлы lnk в папке рекурсивно, включая путь от папки.
i="1"
for PARAMETER in $PARAMETERS
do
    if [ "$i" == "2" ]
    then
        # Если это второй параметр, значит это имя файла в кодировке cp1251, и ее надо перекодировать
        # в utf-8, чтобы сохранить имена файлов
        PARAMETER=`echo $PARAMETER | iconv -f windows-1251 -t utf8`
        echo $PARAMETER >> ./haba-haba2.sh
        i="1"
        continue
    fi
    if [ "$i" == "1" ]
    then
        i="2"
        echo $PARAMETER >> ./haba-haba2.sh
        continue
    fi
done

# Заменяем подстроку на название еще одного скрипта - exchange-lnk.sh
# Единственная задача этого скрипта - Удалить линк-файл и скопировать на его место документ
cat ./haba-haba2.sh 
| sed -e 's/FileName:@/./exchange-lnk.sh /'
| sed -e 's/File@name:@//'
| sed -e 's/\///g'
| sed -e 's/.lnk/.lnk \/g'
| sed -e 's/@/\ /g'
| sed -e 's/(/\(/g'
| sed -e 's/)/\)/g'
>./haba-haba.sh

rm ./haba-haba2.sh
sh ./haba-haba.sh

rm ./haba-haba.sh

Второй скрипт, который удаляет ярлык и на его место копирует документ

#!/bin/sh

# remove first parameter and copy second parameter to first parameter's path

LINK_NAME=`echo $1 | sed -e '{s/ /\ /g}' | sed -e 's/(/\(/g'`

LINK_PATH=$(dirname "$LINK_NAME")
echo $LINK_PATH
echo Copy $2 to path of $1
cp "$2" "$LINK_PATH"
rm "$1"

3) cron
Ну, и последнее, что осталось, это добавить в крон задание, чтобы процесс происходил автоматически.

0 4     * * *   root    /usr/sync/doc-sync.sh

Если есть вопросы, пишите, постараюсь ответить или дополнить/поправить

Автор: 3vi1_0n3


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js