C++ CGI報“資源訪問錯誤”問題分析

 

一線上CGI偶發性會報“資源訪問錯誤”,經過分析得出是因爲CgiHost沒有讀取到CGI的任務輸出,即CGI運行完成後連HTTP頭都沒有一點輸出。

然而實際上,不可能沒有任何輸出,因爲CGI至少有無條件的HTTP頭部分輸出,因此問題是輸出丟失了。CGI和CgiHost間是通過重定向CGI的標準輸出到Unix套接字進行交互的,如果這個套接字壞了,或者CGI的標準輸出關閉了,自然不會有任何輸出。但經測試,如果是關閉了套接字,報的錯誤不一樣,因此直接排除這個可能。

經調查,該CGI的輸出使用是C++庫的std::cout,不是printf這套C庫I/O,初步推斷是std::cout對象的狀態值不是goodbit。不是goodbit主要分三種情況:

  1. 人爲置爲badbit等;
  2. 程序有越界造成狀態爲badbit或failbit等;
  3. 有類似“char* str=NULL; std::cout << str”的調用出現,造成狀態爲badbit;
  4. std::cout調用的basic_streambuf<typename _CharT, typename _Traits>::sputn返回的大小不是期望值,造成狀態爲failbit。

 

從瞭解來看,第三種和第四種情況概率高出許多:

#include <iosfwd>

namespace std

{

  template<typename _CharT, typename _Traits = char_traits<_CharT> >

  class basic_ostream;

  typedef basic_ostream<char> ostream;

}

#include <iostream>

namespace std

{

  extern istream cin; /// Linked to standard input

  extern ostream cout; /// Linked to standard output

  extern ostream cerr; /// Linked to standard error (unbuffered)

  extern ostream clog; /// Linked to standard error (buffered)

}

#include <ostream>

namespace std

{

  // Partial specializations

  template<class _Traits>

  inline basic_ostream<char, _Traits>&

  operator<<(basic_ostream<char, _Traits>& __out, const char* __s) // 一個全局函數

  {

    if (!__s) // 如果“__s”此時是NULL

      __out.setstate(ios_base::badbit); // 其它一些情況,可查看源碼文件ostream.tcc

    else

      __ostream_insert(__out, __s, static_cast<streamsize>(_Traits::length(__s)));

    return __out;

  }

 

  // __ostream_write是一個位於名字空間std中的全局函數

  // __ostream_write被兄弟函數__ostream_insert調用,

  // 而__ostream_insert又被函數全局函數operator<<調用

  template<typename _CharT, typename _Traits>

  inline void

  __ostream_write(basic_ostream<_CharT, _Traits>& __out,

    const _CharT* __s, streamsize __n)

  {

    typedef basic_ostream<_CharT, _Traits> __ostream_type;      

    typedef typename __ostream_type::ios_base __ios_base;

 

    // 類basic_streambuf的成員函數sputn實際調用的是兄弟函數xsputn

    // 而xsputn是一個虛擬函數。

    // 虛類basic_streambuf有兩個具體的子類:stringbuffilebuf

    // 對std::cout而言,對應的是filebuf,xsputn底層調用的實際是write函數。

    // 注:

    // file版的xsputn實現在文件fstream.tcc中,

    // string版的xsputn實現在文件streambuf.tcc中。

    const streamsize __put = __out.rdbuf()->sputn(__s, __n);

    if (__put != __n)

      __out.setstate(__ios_base::badbit);

  }

 

  template<typename _CharT, typename _Traits>

  class basic_streambuf { // 虛擬基類

  public:

    virtual streamsize xsputn(const char_type* __s, streamsize __n);

  };

  class basic_stringbuf: public basic_streambuf; // 字符串子類

  class basic_filebuf: public basic_streambuf; // 文件子類

}

typedef _Ios_Iostate iostate;

enum _Ios_Iostate

{

  _S_goodbit  = 0,

  _S_badbit  = 1L << 0, // cout << (char*)NULL

  _S_eofbit = 1L << 1,

  _S_failbit = 1L << 2, // write(buf,n) < n

  _S_ios_iostate_end = 1L << 16

};

 

// 注:

// ios_base是一個普通類,並不是模板類

class ios_base

{

  Iostate _M_streambuf_state;

  basic_streambuf<_CharT, _Traits>* _M_streambuf; // 對於fstream實際爲basic_filebuf

};

 

template<typename _CharT, typename _Traits>

class basic_ios : public ios_base

{

};

 

// 這裏用到了virtual繼承,

// 因爲子類basic_iostream會同時繼承basic_istream和basic_ostream,

// 出現共享basic_ios,所以需要使用virtual繼承

template<typename _CharT, typename _Traits>

class basic_ostream : virtual public basic_ios<_CharT, _Traits>

{

};

(gdb) ptype std::char_traits<char>

