Xử Lý Triệt Để Lỗi Logic Với Thư Viện libFuzzer

Xử Lý Triệt Để Lỗi Logic Với Thư Viện libFuzzer

Lỗi logic là một lỗi rất thường gặp trong lập trình, là một lỗi mà khi chương trình hoạt động được nhưng lại chạy không đúng với nhiệm vụ của nó, hay còn được biết đến với cái tên "thân thương" là BUG. Vì chương trình vẫn hoạt động được, nên sẽ có tình trạng chủ quan và không kiểm tra lại lỗi này, từ đó dẫn đến code sẽ chạy sai, bay màu, . . . Nói chung là rất phiền toái, nên hôm nay, mình sẽ giới thiệu cho các bạn về thư viện libFuzzer trong C++ để khắc phục triệt để con bọ đáng ghét này nhé.

Tại sao lại dùng libFuzzer?

Về cơ sở của kỹ thuật lập trình, ai cũng biết logic trong lập trình là rất quan trọng, chính cái sự sắp xếp các dòng code theo một trình tự logic sẽ mang đến các hiệu quả và thực thi nhiệm vụ nhất định, chính các logic này sẽ quyết định tính đúng sai, tốc độ chạy của chương trình. Thế nên libFuzzer sẽ là "vị cứu tinh" của bạn trong những trường hợp logic của chương trình chạy sai.

Nhưng mà cũng có một số bất cập, vì libFuzzer tự cung cấp hàm main riêng nên mình không được viết hàm main. Mình phải có một hàm riêng để xử lí input. Trong trường hợp này mình sẽ sử dụng chương trình "Tìm số lớn nhất" này để làm "hàm" xử lý:

int get_max(vector<int> &input)
{
    int result;
    for (int x: input)
        result = max(result, x);
    return result;
}

Chắc chắn rằng, phần lớn các bạn mới học lập trình sẽ lầm tưởng đây là một chương trình đúng, có biến đủ nè, có vòng lặp so sánh và đổi giá trị nè, kết quả result cho ra sẽ là kết quả nhỏ nhất nè, còn gì mà sai nữa đâu nhỉ? 

Nếu bạn nghĩ vậy thì. . . Hehe, sai rồi bạn ơi.

Thử suy xét một chút với biến result xem, điều rõ ràng ở đây là nó không được gán cho bất cứ giá trị nào hết, và chỉ được khai báo là kiểu Integer. Khi nó không mang bất cứ giá trị nào (cho trước), thì quá trình so sánh sẽ không thể nào đúng được, mặc định nó sẽ được gán cho giá trị nào đó xàm xí tầm khoảng 3 mấy nghìn ấy, vì giá trị từ đầu đã sai cho nên tất nhiên là chương trình sẽ chạy sai rồi.

Đây là một lỗi logic khá thường gặp, đặc biệt là đối với người mới, việc không gán giá trị cho biến sau khi khai báo là một hành động khá là nguy hiểm và dễ dẫn đến rủi ro bay màu chương trình. Nếu bạn cứ chủ quan và chạy tay cho rằng nó đã đúng thì kết quả vô cùng khó lường, ví dụ điển hình là mình :0000 lúc mới học lập trình, mình có làm một dự án nhỏ và cũng vì lỗi này mà mình đã phải xóa hơn trăm dòng code, viết đi viết lại để rồi cuối cùng nhận ra là cái sai cơ bản này, MÌNH NGU THẬT. Thế nên các bạn đừng như mình nhé, để mình chỉ cho cách sửa sai nè.

Và chúng ta sẽ đến với, libFuzzer - Phương pháp kiểm lỗi sản phẩm cho lập trình viên C++.

Nền tảng hướng dẫn

Để triển khai thư viện libFuzzer, ở đây mình sẽ dùng Glitch để hướng dẫn cho cụ thể, (đơn giản là vì nó tiện thôi)

Về Glitch, đây là một website, hay nói là nền tảng thì sẽ đúng hơn. Đây là một nền tảng để bạn có thể tạo ra các dự án và push code lên trên này, và có liên kế cả các dự án từ github của bạn. Lý do quan trọng nhất mà mình lựa chọn nó chính là vì nó có hỗ trợ clang++, mình trình biên dịch cho C++ mà tiếp đây mình sẽ dùng để hướng dẫn cho các bạn, tốt nhất là dùng Glitch cho nhanh, chẳng cần phải cài về máy làm gì cho phức tạp. (Nếu thích thì bạn có thể tải về máy rồi cài đặt cũng được, nhưng mà sẽ khá là căng đó)

