경희대학교 장대희 교수님의 소프트웨어보안 수업을 기반으로 정리한 글입니다.
Reverse Engineering (Reversing, 역공학)
Reverse Engineering이란 이미 만들어진 시스템이나 장치에 대한 해체나 분석을 통해 구조, 기능, 디자인 등을 알아내는 과정이다.
즉, 완성품의 설계도 없이 구조와 동작 과정을 알아내는 모든 단계를 말한다.
- e.g., 소스코드가 없는 바이너리 파일(컴파일 된 파일)에 대해 프로그램의 로직을 분석하고 이해함
소프트웨어 분야에서의 Reverse Engineering은 소스코드 없이, 컴파일 된 소프트웨어의 구조를 다양한 방법으로 분석하는 과정을 말하며, 메모리 덤프와 같은 바이너리 분석 결과를 바탕으로 동작 원리와 내부 구조를 파악한 후, 원래의 코드가 어떻게 작성되었는지 추론한다.
- 메모리 덤프:프로그램이나 시스템의 메모리 상태를 그대로 파일로 떠놓은 것으로, GDB 같은 디버거로 덤프 파일을 열 수 있음
이러한 리버싱을 통해 분석된 프로그램에서 소스코드를 추출하거나 이해할 수 있다.

※ 이진 코드를 복원하여 바이너리 로직을 파악하는 리버싱 툴 도구 중 대표적인 도구로 IDA가 있다.
- IDA 사용 방법: 바이너리 파일을 드래그하여 IDA 창에 올리기
실습
문제 접근 방식
- toyransome을 제외하고는 소스코드가 없음
- 따라서 리버스 엔지니어링을 통해 바이너리를 분석 후 flag를 획득
- Flag의 형태는 'flag{/*임의의 문자열*/}'
- 문제풀이 Tip: 바이너리의 이름이 힌트가 되는 경우가 있음
1. runme

위 코드를 보고 sub_140001030 함수를 printf, 혹은 puts와 같은 함수로 추정이 가능하다. 이때 140001030은 디컴파일러가 함수가 존재하는 메모리 주소를 이름으로서 붙인 것이고, 실제 함수 이름은 아닌 경우가 대부분이다.
| 해당 프로그램은 이름 그대로 자신을 실행만 하면 flag가 찍히는 로직을 가지고 있다. 마지막에 fgetchar는 왜 있을까? 'runme2' 바이너리를 보면 이해할 수 있다. 'runme.exe' 파일을 더블 클릭만 하면 윈도우 콘솔 창이 뜨면서 flag 메시지가 보이고, fgetchar 때문에 거기서 멈춘다. 즉, 마우스로 클릭만 해도 손쉽게 flag를 얻을 수 있다. |

이때 디컴파일 된 코드란 원본 코드가 아닌 IDA가 추정하여 디컴파일한 코드이다.
- 디컴파일 하여 정확히 원본 소스 코드를 얻는 것은 수학적으로 불가능
- 똑같은 바이너리로 컴파 일 될 수 있는 원본 소스 코드의 경우의 수가 무한대이기 때문
그렇다면 왜 위에 함수는 printf가 아닌 sub_140001030으로 나오고, fgetchar는 함수 이름이 나올까?
sub_140001030도 내부로 들어가보면 해당 함수의 printf, 혹은 write와 같은 심볼을 발견할 확률이 높다. (static)
static vs danamic
danamic linking을 해서 API를 사용하는 바이너리의 경우에는 실행 시점에 실제 공유 라이브러리의 위치를 찾기 때문에 함수의 이름 정보가 바이너리에 존재해야 한다. 그래서 다이나믹 링킹으로 빌드된 바이너리들은 fgetchar()처럼 심볼 이름이 나타나고, 안 나타나더라도 몇 겹으로 랩핑되어 있는 것이지 뒤져보면 다 존재한다.
하지만 static 컴파일된 바이너리면 라이브러리에 심볼 이름 이런 게 있을 필요가 없고, 그냥 어셈블리 코드가 박혀있으면 되기 때문에 sub_140001030처럼 이름이 안 나타나는 경우가 대부분이다. 디버깅용 정보로서만 존재하고 디버깅 정보를 지우면 찾을 수 없는 경우가 꽤 많다.
그럼 실제 프로그램은 무슨 함수인지 어떻게 알까? 바로 시스템콜 번호를 통해서 파악!
※ IDA에서 디컴파일은 원래 유료 Hex-Rays 플러그인이 맡지만, IDA Freeware는 예외적으로 Hex-Rays를 가지고는 있지만, x86(인텔) 바이너리에 한해서만 디컴파일을 제공하고, 그 외 아키텍처 디컴파일은 유료 버전에서만 지원된다. (현재 버전 기준)
2. runme2

