-
-
[原创]pwnable.kr 解题笔记(一)
-
2023-4-9 11:35 9650
-
pwnable.kr 解题笔记(一)
本文编写的目的并不是单纯的给出题解,网上已经有数不清的题解了,本文想尽可能详细地根据本人的分析来解释一下每道题目背后蕴含的知识点,作为小白学习的资料,同时记录本人复习的过程。
1. fd
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; } |
writeup:
1 2 3 4 5 6 7 | fd = atoi( input ) - 0x1234 len = read(fd, buf, 32 ) 需要 buf = = "LETMEWIN\n" 注意: fd = 0 时,是stdin,标准输入流。这是可以用户控制 |
2. collision
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; } |
writeup:
1 2 3 4 5 6 7 8 9 10 | chat * x = input strlen(x) = 20 int tmp = ( int * ) x x + = 4 result + = x result = 0x21DD09EC 得出, 5 个长度为 4 的数相加值为result 4X + Y = result |
3. bof
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); // smash me! if (key == 0xcafebabe){ system ( "/bin/sh" ); } else { printf ( "Nah..\n" ); } } int main( int argc, char * argv[]){ func(0xdeadbeef); return 0; } |
分析:
传递的参数是0xdeadbeef
,后面检查的是key == 0xcafebabe
, 然后在get(overflowme)
处,有明显的用户可控的溢出点,这里需要的是,使得输入能够覆盖到func到参数处,需要确定的关键信息有:
- overflowme到func函数ebp的距离
- 参数key到ebp的距离
有了以上两个数据,就能得到key到func函数参数的距离,然后就可以很简单的构造exp了。
overflowme距离ebp是0x2c,func参数距离ebp是0x8,所以overflowme距离func参数距离是0x2c+0x8.
4 flag
逆向题,UPX壳,直接upx -d
可脱,手动脱壳可以简单描述为三个小向上跳转之后有一个远跳,在那边下断点dump内存即可。
脱壳之后的程序调试一下即可看见flag值。
5. passcode
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); // ha! mommy told me that 32bit is vulnerable to bruteforcing :) 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(); // something after login... printf ( "Now I can safely trust you that you have credential :)\n" ); return 0; } |
5.1 分析
这里详细一点分析。
5.1.1 scanf
首先是scanf()
函数,参考RE4B解释一下。
1 2 3 4 5 6 7 8 9 10 11 12 | #include <stdio.h> int main() { int x; printf ( "Enter X:\n" ); scanf ( "%d" , &x); printf ( "You entered %d...\n" , x); return 0; } |
man scanf
可以看到如下描述:
The scanf() family of functions scans input according to format as de-scribed below. This format may contain conversion specifications; the results from such conversions, if any, are stored in the locations pointed to by the pointer arguments that follow format. Each pointer argument must be of a type that is appropriate for the value returned by the corresponding conversion specification.
其函数声明如下:
1 | int scanf ( const char *format, ...); |
在format
之后的参数正常情况下应该是一个指向可写的内存地址,如上例中所示,首先定义了int
类型的变量x
,它存储在栈上,简单调试可以看到存储位置是ebp - 0x10
,在调用scanf
函数时,是使用lea eax, [ebp-0x10];push eax
完成该调用,上述调用可以抽象为:scanf(&[ebp-0x1fb9], &[ebp-0x10])
。
而在本题中有:
1 2 3 4 5 6 7 8 | int passcode1; int passcode2; scanf ( "%d" , passcode1); scanf ( "%d" , passcode2); char name[100]; scanf ( "%100s" , name); |
期望的用户输入是一个数据,但是实际上的用户输入的是数据的地址,scanf会取到用户的地址,在上例中,将代码修改为scanf("%d", x);
有:
入栈道是x的值而非地址。
Disassemble & GOT
静态分析:
开启了canary和栈不可执行。但是没有PIE和FORTIFY。
main函数没什么可分析的。
welcome函数:name
距离ebp
的偏移为0x70
。
login函数:passcode1
距离ebp
的偏移是0x10
,passcode2
距离ebp
的偏移是0xc
,注意前面说的,passcode
传递的是地址,我们实际上修改的是这个地址上的数据,可以理解为[ebp-0x10] = *passcode1
。如果我们可以控制ebp-0x10
的值,我们就可以做到任意地址写了(首先在ebp-0x10处写入我们想写的地址,然后通过passcode1写入值,passcode2同理)。
动态调试:
welcome函数和login函数具有相同的ebp,同时有如下栈结构:
1 2 3 4 5 6 7 8 9 10 | ebp - - - - - - - > | | ebp - 0x10 | * passcode1 | | ... | | ebp - 0x70 | name[ 100 ] |
name[96]正好在*passcode上,所有在输入name的时候,控制后四位的数据即可控制任意地址写。
那么,怎么做才能执行system("/bin/cat flag")
呢?
首先要注意栈不可执行。然后直接写入代码地址是不可行的,因为此时EIP没有办法跳转到代码地址上。
如果想要控制任意地址写到任意执行,必须想办法写入一个EIP一定会跳入的地方。在本题中PLT,GOT都是可写的,在之前的文章有介绍过,PLT表项执行的是jmp GOT的操作,而GOT表项中写入的是对应函数的实际地址,因此这里写题目中的GOT表项即可。
选择fflush即可,需要注意的是scanf接受的数据是%d,即十进制数据。
整理一下:
- 通过写name控制*passcode1,即任意地址写,这里选择的是
0x0804a000
- 写passcode1,即
system("/bin/cat flag")
地址:0x080485e3
,注意要转化为10进制。
这里还有其他解法,不过思路是相似的。
6 random
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | int main(){ unsigned int random; random = rand (); // random value! unsigned int key=0; scanf ( "%d" , &key); if ( (key ^ random) == 0xdeadbeef ){ printf ( "Good!\n" ); system ( "/bin/cat flag" ); return 0; } printf ( "Wrong, maybe you should try 2^32 cases.\n" ); return 0; } |
分析:
本题利用的是伪随机数来控制生成random来解题。rand()
使用的时候是通过srand()
来设置种子,正常的情况下应该在每次使用rand()的时候通过srand
重新设置一次seed,但是在本题中并没有这么做,因此每次生成的时候数都是相同的。
同时A ^ B = C ==> A = B ^ C
.
下断,EAX存储的就是random
key = random ^ value
[培训]《安卓高级研修班(网课)》月薪三万计划,掌 握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法