Hình ảnh trang chủ của Glitch

Chuẩn bị

Đầu tiên, chúng ta sẽ bấm vào New Project, rồi chọn hello-express để tạo một vùng làm việc mới, trong đó sẽ sinh ra các file mặc định, tốt nhất là đừng động vào nó.

Tiếp theo, ta sẽ cài đặt thư viện cho quá trình tiền xử lý của C++, bấm vào New File, rồi gõ bits/stdc++.h, sau đó paste cái này vào trong file stdc++.h. Cái này mình chôm chỉa được từ github nên xin giữ nguyên để tránh một số phiền phức không cần thiết. 

// C++ includes used for precompiling -*- C++ -*-

// Copyright (C) 2003-2013 Free Software Foundation, Inc.
//
// This file is part of the GNU ISO C++ Library.  This library is free
// software; you can redistribute it and/or modify it under the
// terms of the GNU General Public License as published by the
// Free Software Foundation; either version 3, or (at your option)
// any later version.

// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// Under Section 7 of GPL version 3, you are granted additional
// permissions described in the GCC Runtime Library Exception, version
// 3.1, as published by the Free Software Foundation.

// You should have received a copy of the GNU General Public License and
// a copy of the GCC Runtime Library Exception along with this program;
// see the files COPYING3 and COPYING.RUNTIME respectively.  If not, see
// <http://www.gnu.org/licenses/>.

/** @file stdc++.h
 *  This is an implementation file for a precompiled header.
  */

// Also writing "using namespace std;" here so that you dont need to write it everytime you start a cpp file

using namespace std;


  // 17.4.1.2 Headers

  // C
  #ifndef _GLIBCXX_NO_ASSERT
  #include <cassert>
  #endif
  #include <cctype>
  #include <cerrno>
  #include <cfloat>
  #include <ciso646>
  #include <climits>
  #include <clocale>
  #include <cmath>
  #include <csetjmp>
  #include <csignal>
  #include <cstdarg>
  #include <cstddef>
  #include <cstdio>
  #include <cstdlib>
  #include <cstring>
  #include <ctime>

  #if __cplusplus >= 201103L
  #include <ccomplex>
  #include <cfenv>
  #include <cinttypes>
  #include <cstdbool>
  #include <cstdint>
  #include <ctgmath>
  #include <cwchar>
  #include <cwctype>
  #endif

  // C++
  #include <algorithm>
  #include <bitset>
  #include <complex>
  #include <deque>
  #include <exception>
  #include <fstream>
  #include <functional>
  #include <iomanip>
  #include <ios>
  #include <iosfwd>
  #include <iostream>
  #include <istream>
  #include <iterator>
  #include <limits>
  #include <list>
  #include <locale>
  #include <map>
  #include <memory>
  #include <new>
  #include <numeric>
  #include <ostream>
  #include <queue>
  #include <set>
  #include <sstream>
  #include <stack>
  #include <stdexcept>
  #include <streambuf>
  #include <string>
  #include <typeinfo>
  #include <utility>
  #include <valarray>
  #include <vector>

  #if __cplusplus >= 201103L
  #include <array>
  #include <atomic>
  #include <chrono>
  #include <condition_variable>
  #include <forward_list>
  #include <future>
  #include <initializer_list>
  #include <mutex>
  #include <random>
  #include <ratio>
  #include <regex>
  #include <scoped_allocator>
  #include <system_error>
  #include <thread>
  #include <tuple>
  #include <typeindex>
  #include <type_traits>
  #include <unordered_map>
  #include <unordered_set>
  #endif
    

Sau đó, chúng ta lại bấm vào New File, tạo một file .cpp để 'làm việc' trên đó, các bạn đặt tên gì cũng được, nhưng ở đây mình sẽ sử dụng tên là main.cpp

Bắt đầu thôi !

Để mình fuzz được chương trình "Tìm số lớn nhất" đã nêu trên, mình phải có một cái hàm tên là LLVMFuzzerTestOneInput. Nhiệm vụ của hàm này nhận hai tham số: tham số thứ nhất là con trỏ đến vị trí bắt đầu của một đống dữ liệu nhị phân được sinh ra từ libFuzzer, tham số thứ hai là độ dài của đống dữ liệu đó để hàm này biết được đống dữ liệu ấy kết thúc ở đâu. Cái qui ước này rất thông dụng trong C đấy. Và hàm này có chữ extern "C" đằng trước để libFuzzer biết đường mà gọi. Nếu không có chữ này thì quá trình name mangling sẽ diễn ra làm cho libFuzzer không thể tìm được hàm.