| 해당 프로그램은 'runme' 바이너리에 비해 fgetchar만 없을 뿐, 나머지는 전부 동일하다. 'runme2.exe' 파일을 마우스로 클릭만 하면 콘솔 창이 순간적으로 뜨며 flag가 찍히지만, 바로 return 0을 하며 종료되기에 사람의 눈으론 flag 확인이 불가능하다. 즉, 마우스와 같은 GUI 인터페이스만 사용해서는 flag 확인이 불가능하고, 콘솔 창에서 실행을 하던가 하는 방법을 사용해야 한다. 물론, 'runme'와 마찬가지로 IDA로 열면 해당 의도를 우회하고 flag 확인이 바로 가능하다. |

※ 'runme', 'runme2'는 IDA 없이 flag를 획득하는 게 원래 문제의 의도이다.
3. number



위 코드를 보고 sub_1400010E0 함수는 printf로, sub_140001070 함수는 scanf로 추정할 수 있다.

※ argv 옆의 envp는 디컴파일이 잘못된 것으로, 필요없는, 즉 무의미한 로직이다. 이렇게 디컴파일된 코드를 볼 때는 백프로 신뢰하면 안 된다. 디컴파일러는 로직이 똑같냐, 즉 똑같은 인풋에 대해 똑같은 결과를 내냐만 중요할 뿐 과정은 중요하지 않다.
4. number2



이때, flags.txt 파일은 여러 개의 정답 후보인 플래그들이 있는 텍스트 파일이다. 만약 'flags.txt' 파일을 담은 Stream이 null이면 else로 분기되어 프로그램이 종료된다.
※ 코드 설명
- fseek(Stream, 24672, 0): flags.txt 파일에서 처음부터 24,672바이트 떨어진 위치로 파일 포인터를 옮겨라
- fread(byte_140024BF0, 0x30ui64, 1ui64, Stream): 현재 파일 위치에서 0x3A0 바이트씩 1개 byte_140024BF0 버퍼에 읽어 와라
- ui64: unsigned 64-bit integer
- i64: signed 64-bit integer

※ %ld는 계산식은 못 읽고 정수만 읽을 수 있기 때문에 '5678900/4'처럼 계산식을 입력으로 넣을시, '/4'는 무시되고 '5678900'이 입력으로 들어간다. 때문에 직접 계산한 정답값을 입력으로 넣어줘야 한다.
5. number_elf

※ 리눅스에서는 'file' 명령어를 통해 파일 정보 확인이 가능하다.


윈도우는 함수 매핑 구조가 복잡해서 심볼이 잘 드러나지 않지만, 리눅스는 표준 C 라이브러리(libc) 함수를 직접 호출하는 방식이라 puts 같은 심볼이 그대로 노출되는 편이다.

※ 실행 파일
- 윈도우: exe
- 리눅스: elf
- 일반적으로 윈도우가 리눅스보다 리버싱하기 더 복잡하다.
6. plusplus



