2019年10月5日 星期六

監控程式實作

程式主旨:
  1. 網際網路下駭客發動攻擊通常少不了木馬程式的植入。因此阻絕攻擊的第一步在於如何防止惡意檔案或程式的植入。
  2. 以Java Web而言,若被植入程式到部署目錄,就可能被以URL的方式執行。
  3. 藉由監控特定資料夾的檔案寫入,避免被植入惡意檔案。
程式架構:
  1. 使用NIO2的WatchService監看特定資料夾是否有檔案異動。
  2. 使用「樣板方法模式(template method)」建立告警程式,負責在WatchService接收到檔案異動事件後,觸發告警程式進行後續作為,如:
    • 日誌紀錄(log)
    • 馬上移除檔案
  3. 打包程式進行部署。
程式內容:

Guardian
package fsWatch;

import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
import static java.nio.file.LinkOption.*;
import java.nio.file.attribute.*;
import java.io.*;
import java.util.*;

import org.apache.log4j.Logger;

public class Guardian {

  static Logger logger = Logger.getLogger(Guardian.class);
  static Properties myProps = new Properties();

  private final WatchService watchService;
  private final Map<WatchKey, Path> keys;
  private final boolean recursive;
  private boolean trace = false;

  // 1. 將路徑dir指定給WatchService監看
  private void register(Path dir) throws IOException {
    WatchKey key = dir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);//
    if (trace) {
     Path prev = keys.get(key);
     if (prev == null) {
         logger.info(String.format("register: %s", dir));
     } else {
         if (!dir.equals(prev)) {
             logger.info(String.format("update: %s -> %s", prev, dir));
         }
     }
    }
    keys.put(key, dir);
  }

  // 2. 使用Files.walkFileTree遞迴走訪所有目錄,都指定給WatchService監看
  private void registerAll(final Path start) throws IOException {
    Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
        @Override
        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) 
                                                          throws IOException {
            register(dir);
            return FileVisitResult.CONTINUE;
        }
    });
  }

  // 3. 建立建構子
  Guardian(Path dir, boolean recursive) throws IOException {
      this.watchService = FileSystems.getDefault().newWatchService();
      this.keys = new HashMap<WatchKey, Path>();
      this.recursive = recursive;

      if (recursive) {
          logger.info(String.format("Scanning %s ...", dir));
          registerAll(dir);
          logger.info("Done.");
      } else {
          register(dir);
      }

      this.trace = true;
  }

  // 4. 建立與告警程式的關聯
  private List<AbstractAlertListener> listeners = new ArrayList<>();
  void addListener(AbstractAlertListener alarmListener) {
      listeners.add(alarmListener);
  }
  
  // 5.  建立監控核心程式
  // 5.1 將WatchService與Map<WatchKey, Path>結合
  // 5.2 觸發事件時一併觸發其他告警程式
  @SuppressWarnings("unchecked")
  void watch() throws Exception {
    for (;;) {

      WatchKey key;
      try {
          key = watchService.take();
      } catch (InterruptedException x) {
          return;
      }

      Path dir = keys.get(key);
      if (dir == null) {
          logger.error("WatchKey not recognized!!");
          continue;
      }

      for (WatchEvent<?> event : key.pollEvents()) {
          WatchEvent.Kind<?> kind = event.kind();
          if (kind == OVERFLOW) {
              continue;
          }
          WatchEvent<Path> ev = (WatchEvent<Path>) event;
          Path name = ev.context();
          Path child = dir.resolve(name);
          // action
          for (AbstractAlertListener l : listeners) {
              l.alertTemplate(event.kind(), child);
          }
          if (recursive && (kind == ENTRY_CREATE)) {
            try {
                if (Files.isDirectory(child, NOFOLLOW_LINKS)) {
                    registerAll(child);
                }
            } catch (IOException x) {
            }
          }
      }
      // reset key and remove from set if directory no longer accessible
      boolean valid = key.reset();
      if (!valid) {
          keys.remove(key);
          // all directories are inaccessible
          if (keys.isEmpty()) {
              break;
          }
      }
    }
  }
  
  
  // 6. 使用Thread啟動Guardian
  private static void launchGuardian(final String path) throws Exception {
    boolean recursive = true;
    Path dir = Paths.get(path);
    Guardian guardian = new Guardian(dir, recursive);
    guardian.addListener(new LogAlertListener());
    guardian.addListener(new FileMoveAlertListener());
    guardian.watch();
  }


  // 7. 載入參數檔並啟動Guardian
  public static void main(String[] args) {
    try {
      FileInputStream fis = new FileInputStream("watchDog.properties");
      myProps.load(fis);

      String watchDir = myProps.getProperty("path.include");
      String moveTo = myProps.getProperty("path.moveTo");
      Files.createDirectories(Paths.get(watchDir));
      Files.createDirectories(Paths.get(moveTo));

      launchGuardian(watchDir);

    } catch (Exception e) {
        logger.error(e.getMessage(), e);
        System.exit(0);
    }

  }   
  
}


