首页
论坛
课程
招聘
[原创]readme-revenge details
2018-1-1 00:11 2873

[原创]readme-revenge details

2018-1-1 00:11
2873

看了这篇帖子 https://bbs.pediy.com/thread-223659.htm

 

前人之述备矣,然在下仍觉细节尚缺,故有此文。

 

看了题目名字就知道这是32c3那道readme翻版。

 

32c3那道题是一个栈溢出,通过stack_chk_fail去调用fortify_fail。

 

当时的利用方法是控制栈上的参数,也就是argv和环境变量。特别注意要控制LIBC_FATAL_STDERR_=1让fortify_fail能够把输出作为stderr给我们。否则fortify_fail下层接的是libc_message,而libc_message调用的是getenv

 

图片描述

 

getenv就是从environ指针那里去找环境变量指针数组。

 

getenv如果发现这环境变量没设置,libc_message会syscall调用open('/dev/tty'),输出你是看不到的。

 

图片描述

 

而34c3这道题的想法应该是源自于0ctf 2017里面的easiest printf(个人猜测)。

 

当时在0ctf结束后的irc里面,rpisec的人说到了一种非预期解法,就是改printf相关的两个函数指针,用自定义扩展%k调用shell。连dragon sector的人都很吃惊。

08:20 < itszn> Murmus: I used the hooks for printf for new char codes
08:20 < Redford> spq: discrete log on polynomial field
08:20 < its_> anyone has a writeup for the webshop?
08:20 < itszn> So I made %k give me a shell
08:20 < Gynvael> uhm
08:20 < Gynvael> printf has such an option?
08:20 < Gynvael> ^_-
08:20 < itszn> Yup
08:20 < Gynvael> TIL
08:20 < Murmus> TIL
08:21 < msm-> TIL
08:21 < Murmus> damn, that's awesome, I had no idea
08:21 < dude12312414_> lol
08:21 < WGH> Gynvael, Murmus: like WTFH4X (apparently) by overwrite vtable file stdout's FILE*
08:21 < itszn> There are two pointers to array of function pointers you have to set though
08:21 < itszn> So probably a little harder
08:21 < itszn> But first thing I thought of

这种精巧的利用来源于printf的一个功能:https://www.gnu.org/software/libc/manual/html_node/Customizing-Printf.html#Customizing-Printf

 

我不知道原帖作者是否仔细看了GNU的文档或者printf的源代码

 

这里实际有两次RIP控制的机会,对应与printf的逻辑:首先是printf_arginfo_table,调用函数指针完成自定义spec的参数数目和类型的指定,后来才是对应的printf_function_table做真正的输出。

 

这里还有一点是,看完printf的代码你会知道,检查自定义spec是优先于printf自带的spec的。

 

于是ESPR他们应该是造了readme-revenge来结合上述两道题。

 

由于这道题是静态连接,又有bss段上的溢出,于是可以控制printf相关的函数指针和libc_argv。那么你就可以去构造%s对应的函数指针去控制RIP。

 

但是需要注意到environ指针是你覆盖不到的,他的位置在输入的上面。

 

所以说这种原帖作者所说的利用是存在问题的,你控制不了envivon啊?我不知道原帖作者是忘了、不知道还是故意没提(我希望是前两者)。在下第一时间就是因为这一点所以觉得这种思路不可用,去漫天找其他gadget。

 

实际上,217的同学也表示队友找了5个小时RCE的gadget找不到。

04:46 < david942j> _2can: yap.. it took my teammate 5hrs to try to do RCE (but fails as well)

 

<del> 是的,想要做这道题,需要撞大运,撞一个主办方帮你设置了环境变量LIBC_FATAL_STDERR_=1。 </del>

 

更新:2018年1月16日 20:41:28

 

发现并不是如此。似乎xinetd有一些奇怪的行为,LIBC_FATAL_STDERR并不需要设置,readme那题我自己用docker+xinetd起了之后发现不设置这个环境变量也有flag回来。

 

于是strace上去看了一波,发现如下操作

write(1, "Thank you, bye!", 15)         = 15
write(1, "\n", 1)                       = 1
open("/dev/tty", O_RDWR|O_NOCTTY|O_NONBLOCK) = -1 ENXIO (No such device or address)
writev(2, [{"*** ", 4}, {"stack smashing detected", 23}, {" ***: ", 6}, {"XXXXXXXXXXXXXXXXXXXXXXXXXXX", 31}, {" terminated\n", 12}], 5) = 76

还是回到__libc_message这个函数:

__libc_message (int do_abort, const char *fmt, ...)
{
  va_list ap;
  int fd = -1;

  va_start (ap, fmt);

#ifdef FATAL_PREPARE
  FATAL_PREPARE;
#endif

  /* Open a descriptor for /dev/tty unless the user explicitly
     requests errors on standard error.  */
  const char *on_2 = __libc_secure_getenv ("LIBC_FATAL_STDERR_");
  if (on_2 == NULL || *on_2 == '\0')
    fd = open_not_cancel_2 (_PATH_TTY, O_RDWR | O_NOCTTY | O_NDELAY);

  if (fd == -1)
    fd = STDERR_FILENO;
...

即是说在docker xinetd的环境下,open('/dev/tty')会失败?

 

对比一下ncat的结果,

write(1, "Thank you, bye!", 15)         = 15
write(1, "\n", 1)                       = 1
open("/dev/tty", O_RDWR|O_NOCTTY|O_NONBLOCK) = 6
writev(6, [{"*** ", 4}, {"stack smashing detected", 23}, {" ***: ", 6}, {"32C3_TheServerHasTheFlagHere...", 31}, {" terminated\n", 12}], 5) = 76

open成功,write也就自然喷给了/dev/tty。

 

于是自己也实际跑了一次xinetd,发现仍然open失败。

 

在网络上搜了一会儿,找不到xinetd和/dev/tty相关的资料。希望有了解的朋友可以分享告诉我,不胜感激。

 

目前的猜测是:xinetd有一些奇怪的操作,导致open失败,然后还是会走stderr喷flag出来。所以之前32c3那道题,设置环境变量是多余的。但是ncat是需要同时设置环境变量和2>&1的。

 

更新:2018年1月19日 23:02:11
今天抽空看了下xinetd的源代码,通过/dev/tty很快定位到了util.c

175 void no_control_tty(void)                                                              
176 {                                                                                      
177 #if !defined(HAVE_SETSID)                                                              
178    int fd ;                                                                            
179    const char *func = "no_control_tty" ;                                               
180                                                                                        
181    if ( ( fd = open( "/dev/tty", O_RDWR ) ) == -1 )                                    
182       msg( LOG_WARNING, func, "open of /dev/tty failed: %m" ) ;                        
183    else                                                                                
184    {                                                                                   
185       if ( ioctl( fd, TIOCNOTTY, (caddr_t)0 ) == -1 )                                  
186          msg( LOG_WARNING, func, "ioctl on /dev/tty failed: %m" ) ;                    
187       (void) Sclose( fd ) ;                                                            
188    }                                                                                   
189    (void) setpgrp( getpid(), 0 ) ;                                                     
190 #else                                                                                  
191    (void) setsid() ;                                                                   
192 #endif                                                                                 
193 }

然后再在init.c中找到了这个函数的调用。

237 static void become_daemon(void)
238 {
239    int   tries ;
240    int   pid ;
241    const char  *func = "become_daemon" ;
242
243    /*
244     * First fork so that the parent will think we have exited
245     */
246    for ( tries = 0 ;; tries++ )
247    {
248       if ( tries == 5 )
249       {
250          msg( LOG_CRIT, func, "fork: %m. Exiting..." ) ;
251          exit( 0 ) ;
252       }
253
254       pid = fork() ;
255
256       if ( pid == -1 )
257       {
258          sleep( 1 ) ;   /* wait for a second */
259          continue ;      /* and then retry      */
260       }
261       else if ( pid == 0 )
262          break ;
263       else
264          exit( 0 ) ;
265    }
266
267    (void) dup2( 0, STDERR_FD ) ;
268    no_control_tty() ;
269
270 #ifdef DEBUG_DAEMON
271    sleep( 20 ) ;       /* XXX: timers will probably not work after this */
272 #endif
273 }

搜索了一番后,网络上的说法是这是一种做daemon的常规套路。

 

此贴至此终结 (应该没后续了吧?)

 

后续:实际上还不对,早上起来想起我们平时都是dontfork的,不应该走daemon,再研究了一下,发现是docker本身的问题。

 

https://github.com/moby/moby/issues/14

 

看起来不-it就不会分配tty。

 

发现删除线不好使了,用引用代替吧。

一个队友去撞了,于是成功get flag……真的是……哎。

如果你想要复现这个题目,要注意服务端把stderr导给stdout,并且把环境变量设好。

 

最后感谢原帖作者的分享。在下写这篇文章不是想批判原帖作者,只是认为有必要做一点细节的补充。因为个人认为,这些质量上乘的题目,好就好在引导我们去深入更加底层的程序运作和代码实现的细节。

 

希望能在pwn板块上看到更多的,经过自己辛勤探索、苦读代码、精心分析所得的干货。

 

PS:呼,不知不觉写了半个小时,不知道能混个精华么?:P


[公告]看雪技术峰会,技术大牛大型线下交流见面会,2020年10月23日 上海浦东喜来登由由大酒店!

收藏
点赞0
打赏
分享
最新回复 (1)
雪    币: 759
活跃值: 活跃值 (30)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
pwnda 活跃值 2 2018-1-2 09:44
2
0
很强
游客
登录 | 注册 方可回帖
返回