1) 첫 번째 while문
strcpy를 통해 flag 값을 v7이라는 변수에 저장한다. 이때, v3이 v7의 인덱스 번호를 의미한다.
리버싱을 할 때는 코드를 보고 어떤 역할을 하는지 보면서 네이밍을 해야 수월하게 리버싱을 할 수 있다.
- e.g., v7: copy_flag, v3: idx(인덱스)
첫 번째 while문의 역할은 copy_flag의 인덱스를 이용해 flag의 길이를 구하는 것이다. (counting)
즉, v3은 결과적으로 idx가 아니라 flag_length를 저장한다.
string의 마지막 문자는 null 문자로, 아스키 코드로 0을 의미하기에, 이 null 문자가 올 때까지 반복하는 것이다.
- while 문의 조건이 0이면 반복문 종료
2) 두 번째 while문
두 번째 while문은 v3이 0이 아니면, 즉 flag_length가 0이 아니면 v5에 copy_flag 값을 담고 실행된다.
- v5: copy_flag2
그리고 함수의 최종 실행이 출력인데 sub_1400010B0 외에는 출력하는 함수로 유추할 수 있는 게 없기에 sub_1400010B0를 printf로 유추할 수 있다.
이때 printf로 유추되는 sub_1400010B0 함수를 통해 copy_flag2에 포인터 변수로 접근하여 +1을 함으로써 flag를 1씩 더해주는 방식으로 암호화하여 출력을 하고, v4를 이용해 글자 수를 counting하여 flag_length만큼 반복하도록 한다.
- &unk_140018C58을 더블 클릭하면 "%c" 포맷이 나옴
- "%c" 포맷을 통해 문자를 출력함을 유추할 수도 있음

※ IDA에서 디컴파일 하는 단축키: F5
즉, flag를 얻기 위해서는 출력되는 string에서 1씩 빼주면 됨을 유추할 수 있다.
encoded = "gmbh|sfwfstjohqmvtqmvt~"
decoded = ""
for ch in encoded:
decoded += chr(ord(ch) - 1)
print(decoded)

7. string_easy



strcmp는 입력받은 두 문자가 다르면 정수 값을 리턴하고 같으면 0을 리턴한다.
여기서는 strcmp를 이용해 input string이 "i8NlM20"과 같으면 flag가 출력된다.

8. string



if문을 이용해 input string이 "Akaqe27J"이면 flag가 출력된다.

9. xor



해당 문제는 원래는 xor 연산이 포함되었으나, 컴파일러가 최적화하는 과정에서 xor 로직이 날라가 10진수 <->16진수 변환 관련 문제로 전환되었다.

10. toyransome
- 문제 설명: 주어진 소스코드 파일을 분석하여 암호화된 파일을 복호화하는 문제
- 파일 구성
- toyransome_flag.enc: 암호화된 flag 파일
- toyransome.c: flag를 암호화하는 바이너리 파일

11. flag_generator

flag가 존재하는 위치를 실행 시점에 디버깅하면 flag를 쉽게 확인 가능하다. (동적 분석)
- 브레이크 포인트를 잘 걸고 (여기에선 return문에 브레이크 포인트) 실행을 시켜주면 flag가 스택에 남아있는 거 확인 가능
이렇게 정적 분석으론 어렵지만 동적 분석으론 쉽게 풀리는 문제들이 많다.
※ IDA는 정적 분석 도구지만 동적 분석도 가능하여 브레이크 포인트를 걸고 실행시킬 수 있다.
- Step into: 함수가 실행되는 그 안으로 들어간다는 의미
- Step over: 그걸 넘어서 가겠다는 의미
동적 디버깅 방법




※ 참고자료
https://arsenic-dev.tistory.com/106
'CS > 소프트웨어보안' 카테고리의 다른 글
| 7-1. BoF Exploit 1 (0) | 2025.12.13 |
|---|---|
| 8. Toyransome (0) | 2025.12.09 |
| 5-2. 리눅스 사용 (0) | 2025.10.17 |
| 5-1. 소프트웨어 보안을 위한 리눅스 기초 (0) | 2025.10.17 |
| 4-3. 어셈블리 실습 (0) | 2025.10.17 |