Kwasek6 polymorphic code, modified Ruby Hash

 

Introduzione

Kwasek6 è un crackme che basa la sua protezione su un keyfile, ed implementa contemporaneamente, codice polimorfico normale (soliti mov, add, sub, etc) ed MMX (paddx, psubd, pxor) oltre ad una versione modificata del RubyHash, e del TEA (Tiny Encryption Algorithm) riscritto in versione MMX.

Tools usati

  • IDA
  • OllyDbg
  • UnFSG (unpacker per FSG)
  • MASM

URL o FTP del programma

www.google.it

Notizie sul programma

Troverete (crackme + keygen) nell' allegato.

Essay

 

Il crackme in questione è protetto con FSG (si può notare immediatamente dalla signature "FSG!" presente fra i primi bytes del file), per risolvere il problema basta cercare su internet un qualsiasi unpacker per FSG. Come si può vedere non esistono editbox, ma analizzando la ImportTable possiamo trovare alcune funzioni interessanti: CreateFile, GetFileSize etc. (si tratta quindi di una keyfile protection). Passiamo a vedere il listato:

00401AFD push 0
00401AFF push 0
00401B01 push 3 ;OPEN_EXISTING
00401B03 push 0
00401B05 push 0
00401B07 push 80000000h ;GENERIC_READ
00401B0C push offset aKwasek_htb ;nome del keyfile -> Kwasek.htb
00401B11 call CreateFileA
00401B16 mov ds:hFile, eax
00401B1B inc eax
00401B1C jz loc_401EEA ;Se il file non esiste salta ad unregistered
00401B22 push 0
00401B24 push ds:hFile
00401B2A call GetFileSize ;Determina la grandezza del file
00401B2F cmp eax, 80h ;Se diverso da 80h (128 caratteri) salta ad unregistered
00401B34 jnz loc_401EEA
00401B3A push 0
00401B3C push offset NumberOfBytesRead
00401B41 push eax
00401B42 push offset Buffer
00401B47 push ds:hFile
00401B4D call ReadFile
00401B52 mov esi, offset Buffer ;Buffer con il contenuto del file
00401B57 mov edi, offset dword_4177D6 ;buffer di destinazione
00401B5C push edi
00401B5D mov ecx, 10h
00401B62 repe movsb
00401B64 pop edi

00401B65 push edi ;Nel buffer di destinazione ci sono i primi 16 bytes del keyfile

La routine che abbiamo appena visto è molto semplice, non fa altro che andare ad aprire un file già esistente, che dev'essere di 128 bytes, e ne legge il contenuto, estrapolandone da esso i primi 16 bytes che andranno a finire in un buffer specifico. Continuiamo con l'analisi:

00401B66 mov eax, [edi] ;Mette in eax una dword per volta, del buffer di destinazione
00401B68 bswap eax
00401B6A xor eax, 12E4B8Ch
00401B6F mov [edi], eax ;mette la dword 'decryptata' in edi
00401B71 inc edi
00401B72 inc ecx ;incrementa il loop counter
00401B73 cmp ecx, 0Dh
00401B76 jnz short loc_401B66 ;ripete per 13 volte
00401B78 pop edi
00401B79 cmp byte ptr [edi], 0 ;controlla che il primo byte non sia 0
00401B7C jz loc_401EEA
00401B82 cmp byte ptr [edi+0Fh], 0 ;e che l'ultimo (il sedicesimo) sia 0
00401B86 jnz loc_401EEA

I primi 16 bytes del nostro keyfile vengono decriptati (attraverso uno bswap ed uno xor), e alla fine la routine controlla che il primo byte non sia 0 ed il sedicesimo sia NULL (questo ci fa pensare che i primi 16 bytes opportunamente trattati devono darci una stringa NULL-Terminated)

00401B8C mov edx, 2 ;Index counter (fa si che la call 00401206 sia chiamata 2 volte)
00401B91 mov eax, offset dword_4177D6 ;Buffer che contiene la stringa decryptata
00401B96 mov ebx, offset dword_4177E7 ;Output buffer (In seguito chiamato Hash)
00401B9B mov cl, 10h
00401B9D push eax
00401B9E push ecx
00401B9F push ebx
00401BA0 call sub_401206 ;Questa call accetta 3 parametri: OutputBuff, 10h, DecryptBuff
00401BA5 mov eax, ebx
00401BA7 add ebx, 8
00401BAA mov cl, 8
00401BAC dec edx
00401BAD test edx, edx