Như vậy việc cần làm tiếp theo rất đơn giản, mình cứ việc lấy vài byte từ cái đống nhị phân ấy, ghép các bit lại thành số nguyên rồi đưa vào chương trình bằng hàm memcpy. Không đủ byte để lấy thì thoát.

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
    int remaining_bytes = size;

    // Điều kiện này nhằm kiểm tra mình có đủ dữ liệu để cấp cho biến hay không.
    if (remaining_bytes < sizeof(int)) return 0;

    int length;
    memcpy(&length, data, sizeof(int));
    data += sizeof(int);
    remaining_bytes -= sizeof(int);
}

Thông qua hàm này, mình đã lấy được thông tin về độ dài của mảng để truyền tham số vào hàm get_max() lúc nãy. Tiếp tục thôi nào !

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
    int remaining_bytes = size;

    if (remaining_bytes < sizeof(int)) return 0;
    int length = *reinterpret_cast<int*>(const_cast<uint8_t*>(data));
    data += sizeof(int);
    remaining_bytes -= sizeof(int);
    
    vector<int> input(length);
    for (int &x: input)
    {
        if (remaining_bytes < sizeof(int)) return 0;
        memcpy(&x, data, sizeof(int));
        data += sizeof(int);
        remaining_bytes -= sizeof(int);
    }
}

MAGIC ! Chúng ta đã có được mảng input rồi, vậy tiếp theo chính là cho chạy hàm get_max() để kiểm tra chương trình có chạy đúng hay không. Điều kiện để hàm get_max() này chạy đúng có hai điều kiện:

  1. Phải có một phần tử "bự" hơn hoặc bằng một giá trị nào đó trong mảng (đương nhiên là giá trị này phải là "bự" nhất trước đã).
  2. Giá trị đó phải ở trong mảng.

(Tới đây có lẽ các bạn thắc mắc input từ đâu mà ra phải không? Thật ra là bằng một cách kỳ diệu nào đó mà thư viện libFuzzer đã tạo ra một mảng nhị phân và input của nó đảm bảo chương trình chạy đến nhiều vùng của mã nguồn nhất có thể. Cho nên các bạn không cần lo về vấn đề input từ đâu ra nhé.)

Tiếp đây, dựa vào điều kiện thứ hai thì phải có giá trị ấy tồn tại trong mảng, thế thì nó dễ kiểm tra hơn rồi.

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
    int remaining_bytes = size;

    if (remaining_bytes < sizeof(int)) return 0;
    int length = *reinterpret_cast<int*>(const_cast<uint8_t*>(data));
    data += sizeof(int);
    remaining_bytes -= sizeof(int);
    
    vector<int> input(length);
    for (int &x: input)
    {
        if (remaining_bytes < sizeof(int)) return 0;
        memcpy(&x, data, sizeof(int));
        data += sizeof(int);
        remaining_bytes -= sizeof(int);
    }
    
    // kiểm tra phần tử mang giá trị lớn nhất mà hàm get_max() trả về có trong mảng input hay không
    int max_value = get_max(input);
    
    bool value_exists = false;
    for (int x: input)
    	if (x == max_value)
            value_exists = true;

    assert(value_exists);

    return 0;
}

Trong chương trình có cái hàm assert(), cái hàm này là để kiểm tra điều kiện, khi điều kiện bằng False thì nó sẽ báo lỗi. Thư viện libFuzzer sẽ báo lại test chạy sai.

Vậy nếu xét lại điều kiện đầu tiên thì sao? Giá trị đó phải lớn nhất trong mảng và bằng hoặc lớn hơn giá trị nào đó trong mảng. 

Thế thì kiểm tra giá trị của các phần tử trong mảng. Nếu tồn tại phần tử mà lớn hơn cái giá trị hàm get_max() trả về thì mình gây lỗi thôi, dùng hàm assert() ấy.

