c - cmd.exe parsing bug leads to other exploits? -


before continue, please note rather lengthy infosec notice windows command prompt have found bug might exploitable using simple batch files. bug prevalent in versions of windows 2000 , works on 64 , 32-bit machines, , being it's batch file parsing bug, requires no additional software install (cmd.exe default part of windows) , can initiated user level of privilege (assuming can run cmd.exe , parse batch files). included in summary assembly of bug happens (with breakdown of code flow show why). isn't rce level bug (not i've been able find yet), dos type , require user run (or have startup item), given simplicity of , ubiquity of windows systems, figured deserved second look. please note i'm not responsible if run of these bug enabling batch files , crash system (task manager , kill pid works on runaway script in case run them).

tldr: batch file line ^ nul<^ cause massive memory leak, while batch file line ^|^ causes command prompt crash due 'infinite recursion'. these behaviors can lead interesting batch 'hacks' on windows system (win2k+) , reason logic error in cmd.exe batch file processing (see assembly , pseudo-c code below more).

background

while answering question on superuser (link) came across interesting anomaly how command interpreter parses batch files. if caret character (^) last character of file, memory leak/cmd.exe crash can happen. caret has last character of file , cannot followed \n (the line feed character), though \r (the carriage return character) ok it's stripped before caret parsed; nothing can trail though cause parser proceed (since ^\r\t become ^\t \t "ignored"). last note carriage return characters being ok bug still happen makes little more interesting because 'fakes' newline in text editors, , in notepad fooled thinking there's newline @ end ('technically' it's carriage return or 'old mac' new line).

after doing quick investigations found ^ @ end of file can lead memory leak or can crash command prompt (more command.com or cmd.exe program), found specific batch files (and sequences thereof) can lead interesting behavior. further investigations lead me other people noting similar issues; a stack overflow question user noted memory leak , message board topic @ ss64.com noted other interesting behaviors caret @ eof. stack overflow question helped confirm suspensions it's infinite loop type situation, didn't try dive deeper that. ss64.org topic discussed various ways make command prompt crash, failed explain kind of crash or why.

naturally lead me question happening , why (and exploited). answers mixed , fix quite simple (at least seems 'simple' assembly inspecting). found there few combinations of batch file trickery can yield memory leak, how fast or slow depends on put in batch file (not how many carets, sequences of pipes , interestingly line length [more later]) , regardless of crash or memory leak, parser code in tight loop on single thread, cpu usage increases (single core cpu or pushed single affinity, yields average of 98+% cpu usage).

the crashing

to crash command prompt simple enough, batch file no newlines containing following characters ^&^ (or ^|^), cause command prompt batch being processed on crash error code of 0xc00000fd, stack/frame overflow due infinite recursion. confirmed 'infinite loop' scenario, didn't quite explain memory leak (or why matter crashing due infinite recursion??). extent, started examine of simplest ways produce memory leak; turns out, 2 byte batch file needed consume 100% cpu , eat memory (albeit @ insanely slow rate, ~8k/5s on 2.2ghz i7).

eat memory

the batch file used test contained following hex bytes:

0x01 0x5e 

0x5e caret character (^) , 0x01 ascii character code 'start of header'. first hex code half significant in bug, can't null (0x00), \r, |, or & these characters cause batch file exit via normal termination (i.e. 'invalid command detected'). used 0x01 demonstrate didn't have valid command (or printable character matter) induce bug, 2 byte batch file containing a^ suffice too.

doing other testing, found quickest way eat memory (and simplest) batch file following line: ^ nul<^

... having inadvertently run myself couple of times, please aware cripple 64-bit system rather quickly; took 20 seconds eat of remaining 14gb of ram (16gb total) on quad core (htt, 8 effective cores) i7, rendered machine useless while trying page (i had restart machine). running 'quick' version on 32-bit system ended command parser out of memory error exhausts 32-bit 2gb limit. doesn't crash command prompt, instead seems there's check ensure memory can mallocd, , when can't aborts batch processing.

chained bug fun

note though of these 'batch exploits' can 'chained' together, on 32-bit system (assuming had 4gb of ram no pae), 1 run following command preform dos on it: cmd eat_mem.bat | eat_mem.bat (this launch 2 command parsers , exhaust 2gb each).

another interesting variation chain 'crash' batch files together; example, assuming there's file called crash.bat , contained our 'crash exploit' above (^&^), following: cmd crash.bat | crash.bat , note interesting behavior. particular 1 causes output of cmd crash.bat written pipe has crashed (due parser bug) , gives wrote non-existent pipe errors when attempt ctrl+c out of it; interestingly enough though still allows execute commands, typing notepad , hitting enter launch notepad. have explored more possible pipe hijacking don't have fruitful yet (more on 'exploits' below).

8k buffer

as stack overflow question/answer had alluded to, more ^ had on line, faster eat memory (aside 'quick' version found). through more experiments, found 'line length' can have interesting effects on bug well. if last line of batch file has ^ @ end of , 8192 bytes long (including caret), bug fails...yet below , above multiples of 8192 cause bug. more bytes in string, faster memory consumed (up multiple of 8192). 8192 number interesting our memory leak 8k (or in larger chunks thereof).

it should noted file size irrelevant bug , because of can put in innocuous batch file. test put 'quick' version (^ nul<^) @ end of batch file use compile lot of legacy code. compiler script under 21kb recursive calls , multiple functions (to allow me compile things) , putting 'buggy' line @ end of file allowed me run compile script normal , complete normal, when hit 'bug' spun cpu , ate memory.

if unassuming software engineer given 'simple' compiler script (with call :eof randomly in there 'forcing' bug), wouldn't think twice if script kicked cpu, , if wasn't paying attention task manager not see how ram eaten , have restart (not fun).

dig deeper

from here decided higher level inspections see happening command prompt while leak happening. opened process explorer , procmon tools inspect happening in processes (on stack pe) , system (via procmon). procmon confirmed 8k (more 8191 bytes) leak call readfile 8191 byte buffer , pe confirmed calls kernel readfile, though wasn't sure why batch file being read?

interestingly, bug allows batch files modified while running , re-parsed (up 8k of 'parsing', line feeds , all). should noted contents can modified , commands processed, i've noticed unless there's lot of other commands (more line feeds before final ^), timing can play part in success of modified batch 'hack' though happens more not (i.e. works of time).

these batch exploits lead me wonder if same done programmatically; first try simple c program had system("crash.bat"); called batch file containing ^&^ in it. launched separate cmd.exe process subsequently crashed returning control program (this expected). switched simple memory leak file (contained a^) , ran program again, time cmd.exe ran , started spin (as expected), when pressed ctrl+c, cmd.exe process orphaned , control returned program leaving me have end rogue cmd.exe process through task manager. tried using lines file directly in program (i.e. calling effect of system("^ nul<^");) got valid results (valid in case meaning command deemed 'invalid' command parser). tried variations of how hook process or take advantage of stack/frame overflow error, due cmd.exe being parser itself, of more obvious exploits won't easy. extent, decided break open cmd.exe process , inspect assembly verify leaking , better understand happening , how exploited (if @ all).

the code (cmd.exe)

i used visual studio 2010 , attached running cmd.exe process, using 'simple' memory leak batch file (a^) paused process step through assembly while parser in error state. after inspecting assembly , using process explorer verify module in during debug, able trace lot of code flow , found parser looked ^ character (hex 0x5e). following assembly flow can see issue lie.

note lot of 'main' , 'parsing' function assembly has been removed brevities sake (relevant code posted full dump of i've found available upon request [1.2 mb asm dump comments]):

; start cmd.exe asm code flow   000000004a161d50 ; { start main  (more init frame/code here) ; { start loop 000000004a161fc1 3b f5                cmp         esi,ebp   ; mem leak here?? esi == #bytes, ebp == our 8191 buffer size number 000000004a161fc3 7d 29                jge         000000004a161fee   000000004a161fc5 0f b7 44 24 20       movzx       eax,word ptr [rsp+20h]   000000004a161fca 48 8d 54 24 70       lea         rdx,[rsp+70h]   000000004a161fcf 48 8d 4c 24 20       lea         rcx,[rsp+20h]   000000004a161fd4 66 89 03             mov         word ptr [rbx],ax   000000004a161fd7 48 83 c3 02          add         rbx,2   000000004a161fdb ff c6                inc         esi  000000004a161fdd 48 89 5c 24 60       mov         qword ptr [rsp+60h],rbx   000000004a161fe2 e8 99 04 00 00       call        000000004a162480  ; main_parser_fn 000000004a161fe7 3d 00 01 00 00       cmp         eax,100h   000000004a161fec 75 d3                jne         000000004a161fc1 ; } end loop 000000004a162058 ; } end main?? function here  ; { start main_parser_fn 000000004a162480 48 8b c4             mov         rax,rsp   000000004a162483 48 89 58 08          mov         qword ptr [rax+8],rbx   000000004a162487 48 89 70 10          mov         qword ptr [rax+10h],rsi   000000004a16248b 48 89 78 18          mov         qword ptr [rax+18h],rdi   000000004a16248f 4c 89 60 20          mov         qword ptr [rax+20h],r12   000000004a162493 41 55                push        r13   000000004a162495 48 83 ec 20          sub         rsp,20h   000000004a162499 48 8b da             mov         rbx,rdx   000000004a16249c 48 8b f9             mov         rdi,rcx   000000004a16249f e8 bc fb ff ff       call        000000004a162060  ; get_next_char 000000004a1624a4 33 f6                xor         esi,esi   000000004a1624a6 66 89 07             mov         word ptr [rdi],ax   000000004a1624a9 39 35 d1 98 03 00    cmp         dword ptr [4a19bd80h],esi   000000004a1624af 0f 85 f1 75 01 00    jne         000000004a179aa6   000000004a1624b5 0f b7 17             movzx       edx,word ptr [rdi]   000000004a1624b8 41 bd 3c 00 00 00    mov         r13d,3ch   000000004a1624be 8b ca                mov         ecx,edx   000000004a1624c0 45 8d 65 ce          lea         r12d,[r13-32h]   000000004a1624c4 3b d6                cmp         edx,esi   000000004a1624c6 74 98                je          000000004a162460   000000004a1624c8 41 2b cc             sub         ecx,r12d   000000004a1624cb 74 93                je          000000004a162460   000000004a1624cd 83 e9 1c             sub         ecx,1ch   000000004a1624d0 74 91                je          000000004a162463   000000004a1624d2 83 e9 02             sub         ecx,2   000000004a1624d5 0f 84 61 ff ff ff    je          000000004a16243c   000000004a1624db 83 e9 01             sub         ecx,1   000000004a1624de 0f 84 6a ff ff ff    je          000000004a16244e   000000004a1624e4 83 e9 13             sub         ecx,13h   000000004a1624e7 0f 84 76 ff ff ff    je          000000004a162463   000000004a1624ed 83 e9 02             sub         ecx,2   000000004a1624f0 0f 84 6d ff ff ff    je          000000004a162463   000000004a1624f6 83 e9 02             sub         ecx,2   000000004a1624f9 0f 84 28 ff ff ff    je          000000004a162427  ; quote_parse_fn 000000004a1624ff 41 3b cd             cmp         ecx,r13d  ; check if it's '<' 000000004a162502 0f 84 5b ff ff ff    je          000000004a162463   000000004a162508 83 fa 5e             cmp         edx,5eh  ; start '^' parse 000000004a16250b 0f 84 86 dc 00 00    je          000000004a170197  ; caret_parse 000000004a162511 83 fa 22             cmp         edx,22h   000000004a162514 0f 84 5e 2f 00 00    je          000000004a165478   000000004a16251a f6 03 23             test        byte ptr [rbx],23h   000000004a16251d 0f 84 e5 00 00 00    je          000000004a162608   000000004a162523 0f b7 0f             movzx       ecx,word ptr [rdi]   000000004a162526 ff 15 54 6c 02 00    call        qword ptr [4a189180h]   000000004a16252c 3b c6                cmp         eax,esi   000000004a16252e 0f 85 c0 10 00 00    jne         000000004a1635f4   000000004a162534 33 c0                xor         eax,eax   000000004a162536 48 8b 5c 24 30       mov         rbx,qword ptr [rsp+30h]   000000004a16253b 48 8b 74 24 38       mov         rsi,qword ptr [rsp+38h]   000000004a162540 48 8b 7c 24 40       mov         rdi,qword ptr [rsp+40h]   000000004a162545 4c 8b 64 24 48       mov         r12,qword ptr [rsp+48h]   000000004a16254a 48 83 c4 20          add         rsp,20h   000000004a16254e 41 5d                pop         r13   000000004a162550 c3                   ret   ; } end main_parser_fn  ; { start get_next_char 000000004a162060 ff f3                push        rbx   000000004a162062 48 83 ec 20          sub         rsp,20h   000000004a162066 48 8b 05 0b c2 02 00 mov         rax,qword ptr [4a18e278h]   000000004a16206d 8b 0d ed 9b 03 00    mov         ecx,dword ptr [4a19bc60h]   000000004a162073 33 db                xor         ebx,ebx   000000004a162075 66 39 18             cmp         word ptr [rax],bx  000000004a162078 74 29                je          000000004a1620a3  ; when bx=0  000000004a16207a 66 83 38 0d          cmp         word ptr [rax],0dh ; 0d = \r 000000004a16207e 0f 84 69 0e 00 00    je          000000004a162eed   000000004a162084 3b cb                cmp         ecx,ebx   000000004a162086 0f 85 46 7a 01 00    jne         000000004a179ad2   000000004a16208c 0f b7 08             movzx       ecx,word ptr [rax]   000000004a16208f 48 83 c0 02          add         rax,2   000000004a162093 48 89 05 de c1 02 00 mov         qword ptr [4a18e278h],rax   000000004a16209a 66 8b c1             mov         ax,cx   000000004a16209d 48 83 c4 20          add         rsp,20h   000000004a1620a1 5b                   pop         rbx   000000004a1620a2 c3                   ret   ; } end get_next_char 000000004a1620a3 e8 18 00 00 00       call        000000004a1620c0   000000004a1620a8 48 8b 05 c9 c1 02 00 mov         rax,qword ptr [4a18e278h]   000000004a1620af 8b 0d ab 9b 03 00    mov         ecx,dword ptr [4a19bc60h]   000000004a1620b5 eb c3                jmp         000000004a16207a    000000004a1620c0  ; starts large chunk of code more parsing (as calls ; criticalsection code) it's omitted because issues prevalent in ; rest of code pertaining 'caret_parser' not returning properly. 'memory leak' ; in section code (an 8k buffer read that's checked in main loop).   ; { start quote_parse 000000004a162463 f6 03 22             test        byte ptr [rbx],22h   000000004a162466 0f 85 9c 00 00 00    jne         000000004a162508   000000004a16246c b8 00 01 00 00       mov         eax,100h   000000004a162471 e9 c0 00 00 00       jmp         000000004a162536   ; } end quote_parse  ; { start caret_parse 000000004a170197 f6 03 22             test        byte ptr [rbx],22h ; check if '"'  000000004a17019a 0f 85 71 23 ff ff    jne         000000004a162511  ; if char == '"' 000000004a1701a0 e8 bb 1e ff ff       call        000000004a162060  ; get_next_char 000000004a1701a5 66 89 07             mov         word ptr [rdi],ax  ; ax 0 if eof 000000004a1701a8 66 41 3b c4          cmp         ax,r12w  ; r12w 0x0a ('\n') here, eol check (fail in eof case) 000000004a1701ac 0f 85 82 23 ff ff    jne         000000004a162534  ; jump 'main_parser_fn' <--error 000000004a1701b2 e9 0b 99 00 00       jmp         000000004a179ac2  ; == call 000000004a162060 (get_next_char) 000000004a1701b7 33 c9                xor         ecx,ecx   000000004a1701b9 e8 22 1b ff ff       call        000000004a161ce0   ; } end caret_parse  ; end cmd.exe asm code flow  

based on assembly, able ascertain cmd.exe parsing code doing (psuedo-c code):

void main_parser_fn() {      /* 'some_read_condition' based on lot of things        interestingly 1 of them 8k buffer size; asm        shows 8191 byte buffer reading/parsing,        couldn't ascertain why having buffer divisible        8192 bytes in line buffer 'ok' more or        less causes continuation (mem leak)?? */     while (some_read_condition) {         // allocate 8k buffer appropriately         x = get_next_char();         if (x == '|' || x == '&') {             main_parser_fn();         }         if (x == '^') {             get_next_char(); // error here             // possible fix:             // if (get_next_char() == 0) { abort_batch(); }             continue;         }         // free buffer (never here due eof error)     } } 

this (of course) rough approximation based on input/output , assembly, seems code next character after caret issue resides.

the bug explained

the bug when caret detected, next character read file (to 'escaped'). if caret last character of file, results in logic error, when call get_next_char made, file pointer incremented one; in case puts passed eof. since eof ignored when command parser continues read next input, 'resets' file pointer due eof+1 error. in case, putting file pointer eof+1 causes pointer @ large negative number, , since file's can't go below 0, it's file pointer reset 0 , parsing continues beginning of file.

this explains memory leaks , why it's 8k (an 8k 'read buffer' being filled), , can explain recursion issue. when | or & detected in batch file, it's recursively parsed out, , since there's eof bug recursion becomes infinite no return paths can had.

edit: comments have pointed out, , further research has shown, caret doesn't have @ end of file bug happen, i'm investigating (and breaking down more of asm) see if there other scenario's , why/how it's happening.

the fix

it seems simple fix check eof on read of next character when parsing caret. check (and subsequent 'proper batch abort' functionality) fix memory leak issue infinite recursion.

exploits?

after inspecting process more , thinking possible exploits, don't see being anywhere serious ms14-019, given ease of use/implementation (and relative ease of fixing it), consider 'medium' level alert 'exploits' require user run batch file, , 'obvious' avenues trying exploit stack/frame overflow error or launch shell code through batch file prove more difficult on many other exploits yield more 'fruitful' outcome vs innocuous batch bug. though can see being used in dos attack it's easy write 1 given it's 7 bytes (^ nul<^) , potentially distributed , 'setup' rather easy.

here's simple vbscript used write , launch 'killer' batch file (and silently)

createobject("scripting.filesystemobject").createtextfile("killer.bat", true).write("^ nul<^") & vbcr createobject("wscript.shell").run "killer.bat", 0, false 

that create batch file named killer.bat ^ nul<^\r in , run it, put in .vbs file , run @ startup, or put in excel macro , run.

echo|set /p="^ nul<^" > killer.bat 

that line command line equivalent create 'killer' batch file (doing normal echo file result in \r\n @ end of file , bug won't present).

as proof of concept, created vbscript (as other batch file tests), , put them in startup folder , registry well. when logged in, greeted command prompt when using batch files , nothing when using vbscript, , in few seconds system came halt , unusable because script had consumed ram. scripts can ended via killing running cmd.exe process, because work fast, might not have enough time launch task manager before consumes of ram. removal through safe mode 'cure' this, not 'fix'.

i envision scenario unsuspecting admin going run backup.bat script unfortunate bug in , inadvertently bringing down server. or fun might had at/schtasks.exe commands on unsecured system.

granted don't see these 'exploits' leaving realm of dos or prank.

i'm still inspecting various avenues piping , redirection of 'broken' scripts possibly lead rce, stands simplest attack vector dos attack 'quick' file. i've tested bug on windows 98, 2000, xp, vista, 7, 8, , server variations (to include 32 , 64 bit flavors). windows 98 command prompt not affected this, every version above (to include command.com since uses cmd.exe parse batch files). out of curiosity tested on reactos , wine (neither of had issue).

questions (edited after more research)

as stated, don't see bug being "exploitable" more denial of service "attack" (or prank on co-worker/friend), got me thinking in general frame overflow's , memory leaks, more if exploitable (just in general).

my experience , understanding software engineering/hacker perspective tells me memory leaks or frame overflow's potentially exploited on older os (say windows 98/2000/xp or older versions of *nix?) don't have protections in place (like use of nx bit or aslr) given right conditions, haven't been able find research these areas aside 'normal' attack vectors (stack based buffer overflows) or general documentation on these things 'are' (i.e. 'white paper' discussions on frame overflow or memory leak , nx/aslr are) , not on 'why' can't.

i've been experimenting injecting thread or other method running cmd.exe process run tests on , analyze frame overflow , memory leak occurs within (as general fun can had bug, using createprocess , emptyworkingset general fun) , know won't 'get anywhere' particular bug, got me thinking (or on thinking rather): has there ever been frame overflow exploit or memory leak exploit in wild or there documentation might able read explains why (more specifically/technically) it's not feasible?

i understand 'why' more specificity, 'the eip register protected because xyz...' opposed 'no it's not possible', helpful; know each architecture different , might asking more details had in answer, links or discussion points reference helpful since can't seem find in reference this.

i've been swimming in assembly, fresh perspective helps :)

note: sent email (on 04/25/2014) microsoft bug , have responded saying have forward development team , investigating, no fix in security bulletin planned (agree them on nothing yet indicates it's serious flaw). edit should more updates come.


Comments

Popular posts from this blog

java - Intellij Synchronizing output directories .. -

git - Initial Commit: "fatal: could not create leading directories of ..." -