00401BAF jnz short loc_401B9D

Adesso andiamo ad analizzare la subroutine 00401206

00401256 mov edi, A
00401258 rol edi, cl
0040125A mov esp, B
0040125C mov ecx, C
0040125E shr ecx, 1Bh
...
00401276 xor edi, esi
00401278 mov A, edi
0040127A add A, 456C6091h ;Magic Value 1
0040127F xchg A, B
00401280 xchg B, C
00401282 xchg C, D
...
004012A8 mov A, edi
004012AA imul A, 0AA7110C3h ;Magic Value 2
004012B0 xchg A, B
004012B1 xchg B, C

004012B3 xchg C, D

Come si può vedere, nella routine sono utilizzati due MagicValues (classica definizione delle costanti crittografiche) 456C6091h e 0AA7110C3h che con una piccola ricerca ci indicano che siamo in presenza di un Ruby Hash, da notare però che in questo caso la routine è leggermente modificata, se ricordate edx ha un counter settato a 2, e di conseguenza otteremo un hash di 16d bytes. Passiamo alla prossima porzione di routine, che implementa un semplicissimo Engine Polimorfico, che genera un set limitato di istruzioni basandosi sull'hash appena determinato:

 

00401BB1 mov edi, offset dword_40407B ;Buffer del codice
00401BB6 mov esi, offset dword_4177E7 ;Hash
00401BBB push esi
00401BBC lodsb
00401BBD cmp al, 0 ;se l'i-esimo carattere del nostro hash è 0
00401BBF jb short loc_401BE8
00401BC1 cmp al, 20h
00401BC3 ja short loc_401BE8 ;se superiore a 20h salta alla prossima routine
...
00401BD0 lodsb
00401BD1 call sub_401AA6 ;Call che analizzeremo inseguito
00401BD6 test cl, cl
00401BD8 jz loc_401CD7
00401BDE add al, 10h
00401BE0 jmp short loc_401BD1
00401BE2 mov al, 3 ;il nostro byte viene bilanciato poi viene chiamata la call 00401BD1h
00401BE4 stosb
00401BE5 lodsb ;Mette nel buffer del codice l'opcode ottenuto
00401BE6 jmp short loc_401BD1

Ho saltato molte parti di codice, perchè non vale la pena andare a commentare ogni singola linea codice, ma solamente capire come funziona il nostro Engine:

00401BE8 cmp al, 21h
00401BEA jb short loc_401C13 ;Salta al prossimo set di byte (Opcode nel nostro caso)
00401BEC cmp al, 2Fh
00401BEE ja short loc_401C13
stosb
lodsb ;Mette nel buffer del codice l'opcode ottenuto
...
00401C13 cmp al, 30h
00401C15 jb short loc_401C3E ;Salta al prossimo set di byte (Opcode nel nostro caso)
00401C17 cmp al, 80h
00401C19 ja short loc_401C3E
stosb
lodsb ;Mette nel buffer del codice l'opcode ottenuto
...
00401C3E cmp al, 81h
00401C40 jb short loc_401C5F ;Salta al prossimo set
00401C42 cmp al, 0A0h
00401C44 ja short loc_401C5F
...
00401C5F cmp al, 0A1h
00401C61 jb short loc_401C9C ;Salta al prossimo set
00401C63 cmp al, 0E0h
00401C65 ja short loc_401C9C
...
00401C9C cmp al, 0E1h
00401C9E jb short loc_401CD7 ;Salta al prossimo set
00401CA0 cmp al, 0FFh
00401CA2 ja short loc_401CD7
...
00401C9C cmp al, 0E1h
00401C9E jb short loc_401CD7 ;Salta al prossimo set
00401CA0 cmp al, 0FFh
00401CA2 ja short loc_401CD7
...
00401CD7 pop esi
00401CD8 inc esi
00401CD9 inc edx
00401CDA cmp edx, 0Dh ;Se non siamo al 13esimo carattere del nostro hash
00401CDD jnz loc_401BBB ;salta all'inizio dell'Engine
00401CE3 mov edx, 4
00401CE8 mov esi, offset dword_4177E7 ;esi punta al nostro Hash
00401CED loc_401CED:
00401CED xor dword ptr [esi], 499602D2h ;queste istruzioni modificano 4 bytes del nostro 00401CF3 ror dword ptr [esi], 7 ;hash
00401CF6 add esi, 4
00401CF9 dec edx
00401CFA jnz short loc_401CED
00401CFC add ds:dword_41774A, 1 ;incrementa il contatore
00401D03 cmp ds:dword_41774A, 10h
00401D0A jnz loc_401BB6 ;salta all'Engine che avrà come input l'hash modificato
00401D10 xor ds:dword_41774A, 10h ;azzera il counter
00401D17 mov al, 0C3h ;chiude il codice generato con un RET
00401D19 stosb

