看了这篇帖子 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
[培训]《安卓高级研修班(网课)》月薪三万计划,掌
握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法