【漏洞挖掘】Race竞争漏洞学习

1.ThreadSanitizer简介

ThreadSanitizer应该是google三年前开发出来的,主要用于检测C/C++中的数据竞争漏洞,但一直没有进行更新(支持clang3.2和gcc4.8),据师傅说里面的模型不实用,不过通过github上的ThreadSanitizer文档来认识一下竞争漏洞,还是挺不错的。

数据竞争指的是两个线程并发访问了一个变量,并且其中一个是写操作。例如,以下代码可能引发crash和内存错误:

    #include 
    #include 
    #include 
    #include 
    
    typedef std::map<std::string, std::string> map_t;
    
    void *threadfunc(void *p) {
      map_t& m = *(map_t*)p;
      m["foo"] = "bar";
      return 0;
    }
    
    int main() {
      map_t m;
      pthread_t t;
      pthread_create(&t, 0, threadfunc, &m);
      printf("foo=%s\n", m["foo"].c_str());
      pthread_join(t, 0);
    }

2.常见的数据竞争

(1)简单的竞争

两个线程访问同一全局变量,没有采取同步操作。通常,这种竞争也是良性的,例如有些计数变量不要求太精确。但有些变量很关键,例如对钱计数的变量。

    int var;
    
    void Thread1() {  // Runs in one thread.
      var++;
    }
    void Thread2() {  // Runs in another thread.
      var++;
    }

(2)线程对立的引用计数

引用计数,竞争可能引起内存泄露或double free。实例可见http://crbug.com/15577。

    // Ref() and Unref() may be called from several threads.
    // Last Unref() destroys the object.
    class RefCountedObject {
     ...
     public:
      void Ref() {
        ref_++;  // Bug!
      }
      void Unref() {
        if (--ref_ == 0)  // Bug! Need to use atomic decrement!
          delete this;
      }
     private:
      int ref_;
    };

(3)复杂对象的竞争

两个线程访问同一个复杂对象(例如STL 容器,c++标准模板库),而没有进行同步操作。

    std::map<int,int> m;
    
    void Thread1() {
      m[123] = 1;
    }
    
    void Thread2() {
      m[345] = 0;
    }

(4)Notification

对于以下代码:

    bool done = false;
    
    void Thread1() {
      while (!done) {
        do_something_useful_in_a_loop_1();
      } 
      do_thread1_cleanup();
    }
    
    void Thread2() {
      do_something_useful_2();
      done = true;
      do_thread2_cleanup();
    }

两个线程的同步是通过boolean变量done实现的,引发错误。

在x86上,编译器会对代码进行优化:一是do_something_useful_2()的部分代码会被挪到"done = true"之后;二是do_thread2_cleanup()部分代码会被挪到"done = true"之前;三是如果do_something_useful_in_a_loop_1()不修改"done",编译器会重写Thread1如下:

     if (!done) {
        while(true) {
          do_something_useful_in_a_loop_1();
        } 
      }
      do_thread1_cleanup();

这样,Thread1就永远不会停止。

除了x86架构,cache或指令乱序执行会导致其他问题。

大多数动态竞争检测器会报告这类数据竞争,也即使用该bool变量进行同步(do_something_useful_2() 和 do_thread1_cleanup()之间)的情况。

(5)更新对象时没有同步

某线程初始化一个对象指针(开始是null),另一个线程等待,直到对象指针非空。如果没有进行正确的同步操作,编译器会做些奇怪的转化,导致错误。除此以外,在某些架构上,这类竞争会因为cache导致错误。

    MyType* obj = NULL;
    
    void Thread1() {
      obj = new MyType();
    }
    
    void Thread2() {
      while(obj == NULL)
        yield();
      obj->DoSomething();
    }

(6)初始化对象时未同步

对象被构造两次,导致内存泄露。

    static MyObj *obj = NULL;
    
    void InitObj() { 
      if (!obj) 
        obj = new MyObj(); 
    }
    
    void Thread1() {
      InitObj();
    }
    
    void Thread2() {
      InitObj();
    }

(7)写时采用读取锁

更新数据时采用读取锁。

    void Thread1() {
      mu.ReaderLock();
      var++;
      mu.ReaderUnlock();
    }
    
    void Thread2() {
      mu.ReaderLock();
      var++;
      mu.ReaderUnlock();
    }

(8)bit域的竞争

以下代码初看是对的,但如果x是struct { int a:4, b:4; },就会有bug。怎么理解?

    void Thread1() {
      x.a++;
    }
    
    void Thread2() {
      x.b++;
    }

(9)double-checked locking

以下是反面教材,该错误还是很常见。重复检查锁,怎么理解?

    bool inited = false;
    void Init() {
      // 可能被多个线程调用
      if (!inited) {
        mu.Lock();
        if (!inited) {
          // .. initialize something
        }
        inited = true;
        mu.Unlock();
      }
    }

(10)破坏时的竞争

    void Thread1() {
      SomeType object;
      ExecuteCallbackInThread2(
        SomeCallback, &object);
      ...
      // "object" is destroyed when
      // leaving its scope.
    }

(11)vptr上的数据竞争

虚函数知识可参考https://www.cnblogs.com/lifexy/p/10629539.html
假如你有以下结构:

    struct A {
      virtual ~A() {
        F();
      }
      virtual void F() {
        printf("In A");
      }
    };
    struct B : public A {
      virtual void F() {
        printf("In B");
      }
    };

当释放B时,你会看到"In A"被打印,执行A::A()会释放A和所有以A为基类的对象(除了被B覆盖的析构函数)。为了实现该机制,gcc会在B::B()开头分配一个vptr指针(指向B::vtable虚函数表的指针),在A::~A()开头也分配指针指向A::vtable。