Come potete vedere il funzionamento dell'Engine non è molto complesso, ad ogni ciclo prende un carattere del nostro hash e lo filtra attraverso una struttura decisionale che contiene un ben definito range di opcodes. Dopo aver iterato l'intero hash, a 00401CEDh abbiamo una banale routine che modifica il nostro hash 4 bytes per volta, che vanno poi a costituire un nuovo input per l'Engine. Se andiamo a fare un pò di conti vediamo, che dal nostro hash vengono generati 1C6h bytes di codice, ed osservando approfonditamente gli opcodes usati nella struttura dell'engine, possiamo notare che il nuovo codice userà un set limitato di istruzioni, e cioè:

add reg, reg
sub reg, reg
rol reg, int
ror reg, int
xchg reg, reg
xor reg, reg
not reg

 

Andiamo ad analizzare adesso la prossima porzione di codice, che costituisce la prima applicazione del codice appena generato:

00401D1A mov esi, offset dword_417762 ;Buffer del nostro keyfile+16
00401D1F mov eax, [esi] ;carica 24 bytes presi dal nostro keyfile
00401D21 mov ebx, [esi+4] ;|
00401D24 mov ecx, [esi+8] ;|
00401D27 mov edx, [esi+0Ch] ;|
00401D2A mov edi, [esi+10h] ;|
00401D2D mov esi, [esi+14h] ;|
00401D30 push ebp
00401D31 mov ebp, offset dword_40407B
00401D36 call ebp ;chiama il codice polimorfico
00401D38 mov ebp, offset dword_404062 ;ebp contiene la nostra stringa decriptata
00401D3D mov [ebp+0], eax
00401D40 mov [ebp+4], ebx
00401D43 mov [ebp+8], ecx
00401D46 mov [ebp+0Ch], edx
00401D49 mov [ebp+10h], edi
00401D4C mov [ebp+14h], esi
00401D4F mov eax, ebp
00401D51 pop ebp
00401D52 mov ecx, 18h ;ecx verrà usato come counter, ed è settato a 24
00401D57 mov esi, eax
00401D59 mov edi, offset aPolymorphisByK ;"Polymorphis by Kwasek :)"
00401D5E call sub_401F08 ;in questa call vengono confrontate la nostra stringa con quella
00401D63 or eax, eax ;in decriptata
00401D65 jnz short loc_401D6C ;vai alla prossima parte della routine di protezione
00401D67 jmp loc_401EEA ;salta ad UNREGISTERED

 

Il codice è molto semplice, vengono presi 24 bytes dal nostro keyfile e decriptati attraverso l'algoritmo polimorfico, ed infine confrontati con la stringa "Polymorphis by Kwasek :)" se sono uguali, possiamo passare alla prossima parte del crackme, altrimenti l'esecuzione salta ad unregistered. Il nostro compito è quindi generare un algoritmo polimorfico con le istruzioni reversate, ed applicare la stringa "Poly...." in modo da ottenere una serie di bytes, che decriptati ci danno la stessa "Poly...", come reversare le istruzioni lo vedremo in fase di keygenning, ora possiamo tranquillamente procedere con la nostra analisi..

