很久之前写的一篇关于环境变量的总结博客,当时没有写完,最近又翻找出来了,有些虎头蛇尾 😅
从 system 函数的实现看 Linux 下对环境变量的处理
前言
操作系统中的环境变量灵活而强大,Linux
系统中常用的环境变量有 HOME/USER/SHELL/PATH/http_proxy
等,许多程序的运行都依赖环境变量的设置。环境变量也是一个常见的攻击面,环境变量设置不当将可能导致提权、RCE
等系统风险。本篇文章记录了 Linux
下对环境变量的处理,涉及到普通程序中的环境变量处理和一个特殊的程序—— bash
中的环境变量处理。
提到环境变量,就不得不提到 execve
系统调用。这个系统调用的原型如下:
1
|
int execve(const char *pathname, char *const argv[], char *const envp[]);
|
-
第一个参数是可执行程序的路径或一个脚本的路径,如果是脚本,则通常(当然存在例外)需要在脚本内指定解释器,即需要以 #!xxxx
开头。
-
第二个参数是传递给可执行文件的参数,是一个字符串数组,且数组的第一个元素通常是执行文件的文件名,最后一个元素须是 NULL
。
-
第三个参数是传递给新进程的环境变量,也是一个字符串数组,每个环境变量的格式为 key=value
,数组的最后一个元素也须是 NULL
。
在载入一个可执行的 ELF
文件时,参数数组和环境变量数组会分别传递给程序中定义的 main
函数的第二个和第三个参数,其通用的函数原型为:
1
|
int main(int argc, char *argv[], char *envp[]);
|
对于 main
函数的参数和返回值就不做过多的介绍。
但是在实际使用 C
库函数编程的时候,如果需要执行一条或多条系统命令,我们并不会直接使用 execve
系统调用,而会使用 system
函数。
犹记得有道题留的后门很有趣:只能输入两个字符,然后将输入作为 system
函数的参数进行执行。这里限制了输入只能是特定的符号和数字,不能有字母。刚开始看到这个后门有点摸不着头脑,直到想出了使用 $0
作为输入,发现竟然可以拿到 shell
。
学过 shell
脚本的都知道,$1
是 shell
脚本的第一个参数,而 $0
代表的是 shell
脚本的路径。如果按这个规律进行推断,那 C
程序中调用 system("$0")
应该是再执行一遍当前程序,但为什么远程的机器上可以拿到 shell
呢,这就不得不探究 glibc
中 system
函数的实现,所有的疑问都可以从源码中获得答案。
在探讨环境变量之前,首先让我们来看看 system
函数的实现。
本文采用的 Linux
系统为 ubuntu-20.04
,阅读的 glibc
源码为 2.31
,测试程序均编译为 amd64-little
。
Linux 下 system 函数实现
起初,我以为 glibc
中 system
函数实现为 fork+execve+waitpid
,那么直接输入 $0
肯定会因为找不到 $0
这个程序而崩溃掉。后来发现 fork+execve+waitpid
确实是对 system
接口的一个实现版本,但 glibc
不是这样做的。阅读源码后发现,glibc
对 system
接口的实现更为全面,其在一个新的进程生命周期内做了更多的准备与清理工作。
首先给出 system
函数实现的主调用链:
1
2
3
4
5
6
7
|
system(__libc_system): sysdeps\posix\system.c#193
do_system: sysdeps\posix\system.c#102
__posix_spawn: posix\spawn.c#25
__spawni: sysdeps\unix\sysv\linux\spawni.c#424
__spawnix: sysdeps\unix\sysv\linux\spawni.c#312
CLONE: sysdeps\unix\sysv\linux\spawni.c#67
__spawni_child:sysdeps\unix\sysv\linux\spawni.c#121
|
下面一层一层来分析。
实现分析
system
1
2
3
4
5
6
7
8
9
10
11
|
int
__libc_system (const char *line)
{
if (line == NULL)
/* Check that we have a command processor available. It might
not be available after a chroot(), for example. */
return do_system ("exit 0") == 0;
return do_system (line);
}
weak_alias (__libc_system, system)
|
需要提一下的是,如果需要在静态编译去符号的 ELF
文件中快速定位 system
函数 (如果有的话),可以寻找 exit 0
这个字符串,然后交叉引用即可找到。
do_system
1
2
3
4
5
6
7
8
9
10
|
__sigaddset (&sa.sa_mask, SIGCHLD);
/* sigprocmask can not fail with SIG_BLOCK used with valid input
arguments. */
__sigprocmask (SIG_BLOCK, &sa.sa_mask, &omask);
__sigemptyset (&reset);
if (intr.sa_handler != SIG_IGN)
__sigaddset(&reset, SIGINT);
if (quit.sa_handler != SIG_IGN)
__sigaddset(&reset, SIGQUIT);
|
这里将 SIGCHLD
阻塞,并设置忽略 SIGINT
和 SIGQUIT
信号。
1
2
3
4
5
|
status = __posix_spawn (&pid, SHELL_PATH, 0, &spawn_attr,
(char *const[]){ (char*) SHELL_NAME,
(char*) "-c",
(char *) line, NULL },
__environ);
|
开始调用 __posix_spawn
函数。
这里的几个参数:
- pid:存储子进程的
pid
- SHELL_PATH:是一个宏,其实就是
"/bin/sh"
- spawn_attr:暂不关注
- SHELL_NAME:也是一个宏,定义为
"sh"
- line:外部传入的命令行参数,也就是
system
的入参
- __environ:指向当前环境变量列表的指针,是一个全局变量
观察几个参数后发现,system("xxx")
的本质就是 /bin/sh(sh) -c xxx
,也就是说会使用系统的 shell
来执行程序;另外,新进程的环境变量继承自原进程。
玩 pwn
的小伙伴想比对 __enviorn
这个全局变量不陌生,默认状态下,这里常常存储着一个栈地址。当然,这个变量我在后面会着重探讨,这里还是先关注 system
的实现机制。
__posix_spawn
1
2
3
4
5
6
7
8
|
int
__posix_spawn (pid_t *pid, const char *path,
const posix_spawn_file_actions_t *file_actions,
const posix_spawnattr_t *attrp, char *const argv[],
char *const envp[])
{
return __spawni (pid, path, file_actions, attrp, argv, envp, 0);
}
|
直接调用 __spawni
函数,前 6
个参数直接传递,最后一个参数给的 0
__spawni
1
2
3
4
5
6
7
8
9
10
11
|
int
__spawni (pid_t * pid, const char *file,
const posix_spawn_file_actions_t * acts,
const posix_spawnattr_t * attrp, char *const argv[],
char *const envp[], int xflags)
{
/* It uses __execvpex to avoid run ENOEXEC in non compatibility mode (it
will be handled by maybe_script_execute). */
return __spawnix (pid, file, acts, attrp, argv, envp, xflags,
xflags & SPAWN_XFLAGS_USE_PATH ? __execvpex :__execve);
}
|
不难发现,最后一个参数决定是使用 __execvpex
还是 __execve
,后者直接调用 execve
系统调用,前者做的事情稍微多一点,源码在 posix\execve.c:196
- 先判断路径是否包含
/
字符,如果不含 /
,则会从 PATH
这个环境变量中寻找
- 如果
PATH
没有找到,就会拼接当前路径,然后执行
__spwanix
1
2
3
4
5
6
|
while (argv[argc++] != NULL)
if (argc == limit)
{
errno = E2BIG;
return errno;
}
|
判断参数是不是超过了界限,limit
的值为 0x7ffffff-1
。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
int prot = (PROT_READ | PROT_WRITE
| ((GL (dl_stack_flags) & PF_X) ? PROT_EXEC : 0));
/* Add a slack area for child's stack. */
size_t argv_size = (argc * sizeof (void *)) + 512;
/* We need at least a few pages in case the compiler's stack checking is
enabled. In some configs, it is known to use at least 24KiB. We use
32KiB to be "safe" from anything the compiler might do. Besides, the
extra pages won't actually be allocated unless they get used. */
argv_size += (32 * 1024);
size_t stack_size = ALIGN_UP (argv_size, GLRO(dl_pagesize));
void *stack = __mmap (NULL, stack_size, prot,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
|
根据 rdlt_global.dl_stack_flags
设置开启的栈的权限,主要判断是否可执行。然后使用 mmap
映射 8K
的空间作为新进程的栈。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
/* Child must set args.err to something non-negative - we rely on
the parent and child sharing VM. */
args.err = 0;
args.file = file;
args.exec = exec;
args.fa = file_actions;
args.attr = attrp ? attrp : &(const posix_spawnattr_t) { 0 };
args.argv = argv;
args.argc = argc;
args.envp = envp;
args.xflags = xflags;
__libc_signal_block_all (&args.oldmask);
/* The clone flags used will create a new child that will run in the same
memory space (CLONE_VM) and the execution of calling thread will be
suspend until the child calls execve or _exit.
Also since the calling thread execution will be suspend, there is not
need for CLONE_SETTLS. Although parent and child share the same TLS
namespace, there will be no concurrent access for TLS variables (errno
for instance). */
new_pid = CLONE (__spawni_child, STACK (stack, stack_size), stack_size,
CLONE_VM | CLONE_VFORK | SIGCHLD, &args);
|
设置好相关参数,然后调用 clone
。clone
这个系统调用非常强大,相比于 fork
可以做更精细化的控制,详情可参考 man手册
__spawni_child
注意到 clone
的第一个参数为 __spawni_child
,这也是新进程将执行的函数,简要审阅其源码发现
这个函数干了这么件事:
- 设置信号处理
- 设置进程组
pgid
,设置权限相关的 euid/gid
- 复制到的文件描述符如果有开启的会关闭,然后打开新的描述符
- 需要
chdir
的话会执行 chdir
- 执行
execve/execvpe
函数,进入系统调用
子进程执行的时候,父进程会调用 waitpid
从而阻塞直到子进程执行完毕,然后后面都是设置错误号和一些清理工作等。
小节
简要总结一下 system
函数的执行流程如下:
glibc
在 linux
下实现的 system
本质上是用 shell
即 /bin/sh
软连接指向的那个程序去执行 sh -c xxxx
,进而执行到用户的命令。
execve
的第二个参数 argv
的第一个元素为 "sh"
- 子进程会拷贝一份父进程的环境变量。
第二条就解释了为啥 system("$0")
会获取到一个 shell
,因为 $0
实际上就是 sh
,而执行 system("sh")
自然会得到一个 shell
。当然,这说明远程使用 shell
启动的题目,如果远程是直接使用 execve
去执行一个新进程,则会一直递归执行本身直到进程爆炸退出….
至于 /bin/sh
,或者说常见的 shell
程序是怎么执行程序和脚本的,这就是另一个话题了,改日开个新篇讲一讲。
以上就是 glibc
在 linux
下对 system
函数的实现。接下来聊一聊环境变量。
环境变量相关函数分析
这里主要分析 glibc
中涉及到环境变量的几个函数:setenv、putenv、getenv、unsetenv、clearenv
。
setenv
第一个参数是环境变量的键,第二个参数是环境变量的值,第三个参数为当已存在环境变量时是否将其替换为新值,传入 1
替换,传入 0
则不替换。
代码在 stdlib\setenv.c:251
1
2
3
4
5
6
7
8
9
10
11
|
int
setenv (const char *name, const char *value, int replace)
{
if (name == NULL || *name == '\0' || strchr (name, '=') != NULL)
{
__set_errno (EINVAL);
return -1;
}
return __add_to_environ (name, value, NULL, replace);
}
|
判断 name
是否为空,或是否为空字符串或是否不含有 =
,通过检查后调用 __add_to_environ
,第三个参数为 NULL
,其余参数直接传递。
__add_to_environ
函数需要重点分析一下,因为下面的 putenv
函数也会调用这个函数,下面会一段一段分析,代码在 stdlib\setenv.c:116
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
int
__add_to_environ (const char *name, const char *value, const char *combined,
int replace)
{
char **ep;
size_t size;
/* Compute lengths before locking, so that the critical section is
less of a performance bottleneck. VALLEN is needed only if
COMBINED is null (unfortunately GCC is not smart enough to deduce
this; see the #pragma at the start of this file). Testing
COMBINED instead of VALUE causes setenv (..., NULL, ...) to dump
core now instead of corrupting memory later. */
const size_t namelen = strlen (name);
size_t vallen;
if (combined == NULL)
vallen = strlen (value) + 1;
//....
}
|
计算了传入的 name
的长度,并且在 combined
为 NULL
的时候,计算了 vallen
。关注到 putenv
的时候设置的 combined
为 NULL
。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/* We have to get the pointer now that we have the lock and not earlier
since another thread might have created a new environment. */
ep = __environ;
size = 0;
if (ep != NULL)
{
for (; *ep != NULL; ++ep)
if (!strncmp (*ep, name, namelen) && (*ep)[namelen] == '=')
break;
else
++size;
}
|
取全局变量 __environ
,这是一个字符串数组,然后其不为空的时候依次取出每一个字符串和 name
也就是 key
进行比较,如果找到一个 key
一样且 key
后面有 =
的字符串,就跳出循环,否则 ++size
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
if (ep == NULL || __builtin_expect (*ep == NULL, 1))
{
char **new_environ;
/* We allocated this space; we can extend it. */
new_environ = (char **) realloc (last_environ,
(size + 2) * sizeof (char *));
if (new_environ == NULL)
{
UNLOCK;
return -1;
}
if (__environ != last_environ)
memcpy ((char *) new_environ, (char *) __environ,
size * sizeof (char *));
new_environ[size] = NULL;
new_environ[size + 1] = NULL;
ep = new_environ + size;
last_environ = __environ = new_environ;
}
|
如果没有找到对应的字符串或者 __environ
数组为空的时候,就会进入到 if
分支。这里会调用 realloc
,入参是 last_environ
,这是一个全局静态变量,初始状态下为 NULL
;大小是 size + 2
个指针大小。
因此,没有找到该环境变量的时候,会扩充数组的大小。判断 __environ
和 last_environ
是否相同,也就是判断是否需要扩充,如果需要的话,就将原数组拷贝过来,并把扩充的那一部分内存的值置为 0
,最后对 __environ
和 last_environ
进行赋值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
if (*ep == NULL || replace)
{
char *np;
/* Use the user string if given. */
if (combined != NULL)
np = (char *) combined;
else
{
const size_t varlen = namelen + 1 + vallen;
#ifdef USE_TSEARCH
char *new_value;
int use_alloca = __libc_use_alloca (varlen);
if (__builtin_expect (use_alloca, 1))
new_value = (char *) alloca (varlen);
else
{
new_value = malloc (varlen);
if (new_value == NULL)
{
UNLOCK;
return -1;
}
}
# ifdef _LIBC
__mempcpy (__mempcpy (__mempcpy (new_value, name, namelen), "=", 1),
value, vallen);
# else
memcpy (new_value, name, namelen);
new_value[namelen] = '=';
memcpy (&new_value[namelen + 1], value, vallen);
# endif
np = KNOWN_VALUE (new_value);
if (__glibc_likely (np == NULL))
#endif
{
#ifdef USE_TSEARCH
if (__glibc_unlikely (! use_alloca))
np = new_value;
else
#endif
{
np = malloc (varlen);
if (__glibc_unlikely (np == NULL))
{
UNLOCK;
return -1;
}
#ifdef USE_TSEARCH
memcpy (np, new_value, varlen);
#else
memcpy (np, name, namelen);
np[namelen] = '=';
memcpy (&np[namelen + 1], value, vallen);
#endif
}
/* And remember the value. */
STORE_VALUE (np);
}
#ifdef USE_TSEARCH
else
{
if (__glibc_unlikely (! use_alloca))
free (new_value);
}
#endif
}
*ep = np;
}
|
最后一段 if
有点长。如果需要替换或者原数组为空,就会进入这个分支。
如果 combined
不是 NULL
,那么会使用用户传入的 combined
字符串,然后直接把这个指针填到环境变量数组的末尾,就完成了 setenv
。
如果 combined
是 NULL
,先判断是否定义了 USE_TSEARCH
宏,这个一般是会定义的,如果 value
大小可以用栈分配,就会用栈,否则使用 malloc
,一般长度小于 65535
的话会使用栈分配,大于这个长度就会使用 malloc
分配。分配成功后会拷贝 value
过去。
如果定义了 USE_TSEARCH
宏,那么会在树里面寻找这个 value
,如果找到了,就会把 malloc
分配的 chunk
给释放掉,而使用二叉树里保存的那个指针;如果没有找到,就会一定用 malloc
分配内存,最后将新的环境变量插入到环境变量数组中。
putenv
代码在 stdlib\putenv.c:52
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
int
putenv (char *string)
{
const char *const name_end = strchr (string, '=');
if (name_end != NULL)
{
char *name;
#ifdef _LIBC
int use_malloc = !__libc_use_alloca (name_end - string + 1);
if (__builtin_expect (use_malloc, 0))
{
name = __strndup (string, name_end - string);
if (name == NULL)
return -1;
}
else
name = strndupa (string, name_end - string);
#else
# define use_malloc 1
name = malloc (name_end - string + 1);
if (name == NULL)
return -1;
memcpy (name, string, name_end - string);
name[name_end - string] = '\0';
#endif
int result = __add_to_environ (name, NULL, string, 1);
if (__glibc_unlikely (use_malloc))
free (name);
return result;
}
__unsetenv (string);
return 0;
}
|
首先判断传入的字符串里面是否有=
,如果没有,就会调用unsetenv
,也就是删除这个环境变量。
如果有=
,会使用栈(大部分情况下会用栈,除非设置的键特别长)或者malloc
分配字符串里面的键部分,然后调用__add_to_environ (name, NULL, string, 1);
,也就是直接用用户传入的这个指针,把这个指针替换环境变量或者放置在环境变量数组的末尾。
若使用malloc
分配了name
,则会释放这个chunk
。
从这里可以看出setenv
和putenv
的区别:
setenv
会拷贝一份环境变量字符串,然后添加到环境变量数组中
putenv
直接使用用户传入的指针,放置在环境变量数组中
getenv
代码在stdlib\getenv. c:33
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
char *
getenv (const char *name)
{
size_t len = strlen (name);
char **ep;
uint 16_t name_start;
if (__environ == NULL || name[0] == '\0')
return NULL;
if (name[1] == '\0')
{
/* The name of the variable consists of only one character. Therefore
the first two characters of the environment entry are this character
and a '=' character. */
#if __BYTE_ORDER == __LITTLE_ENDIAN || !_STRING_ARCH_unaligned
name_start = ('=' << 8) | *(const unsigned char *) name;
#else
name_start = '=' | ((*(const unsigned char *) name) << 8);
#endif
for (ep = __environ; *ep != NULL; ++ep)
{
#if _STRING_ARCH_unaligned
uint 16_t ep_start = *(uint 16_t *) *ep;
#else
uint 16_t ep_start = (((unsigned char *) *ep)[0]
| (((unsigned char *) *ep)[1] << 8));
#endif
if (name_start == ep_start)
return &(*ep)[2];
}
}
else
{
#if _STRING_ARCH_unaligned
name_start = *(const uint 16_t *) name;
#else
name_start = (((const unsigned char *) name)[0]
| (((const unsigned char *) name)[1] << 8));
#endif
len -= 2;
name += 2;
for (ep = __environ; *ep != NULL; ++ep)
{
#if _STRING_ARCH_unaligned
uint 16_t ep_start = *(uint 16_t *) *ep;
#else
uint 16_t ep_start = (((unsigned char *) *ep)[0]
| (((unsigned char *) *ep)[1] << 8));
#endif
if (name_start == ep_start && !strncmp (*ep + 2, name, len)
&& (*ep)[len + 2] == '=')
return &(*ep)[len + 3];
}
}
return NULL;
}
libc_hidden_def (getenv)
|
流程为:
- 判断
__environ
是否为空或者传入的字符串是否为空,如果某一个为空直接返回空
- 不为空的时候,就会根据传入的
name + "="
进行线性寻找,找到了就返回对应的字符串指针,没找到就返回空
unsetenv
在stdlib\setenv. c:263
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
int
unsetenv (const char *name)
{
size_t len;
char **ep;
if (name == NULL || *name == '\0' || strchr (name, '=') != NULL)
{
__set_errno (EINVAL);
return -1;
}
len = strlen (name);
LOCK;
ep = __environ;
if (ep != NULL)
while (*ep != NULL)
{
if (! strncmp (*ep, name, len) && (*ep)[len] == '=')
{
/* Found it. Remove this pointer by moving later ones back. */
char **dp = ep;
do
dp[0] = dp[1];
while (*dp++);
/* Continue the loop in case NAME appears again. */
}
else
++ep;
}
UNLOCK;
return 0;
}
|
总的来看,也是根据传入的键从__envrion
变量从前往后查找环境变量,如果找到了,就会删除找到的这个环境变量。但是这里并没有处理删除的环境变量的指针,依赖调用方去释放内存。
clearenv
在stdlib\setenv. c:305
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
int
clearenv (void)
{
LOCK;
if (__environ == last_environ && __environ != NULL)
{
/* We allocated this environment so we can free it. */
free (__environ);
last_environ = NULL;
}
/* Clear the environment pointer removes the whole environment. */
__environ = NULL;
UNLOCK;
return 0;
}
#ifdef _LIBC
libc_freeres_fn (free_mem)
{
/* Remove all traces. */
clearenv ();
/* Now remove the search tree. */
__tdestroy (known_values, free);
known_values = NULL;
}
|
如果last_environ == __environ
,就说明已经用堆分配内存了,就会遍历__environ
并将其每一个字符串指向的内存释放掉,然后将__environ
置为NULL
。
如果使用了二叉树保存环境变量的话还会删除二叉树。
小节
总结一下以上有关环境变量操作的函数:
setenv
会拷贝一份环境变量,然后插入或替换环境变量到环境变量数组中
putenv
直接使用传入的环境变量字符串指针,然后把这个指针插入到环境变量数组中或者替换原有指针
unsetenv
会删除环境变量,但并不会处理被删除的那个环境变量的指针
- 在查找环境变量的时候,一般是从前往后线性的查找
- 一旦插入了新的环境变量,
__environ
会指向一个堆内存指针而不是栈内存
- 在环境变量变更的过程中,可能会调用
realloc
、malloc
和free
,还有可能在栈上存有环境变量的内容
bash 对环境变量的处理
当我分析完glibc
中对环境变量的查找流程后,下意识地以为bash
中对环境变量的处理也是类似的用列表去处理,犯了经验主义的错误。而在查阅了bash
中的环境变量处理相关代码后发现,bash
处理环境变量使用的数据结构和glibc
并不一样。
从https://ftp.gnu.org/gnu/bash/bash-5.1.tar.gz下载bash
源码,然后定位到main
函数,目前关注环境变量的处理部分简易调用链为:
1
2
3
4
5
|
main ()
shell_initialize ()
initialize_shell_variables ()
bind_variable (): variables. c #3253
bind_variable_internal (): variables. c #3084
|
因为bash
需要处理的场景比较多,如果有机会,可以再写一篇博客分析bash
的处理流程。
定位到bind_variable_internal
函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/* Bind a variable NAME to VALUE in the HASH_TABLE TABLE, which may be the
temporary environment (but usually is not). HFLAGS controls how NAME
is looked up in TABLE; AFLAGS controls how VALUE is assigned */
static SHELL_VAR *
bind_variable_internal (name, value, table, hflags, aflags)
const char *name;
char *value;
HASH_TABLE *table;
int hflags, aflags;
{
char *newval, *tname;
SHELL_VAR *entry, *tentry;
entry = (hflags & HASH_NOSRCH) ? (SHELL_VAR *) NULL : hash_lookup (name, table);
//....
}
|
从注释可以看出来bash
中使用hash table
来处理变量,当然也包括环境变量。
那么,结合上面分析的system
实现细节,当调用system ("xxx")
的时候,实际会执行execve ("/bin/sh", {"sh", "-c", "xxx"}, __environ)
,然后在bash
中,会从前往后遍历__environ
数组中的每一个字符串,然后分割出key
和value
,接着根据key
将字符串存入到hash
表里面。
也就是说,如果__enviorn
指向的环境变量数组中有多个key
相同的环境变量字符串,那么在bash
中处理后,生效的永远是最后那一个。
经过对hashlib. c
的分析,发现bash
中对hash table
的存储方式是拉链法,采用的hash
函数为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
/* This is the best 32-bit string hash function I found. It's one of the
Fowler-Noll-Vo family (FNV-1).
The magic is in the interesting relationship between the special prime
16777619 (2^24 + 403) and 2^32 and 2^8. */
#define FNV_OFFSET 2166136261
#define FNV_PRIME 16777619
/* If you want to use 64 bits, use
FNV_OFFSET 14695981039346656037
FNV_PRIME 1099511628211
*/
/* The `khash' check below requires that strings that compare equally with
strcmp hash to the same value. */
unsigned int
hash_string (s)
const char *s;
{
register unsigned int i;
for (i = FNV_OFFSET; *s; s++)
{
/* FNV-1 a has the XOR first, traditional FNV-1 has the multiply first */
/* was i *= FNV_PRIME */
i += (i<<1) + (i<<4) + (i<<7) + (i<<8) + (i<<24);
i ^= *s;
}
return i;
}
|
看了下维基百科上的介绍,这个函数的散列性很好,简单而又高效。
因此,当一对键值对字符串被放入哈希表时,过程是这样的:
- 根据
key
字符串计算一个index
- 在数组链表中查找对应的元素是否为空
- 如果元素不存在,则直接将
value
放置在这里,并记录下对应的key
- 如果存在,首先判断一下
key
在不在链表中,如果在,那么替换value
;如果不在,那么新建一个结构体,把key
和value
的信息在里面,最后用头插法的方式放入链表中
因此,如果存在相同得环境变量,处于链表后面的环境变量会覆盖之前的环境变量。
环境变量的攻击面
这篇博客写到一半的时候,因为一些缘故挺了下来,现在我也不记得大纲了。而我也已经有1
年的时间没有从事安全方面的研究。继续编写此小节是为了补全该博客,但此小节可能不会像之前写得那么仔细。
至于环境变量的供给面,这里我姑且总结一些点,便由读者自行发散吧。有些最新的利用点我还没有学习,如果没有提及,还请见谅。
- 利用环境变量泄露信息,如典型的
__environ
变量泄露栈地址
- 利用一些敏感的环境变量,如
PATH
- 结合
SUID
位进行利用
- 通过环境变量的覆盖,覆盖原有的环境变量
glibc
中利用GLIBC_TUNABLES
环境变量
思考与总结
本文总结了在 glibc
中 system
函数的一般实现,并分析了与环境变量有关的相关函数,同时,总结了环境变量的一些攻击面!