一次 Shell 脚本加密探索

遇到一个需求,要求 a 切换到 b 用户执行一些操作,但是 b 用户的目录等不能授权给 a 访问。探索一番具体有下列方式。

第一次尝试

方案

在 b 用户下建立脚本 target.sh,a 用户下建立一个中间脚本脚本 helper.sh,在 helper.sh 中通过 su 命令切换用户过去,而 su 命令需要输入密码的过程,当然,b 用户的密码是不能暴露给 a 的。

编写 helper.sh 内容如下:

1
2
3
4
5
6
7
#!/bin/bash

runUserPassword='password'
arg1=$1
arg2=$2
arg3=$3
/usr/bin/expect -f ./helper.exp ${runUserPassword} ${arg1} ${arg2} ${arg3}

利用 expect 去为我们输入密码,所以脚本中调用了 helper.exp,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/expect -f

set timeout 600
set runUserPassword [lindex $argv 0]
set arg1 [lindex $argv 1]
set arg2 [lindex $argv 2]
set arg3 [lindex $argv 3]

spawn su - rfc
expect ":"
send "$runUserPassword\r"
send "./target.sh $arg1 $arg2 $arg3\r"
send "exit\r"
interact

要将用户密码传入 expect 的执行脚本内,由于无法对 expect 文件进行二进制加密,所以将密码保存在 helper.sh 内,在执行时通过参数传入 expect 内。

随后通过 shc 进行加密,helper.sh 被编译为二进制执行文件,保存其中的明文密码无法被查看。

问题

通过这种方式乍看很完美,但是在实际的执行过程中发现,在运行脚本的过程中输入 ctrl+z 发出中断信号,执行会停止,停留在 a 用户的执行环境中。这样 b 用户就获得了 a 用户的环境,前功尽弃了。

后来尝试在脚本中捕获中断信号,在 shell script 中添加:

1
trap "" SIGINT

仍无法解决,它只能忽略 shell 中的中断信号,而我们调用了 expect,无法处理。

查询 expect 的用法,发现了类似的语法:

1
trap SIG_IGN { SIGINT SIGHUP SIGQUIT}

但是在 exp 中配置 trap SIG_IGN SIGINT 无效果,不知原因,因而此方案废弃,改用第二种方案。

第二次尝试

方案

既然 expect 会有中断的问题,那么就只能避免使用 expect,想到使用 ssh 来进行登陆和验证。

添加 b 用户的公钥到 a 用户认证文件,这样 b 用户可以使用密钥登陆 a 用户。为了保证 b 用户无法在脚本外登陆到 a 用户,所以这里的密钥生成过程中需要密码。密码保留,这样就可以避免此问题。

这里又回到了交互式输入密码的问题上,既然 expect 是不可行的,那就只能寻找其它方案。查找资料发现了 sshpass 这个工具,它能帮助我们在 ssh 登陆中输入明文密码。

用法:

1
sshpass -p password ssh a@localhost

不过我这里输入后无响应,随后发现 sshpass 只能响应密码输入,无法响应加密密钥的密码输入。

回到原点,撤销密钥登陆,仍旧使用密码登陆。随意 helper.sh 可以这样写:

1
2
3
4
5
6
7
#!/bin/bash

runUserPassword='password'
arg1=$1
arg2=$2
arg3=$3
sshpass -p "$runUserPassword" ssh a@127.0.0.1 ./target.sh ${arg1} ${arg2} ${arg3}

使用 shc 加密,密码同样可以被保护。

不足

这里为了实现功能引入了额外的软件 sshpass,网上不建议将该软件用于生产环境,容易引发安全问题。所以尽可能使用 Linux 自带的命令来实现,就有了第三次尝试。

第三次尝试

方案

思路同第二种方法一样,都是将密码传入保留,不同的是这里用到了内置的 sudo 命令而不需要安装 sshpass。

将第二种方案最后一行改写一下:

1
2
3
4
5
6
7
#!/bin/bash

runUserPassword='password'
arg1=$1
arg2=$2
arg3=$3
echo $runUserPassword | sudo -S su - a "./target.sh" ${arg1} ${arg2} ${arg3}

sudo 命令的 -S 选项可以接受一个标准输入作为密码传递,同样的可以被加密,实现功能。

有一个不足是自带的 sudo 命令会有一个默认的超时时间,在该事件内使用 sudo 命令无需输入密码,使用者第一次执行后在一段时间内是可以用 sudo 去执行任何操作的,这是危险的。所以要配置为每次执行 sudo 命令都输入密码。

用 root 用户执行 visudo,找到下列行:

1
Defaults    env_reset

将其修改为:

1
Defaults    env_reset,timestamp_timeout=0

这样每次执行 sudo 都需要输入密码了,保证了安全。

不足

这种方式的不足在于更改了 sudo 的默认行为,每次都需要输入密码,对其它用户来讲操作不便。

第二个不足就是在输出结果时,返回的结果会同密码输入提示在同一行:

1
2
[b@localhost ~]$ ./helper test test
[sudo] password for b: "输出结果"

显示效果不美观,待解决。

SHC 加密的注意事项

在某一平台上通过 shc 加密生成的二进制文件是动态链接形式,因而在其它平台无法运行。在其它平台运行可能导致服务器宕机。

可以通过下面的方法生成静态链接的二进制可执行文件:

1
CFLAGs=-static shc -r -f helper.sh

参考

-EOF-

  • 本文作者: Rainy
  • 本文链接: https://rainylog.com/post/shell-encrypt/
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明出处!