type = struct std::char_traits<char> {

  public:

    static void assign(char_type &, const char_type &);

    static char_type * assign(char_type *, std::size_t, char_type);

    static bool eq(const char_type &, const char_type &);

    static bool lt(const char_type &, const char_type &);

    static int_type compare(const char_type *, const char_type *, std::size_t);

    static std::size_t length(const char_type *);

    static const char_type * find(const char_type *, std::size_t, const char_type &);

    static char_type * move(char_type *, const char_type *, std::size_t);

    static char_type * copy(char_type *, const char_type *, std::size_t);

    static char_type to_char_type(const int_type &);

    static int_type to_int_type(const char_type &);

    static bool eq_int_type(const int_type &, const int_type &);

    static int_type eof(void);

    static int_type not_eof(const int_type &);

 

    typedef char char_type;

    typedef int int_type;

}

 

嘗試使用GDB實地考察,遺憾的是無法對std::cout進行Debug,所以只有直接修改代碼線上驗證。但如果有辦法取得std::cout的地址,然後根據對象的內存佈局,找到成員_M_streambuf_state的內存位置,也是可以查看和動態修改的。

(gdb) p std::cout

No symbol "cout" in namespace "std".

 

(gdb) p &std::cout

No symbol "cout" in namespace "std".

 

找到std::cout在進程中的位置,以便找到其成員_M_streambuf_state在進程中的位置

(gdb) p _ZSt4cout

$1 = -144214548

 

(gdb) call write(1,"1234567890",10)

$6 = -1

 

(gdb) p *__errno_location()

$4 = 5

 

(gdb) whatis std::cout

type = std::ostream

 

(gdb) ptype std::cout

type = std::ostream

 

(gdb) p &std::cout

$1 = (std::ostream *) 0x7ffff7dd8700 <std::cout>

 

(gdb) ptype std::ostream   

type = std::ostream

 

(gdb) p std::cout

$1 = <incomplete type>

 

(gdb) p &std::cout

$3 = (std::ostream *) 0x7ffff7dd8700 <std::cout>

(gdb) p *(std::ostream *)&std::cout

$4 = <incomplete type>

 

(gdb) info symbol 0x7ffff7dd8700

std::cout in section .bss of /lib64/libstdc++.so.6

 

(gdb) info address std::cout

Symbol "std::cout" is static storage at address 0x7ffff7dd8700.

 

(gdb) set solib-search-path /lib64

(gdb) info share 或 info sharedlibrary

From                To                  Syms Read   Shared Object Library

0x00007ffff7ddbb10  0x00007ffff7df6460  Yes (*)     /lib64/ld-linux-x86-64.so.2

0x00007ffff7ef4060  0x00007ffff7ef54f8  Yes         /lib64/libonion.so

0x00007ffff7b2e510  0x00007ffff7b9559a  Yes (*)     /lib64/libstdc++.so.6

0x00007ffff77d6370  0x00007ffff7841278  Yes (*)     /lib64/libm.so.6

0x00007ffff75bdaf0  0x00007ffff75cd298  Yes (*)     /lib64/libgcc_s.so.1

0x00007ffff7216480  0x00007ffff735cc00  Yes (*)     /lib64/libc.so.6

0x00007ffff6ff3e60  0x00007ffff6ff4960  Yes (*)     /lib64/libdl.so.2

(*): Shared library is missing debugging information.

(gdb) sharedlibrary libstdc

Symbols already loaded for /lib64/libstdc++.so.6

(gdb) sharedlibrary libstdc++

Symbols already loaded for /lib64/libstdc++.so.6

# lsof -p 4442

cgihost  4442 root    0r   CHR         1,3      0t0      1028 /dev/null

cgihost  4442 root    1u   CHR         136,2    0t0         5 /dev/pts/2 (deleted)

cgihost  4442 root    2u   CHR         136,2    0t0         5 /dev/pts/2 (deleted)

cgihost  4442 root    3u   REG         253,17   1144245   2097190 /data/cgi/log/httpserver/cgi_test.log

cgihost  4442 root    4u  0000         0,9      0      6842 anon_inode

cgihost  4442 root    7u   REG         253,1    144   1360125 /usr/local/httpserver/bin/map/mem-cgi-bin-test

cgihost  4442 root    8u  unix 0xffff880006933b80      0t0 831550706 socket

# pmap 4442

# objdump -t test|grep _ZSt4cout

0000000000601080 g     O .bss   0000000000000110              _ZSt4cout@@GLIBCXX_3.4

# readelf -s /usr/lib/libstdc++.so.6|grep cout

   Num:    Value  Size Type    Bind   Vis      Ndx Name

   884: 000e3ec0   140 OBJECT  GLOBAL DEFAULT   27 _ZSt4cout@@GLIBCXX_3.4

   902: 000e4140   144 OBJECT  GLOBAL DEFAULT   27 _ZSt5wcout@@GLIBCXX_3.4

# readelf -r /usr/lib/libstdc++.so.6|grep cout