AbstractAlertListener
package fsWatch;
import static java.nio.file.StandardWatchEventKinds.*;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import org.apache.log4j.Logger;

public abstract class AbstractAlertListener {
    static int seq;
    protected final Logger logger = Logger.getLogger(this.getClass());

    abstract void strategy(WatchEvent.Kind<?> kind, Path path);

    void hook(WatchEvent.Kind<?> kind, Path path) {
        // default null implementation
    }
    protected void alertTemplate(WatchEvent.Kind<?> kind, Path path) {
        if (kind.equals(ENTRY_CREATE)) {
            strategy(kind, path);
        }
        hook(kind, path);
    }
}


LogAlertListener
package fsWatch;

import java.nio.file.Path;
import java.nio.file.WatchEvent.Kind;

public class LogAlertListener extends AbstractAlertListener {

    @Override
    void strategy(Kind kind, Path path) {
        logger.warn(kind.name() + ", " + path);
    }

}

FileMoveAlertListener
package fsWatch;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.WatchEvent.Kind;

public class FileMoveAlertListener extends AbstractAlertListener {

    @Override
    void strategy(Kind kind, Path path) {
        move(path);
    }

    private void move(Path path) {
        wait4Ready(path);
        Path moveTo = Paths
                .get(Guardian.myProps.getProperty("path.moveTo") + File.separator 
                     + path.getFileName() + "." + ++seq);
        try {
            Files.move(path, moveTo, StandardCopyOption.REPLACE_EXISTING);
            logger.info("File Moved: " + path + " -> " + moveTo);
        } catch (IOException e) {
            logger.error(e.getMessage(), e);
        }
    }

    private void wait4Ready(Path path) {
        boolean locked = true;
        RandomAccessFile raf = null;
        while (locked) {
            File file = null;
            try {
                file = new File(path.toString());
                raf = new RandomAccessFile(file, "r");
                raf.seek(file.length());
                locked = false;
            } catch (IOException e) {
                locked = file.exists();
                if (locked) {
                    logger.debug("File locked: '" + file.getAbsolutePath() + "'");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e1) {
                        logger.error(e1.getMessage(), e);
                    }
                } else {
                    logger.debug("File was deleted while copying: '" 
                                    + file.getAbsolutePath() + "'");
                }
            } finally {
                if (raf != null) {
                    try {
                        raf.close();
                    } catch (IOException e) {
                        logger.error(e.getMessage(), e);
                    }
                }
            }
        }
    }

}

watchDog.properties
path.include = D:\\security\\include
path.moveTo = D:\\security\\bad

log4j.properties
log4j.rootLogger=INFO, stdout, logfile

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%-5p %l - %m%n
log4j.appender.stdout.encoding=UTF-8

log4j.appender.logfile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
#log4j.appender.logfile.layout.ConversionPattern=%d [%t] %-5p (%F:%L) - %m%n  
log4j.appender.logfile.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %l - %m%n
log4j.appender.logfile.File=D:/security/watchDog.log
log4j.appender.logfile.DatePattern='-'yyyyMMdd'.log'
log4j.appender.logfile.encoding=UTF-8


參考資料: http://152.92.236.11/tutorial_java/essential/io/examples/WatchDir.java