Chú ý: libFuzzer sẽ cho chạy các test lần lượt với nhau, giống như một vòng while vậy. Khi còn byte để lấy thì nó tiếp tục chạy và xét điều kiện với byte ấy và đương nhiên là không trùng lặp với các byte trước đó đã lấy ra nên mình chỉ cần thêm điều kiện vào thôi, không cần phải xét lại các giá trị trước đó.

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
    int remaining_bytes = size;

    if (remaining_bytes < sizeof(int)) return 0;
    int length = *reinterpret_cast<int*>(const_cast<uint8_t*>(data));
    data += sizeof(int);
    remaining_bytes -= sizeof(int);
    
    vector<int> input(length);
    for (int &x: input)
    {
        if (remaining_bytes < sizeof(int)) return 0;
        memcpy(&x, data, sizeof(int));
        data += sizeof(int);
        remaining_bytes -= sizeof(int);
    }
    
    int max_value = get_max(input);
    
    bool value_exists = false;
    for (int x: input)
    	if (x == max_value)
            value_exists = true;

    assert(value_exists);

    if (remaining_bytes < sizeof(int)) return 0;
    int counterexample_index;
    memcpy(&counterexample_index, data, sizeof(int));
    data += sizeof(int);
    remaining_bytes -= sizeof(int);
    
    assert(input[counterexample_index] <= max_value);

    return 0;
}

Tới đây, nếu trong trường hợp mà input được sinh ra ngẫu nhiên quá lớn thì chương trình bạn sẽ bị "chết lâm sàng" đấy. Tức là chương trình sẽ chạy cực kỳ lâu, hay nói là một con rùa đang đua trên đường đua F1.

Bởi thế cho nên chúng ta sẽ giới hạn đầu vào lại. Đặt thêm điều kiện khi so sánh cho đầu vào.

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
    int remaining_bytes = size;

    if (remaining_bytes < sizeof(int)) return 0;
    int length;
    memcpy(&length, data, sizeof(int));
    data += sizeof(int);
    remaining_bytes -= sizeof(int);

    //giới hạn độ dài đầu vàoooooo
    if (length < 1 || length > 1000) return 0;
    
    vector<int> input(length);
    for (int &x: input)
    {
        if (remaining_bytes < sizeof(int)) return 0;
        memcpy(&x, data, sizeof(int));
        data += sizeof(int);
        remaining_bytes -= sizeof(int);
    }
    
    int max_value = get_max(input);
    
    bool value_exists = false;
    for (int x: input)
    	if (x == max_value)
            value_exists = true;

    assert(value_exists);

    if (remaining_bytes < sizeof(int)) return 0;
    int counterexample_index;
    memcpy(&counterexample_index, data, sizeof(int));
    data += sizeof(int);
    remaining_bytes -= sizeof(int);

    // thêm điều kiện kiểm traaaa
    if (counterexample_index < 0 || counterexample_index >= length) return 0;
    
    assert(input[counterexample_index] <= max_value);

    return 0;
}

Và toàn bộ mã nguồn nó sẽ trông như thế này:

#include <bits/stdc++.h>
using namespace std;

int get_max(vector<int> &input)
{
    int result;
    for (int x: input)
        result = max(result, x);
    return result;
}

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
    int remaining_bytes = size;

    if (remaining_bytes < sizeof(int)) return 0;
    int length;
    memcpy(&length, data, sizeof(int));
    data += sizeof(int);
    remaining_bytes -= sizeof(int);
    
    if (length < 1 || length > 1000) return 0;
    
    vector<int> input(length);
    for (int &x: input)
    {
        if (remaining_bytes < sizeof(int)) return 0;
        memcpy(&x, data, sizeof(int));
        data += sizeof(int);
        remaining_bytes -= sizeof(int);
    }
    
    int max_value = get_max(input);

    bool value_exists = false;
    for (int x: input)
    	if (x == max_value)
            value_exists = true;

    assert(value_exists);

    if (remaining_bytes < sizeof(int)) return 0;
    int counterexample_index;
    memcpy(&counterexample_index, data, sizeof(int));
    data += sizeof(int);
    remaining_bytes -= sizeof(int);
    
    if (counterexample_index < 0 || counterexample_index >= length) return 0;
    
    assert(input[counterexample_index] <= max_value);

    return 0;
}

Xong rồi, chúng ta sẽ vào Terminal của Glitch và gõ các 'câu thần chú' sau để biên dịch chương trình:

clang++ -g -fsanitize=fuzzer main.cpp
./a.out

Như chúng ta thấy, chương trình đã chạy và báo một lỗi cho chúng ta:

NOTE: libFuzzer has rudimentary signal handlers.
      Combine libFuzzer with AddressSanitizer or similar for better crash reports.
SUMMARY: libFuzzer: deadly signal
MS: 5 ChangeByte-CMP-CrossOver-ChangeBinInt-CMP- DE: "\xff\xff\xff\xff\xff\xff\xff\xff"-"\x01\x00\x00\x00\x00\x00\x00\x00"-; base unit: ec57adb0e69b41836afd139a653570e23667654d
0x1,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
\x01\x00\x00\x00\x00\x00\x00\x00
artifact_prefix='./'; Test unit written to ./crash-3da89ee273be13437e7ecf760f3fbd4dc0e8d1fe
Base64: AQAAAAAAAAA=

Lỗi này đã được ghi vào file crash-... tên của file crash này sẽ khác nhau chứ không giống nhau và có thể có nhiều file crash nhưng chúng ta chỉ cần đọc file được thông báo thôi, trong trường hợp này, file crash được thông báo của mình có tên là crash-3da89ee273be13437e7ecf760f3fbd4dc0e8d1fe. Nhưng file chứa mã nhị phân nên chúng ta không đọc được, để đọc được chúng ta cần có một chương trình biên dịch mã nhị phân tương tự sau:

#include "bits/stdc++.h"
using namespace std;
int main()
{
  ios_base::sync_with_stdio(0);
  cin.tie(0);

  vector<uint8_t> bytes;
  char x;
  while (cin >> x)
    bytes.push_back(x);
  auto data = bytes.data();
  
  int length;
  memcpy(&length, data, sizeof(int));
  data += sizeof(int);
  
  vector<int> input(length);
  
  for (int &x: input)
  {
    memcpy(&x, data, sizeof(int));
    data += sizeof(int);
  }
  
  int counterexample_index;
  memcpy(&counterexample_index, data, sizeof(int));
  data += sizeof(int);
  
  cout << "Array length: " << length << "\n";
  cout << "Array elements: ";
  for (int &x: input) cout << x << " ";
  cout << "\n";
  cout << "Counterexample index: " << counterexample_index << "\n";
}

Mình đã đặt chương trình này trong file binary_decoder.cpp, thế nên bây giờ chúng ta biên dịch và nhờ nó giải mã hộ file crash kia thôi:

Vậy là chúng ta đã tìm ra test làm cho chương trình chạy sai. Giờ mình sửa cho hết sai.

int get_max(vector<int> &input)
{
    int result = input[0];
    int n = input.size();
    for (int i = 1; i < n; i++)
        result = max(result, input[i]);
    return result;
}

Toàn bộ code ĐÚNG sẽ như sau:

#include <bits/stdc++.h>
using namespace std;

int get_max(vector<int> &input)
{
    int result = input[0];
    int n = input.size();
    for (int i = 1; i < n; i++)
        result = max(result, input[i]);
    return result;
}

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
    int remaining_bytes = size;

    if (remaining_bytes < sizeof(int)) return 0;
    int length;
    memcpy(&length, data, sizeof(int));
    data += sizeof(int);
    remaining_bytes -= sizeof(int);
    
    if (length < 1 || length > 1000) return 0;
    
    vector<int> input(length);
    for (int &x: input)
    {
        if (remaining_bytes < sizeof(int)) return 0;
        memcpy(&x, data, sizeof(int));
        data += sizeof(int);
        remaining_bytes -= sizeof(int);
    }
    
    int max_value = get_max(input);
    
    bool value_exists = false;
    for (int x: input)
    	if (x == max_value)
            value_exists = true;

    assert(value_exists);

    if (remaining_bytes < sizeof(int)) return 0;
    int counterexample_index;
    memcpy(&counterexample_index, data, sizeof(int));
    data += sizeof(int);
    remaining_bytes -= sizeof(int);
    
    if (counterexample_index < 0 || counterexample_index >= length) return 0;
    
    assert(input[counterexample_index] <= max_value);

    return 0;
}

Lại thử gõ câu thần chú kia nào:

Hết sai rồi. Khi chạy chương trình, nó sẽ in cả đống thứ test nhưng không hề báo lỗi. Do nó có tìm được chỗ sai đâu.

Thế nên bài này kết thúc ở đây nhá ! Bye bye

Kết bài

Hi vọng bạn đã biết được thêm những thông tin hữu ích ! Chúc bạn một ngày tốt lành

Nguồn tham khảo: