[{"data":1,"prerenderedAt":1228},["ShallowReactive",2],{"post-\u002Fposts\u002Fxb360-exploit-chain":3},{"id":4,"title":5,"body":6,"description":1218,"extension":1219,"meta":1220,"navigation":1221,"order":623,"path":1222,"seo":1223,"stem":1224,"summary":1225,"visibility":1226,"__hash__":1227},"posts\u002Fposts\u002Fxb360-exploit-chain.md","li r3, 1: An Xbox 360 Exploit Chain",{"type":7,"value":8,"toc":1207},"minimark",[9,18,21,24,27,32,55,58,62,96,111,115,118,151,160,177,258,281,285,288,312,315,318,321,325,328,440,458,500,503,507,510,540,564,714,744,753,763,767,784,817,833,862,895,909,913,916,948,964,993,1024,1035,1055,1082,1125,1142,1159,1172,1179,1183,1186,1189,1203],[10,11,12,13,17],"p",{},"A console that only runs signed code has to do the checking somewhere. On the Xbox 360 it happens in the hypervisor, 64KB of code living in memory the CPU keeps encrypted, which nothing in usermode is supposed to read or write. One function in there verifies an RSA signature and returns a boolean. Overwrite it with ",[14,15,16],"code",{},"li r3, 1"," and every signature on the machine is valid.",[10,19,20],{},"I loved Halo Reach growing up. It defined an entire era of my childhood in a way no other game has since, and nothing beats 10-year-old me screaming down a one-ear headset at teammates two or three times my age. So when a friend gave me a Halo Reach Limited Edition Xbox 360 S, I knew I had to do something with it.",[10,22,23],{},"The console came with a 250GB HDD and dashboard 2.0.17526.0. The plan was RGH3 later, a hardware mod that glitches the CPU during boot so the signature checks never complete, which means soldering. I never got that far, because the same day I ran BadUpdate instead: no soldering, no NAND modification, nothing written to persistent storage at all.",[10,25,26],{},"Getting from a text field to that one instruction takes six rungs, and the only reason each one exists is to buy the next. That's the whole shape of it. Nothing in the chain is interesting on its own.",[28,29,31],"h2",{"id":30},"the-board","The board",[10,33,34,35,39,40,47,48,54],{},"The Xbox 360 shipped on seven motherboard revisions ",[36,37,38],"note",{},"Xenon, Zephyr, Falcon, Jasper, Trinity, Corona and Winchester. Xenon through Jasper are the original \"phat\" chassis."," and the Reach Limited Edition is a Trinity, the first slim board, the 360 S redesign from 2010 with integrated WiFi and a single centrifugal blower in place of the dual-fan setup. Code identifies it through ",[41,42,44],"speak",{"as":43},"Xbox Hardware Info, Flags bitwise-AND hex F followed by seven zeroes",[14,45,46],{},"XboxHardwareInfo->Flags & 0xF0000000",", where Trinity returns ",[41,49,51],{"as":50},"hex 4 followed by seven zeroes",[14,52,53],{},"0x40000000",".",[10,56,57],{},"That matters twice. Trinity has stable glitch timing if I ever do the hardware mod, and it sits inside the dashboard range BadUpdate targets. The console still had someone else's data on it, so I formatted the drive and made an offline profile from the Guide menu, since the option is greyed out on the main dashboard.",[28,59,61],{"id":60},"_17559","17559",[10,63,64,65,68,69,75,76,82,83,75,89,95],{},"BadUpdate works on dashboard 2.0.17559.0 and nothing else. Every address in the exploit is hardcoded against that exact kernel build: the ROP gadgets, the syscall ordinals, the function offsets, the patch locations. ",[14,66,67],{},"KernelConfig_Retail_17559.asm"," in the source is 270 lines of nothing but constants, ",[41,70,72],{"as":71},"stack pivot",[14,73,74],{},"stack_pivot"," at ",[41,77,79],{"as":78},"hex 8 1 7 2 5 3 7 8",[14,80,81],{},"0x81725378"," and ",[41,84,86],{"as":85},"call func preload",[14,87,88],{},"call_func_preload",[41,90,92],{"as":91},"hex 8 1 6 9 C D D C",[14,93,94],{},"0x8169CDDC"," among them.",[10,97,98,99,106,107,110],{},"Mine was on 17526, one update behind. So I pulled the ",[100,101,105],"a",{"href":102,"rel":103},"https:\u002F\u002Farchive.org\u002Fdetails\u002Fxbox-360-system-update-17559-usb",[104],"nofollow","17559 USB update from archive.org",", dropped the ",[14,108,109],{},"$SystemUpdate"," folder on a FAT32 stick and let the console find it on boot. Updating a console to make it exploitable is a funny thing to have to do.",[28,112,114],{"id":113},"the-entry-point","The entry point",[10,116,117],{},"BadUpdate is Grimdoomer's, and the version everyone quotes needs you to own Tony Hawk's American Wasteland or Rock Band Blitz, because the overflow is in a save file those games load. I own neither. The variant I used is ABadAvatar by shutterbug2000, which needs no game at all: a malformed Xbox 360 avatar whose name field overruns a fixed-size stack buffer while the dashboard renders it, fired from the profile selection screen with no disc and no input from me.",[119,120,122],"tangent",{"label":121},"The other doors",[10,123,124,125,131,132,138,139,144,145,150],{},"The save-file route is the original and it still reads better as a bug. Tony Hawk's gap name parser takes crafted data at file offset ",[41,126,128],{"as":127},"hex D F 4",[14,129,130],{},"0xDF4",", which lands on the heap at ",[41,133,135],{"as":134},"hex B 4 3 B 6 8 2 E",[14,136,137],{},"0xB43B682E",", and the overflow is 60 bytes of padding followed by controlled values for ",[41,140,142],{"as":141},"r23",[14,143,141],{}," through ",[41,146,148],{"as":147},"r31",[14,149,147],{},". ABadMemUnit and ABadAvatarHDD are the same avatar bug aimed at different storage devices. All four land in the same place, so everything after this point is identical whichever door you came through.",[10,152,153,154,159],{},"I used ",[100,155,158],{"href":156,"rel":157},"https:\u002F\u002Fgithub.com\u002FLxcyDr0p\u002FBadStick",[104],"BadStick"," to lay the USB stick out, a C# WinForms tool that formats the drive, pulls the exploit packages and the homebrew from GitHub releases and puts them where the console expects. It isn't part of the exploit, it just saves you doing the directory layout by hand.",[10,161,162,163,169,170,176],{},"The avatar data lands at ",[41,164,166],{"as":165},"hex 4 3 A B 9 A C 8",[14,167,168],{},"0x43AB9AC8"," and gets relocated to ",[41,171,173],{"as":172},"hex 4 3 6 7 followed by four zeroes",[14,174,175],{},"0x43670000"," by what the source comments call a \"comically large allocation\" from the avatar system. The overflow overwrites the saved registers and the link register, so when the rendering function returns it returns into a gadget in xam.xex, the dashboard's core system library.",[178,179,185],"pre",{"className":180,"code":181,"filename":182,"language":183,"meta":184,"style":184},"language-c shiki shiki-themes dromzeh","lwz  r1, 0(r1)   \u002F\u002F 1\nlwz  r12, -8(r1) \u002F\u002F 2\nmtlr r12         \u002F\u002F 3\nblr              \u002F\u002F 4\n","stack-pivot.asm","c","",[14,186,187,217,240,249],{"__ignoreMap":184},[188,189,192,196,200,204,207,210,213],"span",{"class":190,"line":191},"line",1,[188,193,195],{"class":194},"s_Mgl","lwz  r1",[188,197,199],{"class":198},"sQTeU",",",[188,201,203],{"class":202},"sneuw"," 0",[188,205,206],{"class":198},"(",[188,208,209],{"class":194},"r1",[188,211,212],{"class":198},")",[188,214,216],{"class":215},"sAzwi","   \u002F\u002F 1\n",[188,218,220,223,225,228,231,233,235,237],{"class":190,"line":219},2,[188,221,222],{"class":194},"lwz  r12",[188,224,199],{"class":198},[188,226,227],{"class":198}," -",[188,229,230],{"class":202},"8",[188,232,206],{"class":198},[188,234,209],{"class":194},[188,236,212],{"class":198},[188,238,239],{"class":215}," \u002F\u002F 2\n",[188,241,243,246],{"class":190,"line":242},3,[188,244,245],{"class":194},"mtlr r12",[188,247,248],{"class":215},"         \u002F\u002F 3\n",[188,250,252,255],{"class":190,"line":251},4,[188,253,254],{"class":194},"blr",[188,256,257],{"class":215},"              \u002F\u002F 4\n",[10,259,260,261,266,267,271,272,82,276,280],{},"The ",[262,263,265],"clause",{"n":264},"1","first load"," replaces the stack pointer with attacker-controlled data. The ",[262,268,270],{"n":269},"2","second"," pulls a value out of that new stack, ",[262,273,275],{"n":274},"3","moves it into the link register",[262,277,279],{"n":278},"4","branches to it",". Four instructions, and the machine is now executing whatever sequence of return addresses I put on the stack. That's return-oriented programming, and for the rest of this rung there is no injected code anywhere, only the console's own code fragments chained by their returns.",[28,282,284],{"id":283},"encrypted-memory","Encrypted memory",[10,286,287],{},"Rung one bought instruction control in usermode. It bought nothing against the hypervisor, which is where the signature check lives.",[10,289,290,291,297,298,304,305,311],{},"The way in is a syscall called ",[41,292,294],{"as":293},"H V X Keys Execute",[14,295,296],{},"HvxKeysExecute",", ordinal ",[41,299,301],{"as":300},"hex 4 2",[14,302,303],{},"0x42",", which takes a buffer of LZX-compressed data and decompresses it. That decompression runs in encrypted memory: the CPU encrypts and decrypts transparently in hardware, so the hypervisor reads and writes plaintext, and usermode code holding the physical backing pages sees only ciphertext. The decompressor keeps its state in there, including a pointer called ",[41,306,308],{"as":307},"dec output buffer",[14,309,310],{},"dec_output_buffer"," that says where the output goes.",[10,313,314],{},"So the attack writes itself. Swap that pointer for one aimed at the hypervisor's own code pages and the decompressor does the writing for you.",[10,316,317],{},"Except you can't write a pointer. You can reach the physical page, but everything you put there is read back through the decryption engine as noise, and there is no instruction that says \"encrypt this address for me\". Knowing exactly which eight bytes you want to change and having write access to them is not the same as being able to set them.",[10,319,320],{},"What saves it is that the encryption is weak in a specific way. It uses a per-page \"whitening\" value that rotates through 1024 slots, and within one slot the mapping from plaintext to ciphertext is fixed. Observe the ciphertext of a plaintext you already know, at a given whitening value, and you can produce the ciphertext for any plaintext at that same value. That is an oracle, and building one is the entire next rung.",[28,322,324],{"id":323},"the-oracle","The oracle",[10,326,327],{},"Capturing a ciphertext relies on one property of an encrypted allocation: the physical page behind the encrypted virtual address is also reachable through a plain unencrypted alias.",[178,329,332],{"className":180,"code":330,"filename":331,"language":183,"meta":184,"style":184},"memcpy(encrypted, plaintext, 16);   \u002F\u002F 1\nKeFlushCacheRange(encrypted, 16);   \u002F\u002F 2\nmemcpy(captured, alias, 16);        \u002F\u002F 3\ntable[captured[0] >> 22] = captured; \u002F\u002F 4\n","capture.c",[14,333,334,360,378,401],{"__ignoreMap":184},[188,335,336,340,342,345,347,350,352,355,358],{"class":190,"line":191},[188,337,339],{"class":338},"sxxka","memcpy",[188,341,206],{"class":198},[188,343,344],{"class":194},"encrypted",[188,346,199],{"class":198},[188,348,349],{"class":194}," plaintext",[188,351,199],{"class":198},[188,353,354],{"class":202}," 16",[188,356,357],{"class":198},");",[188,359,216],{"class":215},[188,361,362,365,367,369,371,373,375],{"class":190,"line":219},[188,363,364],{"class":338},"KeFlushCacheRange",[188,366,206],{"class":198},[188,368,344],{"class":194},[188,370,199],{"class":198},[188,372,354],{"class":202},[188,374,357],{"class":198},[188,376,377],{"class":215},"   \u002F\u002F 2\n",[188,379,380,382,384,387,389,392,394,396,398],{"class":190,"line":242},[188,381,339],{"class":338},[188,383,206],{"class":198},[188,385,386],{"class":194},"captured",[188,388,199],{"class":198},[188,390,391],{"class":194}," alias",[188,393,199],{"class":198},[188,395,354],{"class":202},[188,397,357],{"class":198},[188,399,400],{"class":215},"        \u002F\u002F 3\n",[188,402,403,407,410,412,414,417,420,423,426,428,431,434,437],{"class":190,"line":251},[188,404,406],{"class":405},"sH9Dd","table",[188,408,409],{"class":198},"[",[188,411,386],{"class":405},[188,413,409],{"class":198},[188,415,416],{"class":202},"0",[188,418,419],{"class":198},"]",[188,421,422],{"class":198}," >>",[188,424,425],{"class":202}," 22",[188,427,419],{"class":198},[188,429,430],{"class":198}," =",[188,432,433],{"class":194}," captured",[188,435,436],{"class":198},";",[188,438,439],{"class":215}," \u002F\u002F 4\n",[10,441,260,442,445,446,449,450,453,454,457],{},[262,443,444],{"n":264},"known plaintext"," goes in through the encrypted address, the ",[262,447,448],{"n":269},"cache is flushed"," so the bytes actually reach the page, and an ",[262,451,452],{"n":274},"ordinary read of the alias"," hands back the raw ciphertext of what was just written. Same bytes, plaintext through one mapping and ciphertext through the other. The ",[262,455,456],{"n":278},"top ten bits"," index a 1024-slot table, one slot per whitening value.",[10,459,460,461,464,465,468,469,475,476,482,483,489,490,492,493,499],{},"The known plaintext comes from ",[14,462,463],{},"bootanim.xex",", the boot animation, loaded through ",[14,466,467],{},"XexLoadImage"," because its code pages land at a predictable virtual address. Loading and unloading it in a loop at ",[41,470,472],{"as":471},"hex 8 D followed by six zeroes",[14,473,474],{},"0x8D000000",", allocating and freeing an encrypted region each time, walks the whitening counter through all 1024 values. Every cycle captures two things: the ciphertext of the LZX decoder's context header, signature ",[41,477,479],{"as":478},"C I D L",[14,480,481],{},"'CIDL'"," with a ",[41,484,486],{"as":485},"hex 8 triple-zero",[14,487,488],{},"0x8000"," window, which becomes the canary the race watches for, and the ciphertext of a poisoned ",[14,491,310],{}," pointing at ",[41,494,496],{"as":495},"hex 8 triple-zero 0 1 0 6 dot triple-zero 3 0 9 4 0",[14,497,498],{},"0x80000106.00030940"," inside the hypervisor's last segment, which becomes the replacement.",[10,501,502],{},"All of this is ROP, and a ROP chain that runs for an indefinite number of iterations has a problem: it overwrites its own gadget data. The fix is a dual-buffer bounce, where each pass copies its gadgets to the alternate buffer and stack-pivots into it, so the chain never stands where it is writing.",[28,504,506],{"id":505},"the-race","The race",[10,508,509],{},"Two ciphertexts per slot, and now the hard part is time. The window is between the decompressor writing its context header and reading its output pointer back.",[10,511,512,513,516,517,523,524,75,527,533,534,54],{},"Two things happen before the threads start. ",[14,514,515],{},"KeLockL2"," is called twice to lock 256KB of L2 cache each time with rubbish, so the ciphertext under observation evicts to main memory quickly enough to be seen. Then ",[41,518,520],{"as":519},"hex 6 6 6 6 6 6 6 6",[14,521,522],{},"0x66666666"," goes into ",[14,525,526],{},"MmPhysical64KBMappingTable",[41,528,530],{"as":529},"hex 8 0 1 C 1 triple-zero",[14,531,532],{},"0x801C1000",", which makes the hypervisor's encrypted segments visible from usermode at ",[41,535,537],{"as":536},"hex A followed by seven zeroes",[14,538,539],{},"0xA0000000",[10,541,542,543,545,546,552,553,559,560,563],{},"Then two threads, one per hardware core. Thread 1 calls ",[14,544,296],{}," in a tight loop and reads the return code: ",[41,547,549],{"as":548},"hex C 8 four zeroes 1 2",[14,550,551],{},"0xC8000012"," means the corrupted pointer made the decompressor give up, and ",[41,554,556],{"as":555},"hex C 8 five zeroes 6",[14,557,558],{},"0xC8000006"," means a block got overwritten, which it then verifies by comparing ciphertexts. Thread 0 runs the tightest loop the author could write. The C reference in ",[14,561,562],{},"BadUpdatePoc.cpp"," describes a hash table lookup and a 1.5-million-cycle delay; the hand-assembled version that actually ships has neither.",[178,565,568],{"className":180,"code":566,"filename":567,"language":183,"meta":184,"style":184},"loop:\n    ld      r11, 0(r31)    \u002F\u002F 1\n    cmpld   cr6, r11, r30  \u002F\u002F 2\n    bne     cr6, flush\n        mtctr   r25\noverwrite:\n        std     r29, 0x20(r26) \u002F\u002F 3\n        std     r28, 0x28(r26)\n        dcbst   r0, r26\n        bdnz    overwrite\nflush:\n    dcbf    r0, r31 \u002F\u002F 4\n    b       loop\n","race.asm",[14,569,570,575,593,611,621,627,633,654,672,683,689,695,708],{"__ignoreMap":184},[188,571,572],{"class":190,"line":191},[188,573,574],{"class":194},"loop:\n",[188,576,577,580,582,584,586,588,590],{"class":190,"line":219},[188,578,579],{"class":194},"    ld      r11",[188,581,199],{"class":198},[188,583,203],{"class":202},[188,585,206],{"class":198},[188,587,147],{"class":194},[188,589,212],{"class":198},[188,591,592],{"class":215},"    \u002F\u002F 1\n",[188,594,595,598,600,603,605,608],{"class":190,"line":242},[188,596,597],{"class":194},"    cmpld   cr6",[188,599,199],{"class":198},[188,601,602],{"class":194}," r11",[188,604,199],{"class":198},[188,606,607],{"class":194}," r30",[188,609,610],{"class":215},"  \u002F\u002F 2\n",[188,612,613,616,618],{"class":190,"line":251},[188,614,615],{"class":194},"    bne     cr6",[188,617,199],{"class":198},[188,619,620],{"class":194}," flush\n",[188,622,624],{"class":190,"line":623},5,[188,625,626],{"class":194},"        mtctr   r25\n",[188,628,630],{"class":190,"line":629},6,[188,631,632],{"class":194},"overwrite:\n",[188,634,636,639,641,644,646,649,651],{"class":190,"line":635},7,[188,637,638],{"class":194},"        std     r29",[188,640,199],{"class":198},[188,642,643],{"class":202}," 0x20",[188,645,206],{"class":198},[188,647,648],{"class":194},"r26",[188,650,212],{"class":198},[188,652,653],{"class":215}," \u002F\u002F 3\n",[188,655,657,660,662,665,667,669],{"class":190,"line":656},8,[188,658,659],{"class":194},"        std     r28",[188,661,199],{"class":198},[188,663,664],{"class":202}," 0x28",[188,666,206],{"class":198},[188,668,648],{"class":194},[188,670,671],{"class":198},")\n",[188,673,675,678,680],{"class":190,"line":674},9,[188,676,677],{"class":194},"        dcbst   r0",[188,679,199],{"class":198},[188,681,682],{"class":194}," r26\n",[188,684,686],{"class":190,"line":685},10,[188,687,688],{"class":194},"        bdnz    overwrite\n",[188,690,692],{"class":190,"line":691},11,[188,693,694],{"class":194},"flush:\n",[188,696,698,701,703,706],{"class":190,"line":697},12,[188,699,700],{"class":194},"    dcbf    r0",[188,702,199],{"class":198},[188,704,705],{"class":194}," r31",[188,707,439],{"class":215},[188,709,711],{"class":190,"line":710},13,[188,712,713],{"class":194},"    b       loop\n",[10,715,716,717,720,721,724,725,728,729,732,733,739,740,743],{},"It ",[262,718,719],{"n":264},"loads eight bytes"," from the scratch buffer's header and ",[262,722,723],{"n":269},"compares them"," against the canary sitting in ",[14,726,727],{},"r30",". A match means the decoder has just written its context and the state is live. Then it ",[262,730,731],{"n":274},"stores the replacement pointer"," a hundred thousand times over offset ",[41,734,736],{"as":735},"hex 2 B 2 8",[14,737,738],{},"0x2B28",", flushing each line to memory, because it has no idea when in that window the decompressor will read. On a miss it ",[262,741,742],{"n":278},"flushes the header"," out of cache so the next read comes from memory rather than telling it the same stale answer forever.",[745,746,748],"fig",{"caption":747},"Two threads, one encrypted buffer. Thread 1 only exists to keep the decompressor running; thread 0 does the work. The buffer is the only thing both sides touch, and the hypervisor segment is where the decompressor's output ends up when the swap lands in time.",[749,750],"flow-diagram",{":columns":751,":edges":752},"[[{\"id\":\"t1\",\"title\":\"Thread 1\",\"sub\":\"HvxKeysExecute\"},{\"id\":\"t0\",\"title\":\"Thread 0\",\"sub\":\"Watches the canary\"}],[{\"id\":\"buf\",\"title\":\"Scratch buffer\",\"sub\":\"Encrypted\"}],[{\"id\":\"hv\",\"title\":\"Hypervisor segment\",\"sub\":\"Block 14\",\"em\":true}]]","[{\"from\":\"t1\",\"to\":\"buf\",\"label\":\"Decompress\"},{\"from\":\"t0\",\"to\":\"buf\",\"label\":\"Swap the pointer\"},{\"from\":\"buf\",\"to\":\"hv\"}]",[10,754,755,756,762],{},"Block 14 is the target because it is the smallest block in the compressed file at ",[41,757,759],{"as":758},"hex 1 A D 0",[14,760,761],{},"0x1AD0"," bytes, which gives the widest window. The race does not hit every time. Grimdoomer's README quotes a 30% success rate and up to 20 minutes per attempt. Mine usually landed within a few seconds, and when it didn't I'd wait about five minutes and restart.",[28,764,766],{"id":765},"the-write-primitive","The write primitive",[10,768,769,770,776,777,783],{},"What lands in the hypervisor is not shellcode. It is whatever block 14 happens to decompress to, aimed so that its contents at internal offset ",[41,771,773],{"as":772},"hex 1 5 E 8",[14,774,775],{},"0x15E8"," come to rest at hypervisor offset ",[41,778,780],{"as":779},"hex 1 F 2 8",[14,781,782],{},"0x1F28",". At that exact spot in the decompressed output there are two instructions.",[178,785,788],{"className":180,"code":786,"filename":787,"language":183,"meta":184,"style":184},"stb r4, 2(r6) \u002F\u002F 1\nblr           \u002F\u002F 2\n","block14.asm",[14,789,790,810],{"__ignoreMap":184},[188,791,792,795,797,800,802,805,807],{"class":190,"line":191},[188,793,794],{"class":194},"stb r4",[188,796,199],{"class":198},[188,798,799],{"class":202}," 2",[188,801,206],{"class":198},[188,803,804],{"class":194},"r6",[188,806,212],{"class":198},[188,808,809],{"class":215}," \u002F\u002F 1\n",[188,811,812,814],{"class":190,"line":219},[188,813,254],{"class":194},[188,815,816],{"class":215},"           \u002F\u002F 2\n",[10,818,819,822,823,75,826,828,829,832],{},[262,820,821],{"n":264},"Store the low byte"," of ",[14,824,825],{},"r4",[14,827,804],{}," plus two, ",[262,830,831],{"n":269},"then return",". That is the whole of it, and it is enough, because the hypervisor is now holding a routine that writes one attacker-chosen byte to one attacker-chosen address. A 32-bit write is four calls. Every address is passed with two subtracted, to cancel the displacement.",[10,834,835,836,842,843,849,850,853,854,858,859,861],{},"Getting at it needs a way in. The exploit uses the byte-writer to overwrite the syscall table entry for ",[41,837,839],{"as":838},"H V X Post Output",[14,840,841],{},"HvxPostOutput",", syscall ",[41,844,846],{"as":845},"hex 0 D",[14,847,848],{},"0x0D",", with the address of a ",[14,851,852],{},"mtctr r4; bctr"," gadget already sitting in the hypervisor. Syscall ",[41,855,856],{"as":845},[14,857,848],{}," now branches to whatever address you hand it in ",[14,860,825],{},", from usermode, in hypervisor context.",[10,863,864,865,868,869,875,876,879,880,883,884,886,887,890,891,894],{},"From there the shellcode cleans up after itself and patches the two things that matter. The race left 64KB of hypervisor code corrupted, so ",[14,866,867],{},"HvpRelocateCacheLines"," copies a clean copy back from a bundled binary. Then ",[41,870,872],{"as":871},"hex 3 8 6 four zeroes 1",[14,873,874],{},"0x38600001"," goes over the call to ",[14,877,878],{},"XeCryptBnQwBeSigVerify"," inside ",[14,881,882],{},"HvpImageSignatureVerification",", which is ",[14,885,16],{},", return true. The kernel copy of the same check needs RMCI turned off to reach encrypted kernel memory, so ",[14,888,889],{},"HvpSetRMCI(0)"," runs, the same four bytes go into ",[14,892,893],{},"XexpVerifyXexHeaders",", and RMCI goes back on.",[10,896,897,898,904,905,908],{},"The shellcode returns ",[41,899,901],{"as":900},"hex 4 1 4 1 4 1 4 1",[14,902,903],{},"0x41414141"," as a sentinel. The race code checks for it and calls ",[14,906,907],{},"XLaunchNewImage"," on the payload sitting on the USB stick.",[28,910,912],{"id":911},"xeunshackle","XeUnshackle",[10,914,915],{},"The payload is XeUnshackle by Byrom90, and its job is to turn a console that can run one unsigned binary into a console that behaves like a permanently modified one.",[10,917,918,919,923,924,842,930,936,937,943,944,947],{},"Its first move is the BadUpdate backdoor it was launched from. Syscall ",[41,920,921],{"as":845},[14,922,848],{}," runs shellcode that patches three instructions inside ",[41,925,927],{"as":926},"H V X Expansion Install",[14,928,929],{},"HvxExpansionInstall",[41,931,933],{"as":932},"hex 7 2",[14,934,935],{},"0x72",": a branch at HV ",[41,938,940],{"as":939},"hex 3 0 8 9 C",[14,941,942],{},"0x3089C"," flipped to skip the failure path, a ",[14,945,946],{},"li r29, 0"," to clear the check result, and a NOP. Hypervisor expansions are meant to be RSA-signed, and after those three writes they aren't.",[10,949,950,951,842,957,963],{},"So XeUnshackle installs its own. The expansion is a small dispatch table reached through ",[41,952,954],{"as":953},"H V X Expansion Call",[14,955,956],{},"HvxExpansionCall",[41,958,960],{"as":959},"hex 7 3",[14,961,962],{},"0x73",", where modes zero to three read a byte, halfword, word or doubleword of hypervisor memory, modes five to eight write them, and two more do bulk copies with the cache maintenance that makes written instructions actually execute. Arbitrary hypervisor peek and poke from usermode, which is the byte-writer promoted to something you can use.",[10,965,966,967,82,970,973,974,977,978,981,982,985,986,992],{},"Two of the modes read and write special purpose registers, and those can't be done the ordinary way, because PowerPC encodes the register number into the ",[14,968,969],{},"mfspr",[14,971,972],{},"mtspr"," opcodes rather than taking it in a register. The expansion writes the instruction at runtime instead: it finds its own address with ",[14,975,976],{},"bl .+4; mflr",", uses ",[14,979,980],{},"rlwimi"," to drop the number from ",[14,983,984],{},"r5"," into an opcode template ",[41,987,989],{"as":988},"hex 3 0",[14,990,991],{},"0x30"," bytes ahead, flushes the instruction cache and branches into it. Self-modifying code, inside the hypervisor, regenerated on every call.",[10,994,995,996,1002,1003,1006,1007,1010,1011,1017,1018,1020,1021,1023],{},"With peek and poke available, the freeboot patch set goes in, the same one xeBuild writes into NAND on a permanently modified console. The first phase has to include the memory protection disable, and the important patch is 288 bytes at HV ",[41,997,999],{"as":998},"hex B 5 1 0",[14,1000,1001],{},"0xB510"," that replace ",[14,1004,1005],{},"HvxGetVersion",", syscall zero, with a multi-function backdoor. It checks ",[14,1008,1009],{},"r3"," for the magic value ",[41,1012,1014],{"as":1013},"hex 7 2 6 2 7 4 7 2",[14,1015,1016],{},"0x72627472"," and dispatches on ",[14,1019,825],{},": one mode forces RWX on hypervisor pages, one puts the protections back, one does bulk copies. Anything without the magic falls through to the real ",[14,1022,1005],{},", so the syscall keeps working for everyone else. Calling it with the magic is what makes the rest of the address space writable.",[10,1025,1026,1027,1030,1031,1034],{},"The second phase is mostly one move repeated. Overwrite the target function with ",[14,1028,1029],{},"li r3, 1; blr",", or ",[14,1032,1033],{},"li r3, 0; blr"," where the caller reads zero as success. RSA verification, security violation detection, the getters and setters around it, key validation, image loading checks, import resolution, hash verification: all of them unconditionally agree with you now.",[10,1036,1037,1038,1040,1041,1047,1048,1054],{},"Four are worth naming because they aren't that pattern. ",[14,1039,882],{}," at HV ",[41,1042,1044],{"as":1043},"hex 2 9 B 0 8",[14,1045,1046],{},"0x29B08"," gets a 14-doubleword rewrite rather than a stub, because the callers need more granularity than \"yes\". The fuse blow handler at HV ",[41,1049,1051],{"as":1050},"hex A 5 6 0",[14,1052,1053],{},"0xA560"," returns success without burning anything, so the one-time-programmable fuses the console uses to record its update history stay where they are.",[10,1056,1057,1058,1064,1065,82,1071,1077,1078,1081],{},"The devkit XEX AES key at HV ",[41,1059,1061],{"as":1060},"hex 0 0 F 0",[14,1062,1063],{},"0x00F0"," is zeroed. And the machine check exception handler between HV ",[41,1066,1068],{"as":1067},"hex 7 2 B 4",[14,1069,1070],{},"0x72B4",[41,1072,1074],{"as":1073},"hex 7 2 E C",[14,1075,1076],{},"0x72EC"," becomes three NOPs and a ",[14,1079,1080],{},"li r11, 1",". Between them the hypervisor loses every mechanism it had to notice, record or act on the fact that it is running unsigned code.",[10,1083,1084,1085,1087,1088,75,1091,1097,1098,1104,1105,75,1108,1114,1115,75,1118,1124],{},"The kernel patches are the same primitive aimed at kernel addresses. Most are the ",[14,1086,16],{}," stub again, across XEX error handling, media type checks, version checks, revocation, and drive authentication. Three of them open up the hardware: ",[14,1089,1090],{},"SataCdRomAuthenticationExInitialize",[41,1092,1094],{"as":1093},"hex 8 double-zero 9 9 8 D 0",[14,1095,1096],{},"0x800998D0"," has its DVD drive threshold set to ",[41,1099,1101],{"as":1100},"hex F F",[14,1102,1103],{},"0xFF"," so any drive passes, ",[14,1106,1107],{},"SataDiskAuthenticateDevice",[41,1109,1111],{"as":1110},"hex 8 0 1 5 D 9 D 8",[14,1112,1113],{},"0x8015D9D8"," accepts third-party hard drives, and ",[14,1116,1117],{},"VdDisplayFatalError",[41,1119,1121],{"as":1120},"hex 8 double-zero 9 9 2 B 4",[14,1122,1123],{},"0x800992B4"," kills the E66 screen that would otherwise brick the interface on a drive mismatch.",[10,1126,1127,1128,75,1131,1137,1138,1141],{},"Then DashLaunch. 176 bytes go into the body of ",[14,1129,1130],{},"XeKeysConsoleSignatureVerification",[41,1132,1134],{"as":1133},"hex 8 0 1 0 B F 4 0",[14,1135,1136],{},"0x8010BF40",", a function nothing calls any more, containing a polling loop and the string ",[14,1139,1140],{},"\\Device\\Flash\\launch.xex",". Three kernel functions are redirected into it, so whichever one runs first at boot, DashLaunch loads.",[10,1143,1144,1145,1147,1148,1152,1153,1155,1156,1158],{},"The last thing XeUnshackle does is undo BadUpdate. It restores the original ",[14,1146,841],{}," dispatch entry, closing the syscall ",[41,1149,1150],{"as":845},[14,1151,848],{}," backdoor it arrived through, and puts back the original branches that BadUpdate's crude ",[14,1154,16],{}," patches replaced, because freeboot's version of the same check is more careful and DashLaunch depends on the careful one to load patched retail-signed binaries. The peek and poke expansion and the ",[14,1157,1005],{}," backdoor are all the privileged access it needs from here, so the door it came in by gets shut behind it.",[10,1160,1161,1162,1040,1165,1171],{},"One more, easy to miss and annoying to debug: ",[14,1163,1164],{},"HvpProtectedFlags",[41,1166,1168],{"as":1167},"hex 1 6 6 1 8",[14,1169,1170],{},"0x16618"," is zeroed. Those flags collect security violations during boot, before any of this has run, and leaving them set means the DVD checks keep reporting violations and every disc comes back unreadable.",[10,1173,1174,1175,1178],{},"Then it prints the things you actually came for: the CPUKey out of fuse lines 3 and 5, the DVDKey from the keyvault, the board revision, and a 32KB dump of the first bootloader. It plays a success animation. You press Back, DashLaunch reads ",[14,1176,1177],{},"launch.ini",", and Aurora comes up.",[28,1180,1182],{"id":1181},"the-end","The end",[10,1184,1185],{},"None of it touches the NAND. The hypervisor, the kernel and everything above them are loaded into DRAM from flash on every boot, and every patch here is written to DRAM. Power off and the console is retail again, which also means power on and you start at rung one: boot without touching the controller, let the avatar render, watch the front panel LEDs cycle while the race runs, wait for solid green, press Back.",[10,1187,1188],{},"Usually a few seconds. Sometimes five minutes and a restart. That's the price of not soldering anything.",[10,1190,1191,1192,1197,1198,1202],{},"I've skipped the LZX format itself, why block 14's decompressed output happens to contain a useful instruction pair at all, and everything about RGH3, which is the same destination reached by putting a wire on the board instead. Grimdoomer's ",[100,1193,1196],{"href":1194,"rel":1195},"https:\u002F\u002Fgithub.com\u002Fgrimdoomer\u002FXbox360BadUpdate",[104],"BadUpdate"," and Byrom90's ",[100,1199,912],{"href":1200,"rel":1201},"https:\u002F\u002Fgithub.com\u002FByrom90\u002FXeUnshackle",[104]," repositories are where the real detail lives, and both are more readable than they have any right to be.",[1204,1205,1206],"style",{},"html pre.shiki code .s_Mgl, html code.shiki .s_Mgl{--shiki-default:var(--color-body)}html pre.shiki code .sQTeU, html code.shiki .sQTeU{--shiki-default:var(--code-punct)}html pre.shiki code .sneuw, html code.shiki .sneuw{--shiki-default:var(--code-literal)}html pre.shiki code .sAzwi, html code.shiki .sAzwi{--shiki-default:var(--code-comment);--shiki-default-font-style:italic}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sxxka, html code.shiki .sxxka{--shiki-default:var(--code-function)}html pre.shiki code .sH9Dd, html code.shiki .sH9Dd{--shiki-default:var(--color-text)}",{"title":184,"searchDepth":219,"depth":219,"links":1208},[1209,1210,1211,1212,1213,1214,1215,1216,1217],{"id":30,"depth":219,"text":31},{"id":60,"depth":219,"text":61},{"id":113,"depth":219,"text":114},{"id":283,"depth":219,"text":284},{"id":323,"depth":219,"text":324},{"id":505,"depth":219,"text":506},{"id":765,"depth":219,"text":766},{"id":911,"depth":219,"text":912},{"id":1181,"depth":219,"text":1182},"A console that only runs signed code has to do the checking somewhere. On the Xbox 360 it happens in the hypervisor, 64KB of code living in memory the CPU keeps encrypted, which nothing in usermode is supposed to read or write. One function in there verifies an RSA signature and returns a boolean. Overwrite it with li r3, 1 and every signature on the machine is valid.","md",{},true,"\u002Fposts\u002Fxb360-exploit-chain",{"title":5,"description":1218},"posts\u002Fxb360-exploit-chain","A Halo Reach console, a crafted avatar name, and the one instruction that makes every signature valid.","published","LWMOPqFRNEdWQ6WNgAQt7bgzcdZRczv8VJwuSWl3gUI",1790346826780]