Strengir og hrá bæti (Strings vs. Bytes)
Velkomin í einn mikilvægasta kaflann í allri bókini! Í flestum forritunarnámskeiðum er þetta atriði útskýrt mjög hratt, en í netöryggi og tölvuhögun er munurinn á texta (strengjum) og hráum gögnum (bætum) undirstaða alls sem við gerum.
Við ætlum að skoða af hverju texti er í raun blekking, hvernig við meðhöndlum hrá bæti í Python, hvernig við breytum þar á milli og hvernig við beitum svokölluðum „sneiðum“ (e. slicing) til að stýra gögnunum okkar á fljótlegan hátt.
Texti er blekking
Í kaflanum um kóðun rittákna lærðum við að örgjörvi tölvunnar skilur ekkert nema tölur. Þegar þú sérð bókstafinn 'A' á skjánum, þá er tölvan í raun með töluna 65 í minni sínu.
Í Python höfum við tvö gerólík gagnatög til að meðhöndla þetta:
- Strengir (
str): Þetta er texti sem við mennirnir lesum. Í Python getur strengur innihaldið hvaða rittákn sem er í heiminum, þökk sé Unicode staðlinum (t.d. íslenska stafi eins ogæ,öeða jafnvel tjákn eins ogðeða 🚀). - Bæti (
bytes): Þetta eru hrá, óbreytt tvíundagögn — röð af tölum þar sem hvert gildi er nákvæmlega eitt bæti (tala frá 0 upp í 255). Tölvan vinnur einungis með þetta form undir húfnum.
Í Python búum við til bytes breytu með því að setja stafinn b fyrir framan gæsalappirnar:
>>> texti = "halló"
>>> type(texti)
<class 'str'>
>>> gogn = b"halló" # Þetta mun valda villu!
SyntaxError: bytes can only contain ASCII literal characters
ATH: Skrifstofustjóri Python grípur inn í
Af hverju máttirðu ekki búa til b"halló"? Vegna þess að íslenski stafurinn ó er ekki í ASCII staðlinum! Hrá bæti sem eru skrifuð með b"" mega aðeins innihalda hefðbundna ensku stafina (ASCII). Ef við viljum setja flóknari stafi inn í bæti, verðum við að nota sérstök stýritákn eða breyta strengnum á réttan hátt.
Sláum inn gilt dæmi:
>>> gogn = b"hallo"
>>> type(gogn)
<class 'bytes'>
Umbreytingabláin: Encode og Decode
Hvernig breytum við strengnum okkar (Unicode texta) yfir í hrá bæti sem við getum sent yfir netið, eða dulkóðað? Og hvernig breytum við bætunum sem við fáum aftur í lesanlegan texta?
Python býður upp á tvær mjög mikilvægar aðferðir til þess: .encode() og .decode().
Texti (str) "hæ"
│
│ .encode("utf-8") (Kóðun)
▼
Bæti (bytes) b"h\xc3\xa6"
│
│ .decode("utf-8") (Afkóðun)
▼
Texti (str) "hæ"
Prófum þetta í gagnvirku skelinni:
# Úr streng yfir í bæti (Kóðun)
>>> texti = "hæ"
>>> baeti = texti.encode("utf-8")
>>> baeti
b'h\xc3\xa6'
Taktu eftir því að stafurinn h er sýndur sem h (því hann er í ASCII), en íslenski stafurinn æ er sýndur sem \xc3\xa6. Þetta eru tvö bæti skrifuð í sextándakerfinu (0xc3 og 0xa6) sem saman tákna stafinn æ í UTF-8 kóðuninni!
Breytum bætunum aftur í texta:
# Úr bætum yfir í streng (Afkóðun)
>>> baeti.decode("utf-8")
'hæ'
Þegar hlutirnir fara úrskeiðis
Ef þú reynir að afkóða bæti sem innihalda handahófskenndar tölur sem passa ekki við neina UTF-8 stafi, mun Python gefa þér villu: UnicodeDecodeError. Þetta er mjög algengt þegar við erum að vinna með dulkóðuð gögn eða keyranleg forrit — þau eru einfaldlega ekki ætluð til að vera lesin sem texti!
Hex-stríðið: hex og binascii
Byrjendur ruglast mjög oft á þessu tvennu:
* Hráa bætinu sem hefur gildið 65 (sem í ASCII er stafurinn 'A').
* Textastrengnum "41" (sem er hvernig við skrifum töluna 65 í sextándakerfinu).
Í netöryggi fáum við oft sextándakerfisstrengi og þurfum að breyta þeim í hrá bæti til að geta sent þá áfram. Python býður upp á tvær helstu leiðir til að leysa þetta.
1. Innbyggðu föllin í Python (Mælt með)
Auðveldasta leiðin í nýrri útgáfum af Python er að nota .hex() og bytes.fromhex():
# Breyting úr hráum bætum yfir í hex-streng
>>> b"ABC".hex()
'414243'
# Breyting úr hex-streng yfir í hrá bæti
>>> bytes.fromhex("414243")
b'ABC'
2. Standard bókasafnið binascii
Í mörgum forritum og dæmum á netinu muntu sjá bókasafnið binascii notað. Það gerir nákvæmlega það sama en notar föllin hexlify og unhexlify:
>>> import binascii
# binascii.hexlify breytir hráum bætum í hex-bætaskrifaðan streng
>>> binascii.hexlify(b"ABC")
b'414243'
# binascii.unhexlify breytir hex-streng í hrá bæti
>>> binascii.unhexlify("414243")
b'ABC'
Athugaðu að hexlify skilar bytes hlut (b'414243'), en ekki venjulegum streng ('414243'). Til að breyta því í venjulegan streng þarftu að keyra .decode() aftan á það.
Tvíundakóðun í Python: Base64, Base32 og Base85
Í kaflanum um gagnakóðun skoðuðum við hvernig við getum pakkað hráum tvíundagögnum (bætum) inn í textastrengi sem nota einungis ákveðin ASCII tákn, eins og með Base64, Base32 og Base85 kóðunum.
Í netöryggisþrautum og CTF-keppnum muntu stöðugt rekast á fána eða gögn sem hafa verið kóðuð á þennan hátt. Þótt við getum notað CyberChef til að gera þessar breytingar fljótt í vafra, þá er Python okkar öflugasta tól til að sjálfvirknivæða þetta í skriftum.
Python býður upp á innbyggða bókasafnið base64 sem gerir okkur kleift að umbreyta þessum gögnum á mjög einfaldan hátt.
Base64 í Python
Til að nota þessar kóðanir þurfum við að flytja inn base64 safnið. ATH: Þessi föll vinna einungis á bætum (bytes), svo við verðum að gæta þess að nota b"" eða keyra .encode() fyrst.
import base64
# Kóðun: Úr hráum bætum yfir í Base64-bæti
>>> upprunaleg_baeti = b"GGF"
>>> kodi_baeti = base64.b64encode(upprunaleg_baeti)
>>> kodi_baeti
b'R0dG'
# Ef við viljum fá venjulegan textastreng getum við afkóðað bætastrenginn:
>>> kodi_baeti.decode("utf-8")
'R0dG'
# Afkóðun: Úr Base64-streng yfir í hrá bæti
>>> base64.b64decode("R0dG")
b'GGF'
Base32 og Base85
Bókasafnið styður aðra grunna sem við kynntumst áður á nákvæmlega sama hátt með föllunum b32encode/b32decode og b85encode/b85decode:
# Base32 kóðun og afkóðun
>>> base64.b32encode(b"Ha")
b'MJSQ===='
>>> base64.b32decode(b"MJSQ====")
b'Ha'
# Base85 (Ascii85) kóðun og afkóðun
>>> base64.b85encode(b"Ha")
b'cYe|'
>>> base64.b85decode(b"cYe|")
b'Ha'
Sneiðar og skurðir (Slicing)
Þegar við erum að vinna með löng bætafylki (t.d. heilar skrár) þurfum við oft að geta bútað þau niður. Python býður upp á magnaðan rithátt til þess sem kallast sneiðar (e. slices).
Rithátturinn notar hornklofa aftan við breytuna: gogn[start:end:step]
* start: Vísitalan (e. index) þar sem á að byrja (innifalin, byrjar á 0).
* end: Vísitalan þar sem á að hætta (ekki innifalin!).
* step: Hversu mörg skref á að taka í einu (sjálfgefið 1).
Prófum þetta með dæmi:
>>> málsháttur = b"HAKKARASKOLINN"
Sækjum fyrstu 4 bætin:
>>> málsháttur[0:4]
b'HAKK'
Sækjum gildi frá vísitölu 4 til loka (við getum sleppt end ef við viljum fara alla leið á enda):
>>> málsháttur[4:]
b'ARASKOLINN'
Sækjum hvern annan staf í öllum strengnum:
>>> málsháttur[::2]
b'HKKRSOLN'
Öfugi galdurinn: [::-1]
Ef við setjum -1 sem skrefstærð (step), þá fer Python afturábak í gegnum gögnin! Þetta er fljótlegasta leiðin í Python til að snúa streng eða bætum við:
>>> málsháttur[::-1]
b'NNILOKARAKKAH'
Fróðleikur: Bætaröð (Endianness)
Í tölvuarkitektúr eru stærri heiltölur vistuð í minni á tvo ólíka vegu: háenda (e. big-endian, eins og við skrifum tölur á blað) og lágenda (e. little-endian, þar sem minnsta bætið er ritað fyrst). X86/X64 örgjörvar (sem eru í flestum heimilistölvum) nota lágendaframsetningu. Ef við viljum lesa vistfang úr minni tölvunnar í gegnum Python þurfum við mjög oft að snúa bætunum við svo við fáum réttu töluna. Rithátturinn [::-1] er okkar helsta tól til þess!