├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── afl_patches.diff ├── bin ├── dedicated.orig.so ├── engine.orig.so └── libtier0.orig.so ├── main.cpp ├── mini_bsp ├── mini_bsp.py ├── test.bsp └── test_mini.bsp ├── patch.py ├── re └── types.h ├── run_afl.sh ├── setup.sh └── triage ├── .gitignore ├── bugid └── .gitkeep ├── crashes └── .gitkeep ├── gdb.py ├── triage.py ├── triage.sh ├── valgrind.sh └── valgrind └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | /bspfuzz 2 | /steam_* 3 | /bin 4 | /csgo 5 | /fuzz 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Niklas Baumstark 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/bash 2 | 3 | bspfuzz: main.cpp 4 | g++ -Wall -std=c++11 -o $@ $< -m32 -Wl,-rpath=$$(pwd)/bin -ldl -no-pie 5 | 6 | patch: 7 | python2 patch.py 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CS:GO map file fuzzing using AFL in QEMU mode 2 | 3 | Author: [@_niklasb](https://github.com/_niklasb) 4 | 5 | [Overview article.](https://phoenhex.re/2018-08-26/csgo-fuzzing-bsp) 6 | 7 | See LICENSE. 8 | 9 | ## Prerequisites 10 | 11 | ```bash 12 | $ sudo apt install gdb valgrind build-essential python3-minimal python-minimal 13 | $ cd ~ 14 | $ git clone https://github.com/niklasb/gdbinit 15 | $ cd gdbinit 16 | $ ./setup.sh 17 | ``` 18 | 19 | Then, build AFL with qemu mode support and `afl_patches.diff` applied. Set 20 | `AFL_PATH` correctly in your `.bashrc`. 21 | 22 | ## Setup 23 | 24 | 1. `git clone https://github.com/niklasb/bspfuzz/ && cd bspfuzz` 25 | 2. Copy over `bin/` and `csgo/` directories from the CS:GO server installation 26 | into the `bspfuzz` directory 27 | 3. Adapt offsets in `main.cpp` and `patch.py` for your version 28 | 4. `./setup.sh` 29 | 30 | ## Running 31 | 32 | ```bash 33 | $ cd /path/to/bspfuzz 34 | $ ./run_afl.sh 1 35 | $ ./run_afl.sh 2 36 | $ ./run_afl.sh 3 37 | ... 38 | ``` 39 | 40 | ## Triaging 41 | 42 | ```bash 43 | $ sudo sysctl -w kernel.randomize_va_space=0 44 | $ cd /path/to/bspfuzz/triage 45 | $ ./triage.sh 46 | $ ./valgrind.sh 47 | ``` 48 | -------------------------------------------------------------------------------- /afl_patches.diff: -------------------------------------------------------------------------------- 1 | diff --git a/afl-fuzz.c b/afl-fuzz.c 2 | index 01b4afe..082d511 100644 3 | --- a/afl-fuzz.c 4 | +++ b/afl-fuzz.c 5 | @@ -3821,7 +3821,7 @@ static void maybe_delete_out_dir(void) { 6 | 7 | /* And now, for some finishing touches. */ 8 | 9 | - fn = alloc_printf("%s/.cur_input", out_dir); 10 | + fn = alloc_printf("%s/cur_input.bsp", out_dir); 11 | if (unlink(fn) && errno != ENOENT) goto dir_cleanup_failed; 12 | ck_free(fn); 13 | 14 | @@ -7204,7 +7204,7 @@ EXP_ST void setup_dirs_fds(void) { 15 | 16 | EXP_ST void setup_stdio_file(void) { 17 | 18 | - u8* fn = alloc_printf("%s/.cur_input", out_dir); 19 | + u8* fn = alloc_printf("%s/cur_input.bsp", out_dir); 20 | 21 | unlink(fn); /* Ignore errors */ 22 | 23 | @@ -7526,7 +7526,7 @@ EXP_ST void detect_file_args(char** argv) { 24 | /* If we don't have a file name chosen yet, use a safe default. */ 25 | 26 | if (!out_file) 27 | - out_file = alloc_printf("%s/.cur_input", out_dir); 28 | + out_file = alloc_printf("%s/cur_input.bsp", out_dir); 29 | 30 | /* Be sure that we're always using fully-qualified paths. */ 31 | 32 | diff --git a/config.h b/config.h 33 | index e74b3b3..84f8f5b 100644 34 | --- a/config.h 35 | +++ b/config.h 36 | @@ -139,11 +139,11 @@ 37 | 38 | /* Maximum size of input file, in bytes (keep under 100MB): */ 39 | 40 | -#define MAX_FILE (1 * 1024 * 1024) 41 | +#define MAX_FILE (100 * 1024 * 1024) 42 | 43 | /* The same, for the test case minimizer: */ 44 | 45 | -#define TMIN_MAX_FILE (10 * 1024 * 1024) 46 | +#define TMIN_MAX_FILE (100 * 1024 * 1024) 47 | 48 | /* Block normalization steps for afl-tmin: */ 49 | 50 | @@ -294,7 +294,7 @@ 51 | /* Fork server init timeout multiplier: we'll wait the user-selected 52 | timeout plus this much for the fork server to spin up. */ 53 | 54 | -#define FORK_WAIT_MULT 10 55 | +#define FORK_WAIT_MULT 1000 56 | 57 | /* Calibration timeout adjustments, to be a bit more generous when resuming 58 | fuzzing sessions or trying to calibrate already-added internal finds. 59 | diff --git a/qemu_mode/build_qemu_support.sh b/qemu_mode/build_qemu_support.sh 60 | index 827c93d..fe454b1 100755 61 | --- a/qemu_mode/build_qemu_support.sh 62 | +++ b/qemu_mode/build_qemu_support.sh 63 | @@ -145,7 +145,7 @@ echo "[+] Configuration complete." 64 | 65 | echo "[*] Attempting to build QEMU (fingers crossed!)..." 66 | 67 | -make || exit 1 68 | +make -j12 || exit 1 69 | 70 | echo "[+] Build process successful!" 71 | 72 | @@ -164,7 +164,7 @@ if [ "$ORIG_CPU_TARGET" = "" ]; then 73 | 74 | cd .. 75 | 76 | - make >/dev/null || exit 1 77 | + make -j12 >/dev/null || exit 1 78 | 79 | gcc test-instr.c -o test-instr || exit 1 80 | 81 | diff --git a/qemu_mode/patches/afl-qemu-cpu-inl.h b/qemu_mode/patches/afl-qemu-cpu-inl.h 82 | index 8d3133a..bfc39d0 100644 83 | --- a/qemu_mode/patches/afl-qemu-cpu-inl.h 84 | +++ b/qemu_mode/patches/afl-qemu-cpu-inl.h 85 | @@ -48,6 +48,7 @@ 86 | 87 | #define AFL_QEMU_CPU_SNIPPET2 do { \ 88 | if(itb->pc == afl_entry_point) { \ 89 | + fprintf(stderr, "STARTING FORK SERVER\n"); \ 90 | afl_setup(); \ 91 | afl_forkserver(cpu); \ 92 | } \ 93 | diff --git a/qemu_mode/qemu-2.10.0/linux-user/elfload.c b/qemu_mode/qemu-2.10.0/linux-user/elfload.c 94 | index 7906288..6d8ffc6 100644 95 | --- a/qemu_mode/qemu-2.10.0/linux-user/elfload.c 96 | +++ b/qemu_mode/qemu-2.10.0/linux-user/elfload.c 97 | @@ -2085,6 +2085,12 @@ static void load_elf_image(const char *image_name, int image_fd, 98 | info->brk = 0; 99 | info->elf_flags = ehdr->e_flags; 100 | 101 | + const char* entry = getenv("AFL_ENTRY_POINT"); 102 | + if (entry) { 103 | + afl_entry_point = strtol(entry, NULL, 16); 104 | + } 105 | + if (!afl_entry_point) afl_entry_point = info->entry; 106 | + 107 | for (i = 0; i < ehdr->e_phnum; i++) { 108 | struct elf_phdr *eppnt = phdr + i; 109 | if (eppnt->p_type == PT_LOAD) { 110 | -------------------------------------------------------------------------------- /bin/dedicated.orig.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niklasb/bspfuzz/74871e9782c4239c40dc64b6a81cc4a66c4c8297/bin/dedicated.orig.so -------------------------------------------------------------------------------- /bin/engine.orig.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niklasb/bspfuzz/74871e9782c4239c40dc64b6a81cc4a66c4c8297/bin/engine.orig.so -------------------------------------------------------------------------------- /bin/libtier0.orig.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niklasb/bspfuzz/74871e9782c4239c40dc64b6a81cc4a66c4c8297/bin/libtier0.orig.so -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | using namespace std; 8 | 9 | // dedicated 10 | void (*DedicatedMain)(int argc, const char** argv); 11 | 12 | // engine 13 | void (*CModelLoader_GetModelForName)(void*, const char* name, int referencetype); 14 | void** p_modelloader; 15 | 16 | template void ptr(T*& f, void* so, uint32_t offset) { 17 | f = (T*)((char*)so + offset); 18 | } 19 | 20 | void forkserver() { 21 | fprintf(stderr, "forkserver()\n"); 22 | } 23 | 24 | bool dbg; 25 | char *mappath; 26 | 27 | void startpoint() { 28 | fprintf(stderr, "startpoint()\n"); 29 | 30 | void* modelloader = *p_modelloader; 31 | cout << "modelloader @ " << modelloader << endl; 32 | 33 | forkserver(); 34 | 35 | if (dbg) { 36 | cerr << "Press enter to continue" << endl; 37 | getchar(); 38 | } 39 | 40 | void *buf = alloca(0x10000); 41 | 42 | CModelLoader_GetModelForName(modelloader, mappath, 2); 43 | cout << "Done" << endl; 44 | 45 | _exit(0); 46 | } 47 | 48 | int main(int argc, char** argv) { 49 | if (argc < 2) { 50 | cerr << "Usage: " << argv[0] << " bspfile [--dbg]" << endl; 51 | return EXIT_FAILURE; 52 | } 53 | 54 | dbg = argc > 2 && string(argv[2]) == "--dbg"; 55 | if (dbg) { 56 | cerr << "Debug mode enabled" << endl; 57 | } 58 | 59 | struct link_map *lm = (struct link_map*)dlopen("dedicated.so", RTLD_NOW); 60 | void* dedicated = (void*)lm->l_addr; 61 | assert(dedicated); 62 | lm = (struct link_map*)dlopen("engine.so", RTLD_NOW); 63 | void* engine = (void*)lm->l_addr; 64 | assert(engine); 65 | 66 | cout << "dedicated.so loaded at " << dedicated << endl; 67 | cout << "engine.so loaded at " << engine << endl; 68 | 69 | mappath = argv[1]; 70 | if (mappath[0] != '/') { 71 | char tmp[2048]; 72 | getcwd(tmp, sizeof tmp); 73 | strcat(tmp, "/"); 74 | strcat(tmp, mappath); 75 | mappath = strdup(tmp); 76 | } 77 | cout << "Reading from " << mappath << endl; 78 | 79 | // dedicated 80 | ptr(DedicatedMain, dedicated, 0x1beb0); 81 | 82 | // engine 83 | ptr(CModelLoader_GetModelForName, engine, 0x180460); 84 | ptr(p_modelloader, engine, 0x6E3C80); 85 | 86 | const char* args[] = {"x", "-game", "csgo", "-nominidumps", "-nobreakpad"}; 87 | DedicatedMain(sizeof args / sizeof *args, args); 88 | } 89 | -------------------------------------------------------------------------------- /mini_bsp/mini_bsp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | import sys 3 | from struct import * 4 | 5 | ''' 6 | struct lump_t 7 | { 8 | int fileofs; // offset into file (bytes) 9 | int filelen; // length of lump (bytes) 10 | int version; // lump format version 11 | char fourCC[4]; // lump ident code 12 | }; 13 | 14 | #define HEADER_LUMPS 64 15 | 16 | struct dheader_t 17 | { 18 | int ident; // BSP file identifier 19 | int version; // BSP file version 20 | lump_t lumps[HEADER_LUMPS]; // lump directory array 21 | int mapRevision; // the map's revision (iteration, version) number 22 | }; 23 | ''' 24 | 25 | def shorten(i, dat): 26 | if i in (40,53,8): 27 | return '' 28 | if i == 7: 29 | assert len(dat)%0x38 == 0 30 | return dat[:len(dat)//0x380*0x38] 31 | # if i == 29: 32 | # return dat[:len(dat)//0x10] 33 | if i == 13: 34 | assert len(dat)%4 == 0 35 | return dat[:len(dat)//0x40*0x4] 36 | if i == 10: 37 | assert len(dat)%0x20 == 0 38 | return dat[:len(dat)//0x200*0x20] 39 | if i == 3: 40 | assert len(dat)%0xc == 0 41 | return dat[:len(dat)//0xc0*0xc] 42 | if i == 5: 43 | assert len(dat)%0x20 == 0 44 | return dat[:len(dat)//0x200*0x20] 45 | return dat 46 | 47 | dat = bytearray(open(sys.argv[1]).read()) 48 | print len(dat) 49 | 50 | lumpdat = [None]*64 51 | for i in range(64): 52 | fileofs, filelen = unpack(" 64k edges 79 | short numedges; 80 | short texinfo; 81 | // This is a union under the assumption that a fog volume boundary (ie. water surface) 82 | // isn't a displacement map. 83 | // FIXME: These should be made a union with a flags or type field for which one it is 84 | // if we can add more to this. 85 | // union 86 | // { 87 | short dispinfo; 88 | // This is only for surfaces that are the boundaries of fog volumes 89 | // (ie. water surfaces) 90 | // All of the rest of the surfaces can look at their leaf to find out 91 | // what fog volume they are in. 92 | short surfaceFogVolumeID; 93 | // }; 94 | 95 | // lighting info 96 | byte styles[MAXLIGHTMAPS]; 97 | int lightofs; // start of [numstyles*surfsize] samples 98 | float area; 99 | 100 | // TODO: make these unsigned chars? 101 | int m_LightmapTextureMinsInLuxels[2]; 102 | int m_LightmapTextureSizeInLuxels[2]; 103 | 104 | int origFace; // reference the original face this face was derived from 105 | }; 106 | 107 | struct CDispVert 108 | { 109 | Vector m_vVector; // Vector field defining displacement volume. 110 | float m_flDist; // Displacement distances. 111 | float m_flAlpha; // "per vertex" alpha values. 112 | }; 113 | 114 | #define MAX_PATH 256 115 | 116 | struct CDispTri { 117 | unsigned short m_uiTags; // Displacement triangle tags. 118 | }; 119 | 120 | struct CMapLoadHelper { 121 | int m_nLumpSize; 122 | int m_nLumpOffset; 123 | int m_nLumpVersion; 124 | byte *m_pRawData; 125 | byte *m_pData; 126 | byte *m_pUncompressedData; 127 | 128 | // Handling for lump files 129 | int m_nLumpID; 130 | char m_szLumpFilename[MAX_PATH]; 131 | }; 132 | 133 | // sizeof(lump_t) == 16 134 | struct lump_t 135 | { 136 | int fileofs; // offset into file (bytes) 137 | int filelen; // length of lump (bytes) 138 | int version; // lump format version 139 | char fourCC[4]; // lump ident code 140 | }; 141 | 142 | #define HEADER_LUMPS 64 143 | 144 | struct dheader_t 145 | { 146 | int ident; // BSP file identifier 147 | int version; // BSP file version 148 | lump_t lumps[HEADER_LUMPS]; // lump directory array 149 | int mapRevision; // the map's revision (iteration, version) number 150 | }; 151 | 152 | struct CDispSubNeighbor 153 | { 154 | unsigned char m_NeighborOrientation; 155 | unsigned char m_Span; 156 | unsigned char m_NeighborSpan; 157 | }; 158 | 159 | class CDispNeighbor 160 | { 161 | CDispSubNeighbor m_SubNeighbors[2]; 162 | }; 163 | 164 | #define MAX_DISP_CORNER_NEIGHBORS 4 165 | 166 | class CDispCornerNeighbors 167 | { 168 | unsigned short m_Neighbors[MAX_DISP_CORNER_NEIGHBORS]; // indices of neighbors. 169 | unsigned char m_nNeighbors; 170 | }; 171 | 172 | #define MAX_DISPVERTS 289 173 | 174 | class ddispinfo_t 175 | { 176 | Vector startPosition; // start position used for orientation -- (added BSPVERSION 6) 177 | int m_iDispVertStart; // Index into LUMP_DISP_VERTS. 178 | int m_iDispTriStart; // Index into LUMP_DISP_TRIS. 179 | 180 | int power; // power - indicates size of map (2^power + 1) 181 | int minTess; // minimum tesselation allowed 182 | float smoothingAngle; // lighting smoothing angle 183 | int contents; // surface contents 184 | 185 | unsigned short m_iMapFace; // Which map face this displacement comes from. 186 | 187 | int m_iLightmapAlphaStart; // Index into ddisplightmapalpha. 188 | // The count is m_pParent->lightmapTextureSizeInLuxels[0]*m_pParent->lightmapTextureSizeInLuxels[1]. 189 | 190 | int m_iLightmapSamplePositionStart; // Index into LUMP_DISP_LIGHTMAP_SAMPLE_POSITIONS. 191 | 192 | CDispNeighbor m_EdgeNeighbors[4]; // Indexed by NEIGHBOREDGE_ defines. 193 | CDispCornerNeighbors m_CornerNeighbors[4]; // Indexed by CORNER_ defines. 194 | 195 | unsigned long m_AllowedVerts[16]; // This is built based on the layout and sizes of our neighbors 196 | // and tells us which vertices are allowed to be active. 197 | }; 198 | 199 | struct dedge_t 200 | { 201 | unsigned short v[2]; // vertex numbers 202 | }; 203 | 204 | struct dnode_t 205 | { 206 | int planenum; 207 | int children[2]; // negative numbers are -(leafs+1), not nodes 208 | short mins[3]; // for frustom culling 209 | short maxs[3]; 210 | unsigned short firstface; 211 | unsigned short numfaces; // counting both sides 212 | short area; // If all leaves below this node are in the same area, then 213 | // this is the area index. If not, this is -1. 214 | }; 215 | 216 | 217 | struct dvertex_t 218 | { 219 | Vector point; 220 | }; 221 | 222 | #define QUAD_POINT_COUNT 4 223 | #define NUM_BUMP_VECTS 3 224 | 225 | struct CCoreDispSurface { 226 | int m_Index; // parent face (CMapFace, dface_t, msurface_t) index "handle" 227 | 228 | int m_PointCount; // number of points in the face (should be 4!) 229 | Vector m_Points[QUAD_POINT_COUNT]; // points 230 | Vector m_Normals[QUAD_POINT_COUNT]; // normals at points 231 | Vector2D m_TexCoords[QUAD_POINT_COUNT]; // texture coordinates at points 232 | Vector2D m_LuxelCoords[NUM_BUMP_VECTS+1][QUAD_POINT_COUNT]; // lightmap coordinates at points 233 | float m_Alphas[QUAD_POINT_COUNT]; // alpha at points 234 | 235 | // Luxels sizes 236 | int m_nLuxelU; 237 | int m_nLuxelV; 238 | 239 | // Straight from the BSP file. 240 | CDispNeighbor m_EdgeNeighbors[4]; 241 | CDispCornerNeighbors m_CornerNeighbors[4]; 242 | 243 | int m_Flags; // surface flags - inherited from the "parent" face 244 | int m_Contents; // contents flags - inherited from the "parent" face 245 | 246 | Vector sAxis; // used to generate start disp orientation (old method) 247 | Vector tAxis; // used to generate start disp orientation (old method) 248 | int m_PointStartIndex; // index to the starting point -- for saving starting point 249 | Vector m_PointStart; // starting point used to determine the orientation of the displacement map on the surface 250 | }; 251 | 252 | /* 253 | struct CDispCollTree { 254 | Vector m_mins; 255 | int m_iCounter; 256 | Vector m_maxs; 257 | int m_nContents; 258 | 259 | //#ifdef ENGINE_DLL 260 | //memhandle_t m_hCache; 261 | //#endif 262 | 263 | char pad[40-32]; 264 | 265 | // offset 40 266 | int m_nPower; 267 | int m_nFlags; 268 | 269 | Vector m_vecSurfPoints[4]; // Base surface points. 270 | Vector m_vecStabDir; // Direction to stab for this displacement surface (is the base face normal) 271 | short m_nSurfaceProps[2]; // Surface properties (save off from texdata for impact responses) 272 | 273 | CDispVector m_aVerts; // Displacement verts. 274 | CDispVector m_aTris; // Displacement triangles. 275 | CDispVector m_nodes; // Nodes. 276 | CDispVector m_leaves; // Leaves. 277 | // Cache 278 | CUtlVector m_aTrisCache; 279 | CUtlVector m_aEdgePlanes; 280 | 281 | CDispCollHelper m_Helper; 282 | 283 | unsigned int m_nSize; 284 | }; 285 | */ 286 | 287 | #define MAX_NEIGHBOR_VERT_COUNT 8 288 | #define MAX_SURF_AT_NODE_COUNT 8 289 | 290 | struct cplane_t 291 | { 292 | Vector normal; 293 | float dist; 294 | byte type; // for fast side tests 295 | byte signbits; // signx + (signy<<1) + (signz<<1) 296 | byte pad[2]; 297 | }; 298 | 299 | struct CCoreDispNode { 300 | Vector m_BBox[2]; 301 | float m_ErrorTerm; 302 | int m_VertIndex; 303 | int m_NeighborVertIndices[MAX_NEIGHBOR_VERT_COUNT]; 304 | Vector m_SurfBBoxes[MAX_SURF_AT_NODE_COUNT][2]; 305 | cplane_t m_SurfPlanes[MAX_SURF_AT_NODE_COUNT]; 306 | Vector m_RayBBoxes[4][2]; 307 | }; 308 | 309 | struct CCoreDispInfo { 310 | int unknown1; 311 | CCoreDispNode *m_Nodes; // LOD quad-tree nodes 312 | float m_Elevation; // distance along the subdivision normal (should 313 | 314 | // defines the size of the displacement surface 315 | int m_Power; // "size" of the displacement map 316 | 317 | // base surface data 318 | CCoreDispSurface m_Surf; // surface containing displacement data 319 | // be changed to match the paint normal next pass) 320 | // Vertex data.. 321 | void *m_pVerts; 322 | 323 | // Triangle data.. 324 | void *m_pTris; 325 | 326 | // render specific data 327 | int m_RenderIndexCount; // number of indices used in rendering 328 | unsigned short *m_RenderIndices; // rendering index list (list of triangles) 329 | int m_RenderCounter; // counter to verify surfaces are renderered/collided with only once per frame 330 | 331 | // utility data 332 | bool m_bTouched; // touched flag 333 | void *m_pNext; // used for chaining 334 | 335 | // The list that this disp is in (used for CDispUtils::IHelper implementation). 336 | void **m_ppListBase; 337 | int m_ListSize; 338 | 339 | int m_AllowedVerts_bitvec[10]; // Built in VBSP. Defines which verts are allowed to exist based on what the neighbors are. 340 | 341 | int m_nListIndex; 342 | }; 343 | 344 | struct VectorAligned { 345 | vec_t x, y, z, w; 346 | }; 347 | 348 | struct mnode_t 349 | { 350 | // common with leaf 351 | int contents; // <0 to differentiate from leafs 352 | // -1 means check the node for visibility 353 | // -2 means don't check the node for visibility 354 | 355 | int visframe; // node needs to be traversed if current 356 | 357 | mnode_t *parent; 358 | short area; // If all leaves below this node are in the same area, then 359 | // this is the area index. If not, this is -1. 360 | short flags; 361 | 362 | VectorAligned m_vecCenter; 363 | VectorAligned m_vecHalfDiagonal; 364 | 365 | // node specific 366 | cplane_t *plane; 367 | mnode_t *children[2]; 368 | 369 | unsigned short firstsurface; 370 | unsigned short numsurfaces; 371 | }; 372 | 373 | 374 | struct worldbrushdata_t 375 | { 376 | int x; 377 | int numsubmodels; 378 | 379 | int numplanes; 380 | cplane_t *planes; 381 | 382 | int numleafs; // number of visible leafs, not counting 0 383 | void *leafs; 384 | 385 | int numleafwaterdata; 386 | void *leafwaterdata; 387 | 388 | int numvertexes; 389 | void *vertexes; 390 | 391 | int numoccluders; 392 | void *occluders; 393 | 394 | int numoccluderpolys; 395 | void *occluderpolys; 396 | 397 | int numoccludervertindices; 398 | int *occludervertindices; 399 | 400 | int numvertnormalindices; // These index vertnormals. 401 | unsigned short *vertnormalindices; 402 | 403 | int numvertnormals; 404 | Vector *vertnormals; 405 | 406 | int numnodes; 407 | mnode_t *nodes; 408 | unsigned short *m_LeafMinDistToWater; 409 | 410 | int numtexinfo; 411 | void *texinfo; 412 | 413 | int numtexdata; 414 | void *texdata; 415 | 416 | int numDispInfos; 417 | int hDispInfos; // Use DispInfo_Index to get IDispInfos.. 418 | 419 | /* 420 | int numOrigSurfaces; 421 | msurface_t *pOrigSurfaces; 422 | */ 423 | 424 | int numsurfaces; 425 | void *surfaces1; 426 | void *surfaces2; 427 | void *surfacelighting; 428 | void *surfacenormals; 429 | 430 | bool unloadedlightmaps; 431 | 432 | int numvertindices; 433 | unsigned short *vertindices; 434 | 435 | int nummarksurfaces; 436 | void *marksurfaces; 437 | 438 | void *lightdata; 439 | 440 | int numworldlights; 441 | void *worldlights; 442 | 443 | void *shadowzbuffers; 444 | 445 | // non-polygon primitives (strips and lists) 446 | int numprimitives; 447 | void *primitives; 448 | 449 | int numprimverts; 450 | void *primverts; 451 | 452 | int numprimindices; 453 | unsigned short *primindices; 454 | 455 | int m_nAreas; 456 | void *m_pAreas; 457 | 458 | int m_nAreaPortals; 459 | dareaportal_t *m_pAreaPortals; 460 | 461 | int m_nClipPortalVerts; 462 | Vector *m_pClipPortalVerts; 463 | 464 | void *m_pCubemapSamples; 465 | int m_nCubemapSamples; 466 | 467 | int m_nDispInfoReferences; 468 | unsigned short *m_pDispInfoReferences; 469 | 470 | void *m_pLeafAmbient; 471 | void *m_pAmbientSamples; 472 | }; 473 | 474 | struct cnode_t 475 | { 476 | cplane_t *plane; 477 | int children[2]; 478 | }; 479 | 480 | enum lumptype { 481 | LUMP_ENTITIES = 0, 482 | LUMP_PLANES = 1, 483 | LUMP_TEXDATA = 2, 484 | LUMP_VERTEXES = 3, 485 | LUMP_VISIBILITY = 4, 486 | LUMP_NODES = 5, 487 | LUMP_TEXINFO = 6, 488 | LUMP_FACES = 7, 489 | LUMP_LIGHTING = 8, 490 | LUMP_OCCLUSION = 9, 491 | LUMP_LEAFS = 10, 492 | LUMP_FACEIDS = 11, 493 | LUMP_EDGES = 12, 494 | LUMP_SURFEDGES = 13, 495 | LUMP_MODELS = 14, 496 | LUMP_WORLDLIGHTS = 15, 497 | LUMP_LEAFFACES = 16, 498 | LUMP_LEAFBRUSHES = 17, 499 | LUMP_BRUSHES = 18, 500 | LUMP_BRUSHSIDES = 19, 501 | LUMP_AREAS = 20, 502 | LUMP_AREAPORTALS = 21, 503 | LUMP_PROPCOLLISION_NEW = 22, 504 | LUMP_PROPHULLS_NEW = 23, 505 | LUMP_PROPHULLVERTS_NEW = 24, 506 | LUMP_PROPTRIS_NEW = 25, 507 | LUMP_DISPINFO = 26, 508 | LUMP_ORIGINALFACES = 27, 509 | LUMP_PHYSDISP = 28, 510 | LUMP_PHYSCOLLIDE = 29, 511 | LUMP_VERTNORMALS = 30, 512 | LUMP_VERTNORMALINDICES = 31, 513 | LUMP_DISP_LIGHTMAP_ALPHAS = 32, 514 | LUMP_DISP_VERTS = 33, 515 | LUMP_DISP_LIGHTMAP_SAMPLE_POSITIONS = 34, 516 | LUMP_GAME_LUMP = 35, 517 | LUMP_LEAFWATERDATA = 36, 518 | LUMP_PRIMITIVES = 37, 519 | LUMP_PRIMVERTS = 38, 520 | LUMP_PRIMINDICES = 39, 521 | LUMP_PAKFILE = 40, 522 | LUMP_CLIPPORTALVERTS = 41, 523 | LUMP_CUBEMAPS = 42, 524 | LUMP_TEXDATA_STRING_DATA = 43, 525 | LUMP_TEXDATA_STRING_TABLE = 44, 526 | LUMP_OVERLAYS = 45, 527 | LUMP_LEAFMINDISTTOWATER = 46, 528 | LUMP_FACE_MACRO_TEXTURE_INFO = 47, 529 | LUMP_DISP_TRIS = 48, 530 | LUMP_PROP_BLOB_NEW = 49, 531 | LUMP_WATEROVERLAYS = 50, 532 | LUMP_LEAF_AMBIENT_INDEX_HDR = 51, 533 | LUMP_LEAF_AMBIENT_INDEX = 52, 534 | LUMP_LIGHTING_HDR = 53, 535 | LUMP_WORLDLIGHTS_HDR = 54, 536 | LUMP_LEAF_AMBIENT_LIGHTING_HDR = 55, 537 | LUMP_LEAF_AMBIENT_LIGHTING = 56, 538 | LUMP_XZIPPAKFILE = 57, 539 | LUMP_FACES_HDR = 58, 540 | LUMP_MAP_FLAGS = 59, 541 | LUMP_OVERLAY_FADES = 60, 542 | LUMP_OVERLAY_SYSTEM_LEVELS_NEW = 61, 543 | LUMP_PHYSLEVEL_NEW = 62, 544 | LUMP_DISP_MULTIBLEND_NEW = 63, 545 | }; 546 | 547 | struct phyheader_t 548 | { 549 | int size; // Size of this header section (generally 16) 550 | int id; // Often zero, unknown purpose. 551 | int solidCount; // Number of solids in file 552 | long checkSum; // checksum of source .mdl file (4-bytes) 553 | }; 554 | 555 | struct compactsurfaceheader_t 556 | { 557 | int size; // Size of the content after this byte 558 | int vphysicsID; // Generally the ASCII for "VPHY" in newer files 559 | short version; 560 | short modelType; 561 | int surfaceSize; 562 | Vector dragAxisAreas; 563 | int axisMapSize; 564 | }; 565 | 566 | // old style phy format 567 | struct legacysurfaceheader_t 568 | { 569 | int size; 570 | float mass_center[3]; 571 | float rotation_inertia[3]; 572 | float upper_limit_radius; 573 | int max_deviation : 8; 574 | int byte_size : 24; 575 | int offset_ledgetree_root; 576 | int dummy[3]; // dummy[2] is "IVPS" or 0 577 | }; 578 | -------------------------------------------------------------------------------- /run_afl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [[ $# < 1 ]] ; then 3 | echo >&2 "Usage: $0 " 4 | exit 1 5 | fi 6 | 7 | NUM=$1 8 | export AFL_ENTRY_POINT=$(nm bspfuzz |& grep forkserver | cut -d' ' -f1) 9 | export AFL_INST_LIBS=1 10 | echo "Entry point: $AFL_ENTRY_POINT" 11 | if [[ $NUM == 1 ]]; then 12 | echo Running master 13 | screen -dmS afl$NUM $AFL_PATH/afl-fuzz -m 2048 -Q -i fuzz/in -o fuzz/out -M fuzzer$NUM -- ./bspfuzz @@ 14 | elif [[ $NUM < 9 ]]; then 15 | echo Running slave 16 | screen -dmS afl$NUM $AFL_PATH/afl-fuzz -m 2048 -Q -i fuzz/in -o fuzz/out -S fuzzer$NUM -- ./bspfuzz @@ 17 | else 18 | echo 'Running slave (no affinity)' 19 | export AFL_NO_AFFINITY=1 20 | screen -dmS afl$NUM $AFL_PATH/afl-fuzz -m 2048 -Q -i fuzz/in -o fuzz/out -S fuzzer$NUM -- ./bspfuzz @@ 21 | fi 22 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | cp bin/dedicated.{,orig.}so 4 | cp bin/engine.{,orig.}so 5 | cp bin/libtier0.{,orig.}so 6 | make 7 | make patch 8 | mkdir -p fuzz/{in,out} 9 | cp mini_bsp/test_mini.bsp fuzz/in 10 | -------------------------------------------------------------------------------- /triage/.gitignore: -------------------------------------------------------------------------------- 1 | /crashes/* 2 | !/crashes/.gitkeep 3 | /bugid/* 4 | !/bugid/.gitkeep 5 | /valgrind/* 6 | !/valgrind/.gitkeep 7 | __pycache__ 8 | *.pyc 9 | -------------------------------------------------------------------------------- /triage/bugid/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niklasb/bspfuzz/74871e9782c4239c40dc64b6a81cc4a66c4c8297/triage/bugid/.gitkeep -------------------------------------------------------------------------------- /triage/crashes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niklasb/bspfuzz/74871e9782c4239c40dc64b6a81cc4a66c4c8297/triage/crashes/.gitkeep -------------------------------------------------------------------------------- /triage/gdb.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | import sys 4 | import select 5 | import atexit 6 | import time 7 | import threading 8 | from subprocess import Popen, PIPE 9 | from collections import defaultdict 10 | 11 | class Gdb(object): 12 | def __init__(self, binary, timeout=10): 13 | self.binary = binary 14 | self.timeout = timeout 15 | self.setup() 16 | atexit.register(self.kill) 17 | 18 | def setup(self): 19 | self.p = Popen(['gdb', self.binary], stdin=PIPE, stdout=PIPE, 20 | stderr=PIPE, preexec_fn=os.setsid) 21 | self.pid = self.p.pid 22 | self.fdout = self.p.stdout.fileno() 23 | self.fderr = self.p.stderr.fileno() 24 | self.cmd('set confirm off') 25 | self.cmd('set width unlimited') 26 | 27 | def kill(self): 28 | try: 29 | # print('Killing %d' % os.getpgid(self.pid)) 30 | sys.stdout.flush() 31 | os.killpg(os.getpgid(self.pid), signal.SIGKILL) 32 | except ProcessLookupError: 33 | pass 34 | 35 | def reset(self): 36 | self.kill() 37 | self.setup() 38 | 39 | def prompt(self): 40 | self.p.stdin.write(b'python __import__("sys").stdout.write("__MARKER__\\n");__import__("sys").stdout.flush()\n') 41 | self.p.stdin.write(b'python __import__("sys").stderr.write("__MARKER__\\n");__import__("sys").stderr.flush()\n') 42 | self.p.stdin.flush() 43 | out = b'' 44 | err = b'' 45 | t0 = time.time() 46 | while not b'__MARKER__' in out or not b'__MARKER__' in err: 47 | if time.time() > t0 + self.timeout: 48 | raise TimeoutError() 49 | ready,_,_ = select.select([self.fdout, self.fderr], [], [], self.timeout) 50 | if not ready: 51 | raise TimeoutError() 52 | if self.fdout in ready: 53 | c = os.read(self.fdout, 1024) 54 | assert c 55 | out += c 56 | if self.fderr in ready: 57 | c = os.read(self.fderr, 1024) 58 | assert c 59 | err += c 60 | self.p.stdin.write(b'python __import__("sys").stdout.write("__MARKER2__\\n");__import__("sys").stdout.flush()\n') 61 | self.p.stdin.flush() 62 | while not b'__MARKER2__' in out: 63 | c = os.read(self.fdout, 1024) 64 | out += c 65 | while not out.endswith(b'(gdb) '): 66 | c = os.read(self.fdout, 1024) 67 | out += c 68 | out = out.rsplit(b'\n(gdb) __MARKER__', 1)[0] 69 | err = err.rsplit(b'__MARKER__', 1)[0] 70 | return out, err 71 | 72 | def cmd(self, cmd): 73 | self.p.stdin.write(cmd.encode('utf-8') + b'\n') 74 | self.p.stdin.flush() 75 | return self.prompt() 76 | 77 | 78 | class GdbPool(object): 79 | def __init__(self, timeout): 80 | self.idle = defaultdict(list) 81 | self.timeout = timeout 82 | self.mx = threading.Lock() 83 | 84 | def clean(self): 85 | cleaned = defaultdict(list) 86 | for k, v in self.idle.items(): 87 | for since, gdb in v: 88 | if since + 120 >= time.time(): 89 | cleaned[k].append((since, gdb)) 90 | else: 91 | gdb.kill() 92 | self.idle = cleaned 93 | 94 | def get(self, binary): 95 | assert os.path.exists(binary) 96 | with self.mx: 97 | available = self.idle[binary] 98 | if not available: 99 | return Gdb(binary, timeout=self.timeout) 100 | res = available.pop()[1] 101 | self.clean() 102 | return res 103 | 104 | def put(self, gdb): 105 | with self.mx: 106 | self.clean() 107 | self.idle[gdb.binary].append((time.time(), gdb)) 108 | 109 | 110 | class FromGdbPool(object): 111 | def __init__(self, pool, binary): 112 | self.pool = pool 113 | self.binary = binary 114 | 115 | def __enter__(self): 116 | self.instance = self.pool.get(self.binary) 117 | return self.instance 118 | 119 | def __exit__(self, exc_type, exc_val, exc_tb): 120 | self.pool.put(self.instance) 121 | -------------------------------------------------------------------------------- /triage/triage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import gdb 4 | import glob 5 | import hashlib 6 | import os 7 | import random 8 | import shutil 9 | import sys 10 | import tempfile 11 | from subprocess import Popen, check_output 12 | 13 | ROOTDIR = os.path.dirname(os.path.abspath(__file__)) 14 | FUZZDIR = ROOTDIR + '/..' 15 | FUZZBIN = FUZZDIR + '/bspfuzz' 16 | 17 | def bugid(crash): 18 | ''' tries to create a hash describing the crash ''' 19 | res = hashlib.md5('\n'.join(crash['backtrace'][:5]).encode('utf-8')).hexdigest()[:8] 20 | if crash['heuristic'] != 'unknown': 21 | res += '_' + crash['heuristic'] 22 | for x in crash['backtrace']: 23 | if '.so' in x: 24 | res += '_' + x 25 | return res 26 | return res 27 | 28 | def process_bt(bt): 29 | better_bt = [] 30 | modcache = {} 31 | for x in bt: 32 | addr = x.split()[1] 33 | if '0x' in addr: 34 | addr = int(x.split()[1], 16) 35 | if ' from ' in x: 36 | mod = x.split(' from ')[1].split()[0] 37 | mod = mod.split('/')[-1] # basename 38 | if mod not in modcache: 39 | try: 40 | out, err = g.cmd('print $base("%s")' % mod) 41 | except: 42 | raise Exception("You need the base() function from https://github.com/niklasb/gdbinit") 43 | if b'No entry found' not in err: 44 | base = (out.decode('utf-8') 45 | .strip().split(' = ')[1]) 46 | if '0x' in base: 47 | base = int(base, 16) 48 | else: 49 | base = int(base, 10) 50 | modcache[mod] = base 51 | if mod in modcache: 52 | addr = '%s+0x%x' % (mod.split('/')[-1], addr-modcache[mod]) 53 | else: 54 | addr = '0x%08x' % addr 55 | else: 56 | addr = '0x%08x' % addr 57 | better_bt.append(addr) 58 | return better_bt 59 | 60 | def evaluate_impl(bspfile): 61 | target = tmpdir + '/test.bsp' 62 | shutil.copy(bspfile, target) 63 | res,_ = g.cmd("run '" + target + "'") 64 | if b'SIGSEGV' not in res: 65 | return None 66 | 67 | fault,_ = g.cmd("print $_siginfo._sifields._sigfault.si_addr") 68 | fault = 'segfault @ ' + (fault.decode('utf-8').split('=', 1)[1] 69 | .replace('(void *)', '').replace('(void*)', '').strip()) 70 | assert '0x' in fault 71 | fault_addr = int(fault.split(' @ ')[1].strip(), 16) 72 | 73 | eip = int(g.cmd('info register eip')[0].decode('utf-8').split()[1], 16) 74 | bt = g.cmd('backtrace')[0].decode('utf-8').splitlines() 75 | bt = process_bt(bt) 76 | 77 | ctx = (g.cmd('x/3i $eip')[0].decode('utf-8') + '\n' 78 | + g.cmd('info registers')[0].decode('utf-8') + '\n') 79 | ins = ctx.splitlines()[0] 80 | 81 | heuristic = 'unknown' 82 | if fault_addr < 0x100000: 83 | heuristic = 'nullptr' 84 | elif ']' in ins and ',' in ins.split(']')[1]: 85 | heuristic = 'write' 86 | elif '[' in ins and ',' in ins.split('[')[0]: 87 | heuristic = 'read' 88 | return {"fault": fault, 'backtrace': bt, 'eip': eip, 'context': ctx, 89 | 'heuristic': heuristic} 90 | 91 | def evaluate(f): 92 | try: 93 | return evaluate_impl(f) 94 | except TimeoutError: 95 | print(' Timeout!') 96 | g.reset() 97 | return None 98 | 99 | files = [] 100 | for f in sys.argv[1:]: 101 | if os.path.isfile(f): 102 | files.append(os.path.abspath(f)) 103 | elif os.path.isdir(f): 104 | files += map(os.path.abspath, glob.glob(f + '/*')) 105 | 106 | print("Found %d files" % len(files)) 107 | 108 | tmpdir = tempfile.mkdtemp() 109 | def cleanup(): 110 | shutil.rmtree(tmpdir) 111 | 112 | os.chdir(FUZZDIR) 113 | 114 | g = gdb.Gdb(FUZZBIN, timeout=10) 115 | g.setup() 116 | 117 | todo = [] 118 | for f in files: 119 | h = check_output(['sha1sum', f]).split(b' ')[0].decode('utf-8') 120 | crashname = ROOTDIR + '/crashes/' + h + '.bsp' 121 | if not os.path.exists(crashname): 122 | todo.append((f, h)) 123 | print("TODO %d files" % len(todo)) 124 | 125 | random.shuffle(todo) 126 | 127 | cnt = 0 128 | for fname, h in todo: 129 | cnt += 1 130 | crashname = ROOTDIR + '/crashes/' + h + '.bsp' 131 | if os.path.exists(crashname): 132 | print(' Skipped') 133 | continue 134 | 135 | print(" [%d/%d] Processing %s / %s" % (cnt, len(todo), fname, h)) 136 | for _ in range(1): 137 | crash = evaluate(fname) 138 | if crash is not None: 139 | break 140 | print(' retrying...') 141 | 142 | if not crash: 143 | print(' => Could not reproduce!') 144 | continue 145 | 146 | id = bugid(crash) 147 | 148 | odir = ROOTDIR + '/bugid/' + id 149 | try: 150 | os.makedirs(odir) 151 | print(' => new bug id: %s' % id) 152 | except FileExistsError: 153 | pass 154 | 155 | with open(odir + '/' + h + '.gdb.txt', 'w') as f: 156 | f.write(crash['context']) 157 | 158 | if os.path.exists(crashname): 159 | print(' Skipped') 160 | continue 161 | os.symlink(fname, crashname) 162 | -------------------------------------------------------------------------------- /triage/triage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname "$0")" 3 | ./triage.py ../fuzz/out*/fuzzer*/crashes 4 | -------------------------------------------------------------------------------- /triage/valgrind.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | cd "$(dirname "$0")" 4 | ROOT="$(pwd)" 5 | FUZZDIR="$ROOT/.." 6 | FUZZBIN=./bspfuzz 7 | 8 | for f in bugid/*; do 9 | bugid="$(basename "$f")" 10 | for hash in $(ls "$f" | head -n2); do 11 | hash="${hash/.gdb.txt/}" 12 | in="crashes/$hash.bsp" 13 | out="valgrind/${bugid}_${hash}.txt" 14 | if [ ! -e "$out" ]; then 15 | echo 16 | echo 17 | echo 18 | echo "$in => $out" 19 | echo 20 | echo 21 | echo 22 | pushd "$FUZZDIR" &>/dev/null 23 | tmpfile="/tmp/valgrind_$$.txt" 24 | valgrind --undef-value-errors=no "$FUZZBIN" "$ROOT/$in" |& tee "$tmpfile" 25 | popd &>/dev/null 26 | mv "$tmpfile" "$ROOT/$out" 27 | fi 28 | done 29 | done 30 | -------------------------------------------------------------------------------- /triage/valgrind/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niklasb/bspfuzz/74871e9782c4239c40dc64b6a81cc4a66c4c8297/triage/valgrind/.gitkeep --------------------------------------------------------------------------------