Offset     Info    Type            Sym.Value  Sym. Name

000e2b64  00038606 R_386_GLOB_DAT    000e4140   _ZSt5wcout

000e2ea0  00037406 R_386_GLOB_DAT    000e3ec0   _ZSt4cout

 

如果希望能夠Debug標準庫中的設施,可在編譯時加上開關“-D_GLIBCXX_DEBUG”。

實際上,即使不能GDB中直接得到std::cout的地址,特別是其成員_M_streambuf_state的地址,但可採取變通的辦法取得。

先編寫如下一小段代碼,然後執行這一小段代碼,取得成員_M_streambuf_state和std::cout間的偏移Offset,以方便得到std::cout地址時取得成員_M_streambuf_state的地址,以達到修改成員_M_streambuf_state值的目的。

#include <stdio.h>

#include <iostream>

int main()

{

  const int n = (unsigned long)&std::cout._M_streambuf_state - (unsigned long)&std::cout;

  printf("offset: %d\n", n);

  return 0;

}

 

注意,編譯之前需要修改標準庫頭文件ios_base.h,將_M_streambuf_state的類型由protected改成public。不然,應當修改小段代碼成如下:

#include <stdio.h>

#include <iostream>

class X: public std::ostream

{

public:

  using std::ostream::_M_streambuf_state;

};

int main()

{

  X x;

  const int n = (unsigned long)&x._M_streambuf_state - (unsigned long)&x;

  printf("offset: %d\n", n);

  return 0;

}

 

當前的多數環境上,offset值一般爲40

如果是可執行程序文件,找cout的地址簡單多(“0000000000601280”即爲cout的內存地址):

# file xxx

xxx: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.18, not stripped

# objdump -t xxx|grep cout

0000000000601280 g     O .bss   0000000000000110              _ZSt4cout@@GLIBCXX_3.4

 

這裏介紹一種通用的取std::cout地址,及std::cout的_M_streambuf_state成員偏移方式。得到std::cout的地址和成員_M_streambuf_state的偏移,實際也就得到了成員_M_streambuf_state的地址,有了地址就可以控制它了。

首先,編寫如下這樣的一小段Hook代碼:

// hooker.cpp

#include <iostream>

#include <stdio.h>

class Hooker

{

public:

  Hooker()

  {

    // 得到std::cout的成員_M_streambuf_state偏移

    const int offset = (unsigned long)&std::cout._M_streambuf_state - (unsigned long)&std::cout;

    FILE* fp = fopen("/tmp/hooker.txt", "w+");

    fprintf(fp, "&std::cout: %p, offset: %d, _M_streambuf_state: %p\n",

      &std::cout, offset, &std::cout+offset);

    fclose(fp);    

  }

};

static Hooker __hooker;

 

這小段代碼的目的是爲得到std::cout成員_M_streambuf_state的內存地址,以方便在GDB中控制它。將Hook代碼編譯成共享庫:

g++ -g -o libhooker.so -fPIC -shared hooker.cpp

 

如果是64位系統,需要編譯成32位的共享庫,則編譯命令爲:

g++ -m32 -g -o libhooker.so -fPIC -shared hooker.cpp

 

假設將libhooker.so放在/tmp目錄下,先使用GDB進入目標進程,假設目標進程ID爲2019,則:

# gdb -p 2019

在GDB中加載共享庫,這樣共享庫中的全局變量的構造函數將執行,從而可從文件/tmp/hooker.txt中得到想要的信息

(gdb) call dlopen("/tmp/libhooker.so",258)

(gdb) c

 

假設/tmp/hooker.txt中std::cout的地址爲0xf76f9ec0,成員_M_streambuf_state的偏移爲24,則在GDB中可如下查看_M_streambuf_state的值:

(gdb) p *(int*)(0xf76f9ec0+24)

 

也可在GDB中執行如下命令確認:

(gdb) info symbol (0xf76f9ec0+24)

 

可在GDB中執行如下命令修改_M_streambuf_state的值:

(gdb) set *(int*)(0xf76f9ec0+24)=1

 

掌握了Hook方法後,查明原因就十分簡單了。這個方法有一個前提,目標進程有鏈接libdl.so,因爲它提供了加載共享庫函數dlopen的。確認是否鏈接了libdl.so方法,使用ldd即可:

$ ldd z

        linux-vdso.so.1 =>  (0x00007ffde7822000)

        /$LIB/libonion.so => /lib64/libonion.so (0x00007f4872630000)

        libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007f487220f000)

        libm.so.6 => /lib64/libm.so.6 (0x00007f4871f0d000)

        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f4871cf7000)

        libc.so.6 => /lib64/libc.so.6 (0x00007f4871933000)

        libdl.so.2 => /lib64/libdl.so.2 (0x00007f487172f000)

        /lib64/ld-linux-x86-64.so.2 (0x00007f4872517000)

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章