一次 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-

本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明出处! © 雨落
  1. 1. 第一次尝试
    1. 1.1. 方案
    2. 1.2. 问题
  2. 2. 第二次尝试
    1. 2.1. 方案
    2. 2.2. 不足
  3. 3. 第三次尝试
    1. 3.1. 方案
    2. 3.2. 不足
  4. 4. SHC 加密的注意事项
  5. 5. 参考