竞争:类A有函数Done()、虚函数F()和虚析构函数,析构函数等待Done()事件。类B继承A,并覆写了A::F()。

    #include 
    #include 
    
    class A {
     public:
      A() : done_(false) {}
      virtual void F() { printf("A::F\n"); }
      void Done() {
        std::unique_lock<std::mutex> lk(m_);
        done_ = true;
        cv_.notify_one();
      }
      virtual ~A() {
        std::unique_lock<std::mutex> lk(m_);
        cv_.wait(lk, [this] {return done_;});
      }
     private:
      std::mutex m_;
      std::condition_variable cv_;
      bool done_;
    };
    
    class B : public A {
     public:
      virtual void F() { printf("B::F\n"); }
      virtual ~B() {}
    };
    
    int main() {
      A *a = new B;
      std::thread t1([a] {a->F(); a->Done();});
      std::thread t2([a] {delete a;});
      t1.join(); t2.join();
    }

创建对象a(静态类型A和动态类型B),第1个线程执行a->F(),然后给第2个线程发送信号;第2个线程调用delete a(B::B)实际上调用了A::A,A::A会等待第1个线程的信号。析构函数A::A覆盖了指向A::vptr的vptr,如果在第2个线程开始执行A::~A时,第1个线程执行a->F(),则A::F先被执行,而不会执行B::F。

解决办法:如果类包含虚函数,则不要在构造函数和析构函数中使用同步操作,用Start()和Join()方法。ThreadSanitizer可以区别vptr上的有害竞争。

(12)构造函数中的vptr数据竞争

这是vptr竞争的变种:

    class Base {
      Base() {
       global_mutex.Lock();
       global_list.push_back(this);
       global_mutex.Unlock();
       // point (A), see below
      }
      virtual void Execute() = 0;
      ...
    };
    
    class Derived : Base {
      Derived() {
        name_ = ...;
      }
      virtual void Execute() {
        // use name_;
      }
      string name_;
      ...
    };
    
    Mutex global_mutex;
    vector<Base*> global_list;
    
    // Executed by a background thread.
    void ForEach() {
      global_mutex.Lock();
      for (size_t i = 0; i < global_list.size(); i++)
        global_list[i]->Execute()
      global_mutex.Unlock();
    }

看上去,global_list是用global_mutex正确同步了,但是加入到global_list的对象只有一部分被构造(也可以被其他线程访问)。例如,如果线程先被point(A)占用,ForEach()会触发虚函数调用。

(13)free竞争

有时一个线程在释放堆内存,另一个线程使用该内存。

    int *array;
    void Thread1() {
      array[10]++;
    }
    
    void Thread2() {
      free(array);
    }

AddressSanitizer可以检测该漏洞。

(14)exit时的竞争

如果在其他线程还在运行的时候调用exit,静态对象可能被多个线程同时释放和使用(所以尽量别使用静态non-POD对象)。

    #include "pthread.h"
    #include 
    
    static std::map<int, int> my_map;
    
    void *MyThread(void *) {  // runs in a separate thread.
      int i = 0;
      while(1) {
        my_map[(i++) % 1000]++;
      }
      return NULL;
    }
    
    int main() {
      pthread_t t;
      pthread_create(&t, 0, MyThread, 0);
      return 0;
      // exit() is called, my_map is destructed
    }

(15)mutex竞争

对mutex进行加锁和解锁操作是同步的,但mutex可能被不安全释放。

    class Foo {
      Mutex m_;
      ...
    
     public:
      // Asynchronous completion notification.
      void OnDone(Callback *c) {
        MutexLock l(&m_);
        // handle completion
        // ...
        // schedule user completion callback
        ThreadPool::Schedule(c);
      }
      ...
    };
    
    void UserCompletionCallback(Foo *f) {
      delete f;  // don't need it anymore
      // notify another thread about completion
      // ...
    }

本例中,如果UserCompletionCallback已经在另一个线程上执行,析构函数MutexLock可以解锁已经删除的mutex。

(16)文件描述符的竞争

文件描述符内部是同步的,但是用户代码中对文件描述符错误的读写操作可能引发竞争。

    int fd = open(...);
    
    // Thread 1.
    write(fd, ...);
    
    // Thread 2.
    close(fd);

fd描述符可能被释放后使用,此时fd可能是其他文件或socket,导致数据泄露。

3.ThreadSanitizer可检测的竞争

以下是可检测的竞争案例,代码完整,可编译后研究。

  • Normal data races:多个线程写同一变量,没有同步操作。
  • Races on C++ object vptr:vptr的竞争,不太理解?
  • Use after free races:线程a释放某变量,线程b使用该变量。
  • Races on mutexes:两线程同时对1全局变量进行上锁,其一线程对锁进行初始化pthread_mutex_init(&Mtx, 0);
  • Races on file descriptors:线程a写文件,线程b释放文件描述符。
  • Races on pthread_barrier_t:??
  • Destruction of a locked mutex:??
  • Leaked threads:??
  • Signal-unsafe malloc/free calls in signal handlers
  • Signal handler spoils errno
  • Potential deadlocks (lock order inversions)

参考:

ThreadSanitizerCppManual:https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual

ThreadSanitizerPopularDataRaces:https://github.com/google/sanitizers/wiki/ThreadSanitizerPopularDataRaces

ThreadSanitizerDetectableBugs:https://github.com/google/sanitizers/wiki/ThreadSanitizerDetectableBugs

你可能感兴趣的:(竞争漏洞,漏洞)