#!/bin/sh ####################################################################################### # # Copyright (c) Huawei Technologies Co., Ltd. 2019. All rights reserved. # security-tool licensed under the Mulan PSL v1. # You can use this software according to the terms and conditions of the Mulan PSL v1. # You may obtain a copy of Mulan PSL v1 at: # http://license.coscl.org.cn/MulanPSL # THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR # PURPOSE. # See the Mulan PSL v1 for more details. # Description: openEuler Security Tool # ####################################################################################### # name of this script readonly NAME=`basename $0` # working directory readonly WORKD=`pwd`/ # the separator of fields of security configration file readonly FIELD_SEP='@' # distinction DST="" # security configuration file SCONF="" # USER security configuration file USR_SCONF="" # File where to write log LOGFILE="" # flag SILENT=0 # execute configure item's id EXECID=0 # temporary target of decompress and compress TMPTARGET="openEuler" # distinction type(rootfs, ar, cpio.gz) DST_TYPE="rootfs" # directory of decompressed rootfs ROOTFS="" # distinction name when it's not rootfs AR_F="" GZ_F="" ############################################################################## #============================================================================= # Function Name: pushd/popd # Description : the same to standard pushd/popd except that no info printed # Returns : 0 on success, otherwise on fail #============================================================================= function pushd() { builtin pushd "$@" > /dev/null return $? } function popd() { builtin popd "$@" > /dev/null return $? } #============================================================================= # Function Name: fn_test_params_num # Description : test if the num of params is the right num(do not support flexible parameters), quit otherwise # Parameter : params_num # Returns : none #============================================================================= function _fn_test_params_num() { if [ $# -lt 3 ] || [ $2 -ne $3 ]; then echo "Line $1: num of params $2 not equals to $3" exit 1 fi } alias fn_test_params_num='_fn_test_params_num $LINENO $#' #============================================================================= # Function Name: fn_test_type # Description : test if the specific file type by a keyword # Parameter : file, keyword(directory, cpio archive, gzip compressed, ar archive, ...) # Returns : 0 on success, otherwise on fail #============================================================================= function fn_test_type() { fn_test_params_num 2 file "$1"| awk -F: '{print $2}' |grep "$2" >/dev/null return $? } #============================================================================= # Function Name: fn_get_fullpath # Description : get absolute path name of file # Parameter : file # Returns : fullpath #============================================================================= function fn_get_fullpath() { fn_test_params_num 1 local p=$1 local out if [ "${p:0:1}" = "/" ]; then echo $p return fi pushd `dirname $p` out=`pwd` popd echo $out"/"`basename $p` } #============================================================================= # Function Name: fn_escape_string # Description : set special character(/) in the string to be escaped # Parameter : string # Returns : escaped string #============================================================================= function fn_escape_string() { fn_test_params_num 1 echo "$1"| sed 's/\//\\\//g'| sed 's/\./\\\./g'| sed 's/\[/\\[/g'| sed 's/\]/\\]/g' | sed 's/\$/\\\$/g' | sed 's/\*/\\\*/g' } #============================================================================= # Function Name: fn_log # Description : write a message to log file or console # Parameter : lineno level(error, warn, info) message # Returns : none #============================================================================= function fn_log() { fn_test_params_num 3 local lno=$1 local level=$2 shift 2 if [ $SILENT -eq 0 ] || [ "$level" = "error" ]; then output=$@ opt=`echo $output | grep -e "success$"` if [ $? -eq 0 ];then output=`echo $opt | sed -r 's/success$/\\\033\\[32;1msuccess\\\033\\[0m/g'` fi opt=`echo $output | grep -e "fail$"` if [ $? -eq 0 ];then output=`echo $opt | sed -r 's/fail$/\\\033\\[31;1mfail\\\033\\[0m/g'` fi echo -e "[$NAME:$lno] [$level] $output" fi echo "`date +%Y-%m-%d\ %H:%M:%S` [$NAME:$lno] [$level] $@" >> $LOGFILE } alias fn_error='fn_log $LINENO error' alias fn_warn='fn_log $LINENO warn' alias fn_info='fn_log $LINENO info' #============================================================================= # Function Name: fn_exit # Description : to be excuted when exit with return value(0 ok, 1 params error, 2 hardening error) # Parameter : status(0 ok, otherwise error), [message] # Returns : none #============================================================================= function fn_exit() { fn_test_params_num 1 local s=$1 # cleanup when destination is cpio.gz if [ "$DST_TYPE" = "tar.gz" ]; then if [ $s -eq 0 ]; then local new_initrd=$WORKD`basename $GZ_F`".secure" pushd $ROOTFS tar -cf -- "$ROOTFS/$TMPTARGET" * gzip <"$ROOTFS/$TMPTARGET" > $new_initrd fn_info "hardened initrd is $new_initrd" popd fi # cleanup rootfs rm -rf $ROOTFS fi # cleanup when destination is cpio.gz if [ "$DST_TYPE" = "cpio.gz" ]; then if [ $s -eq 0 ]; then local new_initrd=$WORKD`basename $GZ_F`".secure" pushd $ROOTFS find . |cpio --quiet -co |gzip > $new_initrd fn_info "hardened initrd is $new_initrd" popd fi # cleanup rootfs rm -rf $ROOTFS fi # cleanup when destination is ar target if [ "$DST_TYPE" = "ar" ]; then if [ $s -eq 0 ]; then local new_ar=$WORKD`basename $AR_F`".secure" cp $AR_F $new_ar pushd $ROOTFS find . |cpio --quiet -co|gzip > $GZ_F popd ar -r $new_ar $GZ_F if [ $? -eq 0 ]; then fn_info "initrd.cpio.gz updated" else fn_error "fail to replace initrd.cpio.gz in $AR_F by $GZ_F" fn_exit 1 fi # update checksum in new ar target rm -f checksum ar -x $new_ar checksum if [ -f checksum ]; then local sum=`cksum $GZ_F | awk '{print $1}'` sed -i "s/^initrd\.cpio\.gz.*/initrd\.cpio\.gz $sum/" checksum ar -r $new_ar checksum rm checksum fn_info "checksum updated" fi fn_info "finish updating, new target is $new_ar" fi # cleanup initrd and rootfs fn_info "cleanup GZ [$GZ_F] and ROOTFS [$ROOTFS]" rm -f $GZ_F rm -rf $ROOTFS fi # log fn_info "========exit, status is [$s]========" exit $s } #============================================================================= # Function Name: fn_usage # Description : print help messages to console # Parameter : none # Returns : none #============================================================================= function fn_usage() { cat < "$ROOTFS/$TMPTARGET" if [ $? -ne 0 ]; then fn_error "fail to extract [$GZ_F] to $ROOTFS/$TMPTARGET" fn_exit 2 fi fn_test_type "$ROOTFS/$TMPTARGET" "cpio archive" if [ $? -eq 0 ]; then cpio --quiet -id <"$ROOTFS/$TMPTARGET" >/dev/null if [ $? -ne 0 ]; then fn_error "fail to extract [$GZ_F] to $ROOTFS" fn_exit 2 fi if [ "$DST_TYPE" != "ar" ];then DST_TYPE="cpio.gz" fi else tar -xvf "$ROOTFS/$TMPTARGET" >/dev/null if [ $? -ne 0 ]; then fn_error "fail to extract [$GZ_F] to $ROOTFS" fn_exit 2 fi DST_TYPE="tar.gz" fi rm -f "$ROOTFS/$TMPTARGET" popd fn_info "pre_hardening done" } #============================================================================= # Function Name: fn_check_rootfs # Description : examine if rootfs is a standard hiberarchy # Parameter : none # Returns : none #============================================================================= function fn_check_rootfs() { for i in bin usr/bin sbin usr/sbin etc boot lib home root opt var tmp proc sys mnt do if [ ! -d "$ROOTFS/$i" ]; then if [ $i == "boot" ];then continue fi fn_error "[$ROOTFS] is not a standard openEuler rootfs" fn_exit 2 fi done if [ ! -d "$ROOTFS"/boot ]; then fn_info "[$ROOTFS] is a openEuler iSula rootfs" fi } #============================================================================= # Function Name: fn_handle_key # Description : deal with configurations referred to key and value # Parameter : operator, file, key, f4, f5 # Returns : 0 on success, otherwise on fail #============================================================================= function fn_handle_key() { fn_test_params_num 5 local op file op=$1 file=$2 file=$ROOTFS$file if [ ! -w "$file" ]; then fn_warn "file [$file] not existed or writable" return 1 fi # key and value with string escaped local key f4 f5 key=`fn_escape_string "$3"` f4=`fn_escape_string "$4"` f5=`fn_escape_string "$5"` # to ingore the differences of key caused by blank characters echo "$key" | egrep "^-e.*" if [[ $? == 0 ]] then local grepkey="[[:blank:]]*"`echo "$key" | sed -r 's/[[:blank:]]+/[[:blank:]]\+/g'` else local grepkey="[[:blank:]]*"`echo $key | sed -r 's/[[:blank:]]+/[[:blank:]]\+/g'` fi case "$op" in # d@file@key d) grep -E "$grepkey" $file >/dev/null if [ $? -eq 0 ]; then # comment a line sed -ri "s/^[^#]*$grepkey/#&/" $file return $? else return 0 fi ;; # m@file@key[@value] m) grep -E "^$grepkey" $file >/dev/null if [ $? -eq 0 ]; then sed -ri "s/^$grepkey.*/$key$f4/g" $file else # add a blank line to file because sed cannot deal with empty file by 'a' if [ ! -s $file ]; then echo >> $file fi sed -i "\$a $key$f4" $file fi return $? ;; # sm@file@key[@value] similar to m: strict modify on the origin position sm) grep -E "^$grepkey" $file >/dev/null if [ $? -eq 0 ]; then sed -ri "s/$key.*/$key$f4/g" $file else # add a blank line to file because sed cannot deal with empty file by 'a' if [ ! -s $file ]; then echo >> $file fi sed -i "\$a $key$f4" $file fi return $? ;; # M@file@key@key2[@value2] M) grep -E "^$grepkey" $file >/dev/null if [ $? -eq 0 ]; then grep "^$grepkey.*$f4" $file >/dev/null if [ $? -eq 0 ]; then sed -ri "/^$grepkey/ s/$f4[^[:space:]]*/$f4$f5/g" $file else sed -ri "s/^$grepkey.*/&$f4$f5/g" $file fi return $? else fn_warn "key [$key] not found in [$file]" return 1 fi ;; *) fn_error "bad operator [$op]" return 1 ;; esac } #============================================================================= # Function Name: fn_handle_command # Description : deal with configurations referred to operations to files # Parameter : command[option], files # Returns : 0 on success, otherwise on fail #============================================================================= function fn_handle_command() { fn_test_params_num 2 local op=$1 local files=$2 local status=0 # add ROOTFS path for every file for file in `echo "$files" | awk -v rf="$ROOTFS" '{ for(i=1; i<=NF; i++) { printf "%s%s\n",rf,$i } }'`; do ${op} ${file} >/dev/null 2>&1 if [ $? -ne 0 ]; then status=1 fi done unset f return $status } #============================================================================= # Function Name: fn_handle_cp # Description : deal with configurations referred to operations to files # Parameter : src_file dst_file # Returns : 0 on success, otherwise on fail #============================================================================= function fn_handle_cp() { fn_test_params_num 2 src_file=$1 dst_file=$2 cp -p $src_file $ROOTFS/$dst_file if [ $? -ne 0 ]; then return 1 else return 0 fi } #============================================================================= # Function Name: fn_handle_systemctl # Description : start or stop services # Parameter : service_name service_status # Returns : 0 on success, otherwise on fail #============================================================================= function fn_handle_systemctl() { fn_test_params_num 2 syetem_service_name=$1 syetem_service_status=$2 systemctl ${syetem_service_status} ${syetem_service_name} return $? } #============================================================================= # Function Name: fn_handle_umask # Returns : 0 on success, otherwise on fail #============================================================================= function fn_handle_umask() { fn_test_params_num 2 local target=$1 local value=$2 local ret=0 if [ "$target" == "user" ] then echo "umask $value" >> "$ROOTFS/etc/bashrc" echo "umask $value" >> "$ROOTFS/etc/csh.cshrc" for file in $(find "$ROOTFS/etc/profile.d/" -type f) do echo '' >> $file # 防止配置文件末尾没有换行符的情况 echo "umask $value" >> $file done elif [ "$target" == "deamon" ] then echo "umask $value" >> "$ROOTFS/etc/sysconfig/init" else ret=1 fi return $ret } #============================================================================= # Function Name: fn_handle_ln # Returns : 0 on success, otherwise on fail #============================================================================= function fn_handle_ln() { fn_test_params_num 3 local arg=$1 local target=$2 local link_file=$3 chroot $ROOTFS ln "$arg" "$target" "$link_file" return $? } #============================================================================= # Function Name: fn_harden_rootfs # Description : harden the rootfs, according to configuration file # Parameter : none # Returns : none #============================================================================= function fn_harden_rootfs() { fn_check_rootfs fn_info "---begin hardening rootfs by [$SCONF]---" local status local f1 f2 f3 f4 f5 f6 # do configuration traversal, with comments and lines starting with blankspace ignored grep -v '^#' $SCONF| grep -v '^$'| grep -Ev '^[[:space:]]+'| while read line do f1=`echo "$line" | awk -F$FIELD_SEP '{print $1}'` if [ $EXECID -ne 0 ] && [ "$EXECID" -ne "$f1" ];then continue fi if [[ $line =~ "@@" ]] then PRE_IFS=$IFS IFS='@' arr=($line) IFS=$PRE_IFS pos=1 for ((i=2;i<${#arr[*]};i++)) do if [[ x${arr[$i]} = x ]] then tem="${arr[$((i-1))]}@${arr[$((i+1))]}" i=$((i+1)) arr[$pos]=$tem arr[$i]=$tem else pos=$((pos+1)) arr[$pos]=${arr[$i]} fi done pos=$((pos+1)) for ((j=$pos;j<${#arr[*]};j++)) do arr[$j]= done f2=${arr[1]} f3=${arr[2]} f4=${arr[3]} f5=${arr[4]} f6=${arr[5]} else f2=`echo "$line" | awk -F$FIELD_SEP '{print $2}'` f3=`echo "$line" | awk -F$FIELD_SEP '{print $3}'` f4=`echo "$line" | awk -F$FIELD_SEP '{print $4}'` f5=`echo "$line" | awk -F$FIELD_SEP '{print $5}'` f6=`echo "$line" | awk -F$FIELD_SEP '{print $6}'` fi case "$f2" in d|m|sm|M) fn_handle_key "$f2" "$f3" "$f4" "$f5" "$f6" status=$? ;; cp) fn_handle_cp "$f3" "$f4" status=$? ;; systemctl) fn_handle_systemctl "$f3" "$f4" status=$? ;; umask) fn_handle_umask "$f3" "$f4" status=$? ;; ln) fn_handle_ln "$f3" "$f4" "$f5" status=$? ;; *) fn_handle_command "$f2" "$f3" status=$? ;; esac if [ $status -eq 0 ]; then fn_info "-harden [$line]: success" else fn_warn "-harden [$line]: fail" fi done unset line fn_info "---end hardening rootfs---" fn_check_rootfs } #============================================================================= # Function Name: fn_harden_usr_conf # Description : harden the user conf, according to configuration file usr_security.conf # Parameter : none # Returns : none #============================================================================= function fn_harden_usr_conf() { fn_check_rootfs fn_info "---begin hardening SUER CONF by [$USR_SCONF]---" local status local f1 f2 f3 f4 f5 f6 # do configuration traversal, with comments and lines starting with blankspace ignored grep -v '^#' $USR_SCONF| grep -v '^$'| grep -Ev '^[[:space:]]+'| while read line do f1=`echo "$line" | awk -F$FIELD_SEP '{print $1}'` if [ $EXECID -ne 0 ] && [ "$EXECID" -ne "$f1" ];then continue fi if [[ $line =~ "@@" ]] then #eval $(echo $line | awk '{split($0, filearray, "@");for(i in filearray)print "arr["i"]="filearray[i]}') PRE_IFS=$IFS IFS='@' arr=($line) IFS=$PRE_IFS pos=1 for ((i=2;i<${#arr[*]};i++)) do if [[ x${arr[$i]} = x ]] then tem="${arr[$((i-1))]}@${arr[$((i+1))]}" i=$((i+1)) arr[$pos]=$tem arr[$i]=$tem else pos=$((pos+1)) arr[$pos]=${arr[$i]} fi done pos=$((pos+1)) for ((j=$pos;j<${#arr[*]};j++)) do arr[$j]= done f2=${arr[1]} f3=${arr[2]} f4=${arr[3]} f5=${arr[4]} f6=${arr[5]} else f2=`echo "$line" | awk -F$FIELD_SEP '{print $2}'` f3=`echo "$line" | awk -F$FIELD_SEP '{print $3}'` f4=`echo "$line" | awk -F$FIELD_SEP '{print $4}'` f5=`echo "$line" | awk -F$FIELD_SEP '{print $5}'` f6=`echo "$line" | awk -F$FIELD_SEP '{print $6}'` fi case "$f2" in d|m|sm|M) fn_handle_key "$f2" "$f3" "$f4" "$f5" "$f6" status=$? ;; cp) fn_handle_cp "$f3" "$f4" status=$? ;; systemctl) fn_handle_systemctl "$f3" "$f4" status=$? ;; umask) fn_handle_umask "$f3" "$f4" status=$? ;; ln) fn_handle_ln "$f3" "$f4" "$f5" status=$? ;; *) fn_handle_command "$f2" "$f3" status=$? ;; esac if [ $status -eq 0 ]; then fn_info "-harden [$line]: success" else fn_warn "-harden [$line]: fail" fi done unset line fn_info "---end hardening USER CONF---" fn_check_rootfs } #============================================================================= # Function Name: fn_harden_nouser_nogroup # Description : Remove nouser and nogroup files # Parameter : none # Returns : 0 on success, otherwise on fail #============================================================================= function fn_harden_nouser_nogroup() { local option="" local command="chown -R root.root" local dir="" local file="" local dirs=`mount | awk '{ if($5!="proc" && $1!="/proc")print $3}'` for option in -nouser -nogroup; do for dir in ${dirs}; do for file in `find $dir -xdev $option`; do fn_handle_command "$command" "$file" done done done } #============================================================================= # Function Name: fn_harden_grub2 # Returns : 0 on success, otherwise on fail #============================================================================= function fn_harden_grub2() { echo -e "cat <> /etc/grub.d/00_header if [ -d /boot/efi/EFI/openEuler -a -d /sys/firmware/efi ]; then grub2-mkconfig -o /boot/efi/EFI/openEuler/grub.cfg fi } # Function Name: fn_harden_sysctl # Returns : 0 on success, otherwise on fail #============================================================================= function fn_harden_sysctl() { /sbin/sysctl -p /etc/sysctl.conf } #============================================================================ # Function Name: fn_baseStripInvalidLink # Description : removing invalidlink #============================================================================ function fn_baseStripInvalidLink() { echo ' #!/bin/bash for path in /etc /lib /lib64 /usr /var do find $path -type l -follow -exec ls {} \; | while read link_file do if [ ! -z "$(ls -l $link_file | grep -v '/boot/')" ];then stat -L $link_file 1>/dev/null 2>&1 [ "$?" != 0 ] && rm -f $link_file && echo "Removing invalidlink:$link_file" fi done done ' > $ROOTFS/baseStripInvalidLink.sh echo $ROOTFS chroot $ROOTFS chmod u+x baseStripInvalidLink.sh chroot $ROOTFS sh baseStripInvalidLink.sh chroot $ROOTFS rm -rf baseStripInvalidLink.sh } #============================================================================= # Function Name: fn_main # Description : main function # Parameter : command line params # Returns : 0 on success, otherwise on fail #============================================================================= function fn_main() { # operator must be root if [ `id -u` -ne 0 ]; then echo "You must be logged in as root." exit 1 fi # parse user params fn_parse_params "$@" # pre-process fn_pre_hardening if [ "x${OPENEULER_SECURITY}" = "x0" ] then # harden rootfs fn_harden_rootfs # harden grub2 fn_harden_grub2 fn_harden_sysctl sed -i "s/^OPENEULER_SECURITY=.*$/OPENEULER_SECURITY=1/g" /etc/openEuler_security/security elif [ "x${OPENEULER_SECURITY}" = "x1" ] then fn_harden_sysctl else echo "the value of OPENEULER_SECURITY is unexpected! please check it." fi # harden user conf fn_harden_usr_conf # disable the service in system start systemctl disable openEuler-security.service # do cleanup and exit fn_exit 0 } # check cancel action and do cleanup trap "echo 'canceled by user...'; fn_exit 1" INT TERM # main entrance fn_main "$@" exit 0