Rescuing a broken NTFS filesystem

Yesterday, early morning, I was working on a friend’s Windows PC. The plan was simple: migrate the Windows install from an HDD partition to a smaller SSD by freeing up space, running ntfsresize to shrink the partition down to something that fits on the SSD, and then dd-ing it over. I figured ntfsresize would be pretty safe and well-tested, since it’s used by pretty much every Linux distro installer to resize Windows partitions.

Everything was going according to plan, there was enough free space, and ntfsresize did its thing with no error messages. The next step after a resize is always to run chkdsk from Windows, but I was curious and tried ntfsfix in readonly mode just to see what it found. It complained loudly, which I figured was odd, but perhaps expected. I dd-ed the partition over to the SSD, then went to the Windows installer environment and ran chkdsk on it.

Hundreds of thousands of error messages later, most of the contents of the partition were gone, including all of C:\Windows and C:\Users. Crap.

It’s worth noting that I don’t personally keep any valuable data on NTFS. I also keep backups. So, had this happened to me, restoring from backup would’ve been the resonable option. However, this was a PC I was working on for a friend, and there was ample storage available (on another drive) to have performed a full file-based backup beforehand, yet I did not do so, so I considered myself at fault. While most of the critical data is backed up to cloud-based services, a lot of misc bits and pieces would’ve been lost, plus a Windows reinstall would’ve required a very lengthy process of reinstalling every app again.

Now, the good thing is that I still had the original HDD partition around, untouched after ntfsresize did its thing, before chkdsk. And, unlike the usual use case of installing Linux, the tail end of the partition hadn’t been overwritten with anything else, since the goal wasn’t to resize the source partition but rather to copy to a smaller one on the target SSD. My intuition told me that a bug in ntfsresize had caused something to go horribly wrong, but that there was a good chance that the corruption could be mostly (perhaps even completely) fixed, provided I had a good understanding of exactly what had happened. And since ntfsresize’s job is to move data to empty space, anything it had touched should still be in the original location too, except for metadata changes that it had to make.

I spent most of the next day learning more about NTFS than I ever wanted to.

Step one, of course, is to make an image of the victim partition. Or three, just to be sure. I actually dd-ed the entire HDD over to a portable USB3 HDD too, including an unrelated sibling partition, just so I had a pristine dump including partition layout.

Image in hand, let’s look more closely at what ntfsfix is complaining about.

$ ntfsfix -n fail.img
Mounting volume... Failed to load runlist for $MFT/$DATA.
highest_vcn = 0xbb7e, last_vcn - 1 = 0x54bbf
Failed to load $MFT: Input/output error
FAILED
Attempting to correct errors... Failed to load runlist for $MFT/$DATA.
highest_vcn = 0xbb7e, last_vcn - 1 = 0x54bbf
Failed to load $MFT: Input/output error
FAILED
Failed to startup volume: Input/output error
Checking for self-located MFT segment... OK
The startup data can be fixed, but no change was requested
Volume is corrupt. You should run chkdsk.
No change made

That looks pretty bad. $MFT is the Master File Table, the inode table in NTFS-speak. It seems it is unloadable or corrupted. In particular, its ‘runlist’, which is NTFS-speak for extent map, has an issue.

NTFS has a fairly uniform design where everything in the entire partition (except the backup bootsector, for some reason) is pointed to by the MFT, including the MFT itself. The boot sector (sector 0 of the partition) points at the beginning of the MFT, and then the MFT points at everything else. This also means that the MFT has to contain the list of extents of the MFT itself, which poses some chicken-and-egg issues.

Building a debug version of ntfsprogs and running ntfsfix provides more info:

$ ~/software/ntfs-3g_ntfsprogs-2015.3.14/ntfsprogs/ntfsfix -n fail.img
Mounting volume... ntfs_pread(): pos 0, count 512
<.. snip ..>
ntfs_mapping_pairs_decompress_i(): Entering for attr 0x80.
More extents to follow; deltaxcn = 0xbb7e, max_cluster = 0x54bbf
Mapping pairs array successfully decompressed:
NTFS-fs DEBUG: Dumping runlist (values in hex):
VCN              LCN               Run length
0                786432            26096           
26096            9365576           300             
26396            11638698          300             
26696            14869609          300             
26996            21033733          300             
27296            2498893           300             
27596            3716714           300             
27896            5929635           300             
<.. snip 60 more lines like this ..>
45953            3891743           293             
46246            4681687           293             
46539            5107130           292             
46831            10097059          292             
47123            11702900          292             
47415            12898945          292             
47707            18907990          292             
47999            LCN_RL_NOT_MAPPED 299073          
347072           LCN_ENOENT        0                (runlist end)
highest_vcn=0xc0000770000007c
next_vcn=0xbb7f
ntfs_attr_lookup(): Entering for attribute type 0x80
ntfs_external_attr_find(): Entering for inode 0, attribute type 0x80.
ntfs_attr_find(): attribute type 0x80.
ntfs_attr_find(): attribute type 0x80.
Failed to load runlist for $MFT/$DATA.
highest_vcn = 0xbb7e, last_vcn - 1 = 0x54bbf
ntfs_inode_real_close(): Entering for inode 0
Failed to load $MFT: Input/output error
<.. snip ..>

The MFT runlist is stored in the first entry of the MFT, and lists all the extents that make up the MFT. Normally, as far as I know, the MFT is in just one big chunk, perhaps a few if it grows too large (incidentally, I knew that this partition had a problem with a million or so files of junk in C:\Windows\Temp that I was hoping to get rid of after the transfer, once I was on the SSD and things were faster; this partition is also from an install that has been upgraded and is quite a few years old, so this might explain a larger than average MFT). Here, though, it seems to be broken up into 80-odd tiny bits and pieces after one big chunk at the beginning. Worse, only 47999 clusters of the MFT are accounted for, out of 347072 expected ones. Most of the MFT is gone. Ouch.

Let’s look at the beginning of the MFT in more detail. It’s stored at logical cluster 786432 (that’s 0xc0000; the “values in hex” bit is a lie, but I prefer to work in hex), and clusters are 4K in this filesystem:

$ dd if=fail.img bs=4096 count=1 skip=$((0xc0000)) of=mft.bin
$ hexdump -vC mft.bin

MFT entries are 1024 bytes each, and the first one is $MFT itself. Here’s what it looks like:

00000000  46 49 4c 45 30 00 03 00  85 40 4f 0e 71 00 00 00  |FILE0....@O.q...|
00000010  01 00 01 00 38 00 01 00  00 04 00 00 00 04 00 00  |....8...........|
00000020  00 00 00 00 00 00 00 00  10 00 00 00 00 00 00 00  |................|
00000030  50 f0 32 2c 00 00 00 00  10 00 00 00 60 00 00 00  |P.2,........`...|
00000040  00 00 18 00 00 00 00 00  48 00 00 00 18 00 00 00  |........H.......|
00000050  1c 7d 64 0d 1b a1 c8 01  1c 7d 64 0d 1b a1 c8 01  |.}d......}d.....|
00000060  1c 7d 64 0d 1b a1 c8 01  1c 7d 64 0d 1b a1 c8 01  |.}d......}d.....|
00000070  06 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000080  00 00 00 00 00 01 00 00  00 00 00 00 00 00 00 00  |................|
00000090  00 00 00 00 00 00 00 00  20 00 00 00 98 00 00 00  |........ .......|
000000a0  00 00 00 00 00 00 0f 00  80 00 00 00 18 00 00 00  |................|
000000b0  10 00 00 00 20 00 00 1a  00 00 00 00 00 00 00 00  |.... ...........|
000000c0  00 00 00 00 00 00 01 00  00 00 00 00 00 00 00 00  |................|
000000d0  30 00 00 00 20 00 00 1a  00 00 00 00 00 00 00 00  |0... ...........|
000000e0  00 00 00 00 00 00 01 00  03 00 00 00 00 00 00 00  |................|
000000f0  80 00 00 00 20 00 00 1a  00 00 00 00 00 00 00 00  |.... ...........|
00000100  00 00 00 00 00 00 01 00  01 00 00 00 00 00 00 00  |................|
00000110  b0 00 00 00 20 00 00 1a  00 00 00 00 00 00 00 00  |.... ...........|
00000120  00 00 00 00 00 00 01 00  0e 00 00 00 00 00 00 00  |................|
00000130  30 00 00 00 68 00 00 00  00 00 18 00 00 00 03 00  |0...h...........|
00000140  4a 00 00 00 18 00 01 00  05 00 00 00 00 00 05 00  |J...............|
00000150  1c 7d 64 0d 1b a1 c8 01  1c 7d 64 0d 1b a1 c8 01  |.}d......}d.....|
00000160  1c 7d 64 0d 1b a1 c8 01  1c 7d 64 0d 1b a1 c8 01  |.}d......}d.....|
00000170  00 00 bc 54 00 00 00 00  00 00 6c 52 00 00 00 00  |...T......lR....|
00000180  06 00 00 00 00 00 00 00  04 03 24 00 4d 00 46 00  |..........$.M.F.|
00000190  54 00 00 00 00 00 00 00  80 00 00 00 10 02 00 00  |T...............|
000001a0  01 00 40 00 00 00 01 00  00 00 00 00 00 00 00 00  |..@.............|
000001b0  7e bb 00 00 00 00 00 00  40 00 00 00 00 00 00 00  |.K......@.......|
000001c0  00 00 bc 54 00 00 00 00  00 00 bc 54 00 00 00 00  |...T.......T....|
000001d0  00 00 bc 54 00 00 00 00  32 f0 65 00 00 0c 42 2c  |...T....2.e...C.|
000001e0  01 48 e8 82 00 32 2c 01  62 af 22 32 2c 01 bf 4c  |........b."2,..L|
000001f0  31 32 2c 01 9c 0e 5e 42  2c 01 48 2e e5 fe 50 f0  |12,...^B,.H...P.|
00000200  01 1d 95 12 32 2c 01 39  c4 21 32 2c 01 a9 22 08  |....2,.9.!2,..".|
00000210  32 2b 01 66 70 13 32 2b  01 fa ed 39 32 2b 01 e3  |2+.fp.2+...92+..|
00000220  c5 14 32 2b 01 78 9f 70  42 2b 01 3f 31 f0 fe 32  |..2+.x.pB+.?1..2|
00000230  2b 01 e2 24 1e 32 2b 01  fc 39 21 32 2b 01 98 28  |+..$.2+..9!2+..(|
00000240  4f 32 2a 01 07 4b 14 32  2a 01 c1 22 0a 32 2a 01  |O2*..K.2*..".2*.|
00000250  4f d8 45 32 2a 01 7d f1  28 42 2a 01 ea 59 ff fe  |O.E2*.}.(B*..Y..|
00000260  32 2a 01 8d e0 06 32 2a  01 84 80 65 32 2a 01 49  |2*....2*...e2*.I|
00000270  ce 29 42 29 01 d4 a1 62  ff 32 29 01 89 bb 0c 32  |.)B)...b.2)....2|
00000280  29 01 ed ce 04 32 29 01  62 5f 21 32 29 01 17 07  |)....2).b_!2)...|
00000290  09 32 29 01 d1 22 55 32  29 01 23 ec 04 32 29 01  |.2).."U2).#..2).|
000002a0  c4 33 05 42 29 01 f2 ba  71 ff 32 28 01 97 7c 08  |.3.B)...q.2(..|.|
000002b0  32 28 01 8a 6f 15 32 28  01 e3 99 25 32 28 01 87  |2(..o.2(...%2(..|
000002c0  5f 26 32 28 01 1b 85 0e  32 28 01 b5 49 3f 42 28  |_&2(....2(..I?B(|
000002d0  01 f9 aa 3a ff 32 27 01  c4 ee 0c 32 27 01 b9 6b  |...:.2'....2'..k|
000002e0  15 32 27 01 97 6e 10 32  27 01 fc 74 16 32 27 01  |.2'..n.2'..t.2'.|
000002f0  47 24 3e 42 27 01 d0 18  8a 00 42 27 01 4c bd fb  |G$>B'.....B'.L..|
00000300  fe 32 27 01 a1 30 15 32  26 01 d0 43 0c 32 26 01  |.2'..0.2&..C.2&.|
00000310  44 a7 0b 32 26 01 9d 27  0f 32 26 01 9e 83 0f 32  |D..2&..'.2&....2|
00000320  26 01 fe da 24 32 26 01  d0 7a 84 32 26 01 72 db  |&...$2&..z.2&.r.|
00000330  04 32 26 01 30 a9 13 32  26 01 fb 43 17 32 25 01  |.2&.0..2&..C.2%.|
00000340  12 97 37 32 25 01 2d 29  2a 32 25 01 55 3c 4d 32  |..72%.-)*2%.U<M2|
00000350  25 01 9f 68 1b 32 25 01  36 96 29 42 25 01 f3 25  |%..h.2%.6.)B%..%|
00000360  bd fe 32 25 01 ed e3 1a  32 25 01 7c 3d 0c 32 25  |..2%....2%.|=.2%|
00000370  01 1f 56 17 32 25 01 4f  b0 13 32 25 01 c9 30 cd  |..V.2%.O..2%..0.|
00000380  32 25 01 b8 0d 0c 32 24  01 e3 7d 06 32 24 01 e9  |2%....2$..}.2$..|
00000390  23 4c 32 24 01 d1 80 18  32 24 01 0d 40 12 32 24  |#L2$....2$..@.2$|
000003a0  01 d5 b0 5b 00 24 01 32  b0 00 00 00 50 00 00 00  |...[.$.2....P...|
000003b0  01 00 40 00 00 00 0e 00  00 00 00 00 00 00 00 00  |..@.............|
000003c0  2b 00 00 00 00 00 00 00  40 00 00 00 00 00 00 00  |+.......@.......|
000003d0  00 c0 02 00 00 00 00 00  60 b3 02 00 00 00 00 00  |........`.......|
000003e0  60 b3 02 00 00 00 00 00  31 01 ff ff 0b 31 2b 8f  |`.......1....1+.|
000003f0  8c 04 00 08 80 fa ff ff  ff ff ff ff 00 00 50 f0  |..............P.|

I found this NTFS cheat sheet invaluable for figuring out the data structures.

MFT entries (files and file-like things) contain attributes. Here we see the FILE header followed by several attributes and a terminator/padding at the end:

  • Standard Information (0x10)
  • Attribute List (0x20)
  • File Name (0x30)
  • Data (0x80)
  • Bitmap (0xb0)

Attributes can be resident (present inline in the MFT) or nonresident (present on raw clusters on disk, referenced by a runlist of extents). The MFT points to itself, so its data attribute is always nonresident, and therefore always contains a runlist describing the blocks occupied by the MFT.

A picture is starting to emerge here. It seems that the MFT entry for the MFT itself is full to the brim with the runlist for the MFT. Normally, there are mechanisms for breaking this out into multiple MFT entries/slots, for cases of very fragmented files, but due to the chicken-and-egg issue with the MFT, chances are that wouldn’t work here. It seems ntfsresize couldn’t fit all the required MFT extents into a resident attribute of the MFT entry itself… and it just truncated the list.

Interestingly, there’s an Attribute List attribute, which is normally used to point to other MFT entries where attributes may spill into when they do not fit into a single entry, but in this case it just points back into the same MFT entry (0), which seems useless and a waste of space. Perhaps this happened as part of a half-complete attempt to append an extra runlist to the $MFT entry?

This should be recoverable. Assuming the MFT chunks actually exist on the drive, it should be possible to locate them, reassemble them, build a new MFT, and dump out the filesystem. Why is the MFT so fragmented, though? It sounds like this would be ntfsresize’s doing. To figure out what happened, I wanted to take a look at the backup MFT. The backup MFT (which is actually just a backup of the first 4 MFT entries) is normally stored towards the middle of the volume. The volume was ~314G and was being resized down to ~100G, so the old backup MFT should still be in the tail end of the partition, untouched.

To find things that look like an MFT, I used bgrep. The second MFT entry is $MFTMirr, the backup MFT, which seems like a good string to grep for (in UTF-16, prefixed with its length and type):

$ bgrep '080324004d00460054004d00690072007200' fail.img
fail.img: c00004f0
fail.img: 2067334f0
fail.img: 114e7d24f0
fail.img: 26496944f0
fail.img: 26f39174f0
fail.img: 27201fc4f0
fail.img: 2765aff4f0
fail.img: 2871fc04f0
fail.img: 28d28d74f0
fail.img: 2a24b4c4f0
fail.img: 2af62574f0
fail.img: 2b45a984f0
fail.img: 2b7a6b04f0
fail.img: 2bb164f4f0
fail.img: 2bf290f4f0
fail.img: 2c954c44f0
fail.img: 2ca31fb0f0
fail.img: 2ca337f0f0
fail.img: 2d04b384f0
fail.img: 2deb8bc4f0
fail.img: 2f41ddc4f0
fail.img: 2f741304f0
fail.img: 3139d004f0
fail.img: 31d42284f0
fail.img: 3effcbc4f0
fail.img: 40d5e574f0
<snipped some false positives>

That’s a lot of MFTs! Time to write a script to figure out exactly what they are. This script evolved during the rescue process, but essentially it dumps the entries and some attributes of the MFT instances found, and also, for the $MFT entry itself, it collects the data runlist and pieces together (what is available of) the MFT.

The output is pretty long, but looks like this:

MFT @ 0xc0000000
seq 1 hard 1 attr 0x38 flags 0001 used 1024 alloc 1024 base 0x0 next_id 0x10 recno 0
type 0x10 len 0x60 fc 0 nlen 0 noff 0x18 aflags 0x0 aid 0x0
    date: 1c8a11b0d647d1c
type 0x20 len 0x98 fc 0 nlen 0 noff 0x0 aflags 0x0 aid 0xf
type 0x30 len 0x68 fc 0 nlen 0 noff 0x18 aflags 0x0 aid 0x3
u'$MFT'
type 0x80 len 0x210 fc 1 nlen 0 noff 0x40 aflags 0x0 aid 0x1
> mft_0xc0000000.bin
32f06500<..snip..>00240132
    vcn=0x0 lcn=0xc0000 run=0x65f0
    vcn=0x65f0 lcn=0x8ee848 run=0x12c
    vcn=0x671c lcn=0xb197aa run=0x12c
    vcn=0x6848 lcn=0xe2e469 run=0x12c
    <..snip..>
    vcn=0xb6ef lcn=0x9a11a3 run=0x124
    vcn=0xb813 lcn=0xb29274 run=0x124
    vcn=0xb937 lcn=0xc4d281 run=0x124
    vcn=0xba5b lcn=0x1208356 run=0x124

type 0xb0 len 0x50 fc 1 nlen 0 noff 0x40 aflags 0x0 aid 0xe
3101ffff0b312b8f8c04000880faffff
    vcn=0x0 lcn=0xbffff run=0x1
    vcn=0x1 lcn=0x108c8e run=0x2b

seq 1 hard 1 attr 0x38 flags 0001 used 344 alloc 1024 base 0x0 next_id 0x4 recno 1
type 0x10 len 0x60 fc 0 nlen 0 noff 0x18 aflags 0x0 aid 0x0
    date: 1c8a11b0d647d1c
type 0x30 len 0x70 fc 0 nlen 0 noff 0x18 aflags 0x0 aid 0x2
u'$MFTMirr'
type 0x80 len 0x48 fc 1 nlen 0 noff 0x40 aflags 0x0 aid 0x1
3101336720000000
    vcn=0x0 lcn=0x206733 run=0x1

<..snip more MFT candidates..>

Some of the MFT headers were clearly from other filesystems (e.g different creation date), but a lot of them seemed to be from this FS. Interestingly, none of them are the backup MFT. All of the MFT headers point $MFTMirr at cluster number 0x206733, which is ~8GB into the partition, so it seems this may have originally been a smaller partition that was resized up, or the backup MFT was never in the middle of the volume, but in any case ntfsresize didn’t have to move it back into the volume; therefore, I was lucky to find these lying around in the image.

I found several runlists for the MFT that seem to match:

32f06500000c43d047037f08c60300ff
    vcn=0x0 lcn=0xc0000 run=0x65f0
    vcn=0x65f0 lcn=0x3d2087f run=0x347d0

32f06500000c43904e037f08c60300ff
    vcn=0x0 lcn=0xc0000 run=0x65f0
    vcn=0x65f0 lcn=0x3d2087f run=0x34e90

32f06500000c439184037f08c60342ff1d39e1f6fe00ffff
    vcn=0x0 lcn=0xc0000 run=0x65f0
    vcn=0x65f0 lcn=0x3d2087f run=0x38491
    vcn=0x3ea81 lcn=0x2c8e9b8 run=0x1dff

32f06500000c439184037f08c60342001e39e1f6fe313fa04406000880faffff
    vcn=0x0 lcn=0xc0000 run=0x65f0
    vcn=0x65f0 lcn=0x3d2087f run=0x38491
    vcn=0x3ea81 lcn=0x2c8e9b8 run=0x1e00
    vcn=0x40881 lcn=0x2cf2e58 run=0x3f

32f06500000c439184037f08c603<snip>80faffff
    vcn=0x0 lcn=0xc0000 run=0x65f0
    vcn=0x65f0 lcn=0x3d2087f run=0x38491
    vcn=0x3ea81 lcn=0x2c8e9b8 run=0x1e00
    vcn=0x40881 lcn=0x2cf2e58 run=0xa40
    vcn=0x412c1 lcn=0x35db3d0 run=0xdeb0
    vcn=0x4f171 lcn=0x3680000 run=0x2cf

32f06500000c43d0c004f7e3e70300ff
    vcn=0x0 lcn=0xc0000 run=0x65f0
    vcn=0x65f0 lcn=0x3f3e3f7 run=0x4c0d0

32f06500000c43d1c004f7e3e70342ff249a3e9f0000ffff
    vcn=0x0 lcn=0xc0000 run=0x65f0
    vcn=0x65f0 lcn=0x3f3e3f7 run=0x4c0d1
    vcn=0x526c1 lcn=0x4932291 run=0x24ff

32f06500000c43d0e504ab92f90300ff
    vcn=0x0 lcn=0xc0000 run=0x65f0
    vcn=0x65f0 lcn=0x40592ab run=0x4e5d0

I don’t know why these MFT header copies were lying around in the rest of the partition, but they seem to paint a picture of the history of MFT size and fragmentation in this filesystem. I’ve sorted them above in what I think is chronological order. It seems the MFT grew to 6 extents at one point, before being eventually consolidated back to 2 again.

Taking the MFT dumps built from these runlists, I looked at the offsets. Of the above candidates, only the last one had sensible MFT data after the first chunk, so it was probably the location of the MFT before ntfsresize ran. Its size, 0x65f0 + 0x4e5d0 = 0x54bc0, also matches the size of the MFT that ntfsfix was looking for.

At this point the plan is to search the HDD for MFT chunks, and use the old MFT as a template to lay them out in the correct order (the fact that MFT entries have their own index in the header also helps), basically building the missing entries of the runlist that ntfsresize truncated. To start, though, let’s compare what we have of the MFT as ntfsresize wrote it (the original first extent plus 80-odd chunks) with what was in the original location:

$ cmp mft_0xc0000000.bin mft_0x31d4228000.bin
cmp: EOF on mft_0xc0000000.bin

Huh. Of course the beginning of the MFT is the same, as it’s the same extent on disk… but it seems the 80-odd chunks that ntfsresize relocated are also identical to that span of data in the old extent. Could it be that ntfsresize first relocated everything else, and then did the MFT last? This would explain why the MFT got the short end of the stick and ended up broken up into tons of tiny fragments, as all the larger contiguous spaces would’ve been used by relocated files already. In that case, the old MFT location should be fully up to date with the state of the partition.

If this is indeed the case, then recovery becomes very easy: overwrite the MFT runlist with the old one, and insert the correct final cluster number (MFT size):

000001a0  01 00 40 00 00 00 01 00  00 00 00 00 00 00 00 00  |..@.............|
000001b0  bf 4b 05 00 00 00 00 00  40 00 00 00 00 00 00 00  |.K......@.......|
000001c0  00 00 bc 54 00 00 00 00  00 00 bc 54 00 00 00 00  |...T.......T....|
000001d0  00 00 bc 54 00 00 00 00  32 f0 65 00 00 0c 43 d0  |...T....2.e...C.|
000001e0  e5 04 ab 92 f9 03 00 ff  62 af 22 32 2c 01 bf 4c  |........b."2,..L|
000001f0  31 32 2c 01 9c 0e 5e 42  2c 01 48 2e e5 fe 50 f0  |12,...^B,.H...P.|
00000200  01 1d 95 12 32 2c 01 39  c4 21 32 2c 01 a9 22 08  |....2,.9.!2,..".|
00000210  32 2b 01 66 70 13 32 2b  01 fa ed 39 32 2b 01 e3  |2+.fp.2+...92+..|
00000220  c5 14 32 2b 01 78 9f 70  42 2b 01 3f 31 f0 fe 32  |..2+.x.pB+.?1..2|
00000230  2b 01 e2 24 1e 32 2b 01  fc 39 21 32 2b 01 98 28  |+..$.2+..9!2+..(|
00000240  4f 32 2a 01 07 4b 14 32  2a 01 c1 22 0a 32 2a 01  |O2*..K.2*..".2*.|

The attribute is now oversized, but the 00 terminator for the runlist (I added ff but I don’t think it’s required) should be all that’s needed to make it work.

Write it over the MFT and its backup:

$ dd if=mft.bin of=fail.img bs=4096 seek=$((0xc0000)) count=1 conv=notrunc
$ dd if=mft.bin of=fail.img bs=4096 seek=$((0x206733)) count=1 conv=notrunc

Finally, put back the volume size in the boot sector, to make the filesystem cover the entire partition (since the new old MFT extent is, of course, beyond the end to which the partition was resized):

$ dd if=fail.img of=bootsect.bin bs=512 count=1
00000000  eb 52 90 4e 54 46 53 20  20 20 20 00 02 08 00 00  |.R.NTFS    .....|
00000010  00 00 00 00 00 f8 00 00  3f 00 ff 00 00 50 38 01  |........?....P8.|
00000020  00 00 00 00 80 00 00 00  f8 af aa 24 00 00 00 00  |........p;......|
00000030  00 00 0c 00 00 00 00 00  33 67 20 00 00 00 00 00  |........3g .....|
$ dd if=bootsect.bin of=fail.img bs=512 count=1 conv=notrunc

The filesystem bitmap will now be incorrect (wrong size and contents), but chkdsk should be able to fix that.

A total of 26 bytes patched… and a whole day spent figuring out which 26 bytes to patch.

Did it work?

Amazingly enough, chkdsk gave the filesystem a clean bill of health save for the bitmap and the uppercasing table (?). After formatting the new SSD, using robocopy to perform a file-level copy, manually re-creating directory junctions that robocopy can’t transfer, using bcdboot and bootsect to put the bootloader back into a sane state, and messing with the registry to convince windows that the SSD is its new C: drive (all of which are Windowsisms that I can’t claim much experience with, but which Google will tell you all about and which are outside the scope of this post), the entire OS is back the way it was, except now running much faster from the SSD. Victory.

2015-10-25 18:00