
Durante l'incontro 0x0A DC7831 Il 16 febbraio abbiamo presentato una relazione sui principi fondamentali dell'emulazione del codice binario e sul nostro sviluppo – l'emulatore di piattaforme hardware. .
In questo articolo descriveremo come avviare il firmware del dispositivo nell'emulatore, dimostreremo l'interazione con il debugger e realizzeremo una piccola analisi dinamica del firmware.
Contesto
Tanto tempo fa, in una galassia lontana lontana
Qualche anno fa, nel nostro laboratorio è nata la necessità di studiare il firmware di un dispositivo. Il firmware era compresso, veniva decompresso da un bootloader. Lo faceva in un modo piuttosto complicato, trasferendo i dati in memoria più volte. Inoltre, il firmware interagiva attivamente con la periferia. E tutto ciò su un nucleo MIPS.
Gli emulatori disponibili non ci hanno soddisfatto per motivi oggettivi, e volevamo comunque eseguire il codice. Così abbiamo deciso di creare il nostro emulatori, che faccia il minimo e permetta di estrarre il firmware principale. Abbiamo provato e ci siamo riusciti. Ci siamo chiesti, e se aggiungessimo le periferiche, per eseguire anche il firmware principale? Non è stato troppo doloroso, e anche questo è andato a buon fine. Abbiamo pensato di nuovo e deciso di fare un emulatore completo.
Alla fine è nato un emulatore di sistemi di calcolo. .

Perché Kopycat?
C'è un gioco di parole.
- copycat (inglese, sost. [ˈkɒpɪkæt]) — imitatore, emulatore
- cat (inglese, sost. [ˈkæt]) — gatto, gatta — animale domestico preferito di uno degli autori del progetto
- La lettera «K» — dal linguaggio di programmazione Kotlin.
Kopycat
Nella creazione dell'emulatore ci si è posti obiettivi ben definiti:
- possibilità di creare rapidamente nuove periferiche, moduli, unità di elaborazione;
- possibilità di assemblare un dispositivo virtuale da diversi moduli;
- possibilità di caricare in memoria del dispositivo virtuale qualsiasi dato binario (firmware);
- possibilità di lavorare con snapshot (istantanee dello stato del sistema);
- possibilità di interagire con l'emulatore tramite un debugger integrato;
- un linguaggio moderno e piacevole per lo sviluppo.
Alla fine, è stato scelto Kotlin per l'implementazione, l'architettura a bus (che consente ai moduli di comunicare attraverso bus virtuali), JSON come formato di descrizione del dispositivo, e GDB RSP come protocollo di interazione con il debugger.
Lo sviluppo dura da poco più di due anni ed è in corso attivamente. In questo periodo sono stati implementati i core dei processori MIPS, x86, V850ES, ARM e PowerPC.
Il progetto sta crescendo, ed è tempo di presentarlo al pubblico. Un'analisi dettagliata del progetto sarà fornita successivamente, per ora concentriamoci sull'uso di Kopycat.
Per i più impazienti, è possibile scaricare la versione promozionale dell'emulatore da .
Rinoceronte nell'emulatore
Ricordiamo che in precedenza, per la conferenza SMARTRHINO-2018, è stato creato un dispositivo di test "Rinoceronte" per formare competenze di reverse engineering. Il processo di analisi statica del firmware è stato descritto in .
Ora proveremo ad aggiungere un po' di "dinamica" e avvieremo il firmware nell'emulatore.
Avremo bisogno di:
1) Java 1.8
2) Python e il modulo per utilizzare Python all'interno dell'emulatore. Puoi trovare la versione WHL del modulo Jep per Windows .
Per Windows:
1)
2)
Per Linux:
1) socat
Come client GDB puoi utilizzare Eclipse, IDA Pro o radare2.
Come funziona?
Per eseguire il flashing nell'emulatore, è necessario "assemblare" un dispositivo virtuale, che funge da analogia di un dispositivo reale.
Il dispositivo reale ("rinoceronte") può essere mostrato nello schema strutturale:

L'emulatore ha una struttura modulare e il dispositivo virtuale finale può essere descritto in un file JSON.
JSON di 105 righe
{
"top": true,
// Il nome del plugin deve essere lo stesso del nome del file (o percorso completo dall'inizio della libreria)
"plugin": "rhino",
// Directory in cui il plugin colloca
"library": "user",
// Parametri del plugin (parametri del costruttore se versione jar-plugin)
"params": [
{ "name": "tty_dbg", "type": "String"},
{ "name": "tty_bt", "type": "String"},
{ "name": "firmware", "type": "String", "default": "NUL"}
],
// Porte esterne del plugin
"ports": [ ],
// Bus interni del plugin
"buses": [
{ "name": "mem", "size": "BUS30" },
{ "name": "nand", "size": "4" },
{ "name": "gpio", "size": "BUS32" }
],
// Componenti interni del plugin
"modules": [
{
"name": "u1_stm32",
"plugin": "STM32F042",
"library": "mcu",
"params": {
"firmware:String": "params.firmware"
}
},
{
"name": "usart_debug",
"plugin": "UartSerialTerminal",
"library": "terminals",
"params": {
"tty": "params.tty_dbg"
}
},
{
"name": "term_bt",
"plugin": "UartSerialTerminal",
"library": "terminals",
"params": {
"tty": "params.tty_bt"
}
},
{
"name": "bluetooth",
"plugin": "BT",
"library": "mcu"
},
{ "name": "led_0", "plugin": "LED", "library": "mcu" },
{ "name": "led_1", "plugin": "LED", "library": "mcu" },
{ "name": "led_2", "plugin": "LED", "library": "mcu" },
{ "name": "led_3", "plugin": "LED", "library": "mcu" },
{ "name": "led_4", "plugin": "LED", "library": "mcu" },
{ "name": "led_5", "plugin": "LED", "library": "mcu" },
{ "name": "led_6", "plugin": "LED", "library": "mcu" },
{ "name": "led_7", "plugin": "LED", "library": "mcu" },
{ "name": "led_8", "plugin": "LED", "library": "mcu" },
{ "name": "led_9", "plugin": "LED", "library": "mcu" },
{ "name": "led_10", "plugin": "LED", "library": "mcu" },
{ "name": "led_11", "plugin": "LED", "library": "mcu" },
{ "name": "led_12", "plugin": "LED", "library": "mcu" },
{ "name": "led_13", "plugin": "LED", "library": "mcu" },
{ "name": "led_14", "plugin": "LED", "library": "mcu" },
{ "name": "led_15", "plugin": "LED", "library": "mcu" }
],
// Connessione del plugin tra i componenti
"connections": [
[ "u1_stm32.ports.usart1_m", "usart_debug.ports.term_s"],
[ "u1_stm32.ports.usart1_s", "usart_debug.ports.term_m"],
[ "u1_stm32.ports.usart2_m", "bluetooth.ports.usart_m"],
[ "u1_stm32.ports.usart2_s", "bluetooth.ports.usart_s"],
[ "bluetooth.ports.bt_s", "term_bt.ports.term_m"],
[ "bluetooth.ports.bt_m", "term_bt.ports.term_s"],
[ "led_0.ports.pin", "u1_stm32.buses.pin_output_a", "0x00"],
[ "led_1.ports.pin", "u1_stm32.buses.pin_output_a", "0x01"],
[ "led_2.ports.pin", "u1_stm32.buses.pin_output_a", "0x02"],
[ "led_3.ports.pin", "u1_stm32.buses.pin_output_a", "0x03"],
[ "led_4.ports.pin", "u1_stm32.buses.pin_output_a", "0x04"],
[ "led_5.ports.pin", "u1_stm32.buses.pin_output_a", "0x05"],
[ "led_6.ports.pin", "u1_stm32.buses.pin_output_a", "0x06"],
[ "led_7.ports.pin", "u1_stm32.buses.pin_output_a", "0x07"],
[ "led_8.ports.pin", "u1_stm32.buses.pin_output_a", "0x08"],
[ "led_9.ports.pin", "u1_stm32.buses.pin_output_a", "0x09"],
[ "led_10.ports.pin", "u1_stm32.buses.pin_output_a", "0x0A"],
[ "led_11.ports.pin", "u1_stm32.buses.pin_output_a", "0x0B"],
[ "led_12.ports.pin", "u1_stm32.buses.pin_output_a", "0x0C"],
[ "led_13.ports.pin", "u1_stm32.buses.pin_output_a", "0x0D"],
[ "led_14.ports.pin", "u1_stm32.buses.pin_output_a", "0x0E"],
[ "led_15.ports.pin", "u1_stm32.buses.pin_output_a", "0x0F"]
]
}Si prega di prestare attenzione al parametro firmware nella sezione params — è il nome del file che può essere caricato nel dispositivo virtuale come firmware.
Il dispositivo virtuale e la sua interazione con il sistema operativo principale possono essere rappresentati da questo schema:

L'attuale istanza di test dell'emulatore prevede l'interazione con le porte COM del sistema operativo principale (UART di debug e UART per il modulo Bluetooth). Queste possono essere porte reali a cui sono collegati i dispositivi o porte COM virtuali (per questo è necessario com0com / socat).
Attualmente, ci sono due modi principali per interagire con l'emulatore dall'esterno:
- il protocollo GDB RSP (di conseguenza, gli strumenti che supportano questo protocollo — Eclipse / IDA / radare2);
- la riga di comando interna dell'emulatore (Argparse o Python).
Porte COM virtuali
Per interagire con l'UART del dispositivo virtuale sulla macchina locale tramite terminale, è necessario creare una coppia di porte COM virtuali collegate. Nel nostro caso, una porta è utilizzata dall'emulatore, e l'altra dal programma terminale (PuTTY o screen):

Utilizzo di com0com
Le porte COM virtuali vengono configurate utilizzando l'utilità di setup fornita con com0com (versione console — C:Program Files (x86)com0comsetupс.exe, oppure la versione GUI — C:Program Files (x86)com0comsetupg.exe):

Bisogna selezionare le opzioni enable buffer overrun per tutte le porte virtuali create, altrimenti l'emulatore attenderà una risposta dalla porta COM.
Utilizzo di socat
Su sistemi UNIX, le porte COM virtuali vengono create automaticamente dall'emulatore tramite l'utilità socat; è sufficiente specificare il prefisso socat:.
Interfaccia a riga di comando interna (Argparse o Python)
Poiché Kopycat è un'applicazione console, per interagire con i propri oggetti e variabili, l'emulatore offre due opzioni per l'interfaccia a riga di comando: Argparse e Python.
Argparse è la CLI integrata in Kopycat, è sempre disponibile e per tutti.
Una CLI alternativa è l'interprete Python. Per utilizzarlo, è necessario installare il modulo Python Jep e configurare l'emulatore per lavorare con Python (verrà utilizzato l'interprete Python installato nel sistema principale dell'utente).
Installazione del modulo Python Jep
Su Linux, Jep può essere installato tramite pip:
pip install jepPer installare Jep su Windows, è necessario prima installare il Windows SDK e il corrispondente Microsoft Visual Studio. Abbiamo semplificato il vostro compito e creato JEP per le versioni attuali di Python per Windows, quindi il modulo può essere installato dal file:
pip install jep-3.8.2-cp27-cp27m-win_amd64.whlPer verificare l'installazione di Jep, è necessario eseguire nella riga di comando:
python -c "import jep"In risposta, dovrebbe essere ricevuto il messaggio:
ImportError: Jep is not supported in standalone Python, it must be embedded in Java.Nel file batch dell'emulatore per il vostro sistema (kopycat.bat — per Windows, kopycat — per Linux) aggiungere alla lista dei parametri DEFAULT_JVM_OPTS un parametro aggiuntivo Djava.library.path — deve contenere il percorso del modulo Jep installato.
Pertanto, per Windows, dovrebbe risultare una stringa del seguente tipo:
set DEFAULT_JVM_OPTS="-XX:MaxMetaspaceSize=256m" "-XX:+UseParallelGC" "-XX:SurvivorRatio=6" "-XX:-UseGCOverheadLimit" "-Djava.library.path=C:/Python27/Lib/site-packages/jep"Avvio di Kopycat
L'emulatore è un'applicazione JVM console. L'avvio avviene tramite uno script della riga di comando del sistema operativo (sh/cmd).
Comando per avviare su Windows:
binkopycat -g 23946 -n rhino -l user -y library -p firmware=firmwarerhino_pass.bin,tty_dbg=COM26,tty_bt=COM28Comando per avviare su Linux utilizzando l'utilità socat:
./bin/kopycat -g 23946 -n rhino -l user -y library -p firmware=./firmware/rhino_pass.bin, tty_dbg=socat:./COM26,tty_bt=socat:./COM28-g 23646— Porta TCP che sarà aperta per l'accesso al server GDB;-n rhino— Nome del modulo principale del sistema (dispositivo assemblato);-l user— Nome della libreria per cercare il modulo principale;-y library— Percorso per cercare i moduli inclusi nel dispositivo;firmwarerhino_pass.bin— Percorso del file di firmware;- COM26 e COM28 — porte virtuali COM.
Di conseguenza, verrà visualizzato un prompt Python > (o Argparse >):
18:07:59 INFO [eFactoryBuilder.create]: Modulo top creato con successo come top
18:07:59 INFO [Module.initializeAndRes]: Configurazione core a top.u1_stm32.cortexm0.arm per top
18:07:59 INFO [Module.initializeAndRes]: Configurazione debugger a top.u1_stm32.dbg per top
18:07:59 WARN [Module.initializeAndRes]: Il tracer non è stato trovato in top...
18:07:59 INFO [Module.initializeAndRes]: Inizializzazione porte e bus...
18:07:59 WARN [Module.initializePortsA]: ATTENZIONE: Alcune porte hanno avvisi, usa printModulesPortsWarnings per vederli...
18:07:59 FINE [ARMv6CPU.reset]: Imposta l'indirizzo di entry point su 08006A75
18:07:59 INFO [Module.initializeAndRes]: Modulo top inizializzato e resettato con successo come cella principale!
18:07:59 INFO [Kopycat.open]: Avvio della virtualizzazione della scheda top[rhino] con arm[ARMv6Core]
18:07:59 INFO [GDBServer.debuggerModule]: Imposta il nuovo modulo debugger top.u1_stm32.dbg per GDB_SERVER(port=23946,alive=true)
Python >Interazione con IDA Pro
Come file sorgente per l'analisi in IDA per semplificare i test, utilizziamo il firmware 'Rhino' sotto forma di (qui è memorizzata la meta-informazione).
Puoi anche utilizzare il firmware principale senza meta-informazione.
Dopo aver avviato Kopycat in IDA Pro, nel menu Debugger andiamo all'opzione «Cambia debugger…» e selezioniamo «Debugger GDB remoto« . Successivamente, configuriamo la connessione: menu Debugger — Opzioni processo…
Impostiamo i valori:
- Applicazione — qualsiasi valore
- Nome host: 127.0.0.1 (o l'indirizzo IP della macchina remota dove è in esecuzione Kopycat)
- Porta: 23946

Ora il pulsante di avvio del debug (tasto F9) è disponibile:
![]()
Clicchiamolo — si stabilisce una connessione con il modulo del debugger nell'emulatore. IDA passa alla modalità di debug, diventano disponibili finestre aggiuntive: informazioni sui registri, sullo stack.
Ora possiamo utilizzare tutte le funzionalità standard per lavorare con il debugger:
- esecuzione passo-passo delle istruzioni (Entra nel metodo e Salta — tasti F7 e F8, rispettivamente);
- avvio e pausa dell'esecuzione;
- creazione di punti di interruzione sia per il codice che per i dati (tasto F2).
La connessione al debugger non implica l'avvio del codice del firmware. La posizione attuale per l'esecuzione dovrebbe essere l'indirizzo 0x08006A74 — inizio della funzione Reset_Handler. Se scorri il listing in basso, puoi vedere la chiamata alla funzione. main. Puoi posizionare il cursore su questa riga (indirizzo 0x08006ABE) e eseguire l'operazione Esegui fino al cursore (tasto F4).

Dopo, puoi premere F7 per entrare nella funzione main.
Se esegui il comando Continua processo (tasto F9), apparirà una finestra «Please wait» con un unico pulsante Sospendi:

Premendo Sospendi l'esecuzione del codice firmato viene sospesa e può essere ripresa dall'indirizzo esatto nel codice dove è stata interrotta.
Se continui l'esecuzione del codice, nei terminali collegati alle porte COM virtuali puoi vedere le seguenti righe:


La presenza della riga «state bypass» indica che il modulo Bluetooth virtuale è passato alla modalità di ricezione dati dalla porta COM dell'utente.
Ora nel terminale Bluetooth (nell'immagine — COM29) puoi inserire comandi secondo il protocollo «Rhinoceros». Per esempio, al comando «MEOW» nel terminale Bluetooth verrà restituita la riga «mur-mur»:

Emulami non completamente
Durante la costruzione dell'emulatore puoi scegliere il grado di dettaglio/emulazione di un determinato dispositivo. Ad esempio, il modulo Bluetooth può essere emulato in modi diversi:
- il dispositivo emulato è completo con un insieme completo di comandi;
- Le comandi AT vengono emulati e il flusso di dati viene ricevuto dalla porta COM del sistema principale;
- Il dispositivo virtuale garantisce un completo reindirizzamento dei dati verso il dispositivo reale;
- come semplice segnaposto che restituisce sempre 'OK'.
Nell'attuale versione dell'emulatore viene utilizzato il secondo approccio: un modulo Bluetooth virtuale esegue la configurazione, dopo di che passa alla modalità di 'proxy' dei dati dalla porta COM del sistema principale alla porta UART dell'emulatore.

Prendiamo in considerazione la possibilità di un'implementazione semplice del codice nel caso in cui non sia stata implementata qualche parte della periferica. Ad esempio, se non è stato creato un timer che controlla la trasmissione dei dati in DMA (la verifica viene eseguita nella funzione ws2812b_wait, situata all'indirizzo 0x08006840), il firmware attenderà sempre il reset del flag busy, situato all'indirizzo 0x200004C4, che indica l'occupazione della linea dati DMA:

Possiamo evitare tale situazione effettuando un 'reset manuale' del flag busy immediatamente dopo il suo settaggio. In IDA Pro è possibile creare una funzione Python e richiamarla in un breakpoint, posizionando il breakpoint stesso nel codice dopo l'assegnazione del valore 1 al flag busy.
Gestore del Breakpoint
Iniziamo a creare una funzione Python in IDA. Menu File — Script command…
Aggiungiamo un nuovo snippet nell'elenco a sinistra, assegnandogli un nome (ad esempio, BPT),
nel campo di testo a destra inseriamo il codice della funzione:
def skip_dma():
print "Skipping wait ws2812..."
value = Byte(0x200004C4)
if value == 1:
PatchDbgByte(0x200004C4, 0)
return False 
Dopo questo premiamo Esegui e chiudiamo la finestra degli script.
Ora andiamo al codice all'indirizzo 0x0800688A, impostiamo un breakpoint (tasto F2), lo modifichiamo (menu contestuale Edit breakpoint…), ricordiamoci di impostare il tipo di script – Python:


Se il valore attuale del flag busy è uguale a 1, è necessario eseguire la funzione skip_dma nella riga di script:

Se avviamo il firmware per l'esecuzione, possiamo vedere l'attivazione del codice del gestore del breakpoint in IDA nella finestra Uscita nella riga Skipping wait ws2812.... Ora il firmware non attenderà il reset del flag. busy.
Interazione con l'emulatore
L'emulazione fine a se stessa difficilmente susciterà entusiasmo e gioia. È molto più interessante se l'emulatore aiuta il ricercatore a vedere i dati in memoria o a stabilire interazioni tra i thread.
Mostreremo come stabilire interazioni tra i task RTOS in dinamica. Prima di tutto, è necessario sospendere l'esecuzione del codice, se è già avviato. Se passiamo alla funzione bluetooth_task_entry nella sezione di gestione del comando «LED» (indirizzo 0x080057B8), si può osservare che prima viene creato e poi inviato alla coda di sistema ledControlQueueHandle un certo messaggio.

È necessario impostare un breakpoint sull'accesso alla variabile ledControlQueueHandle, situata all'indirizzo 0x20000624 e continuare l'esecuzione del codice:

Di conseguenza, si interromperà prima all'indirizzo 0x080057CA prima della chiamata alla funzione osMailAlloc, quindi — all'indirizzo 0x08005806 prima della chiamata alla funzione osMailPut, poi dopo un certo tempo — all'indirizzo 0x08005BD4 (prima della chiamata alla funzione osMailGet), che appartiene alla funzione leds_task_entry (task LED), cioè è avvenuto uno switch dei task, e ora il controllo è passato al task LED.

In questo modo semplice è possibile stabilire come i task RTOS interagiscono tra loro.
Certo, in realtà, l'interazione tra i task può essere molto più complessa, ma utilizzare un emulatore rende il tracciamento di questa interazione meno faticoso.
si può guardare un breve video di avvio dell'emulatore e dell'interazione con IDA Pro.
Avvio con Radare2
Non si può trascurare uno strumento versatile come Radare2.
Per connettersi all'emulatore usando r2, il comando sarà:
radare2 -A -a arm -b 16 -d gdb://localhost:23946 rhino_fw42k6.elfOra sono disponibili avvio (dc) e interruzione dell'esecuzione (Ctrl+C).
Purtroppo, al momento ci sono problemi nell'r2 con il server gdb hardware e la mappatura della memoria, a causa dei quali i punti di interruzione e i comandi Step non funzionano (comando ds). Ci aspettiamo che questo venga risolto a breve.
Avvio con Eclipse
Una delle modalità d'uso dell'emulatore è il debug del firmware di un dispositivo in fase di sviluppo. Per chiarezza utilizzeremo anche il firmware 'Rinoceronte'. È possibile scaricare i sorgenti del firmware .
Come IDE utilizzeremo Eclipse dal pacchetto .
Per caricare nel simulatore il firmware direttamente compilato in Eclipse, è necessario aggiungere il parametro firmware=null al comando di avvio dell'emulatore:
binkopycat -g 23946 -n rhino -l user -y modules -p firmware=null,tty_dbg=COM26,tty_bt=COM28Impostazione della configurazione di debug
In Eclipse, selezionare il menu Esegui — Configurazioni di Debug… Nella finestra che si apre, nella sezione Debugging Hardware GDB è necessario aggiungere una nuova configurazione, quindi nella scheda 'Main' indicare il progetto corrente e l'applicazione da debuggare:

Nella scheda 'Debugger' è necessario specificare il comando GDB:
${openstm32_compiler_path}arm-none-eabi-gdb
E anche inserire i parametri per la connessione al server GDB (host e porta):

Nella scheda "Startup" è necessario specificare i seguenti parametri:
- attivare la casella Carica immagine (per caricare l'immagine compilata del firmware nell'emulatore);
- attivare la casella Carica simboli;
- aggiungere il comando di avvio:
set $pc = *0x08000004(impostare il valore nel registro PC dalla memoria all'indirizzo0x08000004— qui è memorizzato l'indirizzo del ResetHandler).
Attenzione, se non desideri caricare il file del firmware da Eclipse, non è necessario specificare i parametri Carica immagine e Esegui comandi .

Dopo aver premuto Debug, puoi lavorare in modalità di debug:
- esecuzione passo passo del codice

- interazione con i punti di interruzione

Nota. In Eclipse ci sono, ehm... alcune particolarità... e bisogna convivere con esse. Ad esempio, se durante l'avvio del debug appare il messaggio «No source available for «0x0″», esegui il comando Step (F5)

In conclusion
L'emulazione di codice nativo è un'impresa piuttosto interessante. Per gli sviluppatori di dispositivi si presenta l'opportunità di debuggare il firmware senza il dispositivo reale. Per i ricercatori, la possibilità di eseguire un'analisi dinamica del codice, cosa che non sempre è possibile anche in presenza del dispositivo.
Desideriamo fornire ai professionisti uno strumento che sia comodo, relativamente semplice e che non richieda troppo sforzo e tempo per la sua configurazione e avvio.
Scrivi nei commenti la tua esperienza con gli emulatori hardware. Ti invitiamo a discutere e saremo felici di rispondere alle tue domande.
Solo gli utenti registrati possono partecipare al sondaggio. , per favore.
A cosa usi l'emulatore?
sviluppo (debugging) di firmware
ricerca di firmware
esecuzione di giochi (Dendi, Sega, PSP)
altro (scrivi nei commenti)
Hanno votato 7 utenti. 2 utenti si sono astenuti.
Quale software utilizzi per l'emulazione del codice nativo?
QEMU
Unicorn engine
Proteus
altro (scrivi nei commenti)
Hanno votato 6 utenti. 2 utenti si sono astenuti.
Cosa vorresti migliorare nell'emulatore che usi?
vorrei velocità
vorrei facilità di configurazione/esecuzione
vorrei più possibilità di interazione con l'emulatore (API, hook)
sono soddisfatto di tutto
altro (scrivi nei commenti)
Hanno votato 8 utenti. 1 utente si è astenuto.
Fonte: habr.com