00401D7D mov esi, offset dword_4177E7 ;esi continene il puntatore al nostro Hash
00401D82 loc_401D82:
00401D82 push esi
00401D83 mov al, 0Fh
00401D85 stosb
00401D86 lodsb
00401D87 cmp al, 0
00401D89 jb short loc_401DA0
00401D8B cmp al, 0EFh
00401D8D ja short loc_401DA0
00401D8F mov al, 0EFh
00401D91 stosb
00401D92 lodsb
...
0401DD2 pop esi
00401DD3 inc esi
00401DD4 inc edx
00401DD5 cmp edx, 0Dh
00401DD8 jnz short loc_401D82
00401DDA mov edx, 4
00401DDF mov esi, offset dword_4177E7 ;esi continene il puntatore al nostro Hash
00401DE4 xor dword ptr [esi], 3ADE68B1h
00401DEA ror dword ptr [esi], 3
00401DED add esi, 4
00401DF0 dec edx
00401DF1 jnz short loc_401DE4 ;queste istruzioni modificano 4 bytes del nostro hash
00401DF3 add ds:dword_41774A, 1 ;incrementa il contatore
00401DFA cmp ds:dword_41774A, 10h
00401E01 jnz loc_401D7D ;salta all'Engine che avrà come input l'hash modificato
00401E07 xor ds:dword_41774A, 10h ;azzera il counter
00401E0E mov al, 0C3h ;chiude il codice generato con un RET
00401E10 stosb

Questa parte dovrebbe risultarvi molto familiare, si tratta sempre del nostro Engine, che stavolta utilizza opcodes che rientrano nelle istruzioni MMX. La solita routine, modifica il nostro hash, per un totale di 1C6h bytes di istruzioni mmx polimorfiche. Il set di istruzioni in questo caso è:

paddd mmx,mmx
psubd mmx,mmx
pxor mmx,mmx
00401E13 mov esi, offset dword_41777A ;Buffer del nostro keyfile+40
00401E18 movq mm0, qword ptr [esi] ;carica 64 bytes presi dal nostro keyfile
00401E1B movq mm1, qword ptr [esi+8] ;|
00401E1F movq mm2, qword ptr [esi+10h] ;|
00401E23 movq mm3, qword ptr [esi+18h] ;|
00401E27 movq mm4, qword ptr [esi+20h] ;|
00401E2B movq mm5, qword ptr [esi+28h] ;|
00401E2F movq mm6, qword ptr [esi+30h] ;|
00401E33 movq mm7, qword ptr [esi+38h] ;|
00401E37 mov ebp, offset dword_40447B
00401E3C call ebp ; dword_40447B ;Codice polimorfico MMX appena generato
00401E3E mov esi, offset dword_404021 ;Stringa di 64 bytes decriptata
00401E43 movq qword ptr [esi], mm0
00401E46 movq qword ptr [esi+8], mm1
00401E4A movq qword ptr [esi+10h], mm2
00401E4E movq qword ptr [esi+18h], mm3
00401E52 movq qword ptr [esi+20h], mm4
00401E56 movq qword ptr [esi+28h], mm5
00401E5A movq qword ptr [esi+30h], mm6
00401E5E movq qword ptr [esi+38h], mm7
00401E62 mov ecx, 40h ;Lunghezza della stringa
00401E67 mov edi, offset aPolymorphismmx ;"PolymorphisMMX by Kwasek 2oo3 kwasek2@w..."
00401E6C call sub_401F08 ;Confronta le due stringhe
00401E71 or eax, eax
00401E73 jnz short loc_401E77 ;Se sono uguali procedi con la prossima routine
00401E75 jmp short loc_401EEA ;Altrimenti salta ad UNREGISTERED

Lo schema di protezione è praticamente identico a quello già visto, con la sola accezione che qui vengono usate istruzioni mmx e la stringa è lunga 64 bytes. La tecnica di reverse è sempre la stessa, dobbiamo elaborare un engine che generi le istruzioni mmx reversed. Procediamo con la prossima parte di codice:

00401E77 push offset dword_4177BA ;Buffer del nostro keyfile+104
00401E7C push offset aPolymorphisByK ;"Polymorphis by Kwasek :)"
00401E81 call sub_401F33 ;Call molto importante, che analizzeremo in seguito
00401E86 mov ecx, 8 ;Lunghezza della stringa
00401E8B mov esi, offset dword_4177E7 ;Hash
00401E90 mov edi, offset dword_4177BA ;keyfile+104
00401E95 call sub_401F08 ;Confrontali
00401E9A or eax, eax
00401E9C jnz short loc_401EA0 ;Se sono uguali procedi con la prossima routine
00401E9E jmp short loc_401EEA ;Altrimenti salta ad UNREGISTERED
00401EA0 loc_401EA0:
00401EA0 movd eax, mm7
00401EA3 psllq mm7, 20h ;Equivalente dell'istruzione SHL
00401EA7 movd ebx, mm7
00401EAA test eax, eax
00401EAC jnz short loc_401EEA ;Se eax è diverso da 0 salta ad UNREGISTERED
00401EAE test ebx, ebx
00401EB0 jnz short loc_401EEA ;Se eax è diverso da 0 salta ad UNREGISTERED
00401EB2 mov ecx, 8
00401EB7 mov esi, offset dword_4177EF keyfile+08
00401EBC mov edi, offset dword_404010
00401EC1 call sub_401F08
00401EC6 or eax, eax
00401EC8 jnz short loc_401ECC ;Se sono uguali procedi vai a REGISTERED
00401ECA jmp short loc_401EEA ;Altrimenti salta ad UNREGISTERED

Questa che abbiamo visto, è l'ultima parte di protezione, la struttura rimane invariata, abbiamo infatti delle call che decriptano porzioni di keyfile, che poi vengono confrontate con le solite stringhe. Andiamo quindi ad analizzare queste call, e più precisamente la call 00401F33h:

00401F33 emms
0401F39 mov esi, offset dword_4177C2 ;keyfile+112
00401F3E mov edi, offset aWitamWszystkic ; "Witam wszystkich crackersow a szczegoln"
00401F43 movd mm4, dword ptr [edx] ;in edx keyfile+104
00401F46 call sub_40208D
00401F4B movq mm7, qword ptr [esi];in esi keyfile+112
00401F4E call sub_40208D
...

Ho tagliato molta parte del codice, ma possiamo una cosa molto strana, ci sono numerose chiamate a 0040208Dh, andiamo a vedere cosa succede in questa call:

00402093 mov esi, offset dword_4177CA ;keyfile+120
00402098 mov edi, offset dword_404010 ;buffer finale
0040209D mov ebx, offset aUnregistered ;"UNREGISTERED"
004020A2 lodsb
004020A3 xor al, [ecx+ebx]
004020A6 xor al, 7
004020A8 add al, 6
004020AA sub al, [ecx+ebx]
004020AD stosb
004020AE loop loc_4020A2
004020B0 mov ecx, 8
004020B5 mov esi, offset dword_404010
004020BA mov edi, offset dword_4177CA
004020BF repe movsb ;copia il buffer finale in keyfile+120
004020C1 popa
004020C2 retn

 

Il codice è abbastanza semplice, i bytes del keyfile vengono combinati con la stringa unregistered attraverso una serie di istruzioni come add, sub e xor che come sapete possono essere tranquillamente reversate, poichè non comportano perdite di bit (e quindi di informazione), ed il nostro compito è proprio questo, dobbiamo /notate all'uscita della call a 0040208Dh i valori contenuti nei vari registri rimangono invariati, e la routine (escluse appunto le chiamate a 0040208Dh) fa qualcos'altro..come avrete intuito, la seconda funzione di tutte queste call è confonderci le idee! proviamo infatti a riscrivere l'intera routine, eliminando tutte queste call:

00401F39 mov esi, offset dword_4177C2 ;keyfile+112
00401F3E mov edi, offset aWitamWszystkic
00401F43 movd mm4, dword ptr [edx] ;in mm4 keyfile+104
00401F4B movq mm7, qword ptr [esi] ;in mm7 keyfile+104
00401F53 movd mm1, dword ptr [edx+4]
00401F5C mov ecx, ds:dword_401F1B ;ecx=32375E7Eh
00401F67 movd mm0, ds:dword_401F23 ;MM0=12345678h
00401F73 movd mm6, ecx
00401F7B pxor mm6, mm0
00401F83 pxor mm5, mm5
00401F8B mov ecx, 20h ;Contatore settato a 32d
00401F95 push edx
00401F9B paddd mm5, mm6 ;MM5 + 020030806h
00401FA3 movq mm0, mm1
00401FAB movq mm3, mm1
00401FB3 call sub_402075 ;Equivale a shl MM0,4 cioè MM0*16
00401FBD psrld mm3, 5
00401FC6 paddd mm0, qword ptr [eax] ;"Polymorp" + MM0
00401FCE paddd mm3, qword ptr [eax+4];"hisMMX b" + MM3
00401FDB pxor mm0, mm1
00401FE9 paddd mm0, mm5
00401FF0 pxor mm0, mm3
00401FFE paddd mm0, mm4
00402006 movq mm4, mm0
0040200E call sub_402075 ;MM0*16
00402018 movq mm3, mm4
00402020 psrld mm3, 5
00402029 paddd mm0, qword ptr [eax+8] ;"y Kwasek" + MM0
00402036 pxor mm0, mm4
00402044 paddd mm3, qword ptr [eax+0Ch] " 2oo3 kw" + MM3
0040204D paddd mm0, mm5
00402054 pxor mm0, mm3
00402057 paddd mm7, qword ptr [edi]
00402062 paddd mm1, mm0
00402065 dec ecx
00402066 jnz loc_401F9B
0040206C pop edx ;Buffer del nostro keyfile+104
0040206D movd dword ptr [edx], mm4 ;Inserisci i valori appona trovati nel buffer
00402070 movd dword ptr [edx+4], mm1 ;|
00402074 retn

