NetHack 3.6.6, or, How to Glitch NetHack
The NetHack DevTeam released NetHack 3.6.6 on March 8, 2020, primarily to address CVE-2020-5254, which I reported to them on March 3. Although most of my report was focused on security--and indeed I was able to get full remote code execution (RCE) on pre-release NetHack 3.7 (but not 3.6.5)--I actually thought the more interesting finding was how to exploit the bug as a glitch.
NetHack 3.6.1 introduced the hilite_status option. One version of this option allows a user to configure the status highlights for status conditions and afflictions. This is handled by parse_condition in botl.c. In this function, the specified color is used as an index into g.cond_hilites (merely cond_hilites prior to NetHack 3.7):
As the return value of match_str2clr is then directly used to index g.cond_hilites, this allows an attacker to OR conditions_bitmask to a location in memory outside the bounds of g.cond_hilites. The exploitability of this vulnerability is highly constrained, however:
I employed two tricks to make it easier to write the POC. First, I made the PIC global offset table (GOT) writable by disabling PIE on the link line (-no-pie). Then, I put dosh() in its own section with __attribute__((section(".dosh"))) and located that section at 0x7fffcf using -Wl,--section-start=.dosh=0x7fffcf on the link line. On my machine this lined up the child(0) call at precisely address 0x7fffff. This meant I didn’t need to worry about changing a function pointer that started with a 1 bit in a location where the new, malicious value needed a 0 bit. Recall that the memory write is an OR, so we cannot turn off any bits. In my case, the malicious value was now all 1 bits.
With the GOT now writable and located in memory before g.cond_hilites, I had many available function pointers available to corrupt. I chose the GOT entry for the atol function because I knew that it was rarely used but could easily be triggered when I wanted by adding an OPTIONS=term_rows line to my exploit. The final POC can be found here.
I was not able to write a POC for NetHack 3.6.5. The 13-bit limitation is extremely challenging. For example, dosh() is too far way from the GOT to redirect GOT function pointers to it by just modifying the low 13 bits. The most promising avenue I found was to redirect a PIC call from one innocent standard library function like atol to something more damaging like execv or popen. These are all grouped together in the PLT and GOT, so only a few low bits need to be changed to complete the redirection. A line like OPTIONS=term_rows:/bin/sh would then trigger the exploit. The problem was that while I could control the first parameter to execv/popen, I could not control the second and so these calls always failed with an invalid second argument. NetHack 3.7 adds system(3) to the PLT from the Lua library. This would have worked because system only takes one argument, but again system is not available in NetHack 3.6.5. Nevertheless, it’s still possible to corrupt memory on NetHack 3.6.5 and a very talented and determined attacker may eventually be able to write an exploit.
NetHack 3.6.1 introduced the hilite_status option. One version of this option allows a user to configure the status highlights for status conditions and afflictions. This is handled by parse_condition in botl.c. In this function, the specified color is used as an index into g.cond_hilites (merely cond_hilites prior to NetHack 3.7):
g.cond_hilites[coloridx] |= conditions_bitmask;Prior to this line, coloridx is set to the return value of match_str2clr in options.c. match_str2clr normally does a string match to determine the color identified, but curiously will also parse a number if provided:
if (i == SIZE(colornames) && (*str >= '0' && *str <= '9')) c = atoi(str);The callers of match_str2clr check for values >= CLR_MAX, but never check for values less than zero. While trivially specifying a negative number will fail the (*str >= '0' && *str <= '9') test, specifying a number greater than 2^31 but less than 2^32 will wrap around to negative and this negative number will not be caught by either match_str2clr or its callers.
As the return value of match_str2clr is then directly used to index g.cond_hilites, this allows an attacker to OR conditions_bitmask to a location in memory outside the bounds of g.cond_hilites. The exploitability of this vulnerability is highly constrained, however:
- The target address must be before cond_hilites / g.cond_hilites (because coloridx is negative). The heap and stack cannot be modified.
- The target address must be writable. Read-only data and text (code) cannot be modified.
- In NetHack 3.6.5, cond_hilites is linked from botl.o which appears early in the link line. Therefore cond_hilites appears early in the data segment, and few target addresses are available. The change to g.cond_hilites in NetHack 3.7 relaxes this.
- The operation is an OR, meaning bits in the target address can only be turned on.
- The bitmask is limited to the low 13 bits of the word in NetHack 3.6.5. This is greatly expanded to the low 30 bits in NetHack 3.7 with the addition of new BL_MASK_ constants.
- On my machine, sizeof(cond_hilites[0]) is 8 even though its type is unsigned long. That means the compiler is adding 4 bytes of padding for every cond_hilites[i], so when maliciously modifying &cond_hilites[-1], &cond_hilites[-2], etc., only the first four bytes are modifiable via the vulnerability. A 32-bit build would not have this constraint.
RCE
Despite these constraints, I was able to write a proof-of-concept (POC) exploit that launched a shell in NetHack 3.7. My POC relies on the 30-bit expansion and would not work as-is in NetHack 3.6.5. NetHack conveniently includes a function dosh() in sys/unixunix.c that launches a shell, although the player command for that function is typically disabled. The POC corrupts an arbitrary function pointer to point to the middle of dosh(), after the sysopt checks but before the call to child(0):
int dosh() { char *str; #ifdef SYSCF if (!sysopt.shellers || !sysopt.shellers[0] || !check_user_string(sysopt.shellers)) { /* FIXME: should no longer assume a particular command keystroke, and perhaps ought to say "unavailable" rather than "unknown" */ Norep("Unknown command '!'."); return 0; } #endif ---> if (child(0)) {Once adjusted, the POC causes the corrupted function pointer to be dereferenced and the function invoked.
I employed two tricks to make it easier to write the POC. First, I made the PIC global offset table (GOT) writable by disabling PIE on the link line (-no-pie). Then, I put dosh() in its own section with __attribute__((section(".dosh"))) and located that section at 0x7fffcf using -Wl,--section-start=.dosh=0x7fffcf on the link line. On my machine this lined up the child(0) call at precisely address 0x7fffff. This meant I didn’t need to worry about changing a function pointer that started with a 1 bit in a location where the new, malicious value needed a 0 bit. Recall that the memory write is an OR, so we cannot turn off any bits. In my case, the malicious value was now all 1 bits.
With the GOT now writable and located in memory before g.cond_hilites, I had many available function pointers available to corrupt. I chose the GOT entry for the atol function because I knew that it was rarely used but could easily be triggered when I wanted by adding an OPTIONS=term_rows line to my exploit. The final POC can be found here.
I was not able to write a POC for NetHack 3.6.5. The 13-bit limitation is extremely challenging. For example, dosh() is too far way from the GOT to redirect GOT function pointers to it by just modifying the low 13 bits. The most promising avenue I found was to redirect a PIC call from one innocent standard library function like atol to something more damaging like execv or popen. These are all grouped together in the PLT and GOT, so only a few low bits need to be changed to complete the redirection. A line like OPTIONS=term_rows:/bin/sh would then trigger the exploit. The problem was that while I could control the first parameter to execv/popen, I could not control the second and so these calls always failed with an invalid second argument. NetHack 3.7 adds system(3) to the PLT from the Lua library. This would have worked because system only takes one argument, but again system is not available in NetHack 3.6.5. Nevertheless, it’s still possible to corrupt memory on NetHack 3.6.5 and a very talented and determined attacker may eventually be able to write an exploit.
Glitching
While I have focused on the security aspect of this bug, the vulnerability can also be used—and much more easily—as a glitch. Key data arrays like objects appear before g.cond_hilites in writable memory and can be modified to change game properties. Everything is offset based, so there are no concerns about ASLR, PIE, or the precise values of any function pointers. A glitcher would still be limited to only turning on bits, not turning them off. I have provided a sample .nethackrc that sets the AC value of a cloak of magic resistance to 127. It is portable to either NetHack 3.6.5 or 3.7. Start a new Wizard and see how invincible you are! Many other glitches are possible.
Comments
Post a Comment