Proviamo a riscrivere in C questo pezzetto di codice:

void code(long* v, long* k) {
unsigned long y=v[0],z=v[1], sum=0, delta=0x20030806, n=32 ;
while (n-->0) {
sum += delta ;
y += (z>5)+k[1];
z += (y>5)+k[3];
}
v[0]=y ; v[1]=z;
}

Come per magia scopriamo, che si tratta semplicemente della procedura di Encryption del TEA, solo che riscritto in istruzioni MMX e con il delta cambiato :) Quello che dobbiamo fare è semplicemente reversare questo algoritmo, scrivendo cioè la routine di Decryption.

Piccola nota sul TEA:

TEA

Il TEA (Tiny Encryption Alorithm) e' stato creato da David Wheeler e Roger Needham. Questo algoritmo ha il pregio di essere abbastanza veloce e di offrire una "sicurezza-media", si basa principalmente su un'architettura di tipo Feistel Network, perciò la cifratura/decifratura di dati avviene "a blocchi" solitamente di 64 bits. E' un sistema di cifratura simmetrica quindi se invertiamo l'intero algoritmo ed usiamo la stessa chiave utilizzata per crittare, ed applichiamo in entrata il CipherText, in uscita otterremo il PlainText.

Un ultimo piccolo reverse ed abbiamo finalmente finito, se ricordate all'uscita della call 00401F33h, viene controllato se MM7 è uguale a zero, se non lo è, l'esecuzione salta ad Unregistered. La routine responsabile del valore di MM7 in uscita è contenuta all'interno della procedura del TEA, ben nascosta in modo da essere confusa con l'intero altro codice, eccola:

00401FD7 push ecx
00401FD8 push 0Bh
00401FDA pop ecx
00401FDB pxor mm0, mm1
00401FDE paddd mm7, qword ptr [edi] ;edi punta alla stringa "Witam wszystkich" la quale viene
00401FE1 loop loc_401FDB ;sommata a keyfile+112 che è contenuta in MM7
00401FE3 pop ecx 00402032 push ecx
00402033 push 29h
00402035 pop ecx
00402036 pxor mm0, mm4
00402039 paddd mm7, qword ptr [edi]
0040203C loop loc_402036

Anche qui si tratta di un banalissimo reverse, che vi riporto qui sotto:

pxor mm7, mm7
push 2688 ;(0Bh+0Bh+29h+15h)*20h
pop ecx mov edi, offset Witam
LOOP:
psubd mm7, qword ptr [edi]
loop LOOP
mov edi, offset RisultatoMM7
movq qword ptr [edi], mm7

E con questo abbiamo finalmente finito il reverse di Kwasek6! :)

Note per un eventuale keygen

Un imbocca al lupo per chi volesse cimentarsi nel keygenning di questo crackme, non perchè sia eccessivamente difficile, ma necessita sicuramente di un buon ordine di coding ed una gran pazienza :), credo che possa aiutarvi, suddividere il crackme in fasi successive:

  1. Nome
  2. Ruby_Hash(Nome)
  3. Engine Polimorfico 1
  4. Engine Polimorfico 2 (MMX)
  5. TEA (Decrypter)
  6. MM7 Check
  7. Unreg. loop

Ci vediamo al prossimo tute!!